diff --git a/netbox/utilities/forms/__init__.py b/netbox/utilities/forms/__init__.py index ce958a99e..6b89d600b 100644 --- a/netbox/utilities/forms/__init__.py +++ b/netbox/utilities/forms/__init__.py @@ -1,5 +1,6 @@ from .constants import * from .fields import * from .forms import * +from .mixins import * from .utils import * from .widgets import * diff --git a/netbox/utilities/forms/fields/csv.py b/netbox/utilities/forms/fields/csv.py index f89f5f2ef..5d6258193 100644 --- a/netbox/utilities/forms/fields/csv.py +++ b/netbox/utilities/forms/fields/csv.py @@ -1,14 +1,9 @@ -import csv -from io import StringIO - from django import forms from django.contrib.contenttypes.models import ContentType from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist from django.db.models import Q -from django.utils.translation import gettext as _ from utilities.choices import unpack_grouped_choices -from utilities.forms.utils import parse_csv, validate_csv from utilities.utils import content_type_identifier __all__ = ( diff --git a/netbox/utilities/forms/forms.py b/netbox/utilities/forms/forms.py index b06fb2e48..b1427e5d2 100644 --- a/netbox/utilities/forms/forms.py +++ b/netbox/utilities/forms/forms.py @@ -2,11 +2,9 @@ import re from django import forms from django.utils.translation import gettext as _ - -from .widgets import APISelect, APISelectMultiple, ClearableFileInput +from .mixins import BootstrapMixin __all__ = ( - 'BootstrapMixin', 'BulkEditForm', 'BulkRenameForm', '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): """ Provides a hidden return URL field to control where the user is directed after the form is submitted. diff --git a/netbox/utilities/forms/mixins.py b/netbox/utilities/forms/mixins.py new file mode 100644 index 000000000..dc9c3eb80 --- /dev/null +++ b/netbox/utilities/forms/mixins.py @@ -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 diff --git a/netbox/utilities/forms/widgets/__init__.py b/netbox/utilities/forms/widgets/__init__.py new file mode 100644 index 000000000..3369f44c7 --- /dev/null +++ b/netbox/utilities/forms/widgets/__init__.py @@ -0,0 +1,5 @@ +from .apiselect import * +from .array import * +from .datetime import * +from .misc import * +from .select import * diff --git a/netbox/utilities/forms/widgets.py b/netbox/utilities/forms/widgets/apiselect.py similarity index 56% rename from netbox/utilities/forms/widgets.py rename to netbox/utilities/forms/widgets/apiselect.py index 7b20d00c9..e4b02cb1d 100644 --- a/netbox/utilities/forms/widgets.py +++ b/netbox/utilities/forms/widgets/apiselect.py @@ -1,120 +1,14 @@ import json -from typing import Dict, Sequence, List, Tuple, Union +from typing import Dict, List, Tuple from django import forms 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__ = ( 'APISelect', '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