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

1from collections import namedtuple 

2from decimal import Decimal 

3 

4from django.core.exceptions import FieldDoesNotExist 

5from django.db import models 

6from netaddr import IPAddress, IPNetwork 

7 

8from ipam.fields import IPAddressField, IPNetworkField 

9from netbox.registry import registry 

10 

11ObjectFieldValue = namedtuple('ObjectFieldValue', ('name', 'type', 'weight', 'value')) 

12 

13 

14class FieldTypes: 

15 FLOAT = 'float' 

16 INTEGER = 'int' 

17 STRING = 'str' 

18 INET = 'inet' 

19 CIDR = 'cidr' 

20 

21 

22class LookupTypes: 

23 PARTIAL = 'icontains' 

24 EXACT = 'iexact' 

25 STARTSWITH = 'istartswith' 

26 ENDSWITH = 'iendswith' 

27 REGEX = 'iregex' 

28 

29 

30class SearchIndex: 

31 """ 

32 Base class for building search indexes. 

33 

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 = () 

45 

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 

61 

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 

79 

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 

88 

89 @classmethod 

90 def get_category(cls): 

91 return cls.category or cls.model._meta.app_config.verbose_name 

92 

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. 

97 

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 = [] 

105 

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 ) 

118 

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 ) 

131 

132 return values 

133 

134 

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}' 

140 

141 return registry['search'][label] 

142 

143 

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 

151 

152 return cls