diff --git a/docs/development/application-registry.md b/docs/development/application-registry.md index 41bf6cb3131..c7ac6ca46fc 100644 --- a/docs/development/application-registry.md +++ b/docs/development/application-registry.md @@ -41,6 +41,10 @@ A dictionary of particular features (e.g. custom fields) mapped to the NetBox mo Supported model features are listed in the [features matrix](./models.md#features-matrix). +### `models` + +This key lists all models which have been registered in NetBox which are not designated for private use. (Setting `_netbox_private` to True on a model excludes it from this list.) As with individual features under `model_features`, models are organized by app label. + ### `plugins` This store maintains all registered items for plugins, such as navigation menus, template extensions, etc. diff --git a/netbox/account/models.py b/netbox/account/models.py index 5d65750404d..bd5879a858a 100644 --- a/netbox/account/models.py +++ b/netbox/account/models.py @@ -7,6 +7,8 @@ class UserToken(Token): """ Proxy model for users to manage their own API tokens. """ + _netbox_private = True + class Meta: proxy = True verbose_name = 'token' diff --git a/netbox/core/migrations/0008_contenttype_proxy.py b/netbox/core/migrations/0008_contenttype_proxy.py new file mode 100644 index 00000000000..ac11d906a49 --- /dev/null +++ b/netbox/core/migrations/0008_contenttype_proxy.py @@ -0,0 +1,29 @@ +# Generated by Django 4.2.6 on 2023-10-31 19:38 + +import core.models.contenttypes +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('contenttypes', '0002_remove_content_type_name'), + ('core', '0007_job_add_error_field'), + ] + + operations = [ + migrations.CreateModel( + name='ContentType', + fields=[ + ], + options={ + 'proxy': True, + 'indexes': [], + 'constraints': [], + }, + bases=('contenttypes.contenttype',), + managers=[ + ('objects', core.models.contenttypes.ContentTypeManager()), + ], + ), + ] diff --git a/netbox/core/models/__init__.py b/netbox/core/models/__init__.py index 185622f5f28..c93c392d721 100644 --- a/netbox/core/models/__init__.py +++ b/netbox/core/models/__init__.py @@ -1,3 +1,4 @@ +from .contenttypes import * from .data import * from .files import * from .jobs import * diff --git a/netbox/core/models/contenttypes.py b/netbox/core/models/contenttypes.py new file mode 100644 index 00000000000..18c16a1c212 --- /dev/null +++ b/netbox/core/models/contenttypes.py @@ -0,0 +1,32 @@ +from django.contrib.contenttypes.models import ContentType as ContentType_, ContentTypeManager as ContentTypeManager_ +from django.db.models import Q + +from netbox.registry import registry + +__all__ = ( + 'ContentType', + 'ContentTypeManager', +) + + +class ContentTypeManager(ContentTypeManager_): + + def public(self): + """ + Filter the base queryset to return only ContentTypes corresponding to "public" models; those which are listed + in registry['models'] and intended for reference by other objects. + """ + q = Q() + for app_label, models in registry['models'].items(): + q |= Q(app_label=app_label, model__in=models) + return self.get_queryset().filter(q) + + +class ContentType(ContentType_): + """ + Wrap Django's native ContentType model to use our custom manager. + """ + objects = ContentTypeManager() + + class Meta: + proxy = True diff --git a/netbox/core/models/data.py b/netbox/core/models/data.py index fb764134af6..78f05e462fc 100644 --- a/netbox/core/models/data.py +++ b/netbox/core/models/data.py @@ -378,6 +378,8 @@ class AutoSyncRecord(models.Model): fk_field='object_id' ) + _netbox_private = True + class Meta: constraints = ( models.UniqueConstraint( diff --git a/netbox/core/models/files.py b/netbox/core/models/files.py index 38d82463eca..13852758128 100644 --- a/netbox/core/models/files.py +++ b/netbox/core/models/files.py @@ -44,6 +44,7 @@ class ManagedFile(SyncedDataMixin, models.Model): ) objects = RestrictedQuerySet.as_manager() + _netbox_private = True class Meta: ordering = ('file_root', 'file_path') diff --git a/netbox/dcim/models/cables.py b/netbox/dcim/models/cables.py index 751bca2719e..d248b5cad41 100644 --- a/netbox/dcim/models/cables.py +++ b/netbox/dcim/models/cables.py @@ -431,6 +431,8 @@ class CablePath(models.Model): ) _nodes = PathField() + _netbox_private = True + class Meta: verbose_name = _('cable path') verbose_name_plural = _('cable paths') diff --git a/netbox/extras/dashboard/widgets.py b/netbox/extras/dashboard/widgets.py index 0b185d432ec..7f0229f88a7 100644 --- a/netbox/extras/dashboard/widgets.py +++ b/netbox/extras/dashboard/widgets.py @@ -7,15 +7,13 @@ import requests from django import forms from django.conf import settings -from django.contrib.contenttypes.models import ContentType from django.core.cache import cache -from django.db.models import Q from django.template.loader import render_to_string from django.urls import NoReverseMatch, resolve, reverse from django.utils.translation import gettext as _ +from core.models import ContentType from extras.choices import BookmarkOrderingChoices -from extras.utils import FeatureQuery from utilities.choices import ButtonColorChoices from utilities.forms import BootstrapMixin from utilities.permissions import get_permission_for_model @@ -37,10 +35,7 @@ def get_content_type_labels(): return [ (content_type_identifier(ct), content_type_name(ct)) - for ct in ContentType.objects.filter( - FeatureQuery('export_templates').get_query() | Q(app_label='extras', model='objectchange') | - Q(app_label='extras', model='configcontext') - ).order_by('app_label', 'model') + for ct in ContentType.objects.public().order_by('app_label', 'model') ] diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 79023a74dbe..03a6d118b1e 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -1,9 +1,9 @@ from django import forms -from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.forms import SimpleArrayField from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ +from core.models import ContentType from extras.choices import * from extras.models import * from extras.utils import FeatureQuery @@ -40,8 +40,7 @@ class CustomFieldImportForm(CSVModelForm): ) object_type = CSVContentTypeField( label=_('Object type'), - queryset=ContentType.objects.all(), - limit_choices_to=FeatureQuery('custom_fields'), + queryset=ContentType.objects.public(), required=False, help_text=_("Object type (for object or multi-object fields)") ) diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 7db84d17550..c0c8835b492 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -1,9 +1,8 @@ from django import forms from django.contrib.auth import get_user_model -from django.contrib.contenttypes.models import ContentType from django.utils.translation import gettext_lazy as _ -from core.models import DataFile, DataSource +from core.models import ContentType, DataFile, DataSource from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from extras.choices import * from extras.models import * @@ -196,7 +195,7 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm): ) content_types = ContentTypeMultipleChoiceField( label=_('Content types'), - queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()), + queryset=ContentType.objects.public(), required=False ) enabled = forms.NullBooleanField( diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index fd2ce8f2da3..7ab568ae03c 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -2,12 +2,11 @@ from django import forms from django.conf import settings -from django.db.models import Q -from django.contrib.contenttypes.models import ContentType from django.utils.safestring import mark_safe from django.utils.translation import gettext_lazy as _ from core.forms.mixins import SyncedDataMixin +from core.models import ContentType from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from extras.choices import * from extras.models import * @@ -49,9 +48,7 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm): ) object_type = ContentTypeChoiceField( label=_('Object type'), - queryset=ContentType.objects.all(), - # TODO: Come up with a canonical way to register suitable models - limit_choices_to=FeatureQuery('webhooks').get_query() | Q(app_label='auth', model__in=['user', 'group']), + queryset=ContentType.objects.public(), required=False, help_text=_("Type of the related object (for object/multi-object fields only)") ) diff --git a/netbox/extras/models/configs.py b/netbox/extras/models/configs.py index 2acfcb725b0..425c1386acd 100644 --- a/netbox/extras/models/configs.py +++ b/netbox/extras/models/configs.py @@ -260,12 +260,14 @@ def render(self, context=None): _context = dict() # Populate the default template context with NetBox model classes, namespaced by app - # TODO: Devise a canonical mechanism for identifying the models to include (see #13427) - for app, model_names in registry['model_features']['custom_fields'].items(): + for app, model_names in registry['models'].items(): _context.setdefault(app, {}) for model_name in model_names: - model = apps.get_registered_model(app, model_name) - _context[app][model.__name__] = model + try: + model = apps.get_registered_model(app, model_name) + _context[app][model.__name__] = model + except LookupError: + pass # Add the provided context data, if any if context is not None: diff --git a/netbox/extras/models/search.py b/netbox/extras/models/search.py index debe4c64853..57db73b281f 100644 --- a/netbox/extras/models/search.py +++ b/netbox/extras/models/search.py @@ -49,6 +49,8 @@ class CachedValue(models.Model): default=1000 ) + _netbox_private = True + class Meta: ordering = ('weight', 'object_type', 'object_id') verbose_name = _('cached value') diff --git a/netbox/extras/models/tags.py b/netbox/extras/models/tags.py index f4ba5ea642a..de4f155095a 100644 --- a/netbox/extras/models/tags.py +++ b/netbox/extras/models/tags.py @@ -75,6 +75,8 @@ class TaggedItem(GenericTaggedItemBase): on_delete=models.CASCADE ) + _netbox_private = True + class Meta: indexes = [models.Index(fields=["content_type", "object_id"])] verbose_name = _('tagged item') diff --git a/netbox/extras/utils.py b/netbox/extras/utils.py index 23892e09819..7b9356efb67 100644 --- a/netbox/extras/utils.py +++ b/netbox/extras/utils.py @@ -67,6 +67,10 @@ def register_features(model, features): 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): """ diff --git a/netbox/netbox/registry.py b/netbox/netbox/registry.py index 21a86900119..151eb2f6b4e 100644 --- a/netbox/netbox/registry.py +++ b/netbox/netbox/registry.py @@ -25,6 +25,7 @@ def __delitem__(self, key): 'data_backends': dict(), 'denormalized_fields': collections.defaultdict(list), 'model_features': dict(), + 'models': collections.defaultdict(set), 'plugins': dict(), 'search': dict(), 'views': collections.defaultdict(dict), diff --git a/netbox/users/models.py b/netbox/users/models.py index 1f87727045a..2a345653dcf 100644 --- a/netbox/users/models.py +++ b/netbox/users/models.py @@ -99,6 +99,8 @@ class UserConfig(models.Model): default=dict ) + _netbox_private = True + class Meta: ordering = ['user'] verbose_name = _('user preferences')