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
19 changes: 16 additions & 3 deletions netbox_custom_objects/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1639,15 +1639,28 @@ def register_custom_object_search_index(self, model):
| {f.name for f in model._meta.local_many_to_many}
)
fields = []
for field in self.fields.filter(search_weight__gt=0):
display_attrs = []
for field in self.fields.all():
if field.name not in present:
continue
fields.append((field.name, field.search_weight))
if field.search_weight > 0:
fields.append((field.name, field.search_weight))
# Context fields surface as supplementary "attributes" on global search
# results (issue #655), via the same generic CachedValue.display_attrs
# mechanism every other NetBox model's SearchIndex uses. That mechanism
# renders a field with plain getattr() (falling back to
# get_<field>_display() for Django choices=), so it has no way to render
# a MultiObject field's RelatedManager as anything meaningful -- exclude
# those. Polymorphic and coordinates fields need no such exclusion: they
# have no real backing column under the field's own name, so `present`
# already filters them out above.
if field.context and field.type != CustomFieldTypeChoices.TYPE_MULTIOBJECT:
display_attrs.append(field.name)

attrs = {
"model": model,
"fields": tuple(fields),
"display_attrs": tuple(),
"display_attrs": tuple(display_attrs),
}
search_index = type(
f"{self.name}SearchIndex",
Expand Down
68 changes: 68 additions & 0 deletions netbox_custom_objects/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,74 @@ def test_register_search_index_skips_object_field_absent_from_stub_model(self):
# Must not raise FieldDoesNotExist, RecursionError, or any other exception.
cot.register_custom_object_search_index(stub_model)

def test_register_search_index_includes_context_fields_in_display_attrs(self):
"""Fields marked context=True surface as display_attrs (issue #655), so
NetBox's global search shows them as supplementary "attributes" alongside
each result, the same as any other model's SearchIndex.display_attrs."""
cot = self.create_custom_object_type(name="ContextSearchTest", slug="context-search-test")
self.create_custom_object_type_field(
cot, name="name", label="Name", type="text", primary=True, search_weight=1000,
)
self.create_custom_object_type_field(
cot, name="status", label="Status", type="text", context=True,
)
cot.clear_model_cache(cot.id)
model = cot.get_model()
cot.register_custom_object_search_index(model)

label = f"{APP_LABEL}.{cot.get_table_model_name(cot.id).lower()}"
search_index = registry["search"][label]
self.assertIn("status", search_index.display_attrs)
self.assertNotIn("name", search_index.display_attrs)

def test_register_search_index_includes_object_context_fields(self):
"""A context field of type object (a single real FK, unlike multiobject's
M2M) IS included in display_attrs -- getattr() on it returns the related
instance directly, not a manager, so NetBox's generic renderer handles it
correctly. Confirms the TYPE_MULTIOBJECT exclusion is scoped to just that
one type, not object fields generally."""
cot = self.create_custom_object_type(name="ContextObjectTest", slug="context-object-test")
self.create_custom_object_type_field(
cot, name="name", label="Name", type="text", primary=True, search_weight=1000,
)
self.create_custom_object_type_field(
cot, name="related_site", label="Related Site", type="object",
related_object_type=self.get_site_object_type(), context=True,
)
cot.clear_model_cache(cot.id)
model = cot.get_model()
cot.register_custom_object_search_index(model)

label = f"{APP_LABEL}.{cot.get_table_model_name(cot.id).lower()}"
search_index = registry["search"][label]
self.assertIn("related_site", search_index.display_attrs)

def test_register_search_index_excludes_multiobject_context_fields(self):
"""A context field of type multiobject must not reach display_attrs.

NetBox core's CachedValue.display_attrs (the only consumer of this
attribute) renders each entry via plain getattr() on the instance --
for a real ManyToManyField that returns the RelatedManager itself, not
its contents, so the search UI would show a broken object repr instead
of the related objects. Excluding multiobject context fields here avoids
surfacing that.
"""
cot = self.create_custom_object_type(name="ContextM2MTest", slug="context-m2m-test")
self.create_custom_object_type_field(
cot, name="name", label="Name", type="text", primary=True, search_weight=1000,
)
self.create_custom_object_type_field(
cot, name="related_sites", label="Related Sites", type="multiobject",
related_object_type=self.get_site_object_type(), context=True,
)
cot.clear_model_cache(cot.id)
model = cot.get_model()
cot.register_custom_object_search_index(model)

label = f"{APP_LABEL}.{cot.get_table_model_name(cot.id).lower()}"
search_index = registry["search"][label]
self.assertNotIn("related_sites", search_index.display_attrs)

def test_skipped_object_field_with_stale_content_type_logs_warning(self):
"""When get_model_field raises NotImplementedError for an object field whose
related_object_type_id is non-null (stale/deleted ContentType), a WARNING must
Expand Down