Coverage for utilities/forms/fields/fields.py: 65%

82 statements  

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

1import json 

2 

3from django import forms 

4from django.conf import settings 

5from django.db.models import BigIntegerField as BigIntegerModelField 

6from django.db.models import Count 

7from django.forms.fields import InvalidJSONInput 

8from django.forms.fields import JSONField as _JSONField 

9from django.templatetags.static import static 

10from django.utils.translation import gettext_lazy as _ 

11from netaddr import EUI, AddrFormatError 

12 

13from utilities.forms import widgets 

14from utilities.validators import EnhancedURLValidator 

15 

16__all__ = ( 

17 'BigIntegerField', 

18 'ColorField', 

19 'CommentField', 

20 'JSONField', 

21 'LaxURLField', 

22 'MACAddressField', 

23 'PositiveBigIntegerField', 

24 'QueryField', 

25 'SlugField', 

26 'TagFilterField', 

27) 

28 

29 

30class BigIntegerField(forms.IntegerField): 

31 """ 

32 An IntegerField constrained to the range of a signed 64-bit integer. 

33 """ 

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

35 kwargs.setdefault('min_value', -BigIntegerModelField.MAX_BIGINT - 1) 

36 kwargs.setdefault('max_value', BigIntegerModelField.MAX_BIGINT) 

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

38 

39 

40class PositiveBigIntegerField(BigIntegerField): 

41 """ 

42 An IntegerField constrained to the range supported by Django's 

43 PositiveBigIntegerField model field. 

44 """ 

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

46 kwargs.setdefault('min_value', 0) 

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

48 

49 

50class QueryField(forms.CharField): 

51 """ 

52 A CharField subclass used for global search/query fields in filter forms. 

53 This field type signals to FilterModifierMixin to skip enhancement with lookup modifiers. 

54 """ 

55 pass 

56 

57 

58class CommentField(forms.CharField): 

59 """ 

60 A textarea with support for Markdown rendering. Exists mostly just to add a standard `help_text`. 

61 """ 

62 widget = widgets.MarkdownWidget 

63 label = _('Comments') 

64 help_text = _( 

65 '<i class="mdi mdi-information-outline" aria-hidden="true"></i> ' 

66 '<a href="{url}" target="_blank" tabindex="-1">Markdown</a> syntax is supported' 

67 ).format(url=static('docs/reference/markdown/')) 

68 

69 def __init__(self, *, label=label, help_text=help_text, required=False, **kwargs): 

70 super().__init__(label=label, help_text=help_text, required=required, **kwargs) 

71 

72 

73class SlugField(forms.SlugField): 

74 """ 

75 Extend Django's built-in SlugField to automatically populate from a field called `name` unless otherwise specified. 

76 

77 Parameters: 

78 slug_source: Name of the form field from which the slug value will be derived 

79 """ 

80 widget = widgets.SlugWidget 

81 label = _('Slug') 

82 help_text = _("URL-friendly unique shorthand") 

83 

84 def __init__(self, *, slug_source='name', label=label, help_text=help_text, **kwargs): 

85 super().__init__(label=label, help_text=help_text, **kwargs) 

86 

87 self.widget.attrs['slug-source'] = slug_source 

88 

89 def get_bound_field(self, form, field_name): 

90 if prefix := form.prefix: 

91 slug_source = self.widget.attrs.get('slug-source') 

92 if slug_source and not slug_source.startswith(f'{prefix}-'): 

93 self.widget.attrs['slug-source'] = f"{prefix}-{slug_source}" 

94 

95 return super().get_bound_field(form, field_name) 

96 

97 

98class ColorField(forms.CharField): 

99 """ 

100 A field which represents a color value in hexadecimal `RRGGBB` format. Utilizes NetBox's `ColorSelect` widget to 

101 render choices. 

102 """ 

103 widget = widgets.ColorSelect 

104 

105 

106class TagFilterField(forms.MultipleChoiceField): 

107 """ 

108 A filter field for the tags of a model. Only the tags used by a model are displayed. 

109 

110 :param model: The model of the filter 

111 """ 

112 

113 def __init__(self, model, *args, **kwargs): 

114 def get_choices(): 

115 tags = model.tags.annotate( 

116 count=Count('extras_taggeditem_items') 

117 ).order_by('name') 

118 return [ 

119 (settings.FILTERS_NULL_CHOICE_VALUE, settings.FILTERS_NULL_CHOICE_LABEL), # "None" option 

120 *[(str(tag.slug), f'{tag.name} ({tag.count})') for tag in tags] 

121 ] 

122 

123 # Choices are fetched each time the form is initialized 

124 super().__init__(label=_('Tags'), choices=get_choices, required=False, *args, **kwargs) 

125 

126 

127class LaxURLField(forms.URLField): 

128 """ 

129 Modifies Django's built-in URLField to remove the requirement for fully-qualified domain names 

130 (e.g. http://myserver/ is valid) 

131 """ 

132 default_validators = [EnhancedURLValidator()] 

133 

134 

135class JSONField(_JSONField): 

136 """ 

137 Custom wrapper around Django's built-in JSONField to avoid presenting "null" as the default text. 

138 """ 

139 empty_values = [None, '', ()] 

140 

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

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

143 

144 self.widget.attrs['placeholder'] = '' 

145 self.widget.attrs['class'] = 'font-monospace' 

146 if not self.help_text: 

147 self.help_text = _('Enter context data in <a href="https://json.org/">JSON</a> format.') 

148 

149 def prepare_value(self, value): 

150 if isinstance(value, InvalidJSONInput): 

151 return value 

152 if value in ('', None): 

153 return '' 

154 if type(value) is str: 

155 try: 

156 value = json.loads(value, cls=self.decoder) 

157 except json.decoder.JSONDecodeError: 

158 return f'"{value}"' 

159 return json.dumps(value, sort_keys=True, indent=4, ensure_ascii=False, cls=self.encoder) 

160 

161 

162class MACAddressField(forms.Field): 

163 """ 

164 Validates a 48-bit MAC address. 

165 """ 

166 widget = forms.CharField 

167 default_error_messages = { 

168 'invalid': _('MAC address must be in EUI-48 format'), 

169 } 

170 

171 def to_python(self, value): 

172 value = super().to_python(value) 

173 

174 # Validate MAC address format 

175 try: 

176 value = EUI(value.strip()) 

177 except AddrFormatError: 

178 raise forms.ValidationError(self.error_messages['invalid'], code='invalid') 

179 

180 return value