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
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1import re
3import netaddr
4from django import forms
5from django.utils.translation import gettext_lazy as _
7from utilities.forms.constants import *
8from utilities.forms.utils import expand_alphanumeric_pattern, expand_ipnetwork_pattern
10__all__ = (
11 'ExpandableIPNetworkField',
12 'ExpandableNameField',
13 'ExpandableNumericField',
14)
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 """
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 )
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]
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 """
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>).")
52class ExpandableIPNetworkField(forms.CharField):
53 """
54 A CharField that expands numeric range patterns in IPv4/IPv6 CIDR notation into multiple entries.
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 """
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 )
71 def to_python(self, value):
72 if not value:
73 return [value]
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]
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]