Coverage for netbox/search/__init__.py: 65%
88 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1from collections import namedtuple
2from decimal import Decimal
4from django.core.exceptions import FieldDoesNotExist
5from django.db import models
6from netaddr import IPAddress, IPNetwork
8from ipam.fields import IPAddressField, IPNetworkField
9from netbox.registry import registry
11ObjectFieldValue = namedtuple('ObjectFieldValue', ('name', 'type', 'weight', 'value'))
14class FieldTypes:
15 FLOAT = 'float'
16 INTEGER = 'int'
17 STRING = 'str'
18 INET = 'inet'
19 CIDR = 'cidr'
22class LookupTypes:
23 PARTIAL = 'icontains'
24 EXACT = 'iexact'
25 STARTSWITH = 'istartswith'
26 ENDSWITH = 'iendswith'
27 REGEX = 'iregex'
30class SearchIndex:
31 """
32 Base class for building search indexes.
34 Attributes:
35 model: The model class for which this index is used.
36 category: The label of the group under which this indexer is categorized (for form field display). If none,
37 the name of the model's app will be used.
38 fields: An iterable of two-tuples defining the model fields to be indexed and the weight associated with each.
39 display_attrs: An iterable of additional object attributes to include when displaying search results.
40 """
41 model = None
42 category = None
43 fields = ()
44 display_attrs = ()
46 @staticmethod
47 def get_field_type(instance, field_name):
48 """
49 Return the data type of the specified model field.
50 """
51 field_cls = instance._meta.get_field(field_name).__class__
52 if issubclass(field_cls, (models.FloatField, models.DecimalField)): 52 ↛ 53line 52 didn't jump to line 53 because the condition on line 52 was never true
53 return FieldTypes.FLOAT
54 if issubclass(field_cls, IPAddressField):
55 return FieldTypes.INET
56 if issubclass(field_cls, IPNetworkField):
57 return FieldTypes.CIDR
58 if issubclass(field_cls, models.IntegerField):
59 return FieldTypes.INTEGER
60 return FieldTypes.STRING
62 @staticmethod
63 def get_attr_type(instance, field_name):
64 """
65 Return the data type of the specified object attribute.
66 """
67 value = getattr(instance, field_name)
68 if type(value) is str:
69 return FieldTypes.STRING
70 if type(value) is int:
71 return FieldTypes.INTEGER
72 if type(value) in (float, Decimal):
73 return FieldTypes.FLOAT
74 if type(value) is IPNetwork:
75 return FieldTypes.CIDR
76 if type(value) is IPAddress:
77 return FieldTypes.INET
78 return FieldTypes.STRING
80 @staticmethod
81 def get_field_value(instance, field_name):
82 """
83 Return the value of the specified model field as a string (or None).
84 """
85 if value := getattr(instance, field_name):
86 return str(value)
87 return None
89 @classmethod
90 def get_category(cls):
91 return cls.category or cls.model._meta.app_config.verbose_name
93 @classmethod
94 def to_cache(cls, instance, custom_fields=None):
95 """
96 Return a list of ObjectFieldValue representing the instance fields to be cached.
98 Args:
99 instance: The instance being cached.
100 custom_fields: An iterable of CustomFields to include when caching the instance. If None, all custom fields
101 defined for the model will be included. (This can also be provided during bulk caching to avoid looking
102 up the available custom fields for each instance.)
103 """
104 values = []
106 # Capture built-in fields
107 for name, weight in cls.fields:
108 try:
109 type_ = cls.get_field_type(instance, name)
110 except FieldDoesNotExist:
111 # Not a concrete field; handle as an object attribute
112 type_ = cls.get_attr_type(instance, name)
113 value = cls.get_field_value(instance, name)
114 if type_ and value:
115 values.append(
116 ObjectFieldValue(name, type_, weight, value)
117 )
119 # Capture custom fields
120 if getattr(instance, 'custom_field_data', None): 120 ↛ 121line 120 didn't jump to line 121 because the condition on line 120 was never true
121 if custom_fields is None:
122 custom_fields = instance.custom_fields
123 for cf in custom_fields:
124 type_ = cf.search_type
125 value = instance.custom_field_data.get(cf.name)
126 weight = cf.search_weight
127 if type_ and value and weight:
128 values.append(
129 ObjectFieldValue(f'cf_{cf.name}', type_, weight, value)
130 )
132 return values
135def get_indexer(model):
136 """
137 Get the SearchIndex class for the given model.
138 """
139 label = f'{model._meta.app_label}.{model._meta.model_name}'
141 return registry['search'][label]
144def register_search(cls):
145 """
146 Decorator for registering a SearchIndex class.
147 """
148 model = cls.model
149 label = f'{model._meta.app_label}.{model._meta.model_name}'
150 registry['search'][label] = cls
152 return cls