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
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1import json
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
13from utilities.forms import widgets
14from utilities.validators import EnhancedURLValidator
16__all__ = (
17 'BigIntegerField',
18 'ColorField',
19 'CommentField',
20 'JSONField',
21 'LaxURLField',
22 'MACAddressField',
23 'PositiveBigIntegerField',
24 'QueryField',
25 'SlugField',
26 'TagFilterField',
27)
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)
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)
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
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/'))
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)
73class SlugField(forms.SlugField):
74 """
75 Extend Django's built-in SlugField to automatically populate from a field called `name` unless otherwise specified.
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")
84 def __init__(self, *, slug_source='name', label=label, help_text=help_text, **kwargs):
85 super().__init__(label=label, help_text=help_text, **kwargs)
87 self.widget.attrs['slug-source'] = slug_source
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}"
95 return super().get_bound_field(form, field_name)
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
106class TagFilterField(forms.MultipleChoiceField):
107 """
108 A filter field for the tags of a model. Only the tags used by a model are displayed.
110 :param model: The model of the filter
111 """
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 ]
123 # Choices are fetched each time the form is initialized
124 super().__init__(label=_('Tags'), choices=get_choices, required=False, *args, **kwargs)
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()]
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, '', ()]
141 def __init__(self, *args, **kwargs):
142 super().__init__(*args, **kwargs)
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.')
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)
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 }
171 def to_python(self, value):
172 value = super().to_python(value)
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')
180 return value