diff --git a/netbox/account/views.py b/netbox/account/views.py
index 3dbba9b29..40ce78039 100644
--- a/netbox/account/views.py
+++ b/netbox/account/views.py
@@ -2,8 +2,8 @@ import logging
from django.conf import settings
from django.contrib import messages
-from django.contrib.auth import login as auth_login, logout as auth_logout
-from django.contrib.auth import update_session_auth_hash
+from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
+from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import update_last_login
from django.contrib.auth.signals import user_logged_in
@@ -72,7 +72,7 @@ class LoginView(View):
return auth_backends
def get(self, request):
- form = forms.LoginForm(request)
+ form = AuthenticationForm(request)
if request.user.is_authenticated:
logger = logging.getLogger('netbox.auth.login')
@@ -85,7 +85,7 @@ class LoginView(View):
def post(self, request):
logger = logging.getLogger('netbox.auth.login')
- form = forms.LoginForm(request, data=request.POST)
+ form = AuthenticationForm(request, data=request.POST)
if form.is_valid():
logger.debug("Login form validation was successful")
@@ -220,7 +220,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
return redirect('account:profile')
- form = forms.PasswordChangeForm(user=request.user)
+ form = PasswordChangeForm(user=request.user)
return render(request, self.template_name, {
'form': form,
@@ -228,7 +228,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
})
def post(self, request):
- form = forms.PasswordChangeForm(user=request.user, data=request.POST)
+ form = PasswordChangeForm(user=request.user, data=request.POST)
if form.is_valid():
form.save()
update_session_auth_hash(request, form.user)
diff --git a/netbox/circuits/forms/bulk_import.py b/netbox/circuits/forms/bulk_import.py
index 0c30e3cda..8127d5bcb 100644
--- a/netbox/circuits/forms/bulk_import.py
+++ b/netbox/circuits/forms/bulk_import.py
@@ -7,7 +7,6 @@ from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant
-from utilities.forms import BootstrapMixin
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
__all__ = (
@@ -112,7 +111,7 @@ class CircuitImportForm(NetBoxModelImportForm):
]
-class CircuitTerminationImportForm(BootstrapMixin, forms.ModelForm):
+class CircuitTerminationImportForm(forms.ModelForm):
site = CSVModelChoiceField(
label=_('Site'),
queryset=Site.objects.all(),
diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py
index 652728734..52fa3a608 100644
--- a/netbox/core/forms/model_forms.py
+++ b/netbox/core/forms/model_forms.py
@@ -11,7 +11,7 @@ from netbox.config import get_config, PARAMS
from netbox.forms import NetBoxModelForm
from netbox.registry import registry
from netbox.utils import get_data_backend_choices
-from utilities.forms import BootstrapMixin, get_field_value
+from utilities.forms import get_field_value
from utilities.forms.fields import CommentField
from utilities.forms.widgets import HTMXSelect
@@ -138,7 +138,7 @@ class ConfigFormMetaclass(forms.models.ModelFormMetaclass):
return super().__new__(mcs, name, bases, attrs)
-class ConfigRevisionForm(BootstrapMixin, forms.ModelForm, metaclass=ConfigFormMetaclass):
+class ConfigRevisionForm(forms.ModelForm, metaclass=ConfigFormMetaclass):
"""
Form for creating a new ConfigRevision.
"""
diff --git a/netbox/dcim/forms/bulk_create.py b/netbox/dcim/forms/bulk_create.py
index 2a84a9a51..2939b986e 100644
--- a/netbox/dcim/forms/bulk_create.py
+++ b/netbox/dcim/forms/bulk_create.py
@@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _
from dcim.models import *
from extras.models import Tag
from netbox.forms.mixins import CustomFieldsMixin
-from utilities.forms import BootstrapMixin, form_from_model
+from utilities.forms import form_from_model
from utilities.forms.fields import DynamicModelMultipleChoiceField, ExpandableNameField
from .object_create import ComponentCreateForm
@@ -26,7 +26,7 @@ __all__ = (
# Device components
#
-class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentCreateForm):
+class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm):
pk = forms.ModelMultipleChoiceField(
queryset=Device.objects.all(),
widget=forms.MultipleHiddenInput()
diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py
index da3a2bea4..fdb5f1d5a 100644
--- a/netbox/dcim/forms/model_forms.py
+++ b/netbox/dcim/forms/model_forms.py
@@ -11,7 +11,7 @@ from extras.models import ConfigTemplate
from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
-from utilities.forms import BootstrapMixin, add_blank_choice
+from utilities.forms import add_blank_choice
from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField,
NumericArrayField, SlugField,
@@ -748,7 +748,7 @@ class DeviceVCMembershipForm(forms.ModelForm):
return vc_position
-class VCMemberSelectForm(BootstrapMixin, forms.Form):
+class VCMemberSelectForm(forms.Form):
device = DynamicModelChoiceField(
label=_('Device'),
queryset=Device.objects.all(),
@@ -771,7 +771,7 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form):
# Device component templates
#
-class ComponentTemplateForm(BootstrapMixin, forms.ModelForm):
+class ComponentTemplateForm(forms.ModelForm):
device_type = DynamicModelChoiceField(
label=_('Device type'),
queryset=DeviceType.objects.all()
@@ -1272,7 +1272,7 @@ class DeviceBayForm(DeviceComponentForm):
]
-class PopulateDeviceBayForm(BootstrapMixin, forms.Form):
+class PopulateDeviceBayForm(forms.Form):
installed_device = forms.ModelChoiceField(
queryset=Device.objects.all(),
label=_('Child Device'),
diff --git a/netbox/dcim/forms/object_import.py b/netbox/dcim/forms/object_import.py
index bab8876da..d46ef83ad 100644
--- a/netbox/dcim/forms/object_import.py
+++ b/netbox/dcim/forms/object_import.py
@@ -3,7 +3,6 @@ from django.utils.translation import gettext_lazy as _
from dcim.choices import InterfacePoEModeChoices, InterfacePoETypeChoices, InterfaceTypeChoices, PortTypeChoices
from dcim.models import *
-from utilities.forms import BootstrapMixin
from wireless.choices import WirelessRoleChoices
__all__ = (
@@ -24,11 +23,7 @@ __all__ = (
# Component template import forms
#
-class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm):
- pass
-
-
-class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
+class ConsolePortTemplateImportForm(forms.ModelForm):
class Meta:
model = ConsolePortTemplate
@@ -37,7 +32,7 @@ class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
]
-class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
+class ConsoleServerPortTemplateImportForm(forms.ModelForm):
class Meta:
model = ConsoleServerPortTemplate
@@ -46,7 +41,7 @@ class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
]
-class PowerPortTemplateImportForm(ComponentTemplateImportForm):
+class PowerPortTemplateImportForm(forms.ModelForm):
class Meta:
model = PowerPortTemplate
@@ -55,7 +50,7 @@ class PowerPortTemplateImportForm(ComponentTemplateImportForm):
]
-class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
+class PowerOutletTemplateImportForm(forms.ModelForm):
power_port = forms.ModelChoiceField(
label=_('Power port'),
queryset=PowerPortTemplate.objects.all(),
@@ -84,7 +79,7 @@ class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
return module_type
-class InterfaceTemplateImportForm(ComponentTemplateImportForm):
+class InterfaceTemplateImportForm(forms.ModelForm):
type = forms.ChoiceField(
label=_('Type'),
choices=InterfaceTypeChoices.CHOICES
@@ -113,7 +108,7 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
]
-class FrontPortTemplateImportForm(ComponentTemplateImportForm):
+class FrontPortTemplateImportForm(forms.ModelForm):
type = forms.ChoiceField(
label=_('Type'),
choices=PortTypeChoices.CHOICES
@@ -145,7 +140,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
]
-class RearPortTemplateImportForm(ComponentTemplateImportForm):
+class RearPortTemplateImportForm(forms.ModelForm):
type = forms.ChoiceField(
label=_('Type'),
choices=PortTypeChoices.CHOICES
@@ -158,7 +153,7 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm):
]
-class ModuleBayTemplateImportForm(ComponentTemplateImportForm):
+class ModuleBayTemplateImportForm(forms.ModelForm):
class Meta:
model = ModuleBayTemplate
@@ -167,7 +162,7 @@ class ModuleBayTemplateImportForm(ComponentTemplateImportForm):
]
-class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
+class DeviceBayTemplateImportForm(forms.ModelForm):
class Meta:
model = DeviceBayTemplate
@@ -176,7 +171,7 @@ class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
]
-class InventoryItemTemplateImportForm(ComponentTemplateImportForm):
+class InventoryItemTemplateImportForm(forms.ModelForm):
parent = forms.ModelChoiceField(
label=_('Parent'),
queryset=InventoryItemTemplate.objects.all(),
diff --git a/netbox/extras/dashboard/forms.py b/netbox/extras/dashboard/forms.py
index ab708228c..fd3492726 100644
--- a/netbox/extras/dashboard/forms.py
+++ b/netbox/extras/dashboard/forms.py
@@ -4,7 +4,7 @@ from django.utils.translation import gettext as _
from extras.choices import DashboardWidgetColorChoices
from netbox.registry import registry
-from utilities.forms import BootstrapMixin, add_blank_choice
+from utilities.forms import add_blank_choice
__all__ = (
'DashboardWidgetAddForm',
@@ -16,7 +16,7 @@ def get_widget_choices():
return registry['widgets'].items()
-class DashboardWidgetForm(BootstrapMixin, forms.Form):
+class DashboardWidgetForm(forms.Form):
title = forms.CharField(
required=False
)
diff --git a/netbox/extras/dashboard/widgets.py b/netbox/extras/dashboard/widgets.py
index 8cfbb4c61..7503b7077 100644
--- a/netbox/extras/dashboard/widgets.py
+++ b/netbox/extras/dashboard/widgets.py
@@ -15,7 +15,6 @@ from django.utils.translation import gettext as _
from core.models import ContentType
from extras.choices import BookmarkOrderingChoices
from utilities.choices import ButtonColorChoices
-from utilities.forms import BootstrapMixin
from utilities.permissions import get_permission_for_model
from utilities.templatetags.builtins.filters import render_markdown
from utilities.utils import content_type_identifier, content_type_name, dict_to_querydict, get_viewname
@@ -58,7 +57,7 @@ def get_models_from_content_types(content_types):
return models
-class WidgetConfigForm(BootstrapMixin, forms.Form):
+class WidgetConfigForm(forms.Form):
pass
diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py
index 346225c8a..5495bc87e 100644
--- a/netbox/extras/forms/model_forms.py
+++ b/netbox/extras/forms/model_forms.py
@@ -13,7 +13,7 @@ from extras.choices import *
from extras.models import *
from netbox.forms import NetBoxModelForm
from tenancy.models import Tenant, TenantGroup
-from utilities.forms import BootstrapMixin, add_blank_choice, get_field_value
+from utilities.forms import add_blank_choice, get_field_value
from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, JSONField, SlugField,
@@ -38,7 +38,7 @@ __all__ = (
)
-class CustomFieldForm(BootstrapMixin, forms.ModelForm):
+class CustomFieldForm(forms.ModelForm):
content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
queryset=ContentType.objects.with_feature('custom_fields')
@@ -83,7 +83,7 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
self.fields['type'].disabled = True
-class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm):
+class CustomFieldChoiceSetForm(forms.ModelForm):
extra_choices = forms.CharField(
widget=ChoicesWidget(),
required=False,
@@ -122,7 +122,7 @@ class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm):
return data
-class CustomLinkForm(BootstrapMixin, forms.ModelForm):
+class CustomLinkForm(forms.ModelForm):
content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
queryset=ContentType.objects.with_feature('custom_links')
@@ -149,7 +149,7 @@ class CustomLinkForm(BootstrapMixin, forms.ModelForm):
}
-class ExportTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
+class ExportTemplateForm(SyncedDataMixin, forms.ModelForm):
content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
queryset=ContentType.objects.with_feature('export_templates')
@@ -189,7 +189,7 @@ class ExportTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
return self.cleaned_data
-class SavedFilterForm(BootstrapMixin, forms.ModelForm):
+class SavedFilterForm(forms.ModelForm):
slug = SlugField()
content_types = ContentTypeMultipleChoiceField(
label=_('Content types'),
@@ -216,7 +216,7 @@ class SavedFilterForm(BootstrapMixin, forms.ModelForm):
super().__init__(*args, initial=initial, **kwargs)
-class BookmarkForm(BootstrapMixin, forms.ModelForm):
+class BookmarkForm(forms.ModelForm):
object_type = ContentTypeChoiceField(
label=_('Object type'),
queryset=ContentType.objects.with_feature('bookmarks')
@@ -367,7 +367,7 @@ class EventRuleForm(NetBoxModelForm):
return super().save(*args, **kwargs)
-class TagForm(BootstrapMixin, forms.ModelForm):
+class TagForm(forms.ModelForm):
slug = SlugField()
object_types = ContentTypeMultipleChoiceField(
label=_('Object types'),
@@ -386,7 +386,7 @@ class TagForm(BootstrapMixin, forms.ModelForm):
]
-class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
+class ConfigContextForm(SyncedDataMixin, forms.ModelForm):
regions = DynamicModelMultipleChoiceField(
label=_('Regions'),
queryset=Region.objects.all(),
@@ -497,7 +497,7 @@ class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
return self.cleaned_data
-class ConfigTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
+class ConfigTemplateForm(SyncedDataMixin, forms.ModelForm):
tags = DynamicModelMultipleChoiceField(
label=_('Tags'),
queryset=Tag.objects.all(),
@@ -541,7 +541,7 @@ class ConfigTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
return self.cleaned_data
-class ImageAttachmentForm(BootstrapMixin, forms.ModelForm):
+class ImageAttachmentForm(forms.ModelForm):
class Meta:
model = ImageAttachment
diff --git a/netbox/extras/forms/reports.py b/netbox/extras/forms/reports.py
index 0ff7d1509..ad37eb744 100644
--- a/netbox/extras/forms/reports.py
+++ b/netbox/extras/forms/reports.py
@@ -2,7 +2,6 @@ from django import forms
from django.utils.translation import gettext_lazy as _
from extras.choices import DurationChoices
-from utilities.forms import BootstrapMixin
from utilities.forms.widgets import DateTimePicker, NumberWithOptions
from utilities.utils import local_now
@@ -11,7 +10,7 @@ __all__ = (
)
-class ReportForm(BootstrapMixin, forms.Form):
+class ReportForm(forms.Form):
schedule_at = forms.DateTimeField(
required=False,
widget=DateTimePicker(),
diff --git a/netbox/extras/forms/scripts.py b/netbox/extras/forms/scripts.py
index c555747ae..f67ad3e75 100644
--- a/netbox/extras/forms/scripts.py
+++ b/netbox/extras/forms/scripts.py
@@ -2,7 +2,6 @@ from django import forms
from django.utils.translation import gettext_lazy as _
from extras.choices import DurationChoices
-from utilities.forms import BootstrapMixin
from utilities.forms.widgets import DateTimePicker, NumberWithOptions
from utilities.utils import local_now
@@ -11,7 +10,7 @@ __all__ = (
)
-class ScriptForm(BootstrapMixin, forms.Form):
+class ScriptForm(forms.Form):
_commit = forms.BooleanField(
required=False,
initial=True,
diff --git a/netbox/ipam/forms/bulk_create.py b/netbox/ipam/forms/bulk_create.py
index ea553c655..856476786 100644
--- a/netbox/ipam/forms/bulk_create.py
+++ b/netbox/ipam/forms/bulk_create.py
@@ -1,7 +1,6 @@
from django import forms
from django.utils.translation import gettext_lazy as _
-from utilities.forms import BootstrapMixin
from utilities.forms.fields import ExpandableIPAddressField
__all__ = (
@@ -9,7 +8,7 @@ __all__ = (
)
-class IPAddressBulkCreateForm(BootstrapMixin, forms.Form):
+class IPAddressBulkCreateForm(forms.Form):
pattern = ExpandableIPAddressField(
label=_('Address pattern')
)
diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py
index 6c445ef27..0e2250d2d 100644
--- a/netbox/ipam/forms/model_forms.py
+++ b/netbox/ipam/forms/model_forms.py
@@ -11,7 +11,7 @@ from ipam.models import *
from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm
from utilities.exceptions import PermissionsViolation
-from utilities.forms import BootstrapMixin, add_blank_choice
+from utilities.forms import add_blank_choice
from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField,
SlugField,
@@ -419,7 +419,7 @@ class IPAddressBulkAddForm(TenancyForm, NetBoxModelForm):
]
-class IPAddressAssignForm(BootstrapMixin, forms.Form):
+class IPAddressAssignForm(forms.Form):
vrf_id = DynamicModelChoiceField(
queryset=VRF.objects.all(),
required=False,
@@ -504,7 +504,7 @@ class FHRPGroupForm(NetBoxModelForm):
})
-class FHRPGroupAssignmentForm(BootstrapMixin, forms.ModelForm):
+class FHRPGroupAssignmentForm(forms.ModelForm):
group = DynamicModelChoiceField(
label=_('Group'),
queryset=FHRPGroup.objects.all()
@@ -738,7 +738,6 @@ class ServiceCreateForm(ServiceForm):
# Fields which may be populated from a ServiceTemplate are not required
for field in ('name', 'protocol', 'ports'):
self.fields[field].required = False
- del self.fields[field].widget.attrs['required']
def clean(self):
super().clean()
diff --git a/netbox/netbox/forms/__init__.py b/netbox/netbox/forms/__init__.py
index 65460ebf1..fa82689a5 100644
--- a/netbox/netbox/forms/__init__.py
+++ b/netbox/netbox/forms/__init__.py
@@ -5,7 +5,6 @@ from django.utils.translation import gettext as _
from netbox.search import LookupTypes
from netbox.search.backends import search_backend
-from utilities.forms import BootstrapMixin
from .base import *
@@ -18,7 +17,7 @@ LOOKUP_CHOICES = (
)
-class SearchForm(BootstrapMixin, forms.Form):
+class SearchForm(forms.Form):
q = forms.CharField(
label=_('Search'),
widget=forms.TextInput(
diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py
index 0b0e2036e..7e1eaa80c 100644
--- a/netbox/netbox/forms/base.py
+++ b/netbox/netbox/forms/base.py
@@ -7,7 +7,7 @@ from extras.choices import *
from extras.models import CustomField, Tag
from utilities.forms import CSVModelForm
from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField
-from utilities.forms.mixins import BootstrapMixin, CheckLastUpdatedMixin
+from utilities.forms.mixins import CheckLastUpdatedMixin
from .mixins import CustomFieldsMixin, SavedFiltersMixin, TagsMixin
__all__ = (
@@ -18,7 +18,7 @@ __all__ = (
)
-class NetBoxModelForm(BootstrapMixin, CheckLastUpdatedMixin, CustomFieldsMixin, TagsMixin, forms.ModelForm):
+class NetBoxModelForm(CheckLastUpdatedMixin, CustomFieldsMixin, TagsMixin, forms.ModelForm):
"""
Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields.
@@ -96,7 +96,7 @@ class NetBoxModelImportForm(CSVModelForm, NetBoxModelForm):
return customfield.to_form_field(for_csv_import=True)
-class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
+class NetBoxModelBulkEditForm(CustomFieldsMixin, forms.Form):
"""
Base form for modifying multiple NetBox objects (of the same type) in bulk via the UI. Adds support for custom
fields and adding/removing tags.
@@ -146,7 +146,7 @@ class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
self.nullable_fields = (*self.nullable_fields, *nullable_custom_fields)
-class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, SavedFiltersMixin, forms.Form):
+class NetBoxModelFilterSetForm(CustomFieldsMixin, SavedFiltersMixin, forms.Form):
"""
Base form for FilerSet forms. These are used to filter object lists in the NetBox UI. Note that the
corresponding FilterSet *must* provide a `q` filter.
diff --git a/netbox/project-static/dist/netbox.css b/netbox/project-static/dist/netbox.css
index 2cf01fdfa..5fc874a09 100644
Binary files a/netbox/project-static/dist/netbox.css and b/netbox/project-static/dist/netbox.css differ
diff --git a/netbox/project-static/styles/overrides/_slim-select.scss b/netbox/project-static/styles/overrides/_slim-select.scss
index 419f765cd..e9f079d44 100644
--- a/netbox/project-static/styles/overrides/_slim-select.scss
+++ b/netbox/project-static/styles/overrides/_slim-select.scss
@@ -37,16 +37,6 @@ $spacing-s: $input-padding-x;
.ss-main {
color: $form-select-color;
- &.is-invalid .ss-single-selected,
- &.is-invalid .ss-multi-selected {
- border-color: $form-feedback-icon-invalid-color;
- }
-
- &.is-valid .ss-single-selected,
- &.is-valid .ss-multi-selected {
- border-color: $form-feedback-icon-valid-color;
- }
-
.ss-single-selected,
.ss-multi-selected {
padding: $form-select-padding-y $input-padding-x $form-select-padding-y $form-select-padding-x;
@@ -195,3 +185,11 @@ $spacing-s: $input-padding-x;
}
}
}
+
+// Apply red border for fields inside a row with .has-errors
+.has-errors {
+ .ss-single-selected,
+ .ss-multi-selected {
+ border-color: $red;
+ }
+}
diff --git a/netbox/project-static/styles/transitional/_forms.scss b/netbox/project-static/styles/transitional/_forms.scss
index e53bde667..a489d87de 100644
--- a/netbox/project-static/styles/transitional/_forms.scss
+++ b/netbox/project-static/styles/transitional/_forms.scss
@@ -16,3 +16,12 @@ form.object-edit {
content: '\f06C4';
}
}
+
+// Set red border on form fields inside a row with .has-errors
+.has-errors {
+ input,
+ select,
+ textarea {
+ border: 1px solid $red;
+ }
+}
diff --git a/netbox/templates/django/forms/widgets/attrs.html b/netbox/templates/django/forms/widgets/attrs.html
new file mode 100644
index 000000000..369ae0722
--- /dev/null
+++ b/netbox/templates/django/forms/widgets/attrs.html
@@ -0,0 +1,2 @@
+{# Skip "class" attribute, which needs to be handled on the widget directly. #}
+{% for name, value in widget.attrs.items %}{% if name != 'class' %}{% if value is not False %} {{ name }}{% if value is not True %}="{{ value|stringformat:'s' }}"{% endif %}{% endif %}{% endif %}{% endfor %}
diff --git a/netbox/templates/django/forms/widgets/checkbox.html b/netbox/templates/django/forms/widgets/checkbox.html
index 359657136..f769fce96 100644
--- a/netbox/templates/django/forms/widgets/checkbox.html
+++ b/netbox/templates/django/forms/widgets/checkbox.html
@@ -4,4 +4,4 @@
_selected_action to avoid breaking the admin UI.
{% endcomment %}
{% if widget.name != '_selected_action' %}{% endif %}
-{% include "django/forms/widgets/input.html" %}
+
diff --git a/netbox/templates/django/forms/widgets/clearable_file_input.html b/netbox/templates/django/forms/widgets/clearable_file_input.html
new file mode 100644
index 000000000..a84b941fa
--- /dev/null
+++ b/netbox/templates/django/forms/widgets/clearable_file_input.html
@@ -0,0 +1,5 @@
+{% if widget.is_initial %}{{ widget.initial_text }}: {{ widget.value }}{% if not widget.required %}
+
+{% endif %}
+{{ widget.input_text }}:{% endif %}
+
\ No newline at end of file
diff --git a/netbox/templates/django/forms/widgets/input.html b/netbox/templates/django/forms/widgets/input.html
new file mode 100644
index 000000000..dd6058328
--- /dev/null
+++ b/netbox/templates/django/forms/widgets/input.html
@@ -0,0 +1 @@
+
diff --git a/netbox/templates/django/forms/widgets/select.html b/netbox/templates/django/forms/widgets/select.html
new file mode 100644
index 000000000..0e87394e3
--- /dev/null
+++ b/netbox/templates/django/forms/widgets/select.html
@@ -0,0 +1,5 @@
+
\ No newline at end of file
diff --git a/netbox/templates/django/forms/widgets/textarea.html b/netbox/templates/django/forms/widgets/textarea.html
new file mode 100644
index 000000000..aed4dd3b0
--- /dev/null
+++ b/netbox/templates/django/forms/widgets/textarea.html
@@ -0,0 +1,2 @@
+
\ No newline at end of file
diff --git a/netbox/users/forms/__init__.py b/netbox/users/forms/__init__.py
index a545c3add..1499f98b2 100644
--- a/netbox/users/forms/__init__.py
+++ b/netbox/users/forms/__init__.py
@@ -1,4 +1,3 @@
-from .authentication import *
from .bulk_edit import *
from .bulk_import import *
from .filtersets import *
diff --git a/netbox/users/forms/authentication.py b/netbox/users/forms/authentication.py
deleted file mode 100644
index 2b540b752..000000000
--- a/netbox/users/forms/authentication.py
+++ /dev/null
@@ -1,25 +0,0 @@
-from django.contrib.auth.forms import (
- AuthenticationForm,
- PasswordChangeForm as DjangoPasswordChangeForm,
-)
-
-from utilities.forms import BootstrapMixin
-
-__all__ = (
- 'LoginForm',
- 'PasswordChangeForm',
-)
-
-
-class LoginForm(BootstrapMixin, AuthenticationForm):
- """
- Used to authenticate a user by username and password.
- """
- pass
-
-
-class PasswordChangeForm(BootstrapMixin, DjangoPasswordChangeForm):
- """
- This form enables a user to change his or her own password.
- """
- pass
diff --git a/netbox/users/forms/bulk_edit.py b/netbox/users/forms/bulk_edit.py
index 0e29109a4..f88f71bf0 100644
--- a/netbox/users/forms/bulk_edit.py
+++ b/netbox/users/forms/bulk_edit.py
@@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
from ipam.formfields import IPNetworkFormField
from ipam.validators import prefix_validator
from users.models import *
-from utilities.forms import BootstrapMixin, BulkEditForm
+from utilities.forms import BulkEditForm
from utilities.forms.widgets import BulkEditNullBooleanSelect, DateTimePicker
__all__ = (
@@ -15,7 +15,7 @@ __all__ = (
)
-class UserBulkEditForm(BootstrapMixin, forms.Form):
+class UserBulkEditForm(forms.Form):
pk = forms.ModelMultipleChoiceField(
queryset=NetBoxUser.objects.all(),
widget=forms.MultipleHiddenInput
@@ -53,7 +53,7 @@ class UserBulkEditForm(BootstrapMixin, forms.Form):
nullable_fields = ('first_name', 'last_name')
-class ObjectPermissionBulkEditForm(BootstrapMixin, forms.Form):
+class ObjectPermissionBulkEditForm(forms.Form):
pk = forms.ModelMultipleChoiceField(
queryset=ObjectPermission.objects.all(),
widget=forms.MultipleHiddenInput
diff --git a/netbox/users/forms/model_forms.py b/netbox/users/forms/model_forms.py
index 99320fa25..d5742587f 100644
--- a/netbox/users/forms/model_forms.py
+++ b/netbox/users/forms/model_forms.py
@@ -13,7 +13,6 @@ from ipam.validators import prefix_validator
from netbox.preferences import PREFERENCES
from users.constants import *
from users.models import *
-from utilities.forms import BootstrapMixin
from utilities.forms.fields import ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.widgets import DateTimePicker
from utilities.permissions import qs_filter_from_constraints
@@ -53,7 +52,7 @@ class UserConfigFormMetaclass(forms.models.ModelFormMetaclass):
return super().__new__(mcs, name, bases, attrs)
-class UserConfigForm(BootstrapMixin, forms.ModelForm, metaclass=UserConfigFormMetaclass):
+class UserConfigForm(forms.ModelForm, metaclass=UserConfigFormMetaclass):
fieldsets = (
(_('User Interface'), (
'locale.language',
@@ -109,7 +108,7 @@ class UserConfigForm(BootstrapMixin, forms.ModelForm, metaclass=UserConfigFormMe
]
-class UserTokenForm(BootstrapMixin, forms.ModelForm):
+class UserTokenForm(forms.ModelForm):
key = forms.CharField(
label=_('Key'),
help_text=_(
@@ -167,7 +166,7 @@ class TokenForm(UserTokenForm):
}
-class UserForm(BootstrapMixin, forms.ModelForm):
+class UserForm(forms.ModelForm):
password = forms.CharField(
label=_('Password'),
widget=forms.PasswordInput(),
@@ -214,9 +213,7 @@ class UserForm(BootstrapMixin, forms.ModelForm):
# Password fields are optional for existing Users
self.fields['password'].required = False
- self.fields['password'].widget.attrs.pop('required')
self.fields['confirm_password'].required = False
- self.fields['confirm_password'].widget.attrs.pop('required')
def save(self, *args, **kwargs):
instance = super().save(*args, **kwargs)
@@ -238,7 +235,7 @@ class UserForm(BootstrapMixin, forms.ModelForm):
raise forms.ValidationError(_("Passwords do not match! Please check your input and try again."))
-class GroupForm(BootstrapMixin, forms.ModelForm):
+class GroupForm(forms.ModelForm):
users = DynamicModelMultipleChoiceField(
label=_('Users'),
required=False,
@@ -281,7 +278,7 @@ class GroupForm(BootstrapMixin, forms.ModelForm):
return instance
-class ObjectPermissionForm(BootstrapMixin, forms.ModelForm):
+class ObjectPermissionForm(forms.ModelForm):
object_types = ContentTypeMultipleChoiceField(
label=_('Object types'),
queryset=ContentType.objects.all(),
diff --git a/netbox/utilities/forms/bulk_import.py b/netbox/utilities/forms/bulk_import.py
index 57362d3dd..8ef1f6123 100644
--- a/netbox/utilities/forms/bulk_import.py
+++ b/netbox/utilities/forms/bulk_import.py
@@ -10,10 +10,9 @@ from core.forms.mixins import SyncedDataMixin
from utilities.choices import CSVDelimiterChoices, ImportFormatChoices, ImportMethodChoices
from utilities.constants import CSV_DELIMITERS
from utilities.forms.utils import parse_csv
-from .mixins import BootstrapMixin
-class BulkImportForm(BootstrapMixin, SyncedDataMixin, forms.Form):
+class BulkImportForm(SyncedDataMixin, forms.Form):
import_method = forms.ChoiceField(
choices=ImportMethodChoices,
required=False
diff --git a/netbox/utilities/forms/forms.py b/netbox/utilities/forms/forms.py
index 54c9e41cb..9747de001 100644
--- a/netbox/utilities/forms/forms.py
+++ b/netbox/utilities/forms/forms.py
@@ -2,7 +2,6 @@ import re
from django import forms
from django.utils.translation import gettext as _
-from .mixins import BootstrapMixin
__all__ = (
'BulkEditForm',
@@ -14,7 +13,7 @@ __all__ = (
)
-class ConfirmationForm(BootstrapMixin, forms.Form):
+class ConfirmationForm(forms.Form):
"""
A generic confirmation form. The form is not valid unless the `confirm` field is checked.
"""
@@ -29,14 +28,14 @@ class ConfirmationForm(BootstrapMixin, forms.Form):
)
-class BulkEditForm(BootstrapMixin, forms.Form):
+class BulkEditForm(forms.Form):
"""
Provides bulk edit support for objects.
"""
nullable_fields = ()
-class BulkRenameForm(BootstrapMixin, forms.Form):
+class BulkRenameForm(forms.Form):
"""
An extendable form to be used for renaming objects in bulk.
"""
@@ -90,7 +89,7 @@ class CSVModelForm(forms.ModelForm):
return super().clean()
-class FilterForm(BootstrapMixin, forms.Form):
+class FilterForm(forms.Form):
"""
Base Form class for FilterSet forms.
"""
@@ -100,7 +99,7 @@ class FilterForm(BootstrapMixin, forms.Form):
)
-class TableConfigForm(BootstrapMixin, forms.Form):
+class TableConfigForm(forms.Form):
"""
Form for configuring user's table preferences.
"""
diff --git a/netbox/utilities/forms/mixins.py b/netbox/utilities/forms/mixins.py
index 2d6c20fcc..e89fbb520 100644
--- a/netbox/utilities/forms/mixins.py
+++ b/netbox/utilities/forms/mixins.py
@@ -3,68 +3,11 @@ import time
from django import forms
from django.utils.translation import gettext_lazy as _
-from .widgets import APISelect, APISelectMultiple, ClearableFileInput
-
__all__ = (
- 'BootstrapMixin',
'CheckLastUpdatedMixin',
)
-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) and 'size' in field.widget.attrs:
- # Use native Bootstrap class for multi-line