diff --git a/netbox/circuits/apps.py b/netbox/circuits/apps.py index 3acf3b98c74..df6804303b7 100644 --- a/netbox/circuits/apps.py +++ b/netbox/circuits/apps.py @@ -6,4 +6,8 @@ class CircuitsConfig(AppConfig): verbose_name = "Circuits" def ready(self): + from netbox.models.features import register_models from . import signals, search + + # Register models + register_models(*self.get_models()) diff --git a/netbox/core/apps.py b/netbox/core/apps.py index 2d999c57e03..b1103469c19 100644 --- a/netbox/core/apps.py +++ b/netbox/core/apps.py @@ -16,5 +16,9 @@ class CoreConfig(AppConfig): name = "core" def ready(self): + from core.api import schema # noqa + from netbox.models.features import register_models from . import data_backends, search - from core.api import schema # noqa: E402 + + # Register models + register_models(*self.get_models()) diff --git a/netbox/dcim/apps.py b/netbox/dcim/apps.py index 78ff0d4c1a6..4df66e36747 100644 --- a/netbox/dcim/apps.py +++ b/netbox/dcim/apps.py @@ -8,9 +8,13 @@ class DCIMConfig(AppConfig): verbose_name = "DCIM" def ready(self): + from netbox.models.features import register_models + from utilities.counters import connect_counters from . import signals, search from .models import CableTermination, Device, DeviceType, VirtualChassis - from utilities.counters import connect_counters + + # Register models + register_models(*self.get_models()) # Register denormalized fields denormalized.register(CableTermination, '_device', { diff --git a/netbox/extras/apps.py b/netbox/extras/apps.py index f23e62dd2a1..c565988bcf0 100644 --- a/netbox/extras/apps.py +++ b/netbox/extras/apps.py @@ -5,4 +5,8 @@ class ExtrasConfig(AppConfig): name = "extras" def ready(self): + from netbox.models.features import register_models from . import dashboard, lookups, search, signals + + # Register models + register_models(*self.get_models()) diff --git a/netbox/extras/utils.py b/netbox/extras/utils.py index a3a6670cf86..4464af7185e 100644 --- a/netbox/extras/utils.py +++ b/netbox/extras/utils.py @@ -1,7 +1,5 @@ from taggit.managers import _TaggableManager -from netbox.registry import registry - def is_taggable(obj): """ @@ -29,24 +27,6 @@ def image_upload(instance, filename): return '{}{}_{}_{}'.format(path, instance.content_type.name, instance.object_id, filename) -def register_features(model, features): - """ - Register model features in the application registry. - """ - app_label, model_name = model._meta.label_lower.split('.') - for feature in features: - try: - registry['model_features'][feature][app_label].add(model_name) - except KeyError: - raise KeyError( - f"{feature} is not a valid model feature! Valid keys are: {registry['model_features'].keys()}" - ) - - # Register public models - if not getattr(model, '_netbox_private', False): - registry['models'][app_label].add(model_name) - - def is_script(obj): """ Returns True if the object is a Script or Report. diff --git a/netbox/ipam/apps.py b/netbox/ipam/apps.py index 4b0820fef2c..244ec7d6d0e 100644 --- a/netbox/ipam/apps.py +++ b/netbox/ipam/apps.py @@ -6,4 +6,8 @@ class IPAMConfig(AppConfig): verbose_name = "IPAM" def ready(self): + from netbox.models.features import register_models from . import signals, search + + # Register models + register_models(*self.get_models()) diff --git a/netbox/netbox/models/features.py b/netbox/netbox/models/features.py index a13b84bed51..91765be04b0 100644 --- a/netbox/netbox/models/features.py +++ b/netbox/netbox/models/features.py @@ -5,8 +5,6 @@ from django.contrib.contenttypes.fields import GenericRelation from django.core.validators import ValidationError from django.db import models -from django.db.models.signals import class_prepared -from django.dispatch import receiver from django.utils import timezone from django.utils.translation import gettext_lazy as _ from taggit.managers import TaggableManager @@ -14,7 +12,7 @@ from core.choices import JobStatusChoices from core.models import ContentType from extras.choices import * -from extras.utils import is_taggable, register_features +from extras.utils import is_taggable from netbox.config import get_config from netbox.registry import registry from netbox.signals import post_clean @@ -37,6 +35,7 @@ 'JournalingMixin', 'SyncedDataMixin', 'TagsMixin', + 'register_models', ) @@ -576,36 +575,49 @@ def sync_data(self): }) -@receiver(class_prepared) -def _register_features(sender, **kwargs): - # Record each applicable feature for the model in the registry - features = { - feature for feature, cls in FEATURES_MAP.items() if issubclass(sender, cls) - } - register_features(sender, features) - - # Register applicable feature views for the model - if issubclass(sender, JournalingMixin): - register_model_view( - sender, - 'journal', - kwargs={'model': sender} - )('netbox.views.generic.ObjectJournalView') - if issubclass(sender, ChangeLoggingMixin): - register_model_view( - sender, - 'changelog', - kwargs={'model': sender} - )('netbox.views.generic.ObjectChangeLogView') - if issubclass(sender, JobsMixin): - register_model_view( - sender, - 'jobs', - kwargs={'model': sender} - )('netbox.views.generic.ObjectJobsView') - if issubclass(sender, SyncedDataMixin): - register_model_view( - sender, - 'sync', - kwargs={'model': sender} - )('netbox.views.generic.ObjectSyncDataView') +def register_models(*models): + """ + Register one or more models in NetBox. This entails: + + - Determining whether the model is considered "public" (available for reference by other models) + - Registering which features the model supports (e.g. bookmarks, custom fields, etc.) + - Registering any feature-specific views for the model (e.g. ObjectJournalView instances) + + register_model() should be called for each relevant model under the ready() of an app's AppConfig class. + """ + for model in models: + app_label, model_name = model._meta.label_lower.split('.') + + # Register public models + if not getattr(model, '_netbox_private', False): + registry['models'][app_label].add(model_name) + + # Record each applicable feature for the model in the registry + features = { + feature for feature, cls in FEATURES_MAP.items() if issubclass(model, cls) + } + for feature in features: + try: + registry['model_features'][feature][app_label].add(model_name) + except KeyError: + raise KeyError( + f"{feature} is not a valid model feature! Valid keys are: {registry['model_features'].keys()}" + ) + + # Register applicable feature views for the model + if issubclass(model, JournalingMixin): + register_model_view(model, 'journal', kwargs={'model': model})( + 'netbox.views.generic.ObjectJournalView' + ) + if issubclass(model, ChangeLoggingMixin): + register_model_view(model, 'changelog', kwargs={'model': model})( + 'netbox.views.generic.ObjectChangeLogView' + ) + if issubclass(model, JobsMixin): + register_model_view(model, 'jobs', kwargs={'model': model})( + 'netbox.views.generic.ObjectJobsView' + ) + if issubclass(model, SyncedDataMixin): + register_model_view(model, 'sync', kwargs={'model': model})( + 'netbox.views.generic.ObjectSyncDataView' + ) diff --git a/netbox/netbox/plugins/__init__.py b/netbox/netbox/plugins/__init__.py index 8b6901b7ade..8db945e7413 100644 --- a/netbox/netbox/plugins/__init__.py +++ b/netbox/netbox/plugins/__init__.py @@ -94,6 +94,11 @@ def _load_resource(self, name): pass def ready(self): + from netbox.models.features import register_models + + # Register models + register_models(*self.get_models()) + plugin_name = self.name.rsplit('.', 1)[-1] # Register search extensions (if defined) diff --git a/netbox/netbox/tests/test_plugins.py b/netbox/netbox/tests/test_plugins.py index f7aeb7a5391..24bc5300593 100644 --- a/netbox/netbox/tests/test_plugins.py +++ b/netbox/netbox/tests/test_plugins.py @@ -20,6 +20,10 @@ def test_config(self): self.assertIn('netbox.tests.dummy_plugin.DummyPluginConfig', settings.INSTALLED_APPS) + def test_model_registration(self): + self.assertIn('dummy_plugin', registry['models']) + self.assertIn('dummymodel', registry['models']['dummy_plugin']) + def test_models(self): from netbox.tests.dummy_plugin.models import DummyModel diff --git a/netbox/tenancy/apps.py b/netbox/tenancy/apps.py index eeb1411529a..7fec97d738e 100644 --- a/netbox/tenancy/apps.py +++ b/netbox/tenancy/apps.py @@ -5,4 +5,8 @@ class TenancyConfig(AppConfig): name = 'tenancy' def ready(self): + from netbox.models.features import register_models from . import search + + # Register models + register_models(*self.get_models()) diff --git a/netbox/users/apps.py b/netbox/users/apps.py index 95d9e03a0ef..cd61a59bcd8 100644 --- a/netbox/users/apps.py +++ b/netbox/users/apps.py @@ -5,15 +5,8 @@ class UsersConfig(AppConfig): name = 'users' def ready(self): - import users.signals - from .models import NetBoxGroup, ObjectPermission, Token, User, UserConfig - from netbox.models.features import _register_features + from netbox.models.features import register_models + from . import signals - # have to register these manually as the signal handler for class_prepared does - # not get registered until after these models are loaded. Any models defined in - # users.models should be registered here. - _register_features(NetBoxGroup) - _register_features(ObjectPermission) - _register_features(Token) - _register_features(User) - _register_features(UserConfig) + # Register models + register_models(*self.get_models()) diff --git a/netbox/virtualization/apps.py b/netbox/virtualization/apps.py index fe35fee7827..8a61bc52316 100644 --- a/netbox/virtualization/apps.py +++ b/netbox/virtualization/apps.py @@ -7,9 +7,13 @@ class VirtualizationConfig(AppConfig): name = 'virtualization' def ready(self): + from netbox.models.features import register_models + from utilities.counters import connect_counters from . import search, signals from .models import VirtualMachine - from utilities.counters import connect_counters + + # Register models + register_models(*self.get_models()) # Register denormalized fields denormalized.register(VirtualMachine, 'cluster', { diff --git a/netbox/vpn/apps.py b/netbox/vpn/apps.py index 2254befd3ac..9a3751cb236 100644 --- a/netbox/vpn/apps.py +++ b/netbox/vpn/apps.py @@ -6,4 +6,8 @@ class VPNConfig(AppConfig): verbose_name = 'VPN' def ready(self): + from netbox.models.features import register_models from . import search + + # Register models + register_models(*self.get_models()) diff --git a/netbox/wireless/apps.py b/netbox/wireless/apps.py index 51dee418895..24e1869b61f 100644 --- a/netbox/wireless/apps.py +++ b/netbox/wireless/apps.py @@ -5,4 +5,8 @@ class WirelessConfig(AppConfig): name = 'wireless' def ready(self): + from netbox.models.features import register_models from . import signals, search + + # Register models + register_models(*self.get_models())