Просмотр исходного кода

Closes #13427: Public model registration (#14152)

* Initial work on #13427

* Clarify documentation

* Reference public models registry when populating models for ConfigTemplate context
Jeremy Stretch 2 лет назад
Родитель
Сommit
8dcbd66de6

+ 4 - 0
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.

+ 2 - 0
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'

+ 29 - 0
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()),
+            ],
+        ),
+    ]

+ 1 - 0
netbox/core/models/__init__.py

@@ -1,3 +1,4 @@
+from .contenttypes import *
 from .data import *
 from .files import *
 from .jobs import *

+ 32 - 0
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

+ 2 - 0
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(

+ 1 - 0
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')

+ 2 - 0
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')

+ 2 - 7
netbox/extras/dashboard/widgets.py

@@ -7,15 +7,13 @@ import feedparser
 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 @@ __all__ = (
 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')
     ]
 
 

+ 2 - 3
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)")
     )

+ 2 - 3
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(

+ 2 - 5
netbox/extras/forms/model_forms.py

@@ -2,12 +2,11 @@ import json
 
 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)")
     )

+ 6 - 4
netbox/extras/models/configs.py

@@ -260,12 +260,14 @@ class ConfigTemplate(SyncedDataMixin, ExportTemplatesMixin, TagsMixin, ChangeLog
         _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:

+ 2 - 0
netbox/extras/models/search.py

@@ -49,6 +49,8 @@ class CachedValue(models.Model):
         default=1000
     )
 
+    _netbox_private = True
+
     class Meta:
         ordering = ('weight', 'object_type', 'value', 'object_id')
         verbose_name = _('cached value')

+ 2 - 0
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')

+ 4 - 0
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):
     """

+ 1 - 0
netbox/netbox/registry.py

@@ -25,6 +25,7 @@ registry = Registry({
     'data_backends': dict(),
     'denormalized_fields': collections.defaultdict(list),
     'model_features': dict(),
+    'models': collections.defaultdict(set),
     'plugins': dict(),
     'search': dict(),
     'views': collections.defaultdict(dict),

+ 2 - 0
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')