Skip to content

Commit 919c254

Browse files
committed
Merge branch 'feature' into NPL-379-import
2 parents 942ec5a + 2b9f2b7 commit 919c254

File tree

11 files changed

+341
-98
lines changed

11 files changed

+341
-98
lines changed

netbox_custom_objects/__init__.py

Lines changed: 63 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,36 @@ def is_running_migration():
1818
return False
1919

2020

21+
def is_in_clear_cache():
22+
"""
23+
Check if the code is currently being called from Django's clear_cache() method.
24+
25+
TODO: This is fairly ugly, but in models.CustomObjectType.get_model() we call
26+
meta = type() which calls clear_cache on the model which causes a call to
27+
get_models() which in-turn calls get_model and therefore recurses.
28+
29+
This catches the specific case of a recursive call to get_models() from
30+
clear_cache() which is the only case we care about, so should be relatively
31+
safe. An alternative should be found for this.
32+
"""
33+
import inspect
34+
35+
frame = inspect.currentframe()
36+
try:
37+
# Walk up the call stack to see if we're being called from clear_cache
38+
while frame:
39+
if (
40+
frame.f_code.co_name == "clear_cache"
41+
and "django/apps/registry.py" in frame.f_code.co_filename
42+
):
43+
return True
44+
frame = frame.f_back
45+
return False
46+
finally:
47+
# Clean up the frame reference
48+
del frame
49+
50+
2151
def check_custom_object_type_table_exists():
2252
"""
2353
Check if the CustomObjectType table exists in the database.
@@ -49,6 +79,7 @@ class CustomObjectsPluginConfig(PluginConfig):
4979
default_settings = {}
5080
required_settings = []
5181
template_extensions = "template_content.template_extensions"
82+
_in_get_models = False # Recursion guard
5283

5384
def get_model(self, model_name, require_ready=True):
5485
try:
@@ -84,33 +115,45 @@ def get_model(self, model_name, require_ready=True):
84115

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

91-
# Suppress warnings about database calls during model loading
92-
with warnings.catch_warnings():
93-
warnings.filterwarnings(
94-
"ignore", category=RuntimeWarning, message=".*database.*"
95-
)
96-
warnings.filterwarnings(
97-
"ignore", category=UserWarning, message=".*database.*"
98-
)
123+
# Prevent recursion
124+
if self._in_get_models and is_in_clear_cache():
125+
# Skip dynamic model creation if we're in a recursive get_models call
126+
return
99127

100-
# Skip custom object type model loading if running during migration
101-
if is_running_migration() or not check_custom_object_type_table_exists():
102-
return
103-
104-
# Add custom object type models
105-
from .models import CustomObjectType
106-
107-
custom_object_types = CustomObjectType.objects.all()
108-
for custom_type in custom_object_types:
109-
# Only yield already cached models during discovery
110-
if CustomObjectType.is_model_cached(custom_type.id):
111-
model = CustomObjectType.get_cached_model(custom_type.id)
128+
self._in_get_models = True
129+
try:
130+
# Suppress warnings about database calls during model loading
131+
with warnings.catch_warnings():
132+
warnings.filterwarnings(
133+
"ignore", category=RuntimeWarning, message=".*database.*"
134+
)
135+
warnings.filterwarnings(
136+
"ignore", category=UserWarning, message=".*database.*"
137+
)
138+
139+
# Skip custom object type model loading if running during migration
140+
if (
141+
is_running_migration()
142+
or not check_custom_object_type_table_exists()
143+
):
144+
return
145+
146+
# Add custom object type models
147+
from .models import CustomObjectType
148+
149+
custom_object_types = CustomObjectType.objects.all()
150+
for custom_type in custom_object_types:
151+
model = custom_type.get_model()
112152
if model:
113153
yield model
154+
finally:
155+
# Clean up the recursion guard
156+
self._in_get_models = False
114157

115158

116159
config = CustomObjectsPluginConfig

netbox_custom_objects/api/serializers.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import logging
12
import sys
23

34
from core.models import ObjectType
@@ -12,6 +13,9 @@
1213
from netbox_custom_objects.models import (CustomObject, CustomObjectType,
1314
CustomObjectTypeField)
1415

16+
logger = logging.getLogger('netbox_custom_objects.api.serializers')
17+
18+
1519
__all__ = (
1620
"CustomObjectTypeSerializer",
1721
"CustomObjectSerializer",
@@ -251,8 +255,8 @@ def get_display(self, obj):
251255
try:
252256
attrs[field.name] = field_type.get_serializer_field(field)
253257
except NotImplementedError:
254-
print(
255-
f"serializer: {field.name} field is not implemented; using a default serializer field"
258+
logger.debug(
259+
"serializer: {} field is not implemented; using a default serializer field".format(field.name)
256260
)
257261

258262
serializer_name = f"{model._meta.object_name}Serializer"

netbox_custom_objects/api/views.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class CustomObjectViewSet(ModelViewSet):
2424
def get_view_name(self):
2525
if self.model:
2626
return self.model.custom_object_type.name
27-
return super().get_view_name()
27+
return 'Custom Object'
2828

2929
def get_serializer_class(self):
3030
return serializers.get_serializer_class(self.model)

netbox_custom_objects/choices.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,21 @@ class MappingFieldTypeChoices(ChoiceSet):
1818
(DATETIME, _("DateTime"), "blue"),
1919
(OBJECT, _("Object"), "orange"),
2020
)
21+
22+
23+
#
24+
# Search
25+
#
26+
27+
class SearchWeightChoices(ChoiceSet):
28+
WEIGHT_NONE = 0
29+
WEIGHT_LOW = 1000
30+
WEIGHT_MEDIUM = 500
31+
WEIGHT_HIGH = 100
32+
33+
CHOICES = (
34+
(WEIGHT_HIGH, _('High (100)')),
35+
(WEIGHT_MEDIUM, _('Medium (500)')),
36+
(WEIGHT_LOW, _('Low (1000)')),
37+
(WEIGHT_NONE, _('Not searchable')),
38+
)

netbox_custom_objects/forms.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from utilities.forms.rendering import FieldSet
1010
from utilities.object_types import object_type_name
1111

12+
from netbox_custom_objects.choices import SearchWeightChoices
1213
from netbox_custom_objects.constants import APP_LABEL
1314
from netbox_custom_objects.models import (CustomObjectObjectType,
1415
CustomObjectType,
@@ -104,13 +105,21 @@ class CustomObjectTypeFieldForm(CustomFieldForm):
104105
queryset=CustomObjectObjectType.objects.public(),
105106
help_text=_("Type of the related object (for object/multi-object fields only)"),
106107
)
108+
search_weight = forms.ChoiceField(
109+
choices=SearchWeightChoices,
110+
required=False,
111+
help_text=_(
112+
"Weighting for search. Lower values are considered more important. Fields with a search weight of 0 "
113+
"will be ignored."
114+
),
115+
)
107116

108117
fieldsets = (
109118
FieldSet(
110119
"custom_object_type",
111-
"primary",
112120
"name",
113121
"label",
122+
"primary",
114123
"group_name",
115124
"description",
116125
"type",
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Generated by Django 5.2.4 on 2025-08-12 17:38
2+
3+
import django.core.validators
4+
import re
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
10+
dependencies = [
11+
("netbox_custom_objects", "0005_alter_customobjecttype_description"),
12+
]
13+
14+
operations = [
15+
migrations.AlterField(
16+
model_name="customobjecttypefield",
17+
name="name",
18+
field=models.CharField(
19+
max_length=50,
20+
validators=[
21+
django.core.validators.RegexValidator(
22+
flags=re.RegexFlag["IGNORECASE"],
23+
message="Only alphanumeric characters and underscores are allowed.",
24+
regex="^[a-z0-9_]+$",
25+
),
26+
django.core.validators.RegexValidator(
27+
flags=re.RegexFlag["IGNORECASE"],
28+
inverse_match=True,
29+
message="Double underscores are not permitted in custom object field names.",
30+
regex="__",
31+
),
32+
],
33+
),
34+
),
35+
migrations.AlterField(
36+
model_name="customobjecttypefield",
37+
name="search_weight",
38+
field=models.PositiveSmallIntegerField(default=500),
39+
),
40+
]

0 commit comments

Comments
 (0)