From 6eb2983ccdd55b3a6c21abce8d5a330c17fae2d5 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Wed, 19 Oct 2022 22:20:53 -0400 Subject: [PATCH] Highlight matched portion of field value --- netbox/dcim/search.py | 1 + netbox/netbox/tables/tables.py | 20 ++++++++++++++++++++ netbox/netbox/views/__init__.py | 7 ++++++- netbox/utilities/utils.py | 22 ++++++++++++++++++++++ 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/search.py b/netbox/dcim/search.py index aad5f4019..dbbea7a5d 100644 --- a/netbox/dcim/search.py +++ b/netbox/dcim/search.py @@ -269,6 +269,7 @@ class SiteIndex(SearchIndex): ('description', 500), ('physical_address', 2000), ('shipping_address', 2000), + ('comments', 5000), ) diff --git a/netbox/netbox/tables/tables.py b/netbox/netbox/tables/tables.py index 3f0c07b68..15d302d23 100644 --- a/netbox/netbox/tables/tables.py +++ b/netbox/netbox/tables/tables.py @@ -4,6 +4,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldDoesNotExist from django.db.models.fields.related import RelatedField +from django.utils.safestring import mark_safe from django.utils.translation import gettext as _ from django_tables2.data import TableQuerysetData @@ -11,6 +12,8 @@ from extras.models import CustomField, CustomLink from extras.choices import CustomFieldVisibilityChoices from netbox.tables import columns from utilities.paginator import EnhancedPaginator, get_paginate_count +from utilities.templatetags.builtins.filters import bettertitle +from utilities.utils import highlight_string __all__ = ( 'BaseTable', @@ -206,8 +209,25 @@ class SearchTable(tables.Table): field = tables.Column() value = tables.Column() + trim_length = 30 + class Meta: attrs = { 'class': 'table table-hover object-list', } empty_text = _('No results found') + + def __init__(self, data, highlight=None, **kwargs): + self.highlight = highlight + super().__init__(data, **kwargs) + + def render_field(self, value, record): + return bettertitle(record.object._meta.get_field(value).verbose_name) + + def render_value(self, value): + if not self.highlight: + return value + + value = highlight_string(value, self.highlight, trim_pre=self.trim_length, trim_post=self.trim_length) + + return mark_safe(value) diff --git a/netbox/netbox/views/__init__.py b/netbox/netbox/views/__init__.py index 949aa8d79..74958a86e 100644 --- a/netbox/netbox/views/__init__.py +++ b/netbox/netbox/views/__init__.py @@ -23,6 +23,7 @@ from extras.models import ObjectChange from extras.tables import ObjectChangeTable from ipam.models import Aggregate, IPAddress, IPRange, Prefix, VLAN, VRF from netbox.forms import SearchForm +from netbox.search import LookupTypes from netbox.search.backends import search_backend from netbox.tables import SearchTable from tenancy.models import Tenant @@ -153,6 +154,7 @@ class SearchView(View): def get(self, request): results = [] + highlight = None # Initialize search form form = SearchForm(request.GET) if 'q' in request.GET else SearchForm() @@ -172,7 +174,10 @@ class SearchView(View): lookup=form.cleaned_data['lookup'] ) - table = SearchTable(results) + if form.cleaned_data['lookup'] != LookupTypes.EXACT: + highlight = form.cleaned_data['q'] + + table = SearchTable(results, highlight=highlight) # Paginate the table results RequestConfig(request, { diff --git a/netbox/utilities/utils.py b/netbox/utilities/utils.py index 9f587e88d..e1fbbfe84 100644 --- a/netbox/utilities/utils.py +++ b/netbox/utilities/utils.py @@ -1,6 +1,7 @@ import datetime import decimal import json +import re from decimal import Decimal from itertools import count, groupby @@ -9,6 +10,7 @@ from django.core.serializers import serialize from django.db.models import Count, OuterRef, Subquery from django.db.models.functions import Coalesce from django.http import QueryDict +from django.utils.html import escape from jinja2.sandbox import SandboxedEnvironment from mptt.models import MPTTModel @@ -472,3 +474,23 @@ def clean_html(html, schemes): attributes=ALLOWED_ATTRIBUTES, protocols=schemes ) + + +def highlight_string(value, highlight, trim_pre=None, trim_post=None, trim_placeholder='...'): + """ + Highlight a string within a string and optionally trim the pre/post portions of the original string. + """ + # Split value on highlight string + try: + pre, match, post = re.split(fr'({highlight})', value, maxsplit=1, flags=re.IGNORECASE) + except ValueError: + # Match not found + return escape(value) + + # Trim pre/post sections to length + if trim_pre and len(pre) > trim_pre: + pre = trim_placeholder + pre[-trim_pre:] + if trim_post and len(post) > trim_post: + post = post[:trim_post] + trim_placeholder + + return f'{escape(pre)}{escape(match)}{escape(post)}'