Files
netbox/netbox/utilities/forms/fields/fields.py

181 lines
5.8 KiB
Python

import json
from django import forms
from django.conf import settings
from django.db.models import BigIntegerField as BigIntegerModelField
from django.db.models import Count
from django.forms.fields import InvalidJSONInput
from django.forms.fields import JSONField as _JSONField
from django.templatetags.static import static
from django.utils.translation import gettext_lazy as _
from netaddr import EUI, AddrFormatError
from utilities.forms import widgets
from utilities.validators import EnhancedURLValidator
__all__ = (
'BigIntegerField',
'ColorField',
'CommentField',
'JSONField',
'LaxURLField',
'MACAddressField',
'PositiveBigIntegerField',
'QueryField',
'SlugField',
'TagFilterField',
)
class BigIntegerField(forms.IntegerField):
"""
An IntegerField constrained to the range of a signed 64-bit integer.
"""
def __init__(self, *args, **kwargs):
kwargs.setdefault('min_value', -BigIntegerModelField.MAX_BIGINT - 1)
kwargs.setdefault('max_value', BigIntegerModelField.MAX_BIGINT)
super().__init__(*args, **kwargs)
class PositiveBigIntegerField(BigIntegerField):
"""
An IntegerField constrained to the range supported by Django's
PositiveBigIntegerField model field.
"""
def __init__(self, *args, **kwargs):
kwargs.setdefault('min_value', 0)
super().__init__(*args, **kwargs)
class QueryField(forms.CharField):
"""
A CharField subclass used for global search/query fields in filter forms.
This field type signals to FilterModifierMixin to skip enhancement with lookup modifiers.
"""
pass
class CommentField(forms.CharField):
"""
A textarea with support for Markdown rendering. Exists mostly just to add a standard `help_text`.
"""
widget = widgets.MarkdownWidget
label = _('Comments')
help_text = _(
'<i class="mdi mdi-information-outline"></i> '
'<a href="{url}" target="_blank" tabindex="-1">Markdown</a> syntax is supported'
).format(url=static('docs/reference/markdown/'))
def __init__(self, *, label=label, help_text=help_text, required=False, **kwargs):
super().__init__(label=label, help_text=help_text, required=required, **kwargs)
class SlugField(forms.SlugField):
"""
Extend Django's built-in SlugField to automatically populate from a field called `name` unless otherwise specified.
Parameters:
slug_source: Name of the form field from which the slug value will be derived
"""
widget = widgets.SlugWidget
label = _('Slug')
help_text = _("URL-friendly unique shorthand")
def __init__(self, *, slug_source='name', label=label, help_text=help_text, **kwargs):
super().__init__(label=label, help_text=help_text, **kwargs)
self.widget.attrs['slug-source'] = slug_source
def get_bound_field(self, form, field_name):
if prefix := form.prefix:
slug_source = self.widget.attrs.get('slug-source')
if slug_source and not slug_source.startswith(f'{prefix}-'):
self.widget.attrs['slug-source'] = f"{prefix}-{slug_source}"
return super().get_bound_field(form, field_name)
class ColorField(forms.CharField):
"""
A field which represents a color value in hexadecimal `RRGGBB` format. Utilizes NetBox's `ColorSelect` widget to
render choices.
"""
widget = widgets.ColorSelect
class TagFilterField(forms.MultipleChoiceField):
"""
A filter field for the tags of a model. Only the tags used by a model are displayed.
:param model: The model of the filter
"""
def __init__(self, model, *args, **kwargs):
def get_choices():
tags = model.tags.annotate(
count=Count('extras_taggeditem_items')
).order_by('name')
return [
(settings.FILTERS_NULL_CHOICE_VALUE, settings.FILTERS_NULL_CHOICE_LABEL), # "None" option
*[(str(tag.slug), f'{tag.name} ({tag.count})') for tag in tags]
]
# Choices are fetched each time the form is initialized
super().__init__(label=_('Tags'), choices=get_choices, required=False, *args, **kwargs)
class LaxURLField(forms.URLField):
"""
Modifies Django's built-in URLField to remove the requirement for fully-qualified domain names
(e.g. http://myserver/ is valid)
"""
default_validators = [EnhancedURLValidator()]
class JSONField(_JSONField):
"""
Custom wrapper around Django's built-in JSONField to avoid presenting "null" as the default text.
"""
empty_values = [None, '', ()]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.widget.attrs['placeholder'] = ''
self.widget.attrs['class'] = 'font-monospace'
if not self.help_text:
self.help_text = _('Enter context data in <a href="https://json.org/">JSON</a> format.')
def prepare_value(self, value):
if isinstance(value, InvalidJSONInput):
return value
if value in ('', None):
return ''
if type(value) is str:
try:
value = json.loads(value, cls=self.decoder)
except json.decoder.JSONDecodeError:
return f'"{value}"'
return json.dumps(value, sort_keys=True, indent=4, ensure_ascii=False, cls=self.encoder)
class MACAddressField(forms.Field):
"""
Validates a 48-bit MAC address.
"""
widget = forms.CharField
default_error_messages = {
'invalid': _('MAC address must be in EUI-48 format'),
}
def to_python(self, value):
value = super().to_python(value)
# Validate MAC address format
try:
value = EUI(value.strip())
except AddrFormatError:
raise forms.ValidationError(self.error_messages['invalid'], code='invalid')
return value