diff --git a/netbox/dcim/search.py b/netbox/dcim/search.py index f70c729f4..c314ac459 100644 --- a/netbox/dcim/search.py +++ b/netbox/dcim/search.py @@ -44,6 +44,7 @@ class DeviceIndex(SearchIndex): ('description', 500), ('comments', 5000), ) + display_attrs = ('site', 'location', 'rack', 'device_type', 'description') @register_search @@ -282,6 +283,7 @@ class SiteIndex(SearchIndex): ('shipping_address', 2000), ('comments', 5000), ) + display_attrs = ('region', 'group', 'status', 'description') @register_search diff --git a/netbox/extras/models/search.py b/netbox/extras/models/search.py index debe4c648..5444d64bd 100644 --- a/netbox/extras/models/search.py +++ b/netbox/extras/models/search.py @@ -4,7 +4,9 @@ from django.contrib.contenttypes.models import ContentType from django.db import models from django.utils.translation import gettext_lazy as _ +from netbox.registry import registry from utilities.fields import RestrictedGenericForeignKey +from utilities.utils import content_type_identifier from ..fields import CachedValueField __all__ = ( @@ -56,3 +58,16 @@ class CachedValue(models.Model): def __str__(self): return f'{self.object_type} {self.object_id}: {self.field}={self.value}' + + @property + def display_attrs(self): + indexer = registry['search'].get(content_type_identifier(self.object_type)) + attrs = {} + for attr in getattr(indexer, 'display_attrs', []): + name = self.object._meta.get_field(attr).verbose_name + if value := getattr(self.object, attr): + if display_func := getattr(self.object, f'get_{attr}_display', None): + attrs[name] = display_func() + else: + attrs[name] = value + return attrs diff --git a/netbox/netbox/search/__init__.py b/netbox/netbox/search/__init__.py index 6d53e9a97..590188f21 100644 --- a/netbox/netbox/search/__init__.py +++ b/netbox/netbox/search/__init__.py @@ -33,10 +33,12 @@ class SearchIndex: category: The label of the group under which this indexer is categorized (for form field display). If none, the name of the model's app will be used. fields: An iterable of two-tuples defining the model fields to be indexed and the weight associated with each. + display_attrs: An iterable of additional object attributes to include when displaying search results. """ model = None category = None fields = () + display_attrs = () @staticmethod def get_field_type(instance, field_name): diff --git a/netbox/netbox/search/backends.py b/netbox/netbox/search/backends.py index 4487b6bb8..0ae3201cb 100644 --- a/netbox/netbox/search/backends.py +++ b/netbox/netbox/search/backends.py @@ -3,7 +3,8 @@ from collections import defaultdict from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.core.exceptions import ImproperlyConfigured -from django.db.models import F, Window, Q +from django.db.models import F, Window, Q, prefetch_related_objects +from django.db.models.fields.related import ForeignKey from django.db.models.functions import window from django.db.models.signals import post_delete, post_save from django.utils.module_loading import import_string @@ -13,7 +14,7 @@ from netaddr.core import AddrFormatError from extras.models import CachedValue, CustomField from netbox.registry import registry from utilities.querysets import RestrictedPrefetch -from utilities.utils import title +from utilities.utils import content_type_identifier, title from . import FieldTypes, LookupTypes, get_indexer DEFAULT_LOOKUP_TYPE = LookupTypes.PARTIAL @@ -129,6 +130,10 @@ class CachedValueSearchBackend(SearchBackend): ) )[:MAX_RESULTS] + # Find the ContentTypes for all objects present in the search results + content_type_ids = set(queryset.values_list('object_type', flat=True)) + content_types = ContentType.objects.filter(pk__in=content_type_ids) + # Construct a Prefetch to pre-fetch only those related objects for which the # user has permission to view. if user: @@ -144,12 +149,31 @@ class CachedValueSearchBackend(SearchBackend): params ) + # Prefetch display attributes + for ct in content_types: + model = ct.model_class() + indexer = registry['search'].get(content_type_identifier(ct)) + display_attrs = getattr(indexer, 'display_attrs', None) + if not display_attrs: + continue + + prefetch_fields = [] + for attr in display_attrs: + field = model._meta.get_field(attr) + if type(field) is ForeignKey: + prefetch_fields.append(f'object__{attr}') + + if prefetch_fields: + objects = [r for r in results if r.object_type == ct] + prefetch_related_objects(objects, *prefetch_fields) + # Omit any results pertaining to an object the user does not have permission to view ret = [] for r in results: if r.object is not None: r.name = str(r.object) ret.append(r) + return ret def cache(self, instances, indexer=None, remove_existing=True): diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index 52ff69aa9..fe11bff34 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -15,6 +15,7 @@ from extras.choices import CustomFieldVisibilityChoices from netbox.tables import columns from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.utils import get_viewname, highlight_string, title +from .template_code import * __all__ = ( 'BaseTable', @@ -236,6 +237,10 @@ class SearchTable(tables.Table): value = tables.Column( verbose_name=_('Value'), ) + attrs = columns.TemplateColumn( + template_code=SEARCH_RESULT_ATTRS, + verbose_name=_('Attributes') + ) trim_length = 30 diff --git a/netbox/netbox/tables/template_code.py b/netbox/netbox/tables/template_code.py new file mode 100644 index 000000000..0c3c2307e --- /dev/null +++ b/netbox/netbox/tables/template_code.py @@ -0,0 +1,5 @@ +SEARCH_RESULT_ATTRS = """ +{% for name, value in record.display_attrs.items %} + {{ name|bettertitle }}: {{ value }} +{% endfor %} +"""