Skip to content
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
79 changes: 79 additions & 0 deletions netbox_custom_objects/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
import warnings
import sys

from django.core.exceptions import AppRegistryNotReady
from netbox.plugins import PluginConfig


def is_running_migration():
"""
Check if the code is currently running during a Django migration.
"""
# Check if 'makemigrations' or 'migrate' command is in sys.argv
if any(cmd in sys.argv for cmd in ['makemigrations', 'migrate']):
return True

return False


# Plugin Configuration
class CustomObjectsPluginConfig(PluginConfig):
name = "netbox_custom_objects"
Expand All @@ -13,5 +28,69 @@ class CustomObjectsPluginConfig(PluginConfig):
required_settings = []
template_extensions = "template_content.template_extensions"

def get_model(self, model_name, require_ready=True):
try:
# if the model is already loaded, return it
return super().get_model(model_name, require_ready)
except LookupError:
try:
self.apps.check_apps_ready()
except AppRegistryNotReady:
raise

# only do database calls if we are sure the app is ready to avoid
# Django warnings
if "table" not in model_name.lower() or "model" not in model_name.lower():
raise LookupError(
"App '%s' doesn't have a '%s' model." % (self.label, model_name)
)

from .models import CustomObjectType

custom_object_type_id = int(
model_name.replace("table", "").replace("model", "")
)

try:
obj = CustomObjectType.objects.get(pk=custom_object_type_id)
except CustomObjectType.DoesNotExist:
raise LookupError(
"App '%s' doesn't have a '%s' model." % (self.label, model_name)
)

return obj.get_model()

def get_models(self, include_auto_created=False, include_swapped=False):
"""Return all models for this plugin, including custom object type models."""
# Get the regular Django models first
for model in super().get_models(include_auto_created, include_swapped):
yield model

# Skip custom object type model loading if running during migration
if is_running_migration():
return

# Suppress warnings about database calls during model loading
with warnings.catch_warnings():
warnings.filterwarnings(
"ignore", category=RuntimeWarning, message=".*database.*"
)
warnings.filterwarnings(
"ignore", category=UserWarning, message=".*database.*"
)

# Add custom object type models
from .models import CustomObjectType

# Only load models that are already cached to avoid creating all models at startup
# This prevents the "two TaggableManagers with same through model" error
custom_object_types = CustomObjectType.objects.all()
for custom_type in custom_object_types:
# Only yield already cached models during discovery
if CustomObjectType.is_model_cached(custom_type.id):
model = CustomObjectType.get_cached_model(custom_type.id)
if model:
yield model


config = CustomObjectsPluginConfig
43 changes: 34 additions & 9 deletions netbox_custom_objects/api/serializers.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import sys

from core.models import ObjectType
from django.contrib.contenttypes.models import ContentType
from extras.choices import CustomFieldTypeChoices
from netbox.api.serializers import NetBoxModelSerializer
Expand All @@ -6,7 +9,8 @@
from rest_framework.reverse import reverse

from netbox_custom_objects import field_types
from netbox_custom_objects.models import CustomObject, CustomObjectType, CustomObjectTypeField
from netbox_custom_objects.models import (CustomObject, CustomObjectType,
CustomObjectTypeField)

__all__ = (
"CustomObjectTypeSerializer",
Expand Down Expand Up @@ -59,10 +63,10 @@ def validate(self, attrs):
CustomFieldTypeChoices.TYPE_MULTIOBJECT,
]:
try:
attrs["related_object_type"] = ContentType.objects.get(
attrs["related_object_type"] = ObjectType.objects.get(
app_label=app_label, model=model
)
except ContentType.DoesNotExist:
except ObjectType.DoesNotExist:
raise ValidationError(
"Must provide valid app_label and model for object field type."
)
Expand Down Expand Up @@ -197,17 +201,24 @@ def get_field_data(self, obj):

def get_serializer_class(model):
model_fields = model.custom_object_type.fields.all()

# Create field list including all necessary fields
base_fields = ["id", "url", "display", "created", "last_updated", "tags"]
custom_field_names = [field.name for field in model_fields]
all_fields = base_fields + custom_field_names

meta = type(
"Meta",
(),
{
"model": model,
"fields": "__all__",
"fields": all_fields,
"brief_fields": ("id", "url", "display"),
},
)

def get_url(self, obj):
# Unsaved objects will not yet have a valid URL.
"""Generate the API URL for this object"""
if hasattr(obj, "pk") and obj.pk in (None, ""):
return None

Expand All @@ -221,24 +232,38 @@ def get_url(self, obj):
format = self.context.get("format")
return reverse(view_name, kwargs=kwargs, request=request, format=format)

def get_display(self, obj):
"""Get display representation of the object"""
return str(obj)

# Create basic attributes for the serializer
attrs = {
"Meta": meta,
"__module__": "database.serializers",
"__module__": "netbox_custom_objects.api.serializers",
"url": serializers.SerializerMethodField(),
"get_url": get_url,
"display": serializers.SerializerMethodField(),
"get_display": get_display,
}

for field in model_fields:
field_type = field_types.FIELD_TYPE_CLASS[field.type]()
try:
attrs[field.name] = field_type.get_serializer_field(field)
except NotImplementedError:
print(f"serializer: {field.name} field is not implemented; using a default serializer field")
print(
f"serializer: {field.name} field is not implemented; using a default serializer field"
)

serializer_name = f"{model._meta.object_name}Serializer"
serializer = type(
f"{model._meta.object_name}Serializer",
(serializers.ModelSerializer,),
serializer_name,
(NetBoxModelSerializer,),
attrs,
)

# Register the serializer in the current module so NetBox can find it
current_module = sys.modules[__name__]
setattr(current_module, serializer_name, serializer)

return serializer
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 5.2.2 on 2025-07-30 16:37

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("netbox_custom_objects", "0004_customobjecttype_comments"),
]

operations = [
migrations.AlterField(
model_name="customobjecttype",
name="description",
field=models.CharField(blank=True, max_length=200),
),
]
Loading
Loading