Skip to content

Commit 27fa949

Browse files
authored
NPL-432 Add changes for ObjectType (#125)
1 parent ba0a566 commit 27fa949

File tree

6 files changed

+245
-100
lines changed

6 files changed

+245
-100
lines changed

netbox_custom_objects/__init__.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,21 @@
1+
import warnings
2+
import sys
3+
4+
from django.core.exceptions import AppRegistryNotReady
15
from netbox.plugins import PluginConfig
26

37

8+
def is_running_migration():
9+
"""
10+
Check if the code is currently running during a Django migration.
11+
"""
12+
# Check if 'makemigrations' or 'migrate' command is in sys.argv
13+
if any(cmd in sys.argv for cmd in ['makemigrations', 'migrate']):
14+
return True
15+
16+
return False
17+
18+
419
# Plugin Configuration
520
class CustomObjectsPluginConfig(PluginConfig):
621
name = "netbox_custom_objects"
@@ -13,5 +28,69 @@ class CustomObjectsPluginConfig(PluginConfig):
1328
required_settings = []
1429
template_extensions = "template_content.template_extensions"
1530

31+
def get_model(self, model_name, require_ready=True):
32+
try:
33+
# if the model is already loaded, return it
34+
return super().get_model(model_name, require_ready)
35+
except LookupError:
36+
try:
37+
self.apps.check_apps_ready()
38+
except AppRegistryNotReady:
39+
raise
40+
41+
# only do database calls if we are sure the app is ready to avoid
42+
# Django warnings
43+
if "table" not in model_name.lower() or "model" not in model_name.lower():
44+
raise LookupError(
45+
"App '%s' doesn't have a '%s' model." % (self.label, model_name)
46+
)
47+
48+
from .models import CustomObjectType
49+
50+
custom_object_type_id = int(
51+
model_name.replace("table", "").replace("model", "")
52+
)
53+
54+
try:
55+
obj = CustomObjectType.objects.get(pk=custom_object_type_id)
56+
except CustomObjectType.DoesNotExist:
57+
raise LookupError(
58+
"App '%s' doesn't have a '%s' model." % (self.label, model_name)
59+
)
60+
61+
return obj.get_model()
62+
63+
def get_models(self, include_auto_created=False, include_swapped=False):
64+
"""Return all models for this plugin, including custom object type models."""
65+
# Get the regular Django models first
66+
for model in super().get_models(include_auto_created, include_swapped):
67+
yield model
68+
69+
# Skip custom object type model loading if running during migration
70+
if is_running_migration():
71+
return
72+
73+
# Suppress warnings about database calls during model loading
74+
with warnings.catch_warnings():
75+
warnings.filterwarnings(
76+
"ignore", category=RuntimeWarning, message=".*database.*"
77+
)
78+
warnings.filterwarnings(
79+
"ignore", category=UserWarning, message=".*database.*"
80+
)
81+
82+
# Add custom object type models
83+
from .models import CustomObjectType
84+
85+
# Only load models that are already cached to avoid creating all models at startup
86+
# This prevents the "two TaggableManagers with same through model" error
87+
custom_object_types = CustomObjectType.objects.all()
88+
for custom_type in custom_object_types:
89+
# Only yield already cached models during discovery
90+
if CustomObjectType.is_model_cached(custom_type.id):
91+
model = CustomObjectType.get_cached_model(custom_type.id)
92+
if model:
93+
yield model
94+
1695

1796
config = CustomObjectsPluginConfig

netbox_custom_objects/api/serializers.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
import sys
2+
3+
from core.models import ObjectType
14
from django.contrib.contenttypes.models import ContentType
25
from extras.choices import CustomFieldTypeChoices
36
from netbox.api.serializers import NetBoxModelSerializer
@@ -6,7 +9,8 @@
69
from rest_framework.reverse import reverse
710

811
from netbox_custom_objects import field_types
9-
from netbox_custom_objects.models import CustomObject, CustomObjectType, CustomObjectTypeField
12+
from netbox_custom_objects.models import (CustomObject, CustomObjectType,
13+
CustomObjectTypeField)
1014

1115
__all__ = (
1216
"CustomObjectTypeSerializer",
@@ -59,10 +63,10 @@ def validate(self, attrs):
5963
CustomFieldTypeChoices.TYPE_MULTIOBJECT,
6064
]:
6165
try:
62-
attrs["related_object_type"] = ContentType.objects.get(
66+
attrs["related_object_type"] = ObjectType.objects.get(
6367
app_label=app_label, model=model
6468
)
65-
except ContentType.DoesNotExist:
69+
except ObjectType.DoesNotExist:
6670
raise ValidationError(
6771
"Must provide valid app_label and model for object field type."
6872
)
@@ -197,17 +201,24 @@ def get_field_data(self, obj):
197201

198202
def get_serializer_class(model):
199203
model_fields = model.custom_object_type.fields.all()
204+
205+
# Create field list including all necessary fields
206+
base_fields = ["id", "url", "display", "created", "last_updated", "tags"]
207+
custom_field_names = [field.name for field in model_fields]
208+
all_fields = base_fields + custom_field_names
209+
200210
meta = type(
201211
"Meta",
202212
(),
203213
{
204214
"model": model,
205-
"fields": "__all__",
215+
"fields": all_fields,
216+
"brief_fields": ("id", "url", "display"),
206217
},
207218
)
208219

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

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

235+
def get_display(self, obj):
236+
"""Get display representation of the object"""
237+
return str(obj)
238+
239+
# Create basic attributes for the serializer
224240
attrs = {
225241
"Meta": meta,
226-
"__module__": "database.serializers",
242+
"__module__": "netbox_custom_objects.api.serializers",
227243
"url": serializers.SerializerMethodField(),
228244
"get_url": get_url,
245+
"display": serializers.SerializerMethodField(),
246+
"get_display": get_display,
229247
}
230248

231249
for field in model_fields:
232250
field_type = field_types.FIELD_TYPE_CLASS[field.type]()
233251
try:
234252
attrs[field.name] = field_type.get_serializer_field(field)
235253
except NotImplementedError:
236-
print(f"serializer: {field.name} field is not implemented; using a default serializer field")
254+
print(
255+
f"serializer: {field.name} field is not implemented; using a default serializer field"
256+
)
237257

258+
serializer_name = f"{model._meta.object_name}Serializer"
238259
serializer = type(
239-
f"{model._meta.object_name}Serializer",
240-
(serializers.ModelSerializer,),
260+
serializer_name,
261+
(NetBoxModelSerializer,),
241262
attrs,
242263
)
243264

265+
# Register the serializer in the current module so NetBox can find it
266+
current_module = sys.modules[__name__]
267+
setattr(current_module, serializer_name, serializer)
268+
244269
return serializer
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# Generated by Django 5.2.2 on 2025-07-30 16:37
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("netbox_custom_objects", "0004_customobjecttype_comments"),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name="customobjecttype",
15+
name="description",
16+
field=models.CharField(blank=True, max_length=200),
17+
),
18+
]

0 commit comments

Comments
 (0)