Coverage for utilities/validators.py: 55%

40 statements  

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

1import decimal 

2import re 

3from urllib.parse import urlparse 

4 

5from django.core.exceptions import ValidationError 

6from django.core.validators import BaseValidator, RegexValidator, URLValidator, _lazy_re_compile 

7from django.utils.translation import gettext_lazy as _ 

8 

9from netbox.config import get_config 

10 

11__all__ = ( 

12 'ColorValidator', 

13 'EnhancedURLValidator', 

14 'ExclusionValidator', 

15 'MultipleOfValidator', 

16 'url_scheme_is_allowed', 

17 'validate_regex', 

18) 

19 

20 

21ColorValidator = RegexValidator( 

22 regex='^[0-9a-f]{6}$', 

23 message='Enter a valid hexadecimal RGB color code.', 

24 code='invalid' 

25) 

26 

27 

28class EnhancedURLValidator(URLValidator): 

29 """ 

30 Extends Django's built-in URLValidator to permit the use of hostnames with no domain extension and enforce allowed 

31 schemes specified in the configuration. 

32 """ 

33 fqdn_re = URLValidator.hostname_re + URLValidator.domain_re + URLValidator.tld_re 

34 host_res = [URLValidator.ipv4_re, URLValidator.ipv6_re, fqdn_re, URLValidator.hostname_re] 

35 regex = _lazy_re_compile( 

36 r'^(?:[a-z0-9\.\-\+]*)://' # Scheme (enforced separately) 

37 r'(?:[^\s:@/]+(?::[^\s:@/]*)?@)?' # HTTP basic authentication 

38 r'(?:' + '|'.join(host_res) + ')' # IPv4, IPv6, FQDN, or hostname 

39 r'(?::\d{1,5})?' # Port number 

40 r'(?:[/?#][^\s]*)?' # Path 

41 r'\Z', re.IGNORECASE) 

42 schemes = None 

43 

44 def __call__(self, value): 

45 if self.schemes is None: 

46 # We can't load the allowed schemes until the configuration has been initialized 

47 self.schemes = get_config().ALLOWED_URL_SCHEMES 

48 return super().__call__(value) 

49 

50 

51class ExclusionValidator(BaseValidator): 

52 """ 

53 Ensure that a field's value is not equal to any of the specified values. 

54 """ 

55 message = 'This value may not be %(show_value)s.' 

56 

57 def compare(self, a, b): 

58 return a in b 

59 

60 

61class MultipleOfValidator(BaseValidator): 

62 """ 

63 Checks that a field's value is a numeric multiple of the given value. Both values are 

64 cast as Decimals for comparison. 

65 """ 

66 def __init__(self, multiple): 

67 self.multiple = decimal.Decimal(str(multiple)) 

68 super().__init__(limit_value=None) 

69 

70 def __call__(self, value): 

71 if decimal.Decimal(str(value)) % self.multiple != 0: 

72 raise ValidationError( 

73 _("{value} must be a multiple of {multiple}.").format(value=value, multiple=self.multiple) 

74 ) 

75 

76 

77def url_scheme_is_allowed(value): 

78 """ 

79 Return True if the URL's scheme is permitted by ALLOWED_URL_SCHEMES. A schemeless (relative) value 

80 is considered permitted. 

81 

82 The scheme is compared in lower case. A percent-encoded scheme (e.g. "javascript%3A…") yields no 

83 scheme, matching browser behavior: a browser does not decode the scheme portion of an href, so such 

84 a value is inert and is treated as relative. A malformed URL which cannot be parsed (e.g. 

85 "http://[::1/foo") likewise yields no scheme. 

86 """ 

87 try: 

88 scheme = urlparse(value).scheme.lower() 

89 except ValueError: 

90 scheme = '' 

91 return not scheme or scheme in get_config().ALLOWED_URL_SCHEMES 

92 

93 

94def validate_regex(value): 

95 """ 

96 Checks that the value is a valid regular expression. (Don't confuse this with RegexValidator, which *uses* a regex 

97 to validate a value.) 

98 """ 

99 try: 

100 re.compile(value) 

101 except re.error: 

102 raise ValidationError(_("{value} is not a valid regular expression.").format(value=value))