Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closes #13427: Public model registration #14152

Merged
merged 3 commits into from
Nov 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/development/application-registry.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
jeremystretch marked this conversation as resolved.
Show resolved Hide resolved

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.
Expand Down
2 changes: 2 additions & 0 deletions netbox/account/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
29 changes: 29 additions & 0 deletions netbox/core/migrations/0008_contenttype_proxy.py
Original file line number Diff line number Diff line change
@@ -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 change: 1 addition & 0 deletions netbox/core/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .contenttypes import *
from .data import *
from .files import *
from .jobs import *
32 changes: 32 additions & 0 deletions netbox/core/models/contenttypes.py
Original file line number Diff line number Diff line change
@@ -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 changes: 2 additions & 0 deletions netbox/core/models/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,8 @@ class AutoSyncRecord(models.Model):
fk_field='object_id'
)

_netbox_private = True

class Meta:
constraints = (
models.UniqueConstraint(
Expand Down
1 change: 1 addition & 0 deletions netbox/core/models/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ class ManagedFile(SyncedDataMixin, models.Model):
)

objects = RestrictedQuerySet.as_manager()
_netbox_private = True

class Meta:
ordering = ('file_root', 'file_path')
Expand Down
2 changes: 2 additions & 0 deletions netbox/dcim/models/cables.py
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,8 @@ class CablePath(models.Model):
)
_nodes = PathField()

_netbox_private = True

class Meta:
verbose_name = _('cable path')
verbose_name_plural = _('cable paths')
Expand Down
9 changes: 2 additions & 7 deletions netbox/extras/dashboard/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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')
jeremystretch marked this conversation as resolved.
Show resolved Hide resolved
]


Expand Down
5 changes: 2 additions & 3 deletions netbox/extras/forms/bulk_import.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)")
)
Expand Down
5 changes: 2 additions & 3 deletions netbox/extras/forms/filtersets.py
Original file line number Diff line number Diff line change
@@ -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 *
Expand Down Expand Up @@ -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(
Expand Down
7 changes: 2 additions & 5 deletions netbox/extras/forms/model_forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 *
Expand Down Expand Up @@ -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)")
)
Expand Down
10 changes: 6 additions & 4 deletions netbox/extras/models/configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions netbox/extras/models/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
2 changes: 2 additions & 0 deletions netbox/extras/models/tags.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
4 changes: 4 additions & 0 deletions netbox/extras/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand Down
1 change: 1 addition & 0 deletions netbox/netbox/registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
2 changes: 2 additions & 0 deletions netbox/users/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ class UserConfig(models.Model):
default=dict
)

_netbox_private = True

class Meta:
ordering = ['user']
verbose_name = _('user preferences')
Expand Down