Coverage for extras/validators.py: 32%

82 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 18:35 +0000

1import operator 

2 

3from django.core import validators 

4from django.core.exceptions import ValidationError 

5from django.utils.translation import gettext_lazy as _ 

6 

7# NOTE: As this module may be imported by configuration.py, we cannot import 

8# anything from NetBox itself. 

9 

10 

11class IsEqualValidator(validators.BaseValidator): 

12 """ 

13 Employed by CustomValidator to require a specific value. 

14 """ 

15 message = _("Ensure this value is equal to %(limit_value)s.") 

16 code = "is_equal" 

17 

18 def compare(self, a, b): 

19 return a != b 

20 

21 

22class IsNotEqualValidator(validators.BaseValidator): 

23 """ 

24 Employed by CustomValidator to exclude a specific value. 

25 """ 

26 message = _("Ensure this value does not equal %(limit_value)s.") 

27 code = "is_not_equal" 

28 

29 def compare(self, a, b): 

30 return a == b 

31 

32 

33class IsEmptyValidator: 

34 """ 

35 Employed by CustomValidator to enforce required fields. 

36 """ 

37 message = _("This field must be empty.") 

38 code = 'is_empty' 

39 

40 def __init__(self, enforce=True): 

41 self._enforce = enforce 

42 

43 def __call__(self, value): 

44 if self._enforce and value not in validators.EMPTY_VALUES: 

45 raise ValidationError(self.message, code=self.code) 

46 

47 

48class IsNotEmptyValidator: 

49 """ 

50 Employed by CustomValidator to enforce prohibited fields. 

51 """ 

52 message = _("This field must not be empty.") 

53 code = 'not_empty' 

54 

55 def __init__(self, enforce=True): 

56 self._enforce = enforce 

57 

58 def __call__(self, value): 

59 if self._enforce and value in validators.EMPTY_VALUES: 

60 raise ValidationError(self.message, code=self.code) 

61 

62 

63class CustomValidator: 

64 """ 

65 This class enables the application of user-defined validation rules to NetBox models. It can be instantiated by 

66 passing a dictionary of validation rules in the form {attribute: rules}, where 'rules' is a dictionary mapping 

67 descriptors (e.g. min_length or regex) to values. 

68 

69 A CustomValidator instance is applied by calling it with the instance being validated: 

70 

71 validator = CustomValidator({'name': {'min_length: 10}}) 

72 site = Site(name='abcdef') 

73 validator(site) # Raises ValidationError 

74 

75 :param validation_rules: A dictionary mapping object attributes to validation rules 

76 """ 

77 REQUEST_TOKEN = 'request' 

78 

79 VALIDATORS = { 

80 'eq': IsEqualValidator, 

81 'neq': IsNotEqualValidator, 

82 'min': validators.MinValueValidator, 

83 'max': validators.MaxValueValidator, 

84 'min_length': validators.MinLengthValidator, 

85 'max_length': validators.MaxLengthValidator, 

86 'regex': validators.RegexValidator, 

87 'required': IsNotEmptyValidator, 

88 'prohibited': IsEmptyValidator, 

89 } 

90 

91 def __init__(self, validation_rules=None): 

92 self.validation_rules = validation_rules or {} 

93 if type(self.validation_rules) is not dict: 

94 raise ValueError(_("Validation rules must be passed as a dictionary")) 

95 

96 def __call__(self, instance, request=None): 

97 """ 

98 Validate the instance and (optional) request against the validation rule(s). 

99 """ 

100 for attr_path, rules in self.validation_rules.items(): 

101 

102 # The rule applies to the current request 

103 if attr_path.split('.')[0] == self.REQUEST_TOKEN: 

104 # Skip if no request has been provided (we can't validate) 

105 if request is None: 

106 continue 

107 attr = self._get_request_attr(request, attr_path) 

108 # The rule applies to the instance 

109 else: 

110 attr = self._get_instance_attr(instance, attr_path) 

111 

112 # Validate the attribute's value against each of the rules defined for it 

113 for descriptor, value in rules.items(): 

114 validator = self.get_validator(descriptor, value) 

115 try: 

116 validator(attr) 

117 except ValidationError as exc: 

118 raise ValidationError( 

119 _("Custom validation failed for {attribute}: {exception}").format( 

120 attribute=attr_path, exception=exc 

121 ) 

122 ) 

123 

124 # Execute custom validation logic (if any) 

125 self.validate(instance, request) 

126 

127 @staticmethod 

128 def _get_request_attr(request, name): 

129 name = name.split('.', maxsplit=1)[1] # Remove token 

130 try: 

131 return operator.attrgetter(name)(request) 

132 except AttributeError: 

133 raise ValidationError(_('Invalid attribute "{name}" for request').format(name=name)) 

134 

135 @staticmethod 

136 def _get_instance_attr(instance, name): 

137 # Attempt to resolve many-to-many fields to their stored values 

138 m2m_fields = [f.name for f in instance._meta.local_many_to_many] 

139 if name in m2m_fields: 

140 if name in getattr(instance, '_m2m_values', []): 

141 return instance._m2m_values[name] 

142 if instance.pk: 

143 return list(getattr(instance, name).all()) 

144 return [] 

145 

146 # Raise a ValidationError for unknown attributes 

147 try: 

148 return operator.attrgetter(name)(instance) 

149 except AttributeError: 

150 raise ValidationError(_('Invalid attribute "{name}" for {model}').format( 

151 name=name, 

152 model=instance.__class__.__name__ 

153 )) 

154 

155 def get_validator(self, descriptor, value): 

156 """ 

157 Instantiate and return the appropriate validator based on the descriptor given. For 

158 example, 'min' returns MinValueValidator(value). 

159 """ 

160 if descriptor not in self.VALIDATORS: 

161 raise NotImplementedError( 

162 f"Unknown validation type for {self.__class__.__name__}: '{descriptor}'" 

163 ) 

164 validator_cls = self.VALIDATORS.get(descriptor) 

165 return validator_cls(value) 

166 

167 def validate(self, instance, request): 

168 """ 

169 Custom validation method, to be overridden by the user. Validation failures should 

170 raise a ValidationError exception. 

171 """ 

172 return 

173 

174 def fail(self, message, field=None): 

175 """ 

176 Raise a ValidationError exception. Associate the provided message with a form/serializer field if specified. 

177 """ 

178 if field is not None: 

179 raise ValidationError({field: message}) 

180 raise ValidationError(message)