Coverage for utilities/forms/fields/expandable.py: 49%

41 statements  

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

1import re 

2 

3import netaddr 

4from django import forms 

5from django.utils.translation import gettext_lazy as _ 

6 

7from utilities.forms.constants import * 

8from utilities.forms.utils import expand_alphanumeric_pattern, expand_ipnetwork_pattern 

9 

10__all__ = ( 

11 'ExpandableIPNetworkField', 

12 'ExpandableNameField', 

13 'ExpandableNumericField', 

14) 

15 

16 

17class ExpandableNameField(forms.CharField): 

18 """ 

19 A field which allows for numeric range expansion 

20 Example: 'Gi0/[1-3]' => ['Gi0/1', 'Gi0/2', 'Gi0/3'] 

21 """ 

22 

23 def __init__(self, *args, **kwargs): 

24 super().__init__(*args, **kwargs) 

25 if not self.help_text: 

26 self.help_text = _( 

27 "Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range are " 

28 "not supported (example: <code>[ge,xe]-0/0/[0-9]</code>)." 

29 ) 

30 

31 def to_python(self, value): 

32 if not value: 

33 return '' 

34 if re.search(ALPHANUMERIC_EXPANSION_PATTERN, value): 

35 return list(expand_alphanumeric_pattern(value)) 

36 return [value] 

37 

38 

39class ExpandableNumericField(ExpandableNameField): 

40 """ 

41 An ExpandableNameField intended for numeric values, yielding integer-compatible strings suitable for bulk creation. 

42 Example: '[1-3]' => ['1', '2', '3'] 

43 """ 

44 

45 def __init__(self, *args, **kwargs): 

46 super().__init__(*args, **kwargs) 

47 # Replace the inherited alphanumeric default with numeric-specific guidance (unless one was supplied) 

48 if not kwargs.get('help_text'): 48 ↛ exitline 48 didn't return from function '__init__' because the condition on line 48 was always true

49 self.help_text = _("Numeric ranges are supported for bulk creation (example: <code>[1-24]</code>).") 

50 

51 

52class ExpandableIPNetworkField(forms.CharField): 

53 """ 

54 A CharField that expands numeric range patterns in IPv4/IPv6 CIDR notation into multiple entries. 

55 

56 Examples: 

57 '192.0.2.[1-254]/32' => ['192.0.2.1/32', '192.0.2.2/32', ...] 

58 '10.[0-3,10-13].0.0/16' => ['10.0.0.0/16', '10.1.0.0/16', ..., '10.10.0.0/16', ...] 

59 '2001:db8:[0-f]::/64' => ['2001:db8:0::/64', '2001:db8:1::/64', ...] 

60 """ 

61 

62 def __init__(self, *args, **kwargs): 

63 super().__init__(*args, **kwargs) 

64 if not self.help_text: 64 ↛ exitline 64 didn't return from function '__init__' because the condition on line 64 was always true

65 self.help_text = _( 

66 'Use bracket notation to specify numeric ranges for bulk creation (CIDR required).<br />' 

67 'Examples: <code>192.0.2.[1-10]/32</code>, <code>10.[0-3,10-13].0.0/16</code>, ' 

68 '<code>2001:db8:[a-f]::/64</code>' 

69 ) 

70 

71 def to_python(self, value): 

72 if not value: 

73 return [value] 

74 

75 # Replace expansion brackets with a neutral value to get a parseable IP/CIDR 

76 stripped = re.sub(r'(?>\[[^\]]+\])', '0', value) 

77 try: 

78 family = netaddr.IPNetwork(stripped).version 

79 except (netaddr.AddrFormatError, ValueError): 

80 return [value] 

81 

82 if family == 4 and re.search(IP4_EXPANSION_PATTERN, value): 

83 return list(expand_ipnetwork_pattern(value, 4)) 

84 if family == 6 and re.search(IP6_EXPANSION_PATTERN, value): 

85 return list(expand_ipnetwork_pattern(value.lower(), 6)) 

86 return [value]