1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

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
This commit is contained in:
Jeremy Stretch
2024-01-19 14:02:33 -05:00
committed by GitHub
parent 874685fd6f
commit da085e60c2
33 changed files with 102 additions and 180 deletions

View File

@ -2,8 +2,8 @@ import logging
from django.conf import settings from django.conf import settings
from django.contrib import messages from django.contrib import messages
from django.contrib.auth import login as auth_login, logout as auth_logout from django.contrib.auth import login as auth_login, logout as auth_logout, update_session_auth_hash
from django.contrib.auth import update_session_auth_hash from django.contrib.auth.forms import AuthenticationForm, PasswordChangeForm
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.auth.models import update_last_login from django.contrib.auth.models import update_last_login
from django.contrib.auth.signals import user_logged_in from django.contrib.auth.signals import user_logged_in
@ -72,7 +72,7 @@ class LoginView(View):
return auth_backends return auth_backends
def get(self, request): def get(self, request):
form = forms.LoginForm(request) form = AuthenticationForm(request)
if request.user.is_authenticated: if request.user.is_authenticated:
logger = logging.getLogger('netbox.auth.login') logger = logging.getLogger('netbox.auth.login')
@ -85,7 +85,7 @@ class LoginView(View):
def post(self, request): def post(self, request):
logger = logging.getLogger('netbox.auth.login') logger = logging.getLogger('netbox.auth.login')
form = forms.LoginForm(request, data=request.POST) form = AuthenticationForm(request, data=request.POST)
if form.is_valid(): if form.is_valid():
logger.debug("Login form validation was successful") 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.") messages.warning(request, "LDAP-authenticated user credentials cannot be changed within NetBox.")
return redirect('account:profile') return redirect('account:profile')
form = forms.PasswordChangeForm(user=request.user) form = PasswordChangeForm(user=request.user)
return render(request, self.template_name, { return render(request, self.template_name, {
'form': form, 'form': form,
@ -228,7 +228,7 @@ class ChangePasswordView(LoginRequiredMixin, View):
}) })
def post(self, request): 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(): if form.is_valid():
form.save() form.save()
update_session_auth_hash(request, form.user) update_session_auth_hash(request, form.user)

View File

@ -7,7 +7,6 @@ from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from netbox.forms import NetBoxModelImportForm from netbox.forms import NetBoxModelImportForm
from tenancy.models import Tenant from tenancy.models import Tenant
from utilities.forms import BootstrapMixin
from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField
__all__ = ( __all__ = (
@ -112,7 +111,7 @@ class CircuitImportForm(NetBoxModelImportForm):
] ]
class CircuitTerminationImportForm(BootstrapMixin, forms.ModelForm): class CircuitTerminationImportForm(forms.ModelForm):
site = CSVModelChoiceField( site = CSVModelChoiceField(
label=_('Site'), label=_('Site'),
queryset=Site.objects.all(), queryset=Site.objects.all(),

View File

@ -11,7 +11,7 @@ from netbox.config import get_config, PARAMS
from netbox.forms import NetBoxModelForm from netbox.forms import NetBoxModelForm
from netbox.registry import registry from netbox.registry import registry
from netbox.utils import get_data_backend_choices 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.fields import CommentField
from utilities.forms.widgets import HTMXSelect from utilities.forms.widgets import HTMXSelect
@ -138,7 +138,7 @@ class ConfigFormMetaclass(forms.models.ModelFormMetaclass):
return super().__new__(mcs, name, bases, attrs) 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. Form for creating a new ConfigRevision.
""" """

View File

@ -4,7 +4,7 @@ from django.utils.translation import gettext_lazy as _
from dcim.models import * from dcim.models import *
from extras.models import Tag from extras.models import Tag
from netbox.forms.mixins import CustomFieldsMixin 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 utilities.forms.fields import DynamicModelMultipleChoiceField, ExpandableNameField
from .object_create import ComponentCreateForm from .object_create import ComponentCreateForm
@ -26,7 +26,7 @@ __all__ = (
# Device components # Device components
# #
class DeviceBulkAddComponentForm(BootstrapMixin, CustomFieldsMixin, ComponentCreateForm): class DeviceBulkAddComponentForm(CustomFieldsMixin, ComponentCreateForm):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
widget=forms.MultipleHiddenInput() widget=forms.MultipleHiddenInput()

View File

@ -11,7 +11,7 @@ from extras.models import ConfigTemplate
from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF
from netbox.forms import NetBoxModelForm from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm 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 ( from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField,
NumericArrayField, SlugField, NumericArrayField, SlugField,
@ -748,7 +748,7 @@ class DeviceVCMembershipForm(forms.ModelForm):
return vc_position return vc_position
class VCMemberSelectForm(BootstrapMixin, forms.Form): class VCMemberSelectForm(forms.Form):
device = DynamicModelChoiceField( device = DynamicModelChoiceField(
label=_('Device'), label=_('Device'),
queryset=Device.objects.all(), queryset=Device.objects.all(),
@ -771,7 +771,7 @@ class VCMemberSelectForm(BootstrapMixin, forms.Form):
# Device component templates # Device component templates
# #
class ComponentTemplateForm(BootstrapMixin, forms.ModelForm): class ComponentTemplateForm(forms.ModelForm):
device_type = DynamicModelChoiceField( device_type = DynamicModelChoiceField(
label=_('Device type'), label=_('Device type'),
queryset=DeviceType.objects.all() queryset=DeviceType.objects.all()
@ -1272,7 +1272,7 @@ class DeviceBayForm(DeviceComponentForm):
] ]
class PopulateDeviceBayForm(BootstrapMixin, forms.Form): class PopulateDeviceBayForm(forms.Form):
installed_device = forms.ModelChoiceField( installed_device = forms.ModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
label=_('Child Device'), label=_('Child Device'),

View File

@ -3,7 +3,6 @@ from django.utils.translation import gettext_lazy as _
from dcim.choices import InterfacePoEModeChoices, InterfacePoETypeChoices, InterfaceTypeChoices, PortTypeChoices from dcim.choices import InterfacePoEModeChoices, InterfacePoETypeChoices, InterfaceTypeChoices, PortTypeChoices
from dcim.models import * from dcim.models import *
from utilities.forms import BootstrapMixin
from wireless.choices import WirelessRoleChoices from wireless.choices import WirelessRoleChoices
__all__ = ( __all__ = (
@ -24,11 +23,7 @@ __all__ = (
# Component template import forms # Component template import forms
# #
class ComponentTemplateImportForm(BootstrapMixin, forms.ModelForm): class ConsolePortTemplateImportForm(forms.ModelForm):
pass
class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
class Meta: class Meta:
model = ConsolePortTemplate model = ConsolePortTemplate
@ -37,7 +32,7 @@ class ConsolePortTemplateImportForm(ComponentTemplateImportForm):
] ]
class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm): class ConsoleServerPortTemplateImportForm(forms.ModelForm):
class Meta: class Meta:
model = ConsoleServerPortTemplate model = ConsoleServerPortTemplate
@ -46,7 +41,7 @@ class ConsoleServerPortTemplateImportForm(ComponentTemplateImportForm):
] ]
class PowerPortTemplateImportForm(ComponentTemplateImportForm): class PowerPortTemplateImportForm(forms.ModelForm):
class Meta: class Meta:
model = PowerPortTemplate model = PowerPortTemplate
@ -55,7 +50,7 @@ class PowerPortTemplateImportForm(ComponentTemplateImportForm):
] ]
class PowerOutletTemplateImportForm(ComponentTemplateImportForm): class PowerOutletTemplateImportForm(forms.ModelForm):
power_port = forms.ModelChoiceField( power_port = forms.ModelChoiceField(
label=_('Power port'), label=_('Power port'),
queryset=PowerPortTemplate.objects.all(), queryset=PowerPortTemplate.objects.all(),
@ -84,7 +79,7 @@ class PowerOutletTemplateImportForm(ComponentTemplateImportForm):
return module_type return module_type
class InterfaceTemplateImportForm(ComponentTemplateImportForm): class InterfaceTemplateImportForm(forms.ModelForm):
type = forms.ChoiceField( type = forms.ChoiceField(
label=_('Type'), label=_('Type'),
choices=InterfaceTypeChoices.CHOICES choices=InterfaceTypeChoices.CHOICES
@ -113,7 +108,7 @@ class InterfaceTemplateImportForm(ComponentTemplateImportForm):
] ]
class FrontPortTemplateImportForm(ComponentTemplateImportForm): class FrontPortTemplateImportForm(forms.ModelForm):
type = forms.ChoiceField( type = forms.ChoiceField(
label=_('Type'), label=_('Type'),
choices=PortTypeChoices.CHOICES choices=PortTypeChoices.CHOICES
@ -145,7 +140,7 @@ class FrontPortTemplateImportForm(ComponentTemplateImportForm):
] ]
class RearPortTemplateImportForm(ComponentTemplateImportForm): class RearPortTemplateImportForm(forms.ModelForm):
type = forms.ChoiceField( type = forms.ChoiceField(
label=_('Type'), label=_('Type'),
choices=PortTypeChoices.CHOICES choices=PortTypeChoices.CHOICES
@ -158,7 +153,7 @@ class RearPortTemplateImportForm(ComponentTemplateImportForm):
] ]
class ModuleBayTemplateImportForm(ComponentTemplateImportForm): class ModuleBayTemplateImportForm(forms.ModelForm):
class Meta: class Meta:
model = ModuleBayTemplate model = ModuleBayTemplate
@ -167,7 +162,7 @@ class ModuleBayTemplateImportForm(ComponentTemplateImportForm):
] ]
class DeviceBayTemplateImportForm(ComponentTemplateImportForm): class DeviceBayTemplateImportForm(forms.ModelForm):
class Meta: class Meta:
model = DeviceBayTemplate model = DeviceBayTemplate
@ -176,7 +171,7 @@ class DeviceBayTemplateImportForm(ComponentTemplateImportForm):
] ]
class InventoryItemTemplateImportForm(ComponentTemplateImportForm): class InventoryItemTemplateImportForm(forms.ModelForm):
parent = forms.ModelChoiceField( parent = forms.ModelChoiceField(
label=_('Parent'), label=_('Parent'),
queryset=InventoryItemTemplate.objects.all(), queryset=InventoryItemTemplate.objects.all(),

View File

@ -4,7 +4,7 @@ from django.utils.translation import gettext as _
from extras.choices import DashboardWidgetColorChoices from extras.choices import DashboardWidgetColorChoices
from netbox.registry import registry from netbox.registry import registry
from utilities.forms import BootstrapMixin, add_blank_choice from utilities.forms import add_blank_choice
__all__ = ( __all__ = (
'DashboardWidgetAddForm', 'DashboardWidgetAddForm',
@ -16,7 +16,7 @@ def get_widget_choices():
return registry['widgets'].items() return registry['widgets'].items()
class DashboardWidgetForm(BootstrapMixin, forms.Form): class DashboardWidgetForm(forms.Form):
title = forms.CharField( title = forms.CharField(
required=False required=False
) )

View File

@ -15,7 +15,6 @@ from django.utils.translation import gettext as _
from core.models import ContentType from core.models import ContentType
from extras.choices import BookmarkOrderingChoices from extras.choices import BookmarkOrderingChoices
from utilities.choices import ButtonColorChoices from utilities.choices import ButtonColorChoices
from utilities.forms import BootstrapMixin
from utilities.permissions import get_permission_for_model from utilities.permissions import get_permission_for_model
from utilities.templatetags.builtins.filters import render_markdown from utilities.templatetags.builtins.filters import render_markdown
from utilities.utils import content_type_identifier, content_type_name, dict_to_querydict, get_viewname 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 return models
class WidgetConfigForm(BootstrapMixin, forms.Form): class WidgetConfigForm(forms.Form):
pass pass

View File

@ -13,7 +13,7 @@ from extras.choices import *
from extras.models import * from extras.models import *
from netbox.forms import NetBoxModelForm from netbox.forms import NetBoxModelForm
from tenancy.models import Tenant, TenantGroup 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 ( from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField, CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelChoiceField,
DynamicModelMultipleChoiceField, JSONField, SlugField, DynamicModelMultipleChoiceField, JSONField, SlugField,
@ -38,7 +38,7 @@ __all__ = (
) )
class CustomFieldForm(BootstrapMixin, forms.ModelForm): class CustomFieldForm(forms.ModelForm):
content_types = ContentTypeMultipleChoiceField( content_types = ContentTypeMultipleChoiceField(
label=_('Content types'), label=_('Content types'),
queryset=ContentType.objects.with_feature('custom_fields') queryset=ContentType.objects.with_feature('custom_fields')
@ -83,7 +83,7 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm):
self.fields['type'].disabled = True self.fields['type'].disabled = True
class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm): class CustomFieldChoiceSetForm(forms.ModelForm):
extra_choices = forms.CharField( extra_choices = forms.CharField(
widget=ChoicesWidget(), widget=ChoicesWidget(),
required=False, required=False,
@ -122,7 +122,7 @@ class CustomFieldChoiceSetForm(BootstrapMixin, forms.ModelForm):
return data return data
class CustomLinkForm(BootstrapMixin, forms.ModelForm): class CustomLinkForm(forms.ModelForm):
content_types = ContentTypeMultipleChoiceField( content_types = ContentTypeMultipleChoiceField(
label=_('Content types'), label=_('Content types'),
queryset=ContentType.objects.with_feature('custom_links') 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( content_types = ContentTypeMultipleChoiceField(
label=_('Content types'), label=_('Content types'),
queryset=ContentType.objects.with_feature('export_templates') queryset=ContentType.objects.with_feature('export_templates')
@ -189,7 +189,7 @@ class ExportTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
return self.cleaned_data return self.cleaned_data
class SavedFilterForm(BootstrapMixin, forms.ModelForm): class SavedFilterForm(forms.ModelForm):
slug = SlugField() slug = SlugField()
content_types = ContentTypeMultipleChoiceField( content_types = ContentTypeMultipleChoiceField(
label=_('Content types'), label=_('Content types'),
@ -216,7 +216,7 @@ class SavedFilterForm(BootstrapMixin, forms.ModelForm):
super().__init__(*args, initial=initial, **kwargs) super().__init__(*args, initial=initial, **kwargs)
class BookmarkForm(BootstrapMixin, forms.ModelForm): class BookmarkForm(forms.ModelForm):
object_type = ContentTypeChoiceField( object_type = ContentTypeChoiceField(
label=_('Object type'), label=_('Object type'),
queryset=ContentType.objects.with_feature('bookmarks') queryset=ContentType.objects.with_feature('bookmarks')
@ -367,7 +367,7 @@ class EventRuleForm(NetBoxModelForm):
return super().save(*args, **kwargs) return super().save(*args, **kwargs)
class TagForm(BootstrapMixin, forms.ModelForm): class TagForm(forms.ModelForm):
slug = SlugField() slug = SlugField()
object_types = ContentTypeMultipleChoiceField( object_types = ContentTypeMultipleChoiceField(
label=_('Object types'), 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( regions = DynamicModelMultipleChoiceField(
label=_('Regions'), label=_('Regions'),
queryset=Region.objects.all(), queryset=Region.objects.all(),
@ -497,7 +497,7 @@ class ConfigContextForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
return self.cleaned_data return self.cleaned_data
class ConfigTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm): class ConfigTemplateForm(SyncedDataMixin, forms.ModelForm):
tags = DynamicModelMultipleChoiceField( tags = DynamicModelMultipleChoiceField(
label=_('Tags'), label=_('Tags'),
queryset=Tag.objects.all(), queryset=Tag.objects.all(),
@ -541,7 +541,7 @@ class ConfigTemplateForm(BootstrapMixin, SyncedDataMixin, forms.ModelForm):
return self.cleaned_data return self.cleaned_data
class ImageAttachmentForm(BootstrapMixin, forms.ModelForm): class ImageAttachmentForm(forms.ModelForm):
class Meta: class Meta:
model = ImageAttachment model = ImageAttachment

View File

@ -2,7 +2,6 @@ from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from extras.choices import DurationChoices from extras.choices import DurationChoices
from utilities.forms import BootstrapMixin
from utilities.forms.widgets import DateTimePicker, NumberWithOptions from utilities.forms.widgets import DateTimePicker, NumberWithOptions
from utilities.utils import local_now from utilities.utils import local_now
@ -11,7 +10,7 @@ __all__ = (
) )
class ReportForm(BootstrapMixin, forms.Form): class ReportForm(forms.Form):
schedule_at = forms.DateTimeField( schedule_at = forms.DateTimeField(
required=False, required=False,
widget=DateTimePicker(), widget=DateTimePicker(),

View File

@ -2,7 +2,6 @@ from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from extras.choices import DurationChoices from extras.choices import DurationChoices
from utilities.forms import BootstrapMixin
from utilities.forms.widgets import DateTimePicker, NumberWithOptions from utilities.forms.widgets import DateTimePicker, NumberWithOptions
from utilities.utils import local_now from utilities.utils import local_now
@ -11,7 +10,7 @@ __all__ = (
) )
class ScriptForm(BootstrapMixin, forms.Form): class ScriptForm(forms.Form):
_commit = forms.BooleanField( _commit = forms.BooleanField(
required=False, required=False,
initial=True, initial=True,

View File

@ -1,7 +1,6 @@
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from utilities.forms import BootstrapMixin
from utilities.forms.fields import ExpandableIPAddressField from utilities.forms.fields import ExpandableIPAddressField
__all__ = ( __all__ = (
@ -9,7 +8,7 @@ __all__ = (
) )
class IPAddressBulkCreateForm(BootstrapMixin, forms.Form): class IPAddressBulkCreateForm(forms.Form):
pattern = ExpandableIPAddressField( pattern = ExpandableIPAddressField(
label=_('Address pattern') label=_('Address pattern')
) )

View File

@ -11,7 +11,7 @@ from ipam.models import *
from netbox.forms import NetBoxModelForm from netbox.forms import NetBoxModelForm
from tenancy.forms import TenancyForm from tenancy.forms import TenancyForm
from utilities.exceptions import PermissionsViolation 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 ( from utilities.forms.fields import (
CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField,
SlugField, SlugField,
@ -419,7 +419,7 @@ class IPAddressBulkAddForm(TenancyForm, NetBoxModelForm):
] ]
class IPAddressAssignForm(BootstrapMixin, forms.Form): class IPAddressAssignForm(forms.Form):
vrf_id = DynamicModelChoiceField( vrf_id = DynamicModelChoiceField(
queryset=VRF.objects.all(), queryset=VRF.objects.all(),
required=False, required=False,
@ -504,7 +504,7 @@ class FHRPGroupForm(NetBoxModelForm):
}) })
class FHRPGroupAssignmentForm(BootstrapMixin, forms.ModelForm): class FHRPGroupAssignmentForm(forms.ModelForm):
group = DynamicModelChoiceField( group = DynamicModelChoiceField(
label=_('Group'), label=_('Group'),
queryset=FHRPGroup.objects.all() queryset=FHRPGroup.objects.all()
@ -738,7 +738,6 @@ class ServiceCreateForm(ServiceForm):
# Fields which may be populated from a ServiceTemplate are not required # Fields which may be populated from a ServiceTemplate are not required
for field in ('name', 'protocol', 'ports'): for field in ('name', 'protocol', 'ports'):
self.fields[field].required = False self.fields[field].required = False
del self.fields[field].widget.attrs['required']
def clean(self): def clean(self):
super().clean() super().clean()

View File

@ -5,7 +5,6 @@ from django.utils.translation import gettext as _
from netbox.search import LookupTypes from netbox.search import LookupTypes
from netbox.search.backends import search_backend from netbox.search.backends import search_backend
from utilities.forms import BootstrapMixin
from .base import * from .base import *
@ -18,7 +17,7 @@ LOOKUP_CHOICES = (
) )
class SearchForm(BootstrapMixin, forms.Form): class SearchForm(forms.Form):
q = forms.CharField( q = forms.CharField(
label=_('Search'), label=_('Search'),
widget=forms.TextInput( widget=forms.TextInput(

View File

@ -7,7 +7,7 @@ from extras.choices import *
from extras.models import CustomField, Tag from extras.models import CustomField, Tag
from utilities.forms import CSVModelForm from utilities.forms import CSVModelForm
from utilities.forms.fields import CSVModelMultipleChoiceField, DynamicModelMultipleChoiceField 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 from .mixins import CustomFieldsMixin, SavedFiltersMixin, TagsMixin
__all__ = ( __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. 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) 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 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. fields and adding/removing tags.
@ -146,7 +146,7 @@ class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, forms.Form):
self.nullable_fields = (*self.nullable_fields, *nullable_custom_fields) 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 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. corresponding FilterSet *must* provide a `q` filter.

File diff suppressed because one or more lines are too long

View File

@ -37,16 +37,6 @@ $spacing-s: $input-padding-x;
.ss-main { .ss-main {
color: $form-select-color; 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-single-selected,
.ss-multi-selected { .ss-multi-selected {
padding: $form-select-padding-y $input-padding-x $form-select-padding-y $form-select-padding-x; 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;
}
}

View File

@ -16,3 +16,12 @@ form.object-edit {
content: '\f06C4'; content: '\f06C4';
} }
} }
// Set red border on form fields inside a row with .has-errors
.has-errors {
input,
select,
textarea {
border: 1px solid $red;
}
}

View File

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

View File

@ -4,4 +4,4 @@
_selected_action to avoid breaking the admin UI. _selected_action to avoid breaking the admin UI.
{% endcomment %} {% endcomment %}
{% if widget.name != '_selected_action' %}<input type="hidden" name="{{ widget.name }}" value="">{% endif %} {% 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 %}">

View File

@ -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 %}">

View File

@ -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 %}">

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,7 @@ from django.utils.translation import gettext_lazy as _
from ipam.formfields import IPNetworkFormField from ipam.formfields import IPNetworkFormField
from ipam.validators import prefix_validator from ipam.validators import prefix_validator
from users.models import * from users.models import *
from utilities.forms import BootstrapMixin, BulkEditForm from utilities.forms import BulkEditForm
from utilities.forms.widgets import BulkEditNullBooleanSelect, DateTimePicker from utilities.forms.widgets import BulkEditNullBooleanSelect, DateTimePicker
__all__ = ( __all__ = (
@ -15,7 +15,7 @@ __all__ = (
) )
class UserBulkEditForm(BootstrapMixin, forms.Form): class UserBulkEditForm(forms.Form):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=NetBoxUser.objects.all(), queryset=NetBoxUser.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput
@ -53,7 +53,7 @@ class UserBulkEditForm(BootstrapMixin, forms.Form):
nullable_fields = ('first_name', 'last_name') nullable_fields = ('first_name', 'last_name')
class ObjectPermissionBulkEditForm(BootstrapMixin, forms.Form): class ObjectPermissionBulkEditForm(forms.Form):
pk = forms.ModelMultipleChoiceField( pk = forms.ModelMultipleChoiceField(
queryset=ObjectPermission.objects.all(), queryset=ObjectPermission.objects.all(),
widget=forms.MultipleHiddenInput widget=forms.MultipleHiddenInput

View File

@ -13,7 +13,6 @@ from ipam.validators import prefix_validator
from netbox.preferences import PREFERENCES from netbox.preferences import PREFERENCES
from users.constants import * from users.constants import *
from users.models import * from users.models import *
from utilities.forms import BootstrapMixin
from utilities.forms.fields import ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField from utilities.forms.fields import ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField
from utilities.forms.widgets import DateTimePicker from utilities.forms.widgets import DateTimePicker
from utilities.permissions import qs_filter_from_constraints from utilities.permissions import qs_filter_from_constraints
@ -53,7 +52,7 @@ class UserConfigFormMetaclass(forms.models.ModelFormMetaclass):
return super().__new__(mcs, name, bases, attrs) return super().__new__(mcs, name, bases, attrs)
class UserConfigForm(BootstrapMixin, forms.ModelForm, metaclass=UserConfigFormMetaclass): class UserConfigForm(forms.ModelForm, metaclass=UserConfigFormMetaclass):
fieldsets = ( fieldsets = (
(_('User Interface'), ( (_('User Interface'), (
'locale.language', '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( key = forms.CharField(
label=_('Key'), label=_('Key'),
help_text=_( help_text=_(
@ -167,7 +166,7 @@ class TokenForm(UserTokenForm):
} }
class UserForm(BootstrapMixin, forms.ModelForm): class UserForm(forms.ModelForm):
password = forms.CharField( password = forms.CharField(
label=_('Password'), label=_('Password'),
widget=forms.PasswordInput(), widget=forms.PasswordInput(),
@ -214,9 +213,7 @@ class UserForm(BootstrapMixin, forms.ModelForm):
# Password fields are optional for existing Users # Password fields are optional for existing Users
self.fields['password'].required = False self.fields['password'].required = False
self.fields['password'].widget.attrs.pop('required')
self.fields['confirm_password'].required = False self.fields['confirm_password'].required = False
self.fields['confirm_password'].widget.attrs.pop('required')
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
instance = super().save(*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.")) 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( users = DynamicModelMultipleChoiceField(
label=_('Users'), label=_('Users'),
required=False, required=False,
@ -281,7 +278,7 @@ class GroupForm(BootstrapMixin, forms.ModelForm):
return instance return instance
class ObjectPermissionForm(BootstrapMixin, forms.ModelForm): class ObjectPermissionForm(forms.ModelForm):
object_types = ContentTypeMultipleChoiceField( object_types = ContentTypeMultipleChoiceField(
label=_('Object types'), label=_('Object types'),
queryset=ContentType.objects.all(), queryset=ContentType.objects.all(),

View File

@ -10,10 +10,9 @@ from core.forms.mixins import SyncedDataMixin
from utilities.choices import CSVDelimiterChoices, ImportFormatChoices, ImportMethodChoices from utilities.choices import CSVDelimiterChoices, ImportFormatChoices, ImportMethodChoices
from utilities.constants import CSV_DELIMITERS from utilities.constants import CSV_DELIMITERS
from utilities.forms.utils import parse_csv 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( import_method = forms.ChoiceField(
choices=ImportMethodChoices, choices=ImportMethodChoices,
required=False required=False

View File

@ -2,7 +2,6 @@ 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
__all__ = ( __all__ = (
'BulkEditForm', '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. 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. Provides bulk edit support for objects.
""" """
nullable_fields = () nullable_fields = ()
class BulkRenameForm(BootstrapMixin, forms.Form): class BulkRenameForm(forms.Form):
""" """
An extendable form to be used for renaming objects in bulk. An extendable form to be used for renaming objects in bulk.
""" """
@ -90,7 +89,7 @@ class CSVModelForm(forms.ModelForm):
return super().clean() return super().clean()
class FilterForm(BootstrapMixin, forms.Form): class FilterForm(forms.Form):
""" """
Base Form class for FilterSet forms. 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. Form for configuring user's table preferences.
""" """

View File

@ -3,68 +3,11 @@ import time
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from .widgets import APISelect, APISelectMultiple, ClearableFileInput
__all__ = ( __all__ = (
'BootstrapMixin',
'CheckLastUpdatedMixin', '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): class CheckLastUpdatedMixin(forms.Form):
""" """
Checks whether the object being saved has been updated since the form was initialized. If so, validation fails. Checks whether the object being saved has been updated since the form was initialized. If so, validation fails.

View File

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

View File

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