1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Closes #13427: Public model registration (#14152)

* Initial work on #13427

* Clarify documentation

* Reference public models registry when populating models for ConfigTemplate context
This commit is contained in:
Jeremy Stretch
2023-11-03 10:32:59 -04:00
committed by GitHub
parent f6338abf14
commit 8dcbd66de6
18 changed files with 98 additions and 22 deletions

View File

@ -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). 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` ### `plugins`
This store maintains all registered items for plugins, such as navigation menus, template extensions, etc. This store maintains all registered items for plugins, such as navigation menus, template extensions, etc.

View File

@ -7,6 +7,8 @@ class UserToken(Token):
""" """
Proxy model for users to manage their own API tokens. Proxy model for users to manage their own API tokens.
""" """
_netbox_private = True
class Meta: class Meta:
proxy = True proxy = True
verbose_name = 'token' verbose_name = 'token'

View File

@ -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()),
],
),
]

View File

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

View File

@ -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

View File

@ -378,6 +378,8 @@ class AutoSyncRecord(models.Model):
fk_field='object_id' fk_field='object_id'
) )
_netbox_private = True
class Meta: class Meta:
constraints = ( constraints = (
models.UniqueConstraint( models.UniqueConstraint(

View File

@ -44,6 +44,7 @@ class ManagedFile(SyncedDataMixin, models.Model):
) )
objects = RestrictedQuerySet.as_manager() objects = RestrictedQuerySet.as_manager()
_netbox_private = True
class Meta: class Meta:
ordering = ('file_root', 'file_path') ordering = ('file_root', 'file_path')

View File

@ -431,6 +431,8 @@ class CablePath(models.Model):
) )
_nodes = PathField() _nodes = PathField()
_netbox_private = True
class Meta: class Meta:
verbose_name = _('cable path') verbose_name = _('cable path')
verbose_name_plural = _('cable paths') verbose_name_plural = _('cable paths')

View File

@ -7,15 +7,13 @@ import feedparser
import requests import requests
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.core.cache import cache from django.core.cache import cache
from django.db.models import Q
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import NoReverseMatch, resolve, reverse from django.urls import NoReverseMatch, resolve, reverse
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from core.models import ContentType
from extras.choices import BookmarkOrderingChoices from extras.choices import BookmarkOrderingChoices
from extras.utils import FeatureQuery
from utilities.choices import ButtonColorChoices from utilities.choices import ButtonColorChoices
from utilities.forms import BootstrapMixin from utilities.forms import BootstrapMixin
from utilities.permissions import get_permission_for_model from utilities.permissions import get_permission_for_model
@ -37,10 +35,7 @@ __all__ = (
def get_content_type_labels(): def get_content_type_labels():
return [ return [
(content_type_identifier(ct), content_type_name(ct)) (content_type_identifier(ct), content_type_name(ct))
for ct in ContentType.objects.filter( for ct in ContentType.objects.public().order_by('app_label', 'model')
FeatureQuery('export_templates').get_query() | Q(app_label='extras', model='objectchange') |
Q(app_label='extras', model='configcontext')
).order_by('app_label', 'model')
] ]

View File

@ -1,9 +1,9 @@
from django import forms from django import forms
from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.forms import SimpleArrayField from django.contrib.postgres.forms import SimpleArrayField
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from core.models import ContentType
from extras.choices import * from extras.choices import *
from extras.models import * from extras.models import *
from extras.utils import FeatureQuery from extras.utils import FeatureQuery
@ -40,8 +40,7 @@ class CustomFieldImportForm(CSVModelForm):
) )
object_type = CSVContentTypeField( object_type = CSVContentTypeField(
label=_('Object type'), label=_('Object type'),
queryset=ContentType.objects.all(), queryset=ContentType.objects.public(),
limit_choices_to=FeatureQuery('custom_fields'),
required=False, required=False,
help_text=_("Object type (for object or multi-object fields)") help_text=_("Object type (for object or multi-object fields)")
) )

View File

@ -1,9 +1,8 @@
from django import forms from django import forms
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import gettext_lazy as _ 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 dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from extras.choices import * from extras.choices import *
from extras.models import * from extras.models import *
@ -196,7 +195,7 @@ class SavedFilterFilterForm(SavedFiltersMixin, FilterForm):
) )
content_types = ContentTypeMultipleChoiceField( content_types = ContentTypeMultipleChoiceField(
label=_('Content types'), label=_('Content types'),
queryset=ContentType.objects.filter(FeatureQuery('export_templates').get_query()), queryset=ContentType.objects.public(),
required=False required=False
) )
enabled = forms.NullBooleanField( enabled = forms.NullBooleanField(

View File

@ -2,12 +2,11 @@ import json
from django import forms from django import forms
from django.conf import settings 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.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from core.forms.mixins import SyncedDataMixin from core.forms.mixins import SyncedDataMixin
from core.models import ContentType
from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup from dcim.models import DeviceRole, DeviceType, Location, Platform, Region, Site, SiteGroup
from extras.choices import * from extras.choices import *
from extras.models import * from extras.models import *
@ -49,9 +48,7 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
) )
object_type = ContentTypeChoiceField( object_type = ContentTypeChoiceField(
label=_('Object type'), label=_('Object type'),
queryset=ContentType.objects.all(), queryset=ContentType.objects.public(),
# 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']),
required=False, required=False,
help_text=_("Type of the related object (for object/multi-object fields only)") help_text=_("Type of the related object (for object/multi-object fields only)")
) )

View File

@ -260,12 +260,14 @@ class ConfigTemplate(SyncedDataMixin, ExportTemplatesMixin, TagsMixin, ChangeLog
_context = dict() _context = dict()
# Populate the default template context with NetBox model classes, namespaced by app # 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['models'].items():
for app, model_names in registry['model_features']['custom_fields'].items():
_context.setdefault(app, {}) _context.setdefault(app, {})
for model_name in model_names: for model_name in model_names:
try:
model = apps.get_registered_model(app, model_name) model = apps.get_registered_model(app, model_name)
_context[app][model.__name__] = model _context[app][model.__name__] = model
except LookupError:
pass
# Add the provided context data, if any # Add the provided context data, if any
if context is not None: if context is not None:

View File

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

View File

@ -75,6 +75,8 @@ class TaggedItem(GenericTaggedItemBase):
on_delete=models.CASCADE on_delete=models.CASCADE
) )
_netbox_private = True
class Meta: class Meta:
indexes = [models.Index(fields=["content_type", "object_id"])] indexes = [models.Index(fields=["content_type", "object_id"])]
verbose_name = _('tagged item') verbose_name = _('tagged item')

View File

@ -67,6 +67,10 @@ def register_features(model, features):
f"{feature} is not a valid model feature! Valid keys are: {registry['model_features'].keys()}" 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): def is_script(obj):
""" """

View File

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

View File

@ -99,6 +99,8 @@ class UserConfig(models.Model):
default=dict default=dict
) )
_netbox_private = True
class Meta: class Meta:
ordering = ['user'] ordering = ['user']
verbose_name = _('user preferences') verbose_name = _('user preferences')