mirror of
https://github.com/netbox-community/netbox.git
synced 2025-08-27 01:36:11 -06:00
Clean up forms modules
This commit is contained in:
parent
2dc50b6108
commit
47af8b0208
@ -1,5 +1,6 @@
|
|||||||
from .constants import *
|
from .constants import *
|
||||||
from .fields import *
|
from .fields import *
|
||||||
from .forms import *
|
from .forms import *
|
||||||
|
from .mixins import *
|
||||||
from .utils import *
|
from .utils import *
|
||||||
from .widgets import *
|
from .widgets import *
|
||||||
|
@ -1,14 +1,9 @@
|
|||||||
import csv
|
|
||||||
from io import StringIO
|
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
|
from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils.translation import gettext as _
|
|
||||||
|
|
||||||
from utilities.choices import unpack_grouped_choices
|
from utilities.choices import unpack_grouped_choices
|
||||||
from utilities.forms.utils import parse_csv, validate_csv
|
|
||||||
from utilities.utils import content_type_identifier
|
from utilities.utils import content_type_identifier
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
@ -2,11 +2,9 @@ import re
|
|||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
from .mixins import BootstrapMixin
|
||||||
from .widgets import APISelect, APISelectMultiple, ClearableFileInput
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'BootstrapMixin',
|
|
||||||
'BulkEditForm',
|
'BulkEditForm',
|
||||||
'BulkRenameForm',
|
'BulkRenameForm',
|
||||||
'ConfirmationForm',
|
'ConfirmationForm',
|
||||||
@ -17,69 +15,6 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Mixins
|
|
||||||
#
|
|
||||||
|
|
||||||
class BootstrapMixin:
|
|
||||||
"""
|
|
||||||
Add the base Bootstrap CSS classes to form elements.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
exempt_widgets = [
|
|
||||||
forms.FileInput,
|
|
||||||
forms.RadioSelect,
|
|
||||||
APISelect,
|
|
||||||
APISelectMultiple,
|
|
||||||
ClearableFileInput,
|
|
||||||
]
|
|
||||||
|
|
||||||
for field_name, field in self.fields.items():
|
|
||||||
css = field.widget.attrs.get('class', '')
|
|
||||||
|
|
||||||
if field.widget.__class__ in exempt_widgets:
|
|
||||||
continue
|
|
||||||
|
|
||||||
elif isinstance(field.widget, forms.CheckboxInput):
|
|
||||||
field.widget.attrs['class'] = f'{css} form-check-input'
|
|
||||||
|
|
||||||
elif isinstance(field.widget, forms.SelectMultiple):
|
|
||||||
if 'size' not in field.widget.attrs:
|
|
||||||
field.widget.attrs['class'] = f'{css} netbox-static-select'
|
|
||||||
|
|
||||||
elif isinstance(field.widget, forms.Select):
|
|
||||||
field.widget.attrs['class'] = f'{css} netbox-static-select'
|
|
||||||
|
|
||||||
else:
|
|
||||||
field.widget.attrs['class'] = f'{css} form-control'
|
|
||||||
|
|
||||||
if field.required and not isinstance(field.widget, forms.FileInput):
|
|
||||||
field.widget.attrs['required'] = 'required'
|
|
||||||
|
|
||||||
if 'placeholder' not in field.widget.attrs and field.label is not None:
|
|
||||||
field.widget.attrs['placeholder'] = field.label
|
|
||||||
|
|
||||||
def is_valid(self):
|
|
||||||
is_valid = super().is_valid()
|
|
||||||
|
|
||||||
# Apply is-invalid CSS class to fields with errors
|
|
||||||
if not is_valid:
|
|
||||||
for field_name in self.errors:
|
|
||||||
# Ignore e.g. __all__
|
|
||||||
if field := self.fields.get(field_name):
|
|
||||||
css = field.widget.attrs.get('class', '')
|
|
||||||
field.widget.attrs['class'] = f'{css} is-invalid'
|
|
||||||
|
|
||||||
return is_valid
|
|
||||||
|
|
||||||
|
|
||||||
#
|
|
||||||
# Form classes
|
|
||||||
#
|
|
||||||
|
|
||||||
class ReturnURLForm(forms.Form):
|
class ReturnURLForm(forms.Form):
|
||||||
"""
|
"""
|
||||||
Provides a hidden return URL field to control where the user is directed after the form is submitted.
|
Provides a hidden return URL field to control where the user is directed after the form is submitted.
|
||||||
|
62
netbox/utilities/forms/mixins.py
Normal file
62
netbox/utilities/forms/mixins.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
from django import forms
|
||||||
|
|
||||||
|
from .widgets import APISelect, APISelectMultiple, ClearableFileInput
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'BootstrapMixin',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BootstrapMixin:
|
||||||
|
"""
|
||||||
|
Add the base Bootstrap CSS classes to form elements.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
exempt_widgets = [
|
||||||
|
forms.FileInput,
|
||||||
|
forms.RadioSelect,
|
||||||
|
APISelect,
|
||||||
|
APISelectMultiple,
|
||||||
|
ClearableFileInput,
|
||||||
|
]
|
||||||
|
|
||||||
|
for field_name, field in self.fields.items():
|
||||||
|
css = field.widget.attrs.get('class', '')
|
||||||
|
|
||||||
|
if field.widget.__class__ in exempt_widgets:
|
||||||
|
continue
|
||||||
|
|
||||||
|
elif isinstance(field.widget, forms.CheckboxInput):
|
||||||
|
field.widget.attrs['class'] = f'{css} form-check-input'
|
||||||
|
|
||||||
|
elif isinstance(field.widget, forms.SelectMultiple):
|
||||||
|
if 'size' not in field.widget.attrs:
|
||||||
|
field.widget.attrs['class'] = f'{css} netbox-static-select'
|
||||||
|
|
||||||
|
elif isinstance(field.widget, forms.Select):
|
||||||
|
field.widget.attrs['class'] = f'{css} netbox-static-select'
|
||||||
|
|
||||||
|
else:
|
||||||
|
field.widget.attrs['class'] = f'{css} form-control'
|
||||||
|
|
||||||
|
if field.required and not isinstance(field.widget, forms.FileInput):
|
||||||
|
field.widget.attrs['required'] = 'required'
|
||||||
|
|
||||||
|
if 'placeholder' not in field.widget.attrs and field.label is not None:
|
||||||
|
field.widget.attrs['placeholder'] = field.label
|
||||||
|
|
||||||
|
def is_valid(self):
|
||||||
|
is_valid = super().is_valid()
|
||||||
|
|
||||||
|
# Apply is-invalid CSS class to fields with errors
|
||||||
|
if not is_valid:
|
||||||
|
for field_name in self.errors:
|
||||||
|
# Ignore e.g. __all__
|
||||||
|
if field := self.fields.get(field_name):
|
||||||
|
css = field.widget.attrs.get('class', '')
|
||||||
|
field.widget.attrs['class'] = f'{css} is-invalid'
|
||||||
|
|
||||||
|
return is_valid
|
5
netbox/utilities/forms/widgets/__init__.py
Normal file
5
netbox/utilities/forms/widgets/__init__.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from .apiselect import *
|
||||||
|
from .array import *
|
||||||
|
from .datetime import *
|
||||||
|
from .misc import *
|
||||||
|
from .select import *
|
@ -1,120 +1,14 @@
|
|||||||
import json
|
import json
|
||||||
from typing import Dict, Sequence, List, Tuple, Union
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.postgres.forms import SimpleArrayField
|
|
||||||
|
|
||||||
from utilities.choices import ColorChoices
|
|
||||||
from .utils import add_blank_choice, parse_numeric_range
|
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
'APISelect',
|
'APISelect',
|
||||||
'APISelectMultiple',
|
'APISelectMultiple',
|
||||||
'BulkEditNullBooleanSelect',
|
|
||||||
'ClearableFileInput',
|
|
||||||
'ColorSelect',
|
|
||||||
'DatePicker',
|
|
||||||
'DateTimePicker',
|
|
||||||
'HTMXSelect',
|
|
||||||
'MarkdownWidget',
|
|
||||||
'NumericArrayField',
|
|
||||||
'SelectDurationWidget',
|
|
||||||
'SelectSpeedWidget',
|
|
||||||
'SelectWithPK',
|
|
||||||
'SlugWidget',
|
|
||||||
'TimePicker',
|
|
||||||
)
|
)
|
||||||
|
|
||||||
JSONPrimitive = Union[str, bool, int, float, None]
|
|
||||||
QueryParamValue = Union[JSONPrimitive, Sequence[JSONPrimitive]]
|
|
||||||
QueryParam = Dict[str, QueryParamValue]
|
|
||||||
ProcessedParams = Sequence[Dict[str, Sequence[JSONPrimitive]]]
|
|
||||||
|
|
||||||
|
|
||||||
class SlugWidget(forms.TextInput):
|
|
||||||
"""
|
|
||||||
Subclass TextInput and add a slug regeneration button next to the form field.
|
|
||||||
"""
|
|
||||||
template_name = 'widgets/sluginput.html'
|
|
||||||
|
|
||||||
|
|
||||||
class ColorSelect(forms.Select):
|
|
||||||
"""
|
|
||||||
Extends the built-in Select widget to colorize each <option>.
|
|
||||||
"""
|
|
||||||
option_template_name = 'widgets/colorselect_option.html'
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
kwargs['choices'] = add_blank_choice(ColorChoices)
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.attrs['class'] = 'netbox-color-select'
|
|
||||||
|
|
||||||
|
|
||||||
class BulkEditNullBooleanSelect(forms.NullBooleanSelect):
|
|
||||||
"""
|
|
||||||
A Select widget for NullBooleanFields
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
|
|
||||||
# Override the built-in choice labels
|
|
||||||
self.choices = (
|
|
||||||
('1', '---------'),
|
|
||||||
('2', 'Yes'),
|
|
||||||
('3', 'No'),
|
|
||||||
)
|
|
||||||
self.attrs['class'] = 'netbox-static-select'
|
|
||||||
|
|
||||||
|
|
||||||
class SelectWithPK(forms.Select):
|
|
||||||
"""
|
|
||||||
Include the primary key of each option in the option label (e.g. "Router7 (4721)").
|
|
||||||
"""
|
|
||||||
option_template_name = 'widgets/select_option_with_pk.html'
|
|
||||||
|
|
||||||
|
|
||||||
class SelectSpeedWidget(forms.NumberInput):
|
|
||||||
"""
|
|
||||||
Speed field with dropdown selections for convenience.
|
|
||||||
"""
|
|
||||||
template_name = 'widgets/select_speed.html'
|
|
||||||
|
|
||||||
|
|
||||||
class SelectDurationWidget(forms.NumberInput):
|
|
||||||
"""
|
|
||||||
Dropdown to select one of several common options for a time duration (in minutes).
|
|
||||||
"""
|
|
||||||
template_name = 'widgets/select_duration.html'
|
|
||||||
|
|
||||||
|
|
||||||
class MarkdownWidget(forms.Textarea):
|
|
||||||
template_name = 'widgets/markdown_input.html'
|
|
||||||
|
|
||||||
|
|
||||||
class NumericArrayField(SimpleArrayField):
|
|
||||||
|
|
||||||
def clean(self, value):
|
|
||||||
if value and not self.to_python(value):
|
|
||||||
raise forms.ValidationError(f'Invalid list ({value}). '
|
|
||||||
f'Must be numeric and ranges must be in ascending order')
|
|
||||||
return super().clean(value)
|
|
||||||
|
|
||||||
def to_python(self, value):
|
|
||||||
if not value:
|
|
||||||
return []
|
|
||||||
if isinstance(value, str):
|
|
||||||
value = ','.join([str(n) for n in parse_numeric_range(value)])
|
|
||||||
return super().to_python(value)
|
|
||||||
|
|
||||||
|
|
||||||
class ClearableFileInput(forms.ClearableFileInput):
|
|
||||||
"""
|
|
||||||
Override Django's stock ClearableFileInput with a custom template.
|
|
||||||
"""
|
|
||||||
template_name = 'widgets/clearable_file_input.html'
|
|
||||||
|
|
||||||
|
|
||||||
class APISelect(forms.Select):
|
class APISelect(forms.Select):
|
||||||
"""
|
"""
|
||||||
@ -144,7 +38,7 @@ class APISelect(forms.Select):
|
|||||||
result.static_params = {}
|
result.static_params = {}
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _process_query_param(self, key: str, value: JSONPrimitive) -> None:
|
def _process_query_param(self, key, value) -> None:
|
||||||
"""
|
"""
|
||||||
Based on query param value's type and value, update instance's dynamic/static params.
|
Based on query param value's type and value, update instance's dynamic/static params.
|
||||||
"""
|
"""
|
||||||
@ -187,7 +81,7 @@ class APISelect(forms.Select):
|
|||||||
else:
|
else:
|
||||||
self.static_params[key] = [value]
|
self.static_params[key] = [value]
|
||||||
|
|
||||||
def _process_query_params(self, query_params: QueryParam) -> None:
|
def _process_query_params(self, query_params):
|
||||||
"""
|
"""
|
||||||
Process an entire query_params dictionary, and handle primitive or list values.
|
Process an entire query_params dictionary, and handle primitive or list values.
|
||||||
"""
|
"""
|
||||||
@ -199,7 +93,7 @@ class APISelect(forms.Select):
|
|||||||
else:
|
else:
|
||||||
self._process_query_param(key, value)
|
self._process_query_param(key, value)
|
||||||
|
|
||||||
def _serialize_params(self, key: str, params: ProcessedParams) -> None:
|
def _serialize_params(self, key, params):
|
||||||
"""
|
"""
|
||||||
Serialize dynamic or static query params to JSON and add the serialized value to
|
Serialize dynamic or static query params to JSON and add the serialized value to
|
||||||
the widget attributes by `key`.
|
the widget attributes by `key`.
|
||||||
@ -214,7 +108,7 @@ class APISelect(forms.Select):
|
|||||||
# attributes to HTML elements and parsed on the client.
|
# attributes to HTML elements and parsed on the client.
|
||||||
self.attrs[key] = json.dumps([*current, *params], separators=(',', ':'))
|
self.attrs[key] = json.dumps([*current, *params], separators=(',', ':'))
|
||||||
|
|
||||||
def _add_dynamic_params(self) -> None:
|
def _add_dynamic_params(self):
|
||||||
"""
|
"""
|
||||||
Convert post-processed dynamic query params to data structure expected by front-
|
Convert post-processed dynamic query params to data structure expected by front-
|
||||||
end, serialize the value to JSON, and add it to the widget attributes.
|
end, serialize the value to JSON, and add it to the widget attributes.
|
||||||
@ -227,7 +121,7 @@ class APISelect(forms.Select):
|
|||||||
except IndexError as error:
|
except IndexError as error:
|
||||||
raise RuntimeError(f"Missing required value for dynamic query param: '{self.dynamic_params}'") from error
|
raise RuntimeError(f"Missing required value for dynamic query param: '{self.dynamic_params}'") from error
|
||||||
|
|
||||||
def _add_static_params(self) -> None:
|
def _add_static_params(self):
|
||||||
"""
|
"""
|
||||||
Convert post-processed static query params to data structure expected by front-
|
Convert post-processed static query params to data structure expected by front-
|
||||||
end, serialize the value to JSON, and add it to the widget attributes.
|
end, serialize the value to JSON, and add it to the widget attributes.
|
||||||
@ -240,7 +134,7 @@ class APISelect(forms.Select):
|
|||||||
except IndexError as error:
|
except IndexError as error:
|
||||||
raise RuntimeError(f"Missing required value for static query param: '{self.static_params}'") from error
|
raise RuntimeError(f"Missing required value for static query param: '{self.static_params}'") from error
|
||||||
|
|
||||||
def add_query_params(self, query_params: QueryParam) -> None:
|
def add_query_params(self, query_params):
|
||||||
"""
|
"""
|
||||||
Proccess & add a dictionary of URL query parameters to the widget attributes.
|
Proccess & add a dictionary of URL query parameters to the widget attributes.
|
||||||
"""
|
"""
|
||||||
@ -251,7 +145,7 @@ class APISelect(forms.Select):
|
|||||||
# Add processed static parameters to widget attributes.
|
# Add processed static parameters to widget attributes.
|
||||||
self._add_static_params()
|
self._add_static_params()
|
||||||
|
|
||||||
def add_query_param(self, key: str, value: QueryParamValue) -> None:
|
def add_query_param(self, key, value) -> None:
|
||||||
"""
|
"""
|
||||||
Process & add a key/value pair of URL query parameters to the widget attributes.
|
Process & add a key/value pair of URL query parameters to the widget attributes.
|
||||||
"""
|
"""
|
||||||
@ -264,49 +158,3 @@ class APISelectMultiple(APISelect, forms.SelectMultiple):
|
|||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.attrs['data-multiple'] = 1
|
self.attrs['data-multiple'] = 1
|
||||||
|
|
||||||
|
|
||||||
class DatePicker(forms.TextInput):
|
|
||||||
"""
|
|
||||||
Date picker using Flatpickr.
|
|
||||||
"""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.attrs['class'] = 'date-picker'
|
|
||||||
self.attrs['placeholder'] = 'YYYY-MM-DD'
|
|
||||||
|
|
||||||
|
|
||||||
class DateTimePicker(forms.TextInput):
|
|
||||||
"""
|
|
||||||
DateTime picker using Flatpickr.
|
|
||||||
"""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.attrs['class'] = 'datetime-picker'
|
|
||||||
self.attrs['placeholder'] = 'YYYY-MM-DD hh:mm:ss'
|
|
||||||
|
|
||||||
|
|
||||||
class TimePicker(forms.TextInput):
|
|
||||||
"""
|
|
||||||
Time picker using Flatpickr.
|
|
||||||
"""
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.attrs['class'] = 'time-picker'
|
|
||||||
self.attrs['placeholder'] = 'hh:mm:ss'
|
|
||||||
|
|
||||||
|
|
||||||
class HTMXSelect(forms.Select):
|
|
||||||
"""
|
|
||||||
Selection widget that will re-generate the HTML form upon the selection of a new option.
|
|
||||||
"""
|
|
||||||
def __init__(self, hx_url='.', hx_target_id='form_fields', attrs=None, **kwargs):
|
|
||||||
_attrs = {
|
|
||||||
'hx-get': hx_url,
|
|
||||||
'hx-include': f'#{hx_target_id}',
|
|
||||||
'hx-target': f'#{hx_target_id}',
|
|
||||||
}
|
|
||||||
if attrs:
|
|
||||||
_attrs.update(attrs)
|
|
||||||
|
|
||||||
super().__init__(attrs=_attrs, **kwargs)
|
|
24
netbox/utilities/forms/widgets/array.py
Normal file
24
netbox/utilities/forms/widgets/array.py
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
from django import forms
|
||||||
|
from django.contrib.postgres.forms import SimpleArrayField
|
||||||
|
|
||||||
|
from ..utils import parse_numeric_range
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'NumericArrayField',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NumericArrayField(SimpleArrayField):
|
||||||
|
|
||||||
|
def clean(self, value):
|
||||||
|
if value and not self.to_python(value):
|
||||||
|
raise forms.ValidationError(f'Invalid list ({value}). '
|
||||||
|
f'Must be numeric and ranges must be in ascending order')
|
||||||
|
return super().clean(value)
|
||||||
|
|
||||||
|
def to_python(self, value):
|
||||||
|
if not value:
|
||||||
|
return []
|
||||||
|
if isinstance(value, str):
|
||||||
|
value = ','.join([str(n) for n in parse_numeric_range(value)])
|
||||||
|
return super().to_python(value)
|
37
netbox/utilities/forms/widgets/datetime.py
Normal file
37
netbox/utilities/forms/widgets/datetime.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
from django import forms
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'DatePicker',
|
||||||
|
'DateTimePicker',
|
||||||
|
'TimePicker',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class DatePicker(forms.TextInput):
|
||||||
|
"""
|
||||||
|
Date picker using Flatpickr.
|
||||||
|
"""
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.attrs['class'] = 'date-picker'
|
||||||
|
self.attrs['placeholder'] = 'YYYY-MM-DD'
|
||||||
|
|
||||||
|
|
||||||
|
class DateTimePicker(forms.TextInput):
|
||||||
|
"""
|
||||||
|
DateTime picker using Flatpickr.
|
||||||
|
"""
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.attrs['class'] = 'datetime-picker'
|
||||||
|
self.attrs['placeholder'] = 'YYYY-MM-DD hh:mm:ss'
|
||||||
|
|
||||||
|
|
||||||
|
class TimePicker(forms.TextInput):
|
||||||
|
"""
|
||||||
|
Time picker using Flatpickr.
|
||||||
|
"""
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.attrs['class'] = 'time-picker'
|
||||||
|
self.attrs['placeholder'] = 'hh:mm:ss'
|
28
netbox/utilities/forms/widgets/misc.py
Normal file
28
netbox/utilities/forms/widgets/misc.py
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
from django import forms
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'ClearableFileInput',
|
||||||
|
'MarkdownWidget',
|
||||||
|
'SlugWidget',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ClearableFileInput(forms.ClearableFileInput):
|
||||||
|
"""
|
||||||
|
Override Django's stock ClearableFileInput with a custom template.
|
||||||
|
"""
|
||||||
|
template_name = 'widgets/clearable_file_input.html'
|
||||||
|
|
||||||
|
|
||||||
|
class MarkdownWidget(forms.Textarea):
|
||||||
|
"""
|
||||||
|
Provide a live preview for Markdown-formatted content.
|
||||||
|
"""
|
||||||
|
template_name = 'widgets/markdown_input.html'
|
||||||
|
|
||||||
|
|
||||||
|
class SlugWidget(forms.TextInput):
|
||||||
|
"""
|
||||||
|
Subclass TextInput and add a slug regeneration button next to the form field.
|
||||||
|
"""
|
||||||
|
template_name = 'widgets/sluginput.html'
|
79
netbox/utilities/forms/widgets/select.py
Normal file
79
netbox/utilities/forms/widgets/select.py
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
from django import forms
|
||||||
|
|
||||||
|
from utilities.choices import ColorChoices
|
||||||
|
from ..utils import add_blank_choice
|
||||||
|
|
||||||
|
__all__ = (
|
||||||
|
'BulkEditNullBooleanSelect',
|
||||||
|
'ColorSelect',
|
||||||
|
'HTMXSelect',
|
||||||
|
'SelectDurationWidget',
|
||||||
|
'SelectSpeedWidget',
|
||||||
|
'SelectWithPK',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class BulkEditNullBooleanSelect(forms.NullBooleanSelect):
|
||||||
|
"""
|
||||||
|
A Select widget for NullBooleanFields
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Override the built-in choice labels
|
||||||
|
self.choices = (
|
||||||
|
('1', '---------'),
|
||||||
|
('2', 'Yes'),
|
||||||
|
('3', 'No'),
|
||||||
|
)
|
||||||
|
self.attrs['class'] = 'netbox-static-select'
|
||||||
|
|
||||||
|
|
||||||
|
class ColorSelect(forms.Select):
|
||||||
|
"""
|
||||||
|
Extends the built-in Select widget to colorize each <option>.
|
||||||
|
"""
|
||||||
|
option_template_name = 'widgets/colorselect_option.html'
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
kwargs['choices'] = add_blank_choice(ColorChoices)
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.attrs['class'] = 'netbox-color-select'
|
||||||
|
|
||||||
|
|
||||||
|
class HTMXSelect(forms.Select):
|
||||||
|
"""
|
||||||
|
Selection widget that will re-generate the HTML form upon the selection of a new option.
|
||||||
|
"""
|
||||||
|
def __init__(self, hx_url='.', hx_target_id='form_fields', attrs=None, **kwargs):
|
||||||
|
_attrs = {
|
||||||
|
'hx-get': hx_url,
|
||||||
|
'hx-include': f'#{hx_target_id}',
|
||||||
|
'hx-target': f'#{hx_target_id}',
|
||||||
|
}
|
||||||
|
if attrs:
|
||||||
|
_attrs.update(attrs)
|
||||||
|
|
||||||
|
super().__init__(attrs=_attrs, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class SelectWithPK(forms.Select):
|
||||||
|
"""
|
||||||
|
Include the primary key of each option in the option label (e.g. "Router7 (4721)").
|
||||||
|
"""
|
||||||
|
option_template_name = 'widgets/select_option_with_pk.html'
|
||||||
|
|
||||||
|
|
||||||
|
class SelectDurationWidget(forms.NumberInput):
|
||||||
|
"""
|
||||||
|
Dropdown to select one of several common options for a time duration (in minutes).
|
||||||
|
"""
|
||||||
|
template_name = 'widgets/select_duration.html'
|
||||||
|
|
||||||
|
|
||||||
|
class SelectSpeedWidget(forms.NumberInput):
|
||||||
|
"""
|
||||||
|
Speed field with dropdown selections for convenience.
|
||||||
|
"""
|
||||||
|
template_name = 'widgets/select_speed.html'
|
Loading…
Reference in New Issue
Block a user