Skip to content

Commit a59da37

Browse files
Closes #20129: Enable dynamic model feature registration (#20130)
* Closes #20129: Enable dynamic model feature registration * Correct import path for register_model_feature()
1 parent 6d4cc16 commit a59da37

File tree

6 files changed

+98
-85
lines changed

6 files changed

+98
-85
lines changed

docs/development/application-registry.md

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,24 +22,9 @@ Stores registration made using `netbox.denormalized.register()`. For each model,
2222

2323
### `model_features`
2424

25-
A dictionary of particular features (e.g. custom fields) mapped to the NetBox models which support them, arranged by app. For example:
26-
27-
```python
28-
{
29-
'custom_fields': {
30-
'circuits': ['provider', 'circuit'],
31-
'dcim': ['site', 'rack', 'devicetype', ...],
32-
...
33-
},
34-
'event_rules': {
35-
'extras': ['configcontext', 'tag', ...],
36-
'dcim': ['site', 'rack', 'devicetype', ...],
37-
},
38-
...
39-
}
40-
```
41-
42-
Supported model features are listed in the [features matrix](./models.md#features-matrix).
25+
A dictionary of model features (e.g. custom fields, tags, etc.) mapped to the functions used to qualify a model as supporting each feature. Model features are registered using the `register_model_feature()` function in `netbox.utils`.
26+
27+
Core model features are listed in the [features matrix](./models.md#features-matrix).
4328

4429
### `models`
4530

docs/development/models.md

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,26 @@ The Django [content types](https://docs.djangoproject.com/en/stable/ref/contrib/
1010

1111
Depending on its classification, each NetBox model may support various features which enhance its operation. Each feature is enabled by inheriting from its designated mixin class, and some features also make use of the [application registry](./application-registry.md#model_features).
1212

13-
| Feature | Feature Mixin | Registry Key | Description |
14-
|------------------------------------------------------------|-------------------------|--------------------|-----------------------------------------------------------------------------------------|
15-
| [Change logging](../features/change-logging.md) | `ChangeLoggingMixin` | - | Changes to these objects are automatically recorded in the change log |
16-
| Cloning | `CloningMixin` | - | Provides the `clone()` method to prepare a copy |
17-
| [Custom fields](../customization/custom-fields.md) | `CustomFieldsMixin` | `custom_fields` | These models support the addition of user-defined fields |
18-
| [Custom links](../customization/custom-links.md) | `CustomLinksMixin` | `custom_links` | These models support the assignment of custom links |
19-
| [Custom validation](../customization/custom-validation.md) | `CustomValidationMixin` | - | Supports the enforcement of custom validation rules |
20-
| [Export templates](../customization/export-templates.md) | `ExportTemplatesMixin` | `export_templates` | Users can create custom export templates for these models |
21-
| [Job results](../features/background-jobs.md) | `JobsMixin` | `jobs` | Background jobs can be scheduled for these models |
22-
| [Journaling](../features/journaling.md) | `JournalingMixin` | `journaling` | These models support persistent historical commentary |
23-
| [Synchronized data](../integrations/synchronized-data.md) | `SyncedDataMixin` | `synced_data` | Certain model data can be automatically synchronized from a remote data source |
24-
| [Tagging](../models/extras/tag.md) | `TagsMixin` | `tags` | The models can be tagged with user-defined tags |
25-
| [Event rules](../features/event-rules.md) | `EventRulesMixin` | `event_rules` | Event rules can send webhooks or run custom scripts automatically in response to events |
13+
| Feature | Feature Mixin | Registry Key | Description |
14+
|------------------------------------------------------------|-------------------------|---------------------|-----------------------------------------------------------------------------------------|
15+
| [Bookmarks](../features/customization.md#bookmarks) | `BookmarksMixin` | `bookmarks` | These models can be bookmarked natively in the user interface |
16+
| [Change logging](../features/change-logging.md) | `ChangeLoggingMixin` | `change_logging` | Changes to these objects are automatically recorded in the change log |
17+
| Cloning | `CloningMixin` | `cloning` | Provides the `clone()` method to prepare a copy |
18+
| [Contacts](../features/contacts.md) | `ContactsMixin` | `contacts` | Contacts can be associated with these models |
19+
| [Custom fields](../customization/custom-fields.md) | `CustomFieldsMixin` | `custom_fields` | These models support the addition of user-defined fields |
20+
| [Custom links](../customization/custom-links.md) | `CustomLinksMixin` | `custom_links` | These models support the assignment of custom links |
21+
| [Custom validation](../customization/custom-validation.md) | `CustomValidationMixin` | - | Supports the enforcement of custom validation rules |
22+
| [Event rules](../features/event-rules.md) | `EventRulesMixin` | `event_rules` | Event rules can send webhooks or run custom scripts automatically in response to events |
23+
| [Export templates](../customization/export-templates.md) | `ExportTemplatesMixin` | `export_templates` | Users can create custom export templates for these models |
24+
| [Image attachments](../models/extras/imageattachment.md) | `ImageAttachmentsMixin` | `image_attachments` | Image uploads can be attached to these models |
25+
| [Jobs](../features/background-jobs.md) | `JobsMixin` | `jobs` | Background jobs can be scheduled for these models |
26+
| [Journaling](../features/journaling.md) | `JournalingMixin` | `journaling` | These models support persistent historical commentary |
27+
| [Notifications](../features/notifications.md) | `NotificationsMixin` | `notifications` | These models support user notifications |
28+
| [Synchronized data](../integrations/synchronized-data.md) | `SyncedDataMixin` | `synced_data` | Certain model data can be automatically synchronized from a remote data source |
29+
| [Tagging](../models/extras/tag.md) | `TagsMixin` | `tags` | The models can be tagged with user-defined tags |
30+
31+
!!! note
32+
The above listed features are supported natively by NetBox. Beginning with NetBox v4.4.0, plugins can register their own model features as well.
2633

2734
## Models Index
2835

docs/plugins/development/models.md

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,7 @@ Every model includes by default a numeric primary key. This value is generated a
2424

2525
## Enabling NetBox Features
2626

27-
Plugin models can leverage certain NetBox features by inheriting from NetBox's `NetBoxModel` class. This class extends the plugin model to enable features unique to NetBox, including:
28-
29-
* Bookmarks
30-
* Change logging
31-
* Cloning
32-
* Custom fields
33-
* Custom links
34-
* Custom validation
35-
* Export templates
36-
* Journaling
37-
* Tags
38-
* Webhooks
39-
40-
This class performs two crucial functions:
27+
Plugin models can leverage certain [model features](../development/models.md#features-matrix) (such as tags, custom fields, event rules, etc.) by inheriting from NetBox's `NetBoxModel` class. This class performs two crucial functions:
4128

4229
1. Apply any fields, methods, and/or attributes necessary to the operation of these features
4330
2. Register the model with NetBox as utilizing these features
@@ -135,6 +122,27 @@ For more information about database migrations, see the [Django documentation](h
135122

136123
::: netbox.models.features.TagsMixin
137124

125+
## Custom Model Features
126+
127+
In addition to utilizing the model features provided natively by NetBox (listed above), plugins can register their own model features. This is done using the `register_model_feature()` function from `netbox.utils`. This function takes two arguments: a feature name, and a callable which accepts a model class. The callable must return a boolean value indicting whether the given model supports the named feature.
128+
129+
This function can be used as a decorator:
130+
131+
```python
132+
@register_model_feature('foo')
133+
def supports_foo(model):
134+
# Your logic here
135+
```
136+
137+
Or it can be called directly:
138+
139+
```python
140+
register_model_feature('foo', supports_foo)
141+
```
142+
143+
!!! tip
144+
Consider performing feature registration inside your PluginConfig's `ready()` method.
145+
138146
## Choice Sets
139147

140148
For model fields which support the selection of one or more values from a predefined list of choices, NetBox provides the `ChoiceSet` utility class. This can be used in place of a regular choices tuple to provide enhanced functionality, namely dynamic configuration and colorization. (See [Django's documentation](https://docs.djangoproject.com/en/stable/ref/models/fields/#choices) on the `choices` parameter for supported model fields.)

netbox/core/models/object_types.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -135,9 +135,9 @@ def with_feature(self, feature):
135135
"""
136136
Return ObjectTypes only for models which support the given feature.
137137
138-
Only ObjectTypes which list the specified feature will be included. Supported features are declared in
139-
netbox.models.features.FEATURES_MAP. For example, we can find all ObjectTypes for models which support event
140-
rules with:
138+
Only ObjectTypes which list the specified feature will be included. Supported features are declared in the
139+
application registry under `registry["model_features"]`. For example, we can find all ObjectTypes for models
140+
which support event rules with:
141141
142142
ObjectType.objects.with_feature('event_rules')
143143
"""

netbox/netbox/models/features.py

Lines changed: 20 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from netbox.plugins import PluginConfig
2323
from netbox.registry import registry
2424
from netbox.signals import post_clean
25+
from netbox.utils import register_model_feature
2526
from utilities.json import CustomFieldJSONEncoder
2627
from utilities.serialization import serialize_object
2728

@@ -35,7 +36,6 @@
3536
'CustomValidationMixin',
3637
'EventRulesMixin',
3738
'ExportTemplatesMixin',
38-
'FEATURES_MAP',
3939
'ImageAttachmentsMixin',
4040
'JobsMixin',
4141
'JournalingMixin',
@@ -628,28 +628,21 @@ def sync_data(self):
628628
# Feature registration
629629
#
630630

631-
FEATURES_MAP = {
632-
'bookmarks': BookmarksMixin,
633-
'change_logging': ChangeLoggingMixin,
634-
'cloning': CloningMixin,
635-
'contacts': ContactsMixin,
636-
'custom_fields': CustomFieldsMixin,
637-
'custom_links': CustomLinksMixin,
638-
'custom_validation': CustomValidationMixin,
639-
'event_rules': EventRulesMixin,
640-
'export_templates': ExportTemplatesMixin,
641-
'image_attachments': ImageAttachmentsMixin,
642-
'jobs': JobsMixin,
643-
'journaling': JournalingMixin,
644-
'notifications': NotificationsMixin,
645-
'synced_data': SyncedDataMixin,
646-
'tags': TagsMixin,
647-
}
648-
649-
# TODO: Remove in NetBox v4.5
650-
registry['model_features'].update({
651-
feature: defaultdict(set) for feature in FEATURES_MAP.keys()
652-
})
631+
register_model_feature('bookmarks', lambda model: issubclass(model, BookmarksMixin))
632+
register_model_feature('change_logging', lambda model: issubclass(model, ChangeLoggingMixin))
633+
register_model_feature('cloning', lambda model: issubclass(model, CloningMixin))
634+
register_model_feature('contacts', lambda model: issubclass(model, ContactsMixin))
635+
register_model_feature('custom_fields', lambda model: issubclass(model, CustomFieldsMixin))
636+
register_model_feature('custom_links', lambda model: issubclass(model, CustomLinksMixin))
637+
register_model_feature('custom_validation', lambda model: issubclass(model, CustomValidationMixin))
638+
register_model_feature('event_rules', lambda model: issubclass(model, EventRulesMixin))
639+
register_model_feature('export_templates', lambda model: issubclass(model, ExportTemplatesMixin))
640+
register_model_feature('image_attachments', lambda model: issubclass(model, ImageAttachmentsMixin))
641+
register_model_feature('jobs', lambda model: issubclass(model, JobsMixin))
642+
register_model_feature('journaling', lambda model: issubclass(model, JournalingMixin))
643+
register_model_feature('notifications', lambda model: issubclass(model, NotificationsMixin))
644+
register_model_feature('synced_data', lambda model: issubclass(model, SyncedDataMixin))
645+
register_model_feature('tags', lambda model: issubclass(model, TagsMixin))
653646

654647

655648
def model_is_public(model):
@@ -665,8 +658,11 @@ def model_is_public(model):
665658

666659

667660
def get_model_features(model):
661+
"""
662+
Return all features supported by the given model.
663+
"""
668664
return [
669-
feature for feature, cls in FEATURES_MAP.items() if issubclass(model, cls)
665+
feature for feature, test_func in registry['model_features'].items() if test_func(model)
670666
]
671667

672668

@@ -710,19 +706,6 @@ def register_models(*models):
710706
if not getattr(model, '_netbox_private', False):
711707
registry['models'][app_label].add(model_name)
712708

713-
# TODO: Remove in NetBox v4.5
714-
# Record each applicable feature for the model in the registry
715-
features = {
716-
feature for feature, cls in FEATURES_MAP.items() if issubclass(model, cls)
717-
}
718-
for feature in features:
719-
try:
720-
registry['model_features'][feature][app_label].add(model_name)
721-
except KeyError:
722-
raise KeyError(
723-
f"{feature} is not a valid model feature! Valid keys are: {registry['model_features'].keys()}"
724-
)
725-
726709
# Register applicable feature views for the model
727710
if issubclass(model, ContactsMixin):
728711
register_model_view(model, 'contacts', kwargs={'model': model})(

netbox/netbox/utils.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
__all__ = (
44
'get_data_backend_choices',
55
'register_data_backend',
6+
'register_model_feature',
67
'register_request_processor',
78
)
89

@@ -27,6 +28,35 @@ def _wrapper(cls):
2728
return _wrapper
2829

2930

31+
def register_model_feature(name, func=None):
32+
"""
33+
Register a model feature with its qualifying function.
34+
35+
The qualifying function must accept a single `model` argument. It will be called to determine whether the given
36+
model supports the corresponding feature.
37+
38+
This function can be used directly:
39+
40+
register_model_feature('my_feature', my_func)
41+
42+
Or as a decorator:
43+
44+
@register_model_feature('my_feature')
45+
def my_func(model):
46+
...
47+
"""
48+
def decorator(f):
49+
registry['model_features'][name] = f
50+
return f
51+
52+
if name in registry['model_features']:
53+
raise ValueError(f"A model feature named {name} is already registered.")
54+
55+
if func is None:
56+
return decorator
57+
return decorator(func)
58+
59+
3060
def register_request_processor(func):
3161
"""
3262
Decorator for registering a request processor.

0 commit comments

Comments
 (0)