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