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
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 102 additions and 180 deletions

View File

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

View File

@ -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(),

View File

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

View File

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

View File

@ -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'),

View File

@ -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(),

View File

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

View File

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

View File

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

View File

@ -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(),

View File

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

View File

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

View File

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

View File

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

View File

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

File diff suppressed because one or more lines are too long

View File

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

View File

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

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.
{% 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 %}">

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_import 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.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

View File

@ -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(),

View File

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

View File

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

View File

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

View File

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

View File

@ -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(),