mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-24 16:26:09 -06:00
#6003: Implement global search functionality for custom field values
This commit is contained in:
parent
a237b2a10b
commit
3dfec4925d
@ -91,8 +91,8 @@ class CustomFieldSerializer(ValidatedModelSerializer):
|
|||||||
model = CustomField
|
model = CustomField
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name',
|
'id', 'url', 'display', 'content_types', 'type', 'object_type', 'data_type', 'name', 'label', 'group_name',
|
||||||
'description', 'required', 'filter_logic', 'ui_visibility', 'default', 'weight', 'validation_minimum',
|
'description', 'required', 'search_weight', 'filter_logic', 'ui_visibility', 'default', 'weight',
|
||||||
'validation_maximum', 'validation_regex', 'choices', 'created', 'last_updated',
|
'validation_minimum', 'validation_maximum', 'validation_regex', 'choices', 'created', 'last_updated',
|
||||||
]
|
]
|
||||||
|
|
||||||
def get_data_type(self, obj):
|
def get_data_type(self, obj):
|
||||||
|
@ -72,8 +72,8 @@ class CustomFieldFilterSet(BaseFilterSet):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'content_types', 'name', 'group_name', 'required', 'filter_logic', 'ui_visibility', 'weight',
|
'id', 'content_types', 'name', 'group_name', 'required', 'search_weight', 'filter_logic', 'ui_visibility',
|
||||||
'description',
|
'weight', 'description',
|
||||||
]
|
]
|
||||||
|
|
||||||
def search(self, queryset, name, value):
|
def search(self, queryset, name, value):
|
||||||
|
@ -46,8 +46,8 @@ class CustomFieldCSVForm(CSVModelForm):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fields = (
|
fields = (
|
||||||
'name', 'label', 'group_name', 'type', 'content_types', 'object_type', 'required', 'description', 'weight',
|
'name', 'label', 'group_name', 'type', 'content_types', 'object_type', 'required', 'description',
|
||||||
'filter_logic', 'default', 'choices', 'weight', 'validation_minimum', 'validation_maximum',
|
'search_weight', 'filter_logic', 'default', 'choices', 'weight', 'validation_minimum', 'validation_maximum',
|
||||||
'validation_regex', 'ui_visibility',
|
'validation_regex', 'ui_visibility',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -41,9 +41,9 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
|
|||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Custom Field', (
|
('Custom Field', (
|
||||||
'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'weight', 'required', 'description',
|
'content_types', 'name', 'label', 'group_name', 'type', 'object_type', 'required', 'description',
|
||||||
)),
|
)),
|
||||||
('Behavior', ('filter_logic', 'ui_visibility')),
|
('Behavior', ('search_weight', 'filter_logic', 'ui_visibility', 'weight')),
|
||||||
('Values', ('default', 'choices')),
|
('Values', ('default', 'choices')),
|
||||||
('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
|
('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')),
|
||||||
)
|
)
|
||||||
|
18
netbox/extras/migrations/0080_customfield_search_weight.py
Normal file
18
netbox/extras/migrations/0080_customfield_search_weight.py
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 4.1.1 on 2022-10-12 13:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('extras', '0079_search'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='customfield',
|
||||||
|
name='search_weight',
|
||||||
|
field=models.PositiveSmallIntegerField(default=1000),
|
||||||
|
),
|
||||||
|
]
|
@ -16,6 +16,7 @@ from extras.choices import *
|
|||||||
from extras.utils import FeatureQuery
|
from extras.utils import FeatureQuery
|
||||||
from netbox.models import ChangeLoggedModel
|
from netbox.models import ChangeLoggedModel
|
||||||
from netbox.models.features import CloningMixin, ExportTemplatesMixin, WebhooksMixin
|
from netbox.models.features import CloningMixin, ExportTemplatesMixin, WebhooksMixin
|
||||||
|
from netbox.search import FieldTypes
|
||||||
from utilities import filters
|
from utilities import filters
|
||||||
from utilities.forms import (
|
from utilities.forms import (
|
||||||
CSVChoiceField, CSVMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
CSVChoiceField, CSVMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
|
||||||
@ -30,6 +31,15 @@ __all__ = (
|
|||||||
'CustomFieldManager',
|
'CustomFieldManager',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SEARCH_TYPES = {
|
||||||
|
CustomFieldTypeChoices.TYPE_TEXT: FieldTypes.STRING,
|
||||||
|
CustomFieldTypeChoices.TYPE_LONGTEXT: FieldTypes.STRING,
|
||||||
|
CustomFieldTypeChoices.TYPE_INTEGER: FieldTypes.INTEGER,
|
||||||
|
CustomFieldTypeChoices.TYPE_DECIMAL: FieldTypes.FLOAT,
|
||||||
|
CustomFieldTypeChoices.TYPE_DATE: FieldTypes.STRING,
|
||||||
|
CustomFieldTypeChoices.TYPE_URL: FieldTypes.STRING,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
|
class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
|
||||||
use_in_migrations = True
|
use_in_migrations = True
|
||||||
@ -94,6 +104,11 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
|||||||
help_text='If true, this field is required when creating new objects '
|
help_text='If true, this field is required when creating new objects '
|
||||||
'or editing an existing object.'
|
'or editing an existing object.'
|
||||||
)
|
)
|
||||||
|
search_weight = models.PositiveSmallIntegerField(
|
||||||
|
default=1000,
|
||||||
|
help_text='Weighting for search. Lower values are considered more important. '
|
||||||
|
'Fields with a search weight of zero will be ignored.'
|
||||||
|
)
|
||||||
filter_logic = models.CharField(
|
filter_logic = models.CharField(
|
||||||
max_length=50,
|
max_length=50,
|
||||||
choices=CustomFieldFilterLogicChoices,
|
choices=CustomFieldFilterLogicChoices,
|
||||||
@ -109,6 +124,7 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
|||||||
)
|
)
|
||||||
weight = models.PositiveSmallIntegerField(
|
weight = models.PositiveSmallIntegerField(
|
||||||
default=100,
|
default=100,
|
||||||
|
verbose_name='Display weight',
|
||||||
help_text='Fields with higher weights appear lower in a form.'
|
help_text='Fields with higher weights appear lower in a form.'
|
||||||
)
|
)
|
||||||
validation_minimum = models.IntegerField(
|
validation_minimum = models.IntegerField(
|
||||||
@ -148,8 +164,9 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
|||||||
objects = CustomFieldManager()
|
objects = CustomFieldManager()
|
||||||
|
|
||||||
clone_fields = (
|
clone_fields = (
|
||||||
'content_types', 'type', 'object_type', 'group_name', 'description', 'required', 'filter_logic', 'default',
|
'content_types', 'type', 'object_type', 'group_name', 'description', 'required', 'search_weight',
|
||||||
'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices', 'ui_visibility',
|
'filter_logic', 'default', 'weight', 'validation_minimum', 'validation_maximum', 'validation_regex', 'choices',
|
||||||
|
'ui_visibility',
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -167,6 +184,10 @@ class CustomField(CloningMixin, ExportTemplatesMixin, WebhooksMixin, ChangeLogge
|
|||||||
# Cache instance's original name so we can check later whether it has changed
|
# Cache instance's original name so we can check later whether it has changed
|
||||||
self._name = self.name
|
self._name = self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def search_type(self):
|
||||||
|
return SEARCH_TYPES.get(self.type)
|
||||||
|
|
||||||
def populate_initial_data(self, content_types):
|
def populate_initial_data(self, content_types):
|
||||||
"""
|
"""
|
||||||
Populate initial custom field data upon either a) the creation of a new CustomField, or
|
Populate initial custom field data upon either a) the creation of a new CustomField, or
|
||||||
|
@ -33,8 +33,8 @@ class CustomFieldTable(NetBoxTable):
|
|||||||
class Meta(NetBoxTable.Meta):
|
class Meta(NetBoxTable.Meta):
|
||||||
model = CustomField
|
model = CustomField
|
||||||
fields = (
|
fields = (
|
||||||
'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'weight', 'default',
|
'pk', 'id', 'name', 'content_types', 'label', 'type', 'group_name', 'required', 'default', 'description',
|
||||||
'description', 'filter_logic', 'ui_visibility', 'choices', 'created', 'last_updated',
|
'search_weight', 'filter_logic', 'ui_visibility', 'weight', 'choices', 'created', 'last_updated',
|
||||||
)
|
)
|
||||||
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description')
|
default_columns = ('pk', 'name', 'content_types', 'label', 'group_name', 'type', 'required', 'description')
|
||||||
|
|
||||||
|
@ -292,6 +292,7 @@ class CustomFieldTest(TestCase):
|
|||||||
cf = CustomField.objects.create(
|
cf = CustomField.objects.create(
|
||||||
name='object_field',
|
name='object_field',
|
||||||
type=CustomFieldTypeChoices.TYPE_OBJECT,
|
type=CustomFieldTypeChoices.TYPE_OBJECT,
|
||||||
|
object_type=ContentType.objects.get_for_model(VLAN),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cf.content_types.set([self.object_type])
|
cf.content_types.set([self.object_type])
|
||||||
@ -323,6 +324,7 @@ class CustomFieldTest(TestCase):
|
|||||||
cf = CustomField.objects.create(
|
cf = CustomField.objects.create(
|
||||||
name='object_field',
|
name='object_field',
|
||||||
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
|
type=CustomFieldTypeChoices.TYPE_MULTIOBJECT,
|
||||||
|
object_type=ContentType.objects.get_for_model(VLAN),
|
||||||
required=False
|
required=False
|
||||||
)
|
)
|
||||||
cf.content_types.set([self.object_type])
|
cf.content_types.set([self.object_type])
|
||||||
|
@ -32,6 +32,7 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
'label': 'Field X',
|
'label': 'Field X',
|
||||||
'type': 'text',
|
'type': 'text',
|
||||||
'content_types': [site_ct.pk],
|
'content_types': [site_ct.pk],
|
||||||
|
'search_weight': 2000,
|
||||||
'filter_logic': CustomFieldFilterLogicChoices.FILTER_EXACT,
|
'filter_logic': CustomFieldFilterLogicChoices.FILTER_EXACT,
|
||||||
'default': None,
|
'default': None,
|
||||||
'weight': 200,
|
'weight': 200,
|
||||||
@ -40,11 +41,11 @@ class CustomFieldTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
cls.csv_data = (
|
cls.csv_data = (
|
||||||
'name,label,type,content_types,object_type,weight,filter_logic,choices,validation_minimum,validation_maximum,validation_regex,ui_visibility',
|
'name,label,type,content_types,object_type,weight,search_weight,filter_logic,choices,validation_minimum,validation_maximum,validation_regex,ui_visibility',
|
||||||
'field4,Field 4,text,dcim.site,,100,exact,,,,[a-z]{3},read-write',
|
'field4,Field 4,text,dcim.site,,100,1000,exact,,,,[a-z]{3},read-write',
|
||||||
'field5,Field 5,integer,dcim.site,,100,exact,,1,100,,read-write',
|
'field5,Field 5,integer,dcim.site,,100,2000,exact,,1,100,,read-write',
|
||||||
'field6,Field 6,select,dcim.site,,100,exact,"A,B,C",,,,read-write',
|
'field6,Field 6,select,dcim.site,,100,3000,exact,"A,B,C",,,,read-write',
|
||||||
'field7,Field 7,object,dcim.site,dcim.region,100,exact,,,,,read-write',
|
'field7,Field 7,object,dcim.site,dcim.region,100,4000,exact,,,,,read-write',
|
||||||
)
|
)
|
||||||
|
|
||||||
cls.bulk_edit_data = {
|
cls.bulk_edit_data = {
|
||||||
|
@ -51,6 +51,8 @@ class SearchIndex:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def to_cache(cls, instance):
|
def to_cache(cls, instance):
|
||||||
values = []
|
values = []
|
||||||
|
|
||||||
|
# Capture built-in fields
|
||||||
for name, weight in cls.fields:
|
for name, weight in cls.fields:
|
||||||
type_ = cls.get_field_type(instance, name)
|
type_ = cls.get_field_type(instance, name)
|
||||||
value = cls.get_field_value(instance, name)
|
value = cls.get_field_value(instance, name)
|
||||||
@ -58,6 +60,18 @@ class SearchIndex:
|
|||||||
ObjectFieldValue(name, type_, weight, value)
|
ObjectFieldValue(name, type_, weight, value)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Capture custom fields
|
||||||
|
if hasattr(instance, 'custom_field_data'):
|
||||||
|
for cf, value in instance.get_custom_fields().items():
|
||||||
|
type_ = cf.search_type
|
||||||
|
value = instance.custom_field_data.get(cf.name)
|
||||||
|
weight = cf.search_weight
|
||||||
|
if not type_ or not value or not weight:
|
||||||
|
continue
|
||||||
|
values.append(
|
||||||
|
ObjectFieldValue(f'cf_{cf.name}', type_, weight, value)
|
||||||
|
)
|
||||||
|
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
@ -39,13 +39,23 @@
|
|||||||
<td>{% checkmark object.required %}</td>
|
<td>{% checkmark object.required %}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Weight</th>
|
<th scope="row">Search Weight</th>
|
||||||
<td>{{ object.weight }}</td>
|
<td>
|
||||||
|
{% if object.search_weight %}
|
||||||
|
{{ object.search_weight }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">Disabled</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">Filter Logic</th>
|
<th scope="row">Filter Logic</th>
|
||||||
<td>{{ object.get_filter_logic_display }}</td>
|
<td>{{ object.get_filter_logic_display }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Display Weight</th>
|
||||||
|
<td>{{ object.weight }}</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">UI Visibility</th>
|
<th scope="row">UI Visibility</th>
|
||||||
<td>{{ object.get_ui_visibility_display }}</td>
|
<td>{{ object.get_ui_visibility_display }}</td>
|
||||||
|
Loading…
Reference in New Issue
Block a user