Просмотр исходного кода

Closes #14740: Remove BootstrapMixin (#14841)

* Introduce custom form widget templates to apply CSS classes

* Apply both mandatory and optional CSS classes to form widgets

* Omit required & placeholder attrs

* Move annotation of field validation failures to CSS

* Remove BootstrapMixin class

* Remove obsolete ComponentTemplateImportForm class

* Remove obsolete custom forms for login & password change

* Clean up obsolete accommodations for 'required' widget attr
Jeremy Stretch 2 лет назад
Родитель
Сommit
da085e60c2
33 измененных файлов с 101 добавлено и 179 удалено
  1. 6 6
      netbox/account/views.py
  2. 1 2
      netbox/circuits/forms/bulk_import.py
  3. 2 2
      netbox/core/forms/model_forms.py
  4. 2 2
      netbox/dcim/forms/bulk_create.py
  5. 4 4
      netbox/dcim/forms/model_forms.py
  6. 10 15
      netbox/dcim/forms/object_import.py
  7. 2 2
      netbox/extras/dashboard/forms.py
  8. 1 2
      netbox/extras/dashboard/widgets.py
  9. 11 11
      netbox/extras/forms/model_forms.py
  10. 1 2
      netbox/extras/forms/reports.py
  11. 1 2
      netbox/extras/forms/scripts.py
  12. 1 2
      netbox/ipam/forms/bulk_create.py
  13. 3 4
      netbox/ipam/forms/model_forms.py
  14. 1 2
      netbox/netbox/forms/__init__.py
  15. 4 4
      netbox/netbox/forms/base.py
  16. 0 0
      netbox/project-static/dist/netbox.css
  17. 8 10
      netbox/project-static/styles/overrides/_slim-select.scss
  18. 9 0
      netbox/project-static/styles/transitional/_forms.scss
  19. 2 0
      netbox/templates/django/forms/widgets/attrs.html
  20. 1 1
      netbox/templates/django/forms/widgets/checkbox.html
  21. 5 0
      netbox/templates/django/forms/widgets/clearable_file_input.html
  22. 1 0
      netbox/templates/django/forms/widgets/input.html
  23. 5 0
      netbox/templates/django/forms/widgets/select.html
  24. 2 0
      netbox/templates/django/forms/widgets/textarea.html
  25. 0 1
      netbox/users/forms/__init__.py
  26. 0 25
      netbox/users/forms/authentication.py
  27. 3 3
      netbox/users/forms/bulk_edit.py
  28. 5 8
      netbox/users/forms/model_forms.py
  29. 1 2
      netbox/utilities/forms/bulk_import.py
  30. 5 6
      netbox/utilities/forms/forms.py
  31. 0 57
      netbox/utilities/forms/mixins.py
  32. 2 2
      netbox/virtualization/forms/bulk_create.py
  33. 2 2
      netbox/virtualization/forms/model_forms.py

+ 6 - 6
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)

+ 1 - 2
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(),

+ 2 - 2
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.
     """

+ 2 - 2
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()

+ 4 - 4
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'),

+ 10 - 15
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(),

+ 2 - 2
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
     )

+ 1 - 2
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
 
 

+ 11 - 11
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

+ 1 - 2
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(),

+ 1 - 2
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,

+ 1 - 2
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')
     )

+ 3 - 4
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()

+ 1 - 2
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(

+ 4 - 4
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.

Разница между файлами не показана из-за своего большого размера
+ 0 - 0
netbox/project-static/dist/netbox.css


+ 8 - 10
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;
+  }
+}

+ 9 - 0
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;
+  }
+}

+ 2 - 0
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 %}

+ 1 - 1
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' %}<input type="hidden" name="{{ widget.name }}" value="">{% endif %}
-{% include "django/forms/widgets/input.html" %}
+<input type="checkbox" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %} {% include "django/forms/widgets/attrs.html" %} class="form-check-input{% if 'class' in widget.attrs %} {{ widget.attrs.class }}{% endif %}">

+ 5 - 0
netbox/templates/django/forms/widgets/clearable_file_input.html

@@ -0,0 +1,5 @@
+{% if widget.is_initial %}{{ widget.initial_text }}: <a href="{{ widget.value.url }}">{{ widget.value }}</a>{% if not widget.required %}
+<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}"{% if widget.attrs.disabled %} disabled{% endif %}>
+<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}<br>
+{{ widget.input_text }}:{% endif %}
+<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %} class="form-control{% if 'class' in widget.attrs %} {{ widget.attrs.class }}{% endif %}">

+ 1 - 0
netbox/templates/django/forms/widgets/input.html

@@ -0,0 +1 @@
+<input type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %} class="form-control{% if 'class' in widget.attrs %} {{ widget.attrs.class }}{% endif %}">

+ 5 - 0
netbox/templates/django/forms/widgets/select.html

@@ -0,0 +1,5 @@
+<select name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %} class="{% if 'size' in widget.attrs %}form-select form-select-sm{% else %}netbox-static-select{% endif %}{% if 'class' in widget.attrs %} {{ widget.attrs.class }}{% endif %}">{% for group_name, group_choices, group_index in widget.optgroups %}{% if group_name %}
+  <optgroup label="{{ group_name }}">{% endif %}{% for option in group_choices %}
+  {% include option.template_name with widget=option %}{% endfor %}{% if group_name %}
+  </optgroup>{% endif %}{% endfor %}
+</select>

+ 2 - 0
netbox/templates/django/forms/widgets/textarea.html

@@ -0,0 +1,2 @@
+<textarea name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %} class="form-control{% if 'class' in widget.attrs %} {{ widget.attrs.class }}{% endif %}">
+{% if widget.value %}{{ widget.value }}{% endif %}</textarea>

+ 0 - 1
netbox/users/forms/__init__.py

@@ -1,4 +1,3 @@
-from .authentication import *
 from .bulk_edit import *
 from .bulk_import import *
 from .filtersets import *

+ 0 - 25
netbox/users/forms/authentication.py

@@ -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

+ 3 - 3
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

+ 5 - 8
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(),

+ 1 - 2
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

+ 5 - 6
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.
     """

+ 0 - 57
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 <select> widgets
-                field.widget.attrs['class'] = f'{css} form-select form-select-sm'
-
-            elif isinstance(field.widget, (forms.Select, forms.SelectMultiple)):
-                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
-
-
 class CheckLastUpdatedMixin(forms.Form):
     """
     Checks whether the object being saved has been updated since the form was initialized. If so, validation fails.

+ 2 - 2
netbox/virtualization/forms/bulk_create.py

@@ -1,7 +1,7 @@
 from django import forms
 from django.utils.translation import gettext_lazy as _
 
-from utilities.forms import BootstrapMixin, form_from_model
+from utilities.forms import form_from_model
 from utilities.forms.fields import ExpandableNameField
 from virtualization.models import VirtualDisk, VMInterface, VirtualMachine
 
@@ -11,7 +11,7 @@ __all__ = (
 )
 
 
-class VirtualMachineBulkAddComponentForm(BootstrapMixin, forms.Form):
+class VirtualMachineBulkAddComponentForm(forms.Form):
     pk = forms.ModelMultipleChoiceField(
         queryset=VirtualMachine.objects.all(),
         widget=forms.MultipleHiddenInput()

+ 2 - 2
netbox/virtualization/forms/model_forms.py

@@ -9,7 +9,7 @@ from extras.models import ConfigTemplate
 from ipam.models import IPAddress, VLAN, VLANGroup, VRF
 from netbox.forms import NetBoxModelForm
 from tenancy.forms import TenancyForm
-from utilities.forms import BootstrapMixin, ConfirmationForm
+from utilities.forms import ConfirmationForm
 from utilities.forms.fields import (
     CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, SlugField,
 )
@@ -90,7 +90,7 @@ class ClusterForm(TenancyForm, NetBoxModelForm):
         )
 
 
-class ClusterAddDevicesForm(BootstrapMixin, forms.Form):
+class ClusterAddDevicesForm(forms.Form):
     region = DynamicModelChoiceField(
         label=_('Region'),
         queryset=Region.objects.all(),

Некоторые файлы не были показаны из-за большого количества измененных файлов