From d470848b2932b7ef2dc68469d11bfa1609d40403 Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Fri, 14 Apr 2023 10:33:53 -0400 Subject: [PATCH] Closes #12246: General cleanup of utilities modules * Clean up base modules * Clean up forms modules * Clean up templatetags modules * Replace custom simplify_decimal filter with floatformat * Misc cleanup * Merge ReturnURLForm into ConfirmationForm * Clean up import statements for utilities.forms * Fix field class references in docs --- docs/plugins/development/forms.md | 32 ++-- netbox/circuits/forms/bulk_edit.py | 6 +- netbox/circuits/forms/bulk_import.py | 3 +- netbox/circuits/forms/filtersets.py | 3 +- netbox/circuits/forms/model_forms.py | 5 +- netbox/core/forms/bulk_edit.py | 4 +- netbox/core/forms/filtersets.py | 7 +- netbox/core/forms/model_forms.py | 3 +- netbox/dcim/forms/bulk_create.py | 3 +- netbox/dcim/forms/bulk_edit.py | 7 +- netbox/dcim/forms/bulk_import.py | 5 +- netbox/dcim/forms/common.py | 2 +- netbox/dcim/forms/connections.py | 4 +- netbox/dcim/forms/filtersets.py | 7 +- netbox/dcim/forms/model_forms.py | 9 +- netbox/dcim/forms/object_create.py | 2 +- netbox/dcim/tables/template_code.py | 4 +- netbox/extras/forms/bulk_edit.py | 6 +- netbox/extras/forms/bulk_import.py | 3 +- netbox/extras/forms/filtersets.py | 7 +- netbox/extras/forms/model_forms.py | 7 +- netbox/extras/forms/reports.py | 4 +- netbox/extras/forms/scripts.py | 3 +- netbox/extras/scripts.py | 3 +- netbox/ipam/api/views.py | 4 +- netbox/ipam/forms/bulk_create.py | 3 +- netbox/ipam/forms/bulk_edit.py | 7 +- netbox/ipam/forms/bulk_import.py | 2 +- netbox/ipam/forms/filtersets.py | 6 +- netbox/ipam/forms/model_forms.py | 8 +- netbox/netbox/constants.py | 11 ++ netbox/netbox/filtersets.py | 2 +- netbox/templates/dcim/interface.html | 8 +- .../wireless/inc/wirelesslink_interface.html | 4 +- netbox/tenancy/forms/bulk_edit.py | 3 +- netbox/tenancy/forms/bulk_import.py | 2 +- netbox/tenancy/forms/forms.py | 2 +- netbox/tenancy/forms/model_forms.py | 5 +- netbox/users/forms.py | 3 +- netbox/utilities/api.py | 8 + netbox/utilities/constants.py | 15 -- netbox/utilities/exceptions.py | 1 + netbox/utilities/fields.py | 14 +- netbox/utilities/files.py | 4 + netbox/utilities/filters.py | 16 ++ netbox/utilities/forms/__init__.py | 3 +- netbox/utilities/forms/fields/__init__.py | 1 + netbox/utilities/forms/fields/array.py | 24 +++ .../utilities/forms/fields/content_types.py | 1 - netbox/utilities/forms/fields/csv.py | 5 - netbox/utilities/forms/forms.py | 89 ++-------- netbox/utilities/forms/mixins.py | 62 +++++++ netbox/utilities/forms/widgets/__init__.py | 4 + .../{widgets.py => widgets/apiselect.py} | 168 +----------------- netbox/utilities/forms/widgets/datetime.py | 37 ++++ netbox/utilities/forms/widgets/misc.py | 28 +++ netbox/utilities/forms/widgets/select.py | 79 ++++++++ netbox/utilities/graphql_optimizer.py | 9 +- netbox/utilities/htmx.py | 5 + netbox/utilities/markdown.py | 4 + netbox/utilities/migration.py | 4 + netbox/utilities/mptt.py | 5 + netbox/utilities/ordering.py | 5 + netbox/utilities/paginator.py | 6 + netbox/utilities/query_functions.py | 7 +- netbox/utilities/querysets.py | 5 + netbox/utilities/tables.py | 5 + .../templatetags/builtins/filters.py | 15 ++ .../utilities/templatetags/builtins/tags.py | 7 + netbox/utilities/templatetags/buttons.py | 12 ++ netbox/utilities/templatetags/form_helpers.py | 9 + netbox/utilities/templatetags/helpers.py | 45 ++--- netbox/utilities/templatetags/navigation.py | 4 + netbox/utilities/templatetags/perms.py | 8 + netbox/utilities/templatetags/tabs.py | 4 + netbox/utilities/urls.py | 4 + netbox/utilities/validators.py | 16 +- netbox/virtualization/forms/bulk_create.py | 3 +- netbox/virtualization/forms/bulk_edit.py | 7 +- netbox/virtualization/forms/bulk_import.py | 5 +- netbox/virtualization/forms/filtersets.py | 5 +- netbox/virtualization/forms/model_forms.py | 6 +- netbox/virtualization/forms/object_create.py | 2 +- netbox/wireless/forms/bulk_edit.py | 3 +- netbox/wireless/forms/bulk_import.py | 3 +- netbox/wireless/forms/filtersets.py | 3 +- netbox/wireless/forms/model_forms.py | 7 +- 87 files changed, 585 insertions(+), 406 deletions(-) create mode 100644 netbox/utilities/forms/fields/array.py create mode 100644 netbox/utilities/forms/mixins.py create mode 100644 netbox/utilities/forms/widgets/__init__.py rename netbox/utilities/forms/{widgets.py => widgets/apiselect.py} (56%) create mode 100644 netbox/utilities/forms/widgets/datetime.py create mode 100644 netbox/utilities/forms/widgets/misc.py create mode 100644 netbox/utilities/forms/widgets/select.py diff --git a/docs/plugins/development/forms.md b/docs/plugins/development/forms.md index 976997beac2..51f6c70dec6 100644 --- a/docs/plugins/development/forms.md +++ b/docs/plugins/development/forms.md @@ -145,23 +145,23 @@ class MyModelFilterForm(NetBoxModelFilterSetForm): In addition to the [form fields provided by Django](https://docs.djangoproject.com/en/stable/ref/forms/fields/), NetBox provides several field classes for use within forms to handle specific types of data. These can be imported from `utilities.forms.fields` and are documented below. -::: utilities.forms.ColorField +::: utilities.forms.fields.ColorField options: members: false -::: utilities.forms.CommentField +::: utilities.forms.fields.CommentField options: members: false -::: utilities.forms.JSONField +::: utilities.forms.fields.JSONField options: members: false -::: utilities.forms.MACAddressField +::: utilities.forms.fields.MACAddressField options: members: false -::: utilities.forms.SlugField +::: utilities.forms.fields.SlugField options: members: false @@ -170,52 +170,52 @@ In addition to the [form fields provided by Django](https://docs.djangoproject.c !!! warning "Obsolete Fields" NetBox's custom `ChoiceField` and `MultipleChoiceField` classes are no longer necessary thanks to improvements made to the user interface. Django's native form fields can be used instead. These custom field classes will be removed in NetBox v3.6. -::: utilities.forms.ChoiceField +::: utilities.forms.fields.ChoiceField options: members: false -::: utilities.forms.MultipleChoiceField +::: utilities.forms.fields.MultipleChoiceField options: members: false ## Dynamic Object Fields -::: utilities.forms.DynamicModelChoiceField +::: utilities.forms.fields.DynamicModelChoiceField options: members: false -::: utilities.forms.DynamicModelMultipleChoiceField +::: utilities.forms.fields.DynamicModelMultipleChoiceField options: members: false ## Content Type Fields -::: utilities.forms.ContentTypeChoiceField +::: utilities.forms.fields.ContentTypeChoiceField options: members: false -::: utilities.forms.ContentTypeMultipleChoiceField +::: utilities.forms.fields.ContentTypeMultipleChoiceField options: members: false ## CSV Import Fields -::: utilities.forms.CSVChoiceField +::: utilities.forms.fields.CSVChoiceField options: members: false -::: utilities.forms.CSVMultipleChoiceField +::: utilities.forms.fields.CSVMultipleChoiceField options: members: false -::: utilities.forms.CSVModelChoiceField +::: utilities.forms.fields.CSVModelChoiceField options: members: false -::: utilities.forms.CSVContentTypeField +::: utilities.forms.fields.CSVContentTypeField options: members: false -::: utilities.forms.CSVMultipleContentTypeField +::: utilities.forms.fields.CSVMultipleContentTypeField options: members: false diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index 39cdd85d0e3..efc9b5f3a41 100644 --- a/netbox/circuits/forms/bulk_edit.py +++ b/netbox/circuits/forms/bulk_edit.py @@ -6,9 +6,9 @@ from circuits.models import * from ipam.models import ASN from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant -from utilities.forms import ( - add_blank_choice, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, -) +from utilities.forms import add_blank_choice +from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.widgets import DatePicker __all__ = ( 'CircuitBulkEditForm', diff --git a/netbox/circuits/forms/bulk_import.py b/netbox/circuits/forms/bulk_import.py index 690cea8288c..d558310087b 100644 --- a/netbox/circuits/forms/bulk_import.py +++ b/netbox/circuits/forms/bulk_import.py @@ -6,7 +6,8 @@ from dcim.models import Site from django.utils.translation import gettext as _ from netbox.forms import NetBoxModelImportForm from tenancy.models import Tenant -from utilities.forms import BootstrapMixin, CSVChoiceField, CSVModelChoiceField, SlugField +from utilities.forms import BootstrapMixin +from utilities.forms.fields import CSVChoiceField, CSVModelChoiceField, SlugField __all__ = ( 'CircuitImportForm', diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py index aeeddfd36b2..075855f3be7 100644 --- a/netbox/circuits/forms/filtersets.py +++ b/netbox/circuits/forms/filtersets.py @@ -7,7 +7,8 @@ from dcim.models import Region, Site, SiteGroup from ipam.models import ASN from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import TenancyFilterForm, ContactModelFilterForm -from utilities.forms import DatePicker, DynamicModelMultipleChoiceField, TagFilterField +from utilities.forms.fields import DynamicModelMultipleChoiceField, TagFilterField +from utilities.forms.widgets import DatePicker __all__ = ( 'CircuitFilterForm', diff --git a/netbox/circuits/forms/model_forms.py b/netbox/circuits/forms/model_forms.py index 8aeaa9619c5..2925efec190 100644 --- a/netbox/circuits/forms/model_forms.py +++ b/netbox/circuits/forms/model_forms.py @@ -5,9 +5,8 @@ from dcim.models import Site from ipam.models import ASN from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm -from utilities.forms import ( - CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SelectSpeedWidget, SlugField, -) +from utilities.forms.fields import CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField +from utilities.forms.widgets import DatePicker, SelectSpeedWidget __all__ = ( 'CircuitForm', diff --git a/netbox/core/forms/bulk_edit.py b/netbox/core/forms/bulk_edit.py index 5a24ba90ff1..de87276432d 100644 --- a/netbox/core/forms/bulk_edit.py +++ b/netbox/core/forms/bulk_edit.py @@ -4,7 +4,9 @@ from django.utils.translation import gettext as _ from core.choices import DataSourceTypeChoices from core.models import * from netbox.forms import NetBoxModelBulkEditForm -from utilities.forms import add_blank_choice, BulkEditNullBooleanSelect, CommentField +from utilities.forms import add_blank_choice +from utilities.forms.fields import CommentField +from utilities.forms.widgets import BulkEditNullBooleanSelect __all__ = ( 'DataSourceBulkEditForm', diff --git a/netbox/core/forms/filtersets.py b/netbox/core/forms/filtersets.py index ee8faa125d3..7c3f2ab096e 100644 --- a/netbox/core/forms/filtersets.py +++ b/netbox/core/forms/filtersets.py @@ -8,10 +8,9 @@ from core.models import * from extras.forms.mixins import SavedFiltersMixin from extras.utils import FeatureQuery from netbox.forms import NetBoxModelFilterSetForm -from utilities.forms import ( - APISelectMultiple, BOOLEAN_WITH_BLANK_CHOICES, ContentTypeChoiceField, DateTimePicker, - DynamicModelMultipleChoiceField, FilterForm, -) +from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm +from utilities.forms.fields import ContentTypeChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.widgets import APISelectMultiple, DateTimePicker __all__ = ( 'DataFileFilterForm', diff --git a/netbox/core/forms/model_forms.py b/netbox/core/forms/model_forms.py index 67732d5be4e..304bc346a77 100644 --- a/netbox/core/forms/model_forms.py +++ b/netbox/core/forms/model_forms.py @@ -6,7 +6,8 @@ from core.models import * from extras.forms.mixins import SyncedDataMixin from netbox.forms import NetBoxModelForm from netbox.registry import registry -from utilities.forms import CommentField, get_field_value +from utilities.forms import get_field_value +from utilities.forms.fields import CommentField from utilities.forms.widgets import HTMXSelect __all__ = ( diff --git a/netbox/dcim/forms/bulk_create.py b/netbox/dcim/forms/bulk_create.py index 4127aa3ea95..179ff9b678b 100644 --- a/netbox/dcim/forms/bulk_create.py +++ b/netbox/dcim/forms/bulk_create.py @@ -4,7 +4,8 @@ from dcim.models import * from django.utils.translation import gettext as _ from extras.forms import CustomFieldsMixin from extras.models import Tag -from utilities.forms import BootstrapMixin, DynamicModelMultipleChoiceField, ExpandableNameField, form_from_model +from utilities.forms import BootstrapMixin, form_from_model +from utilities.forms.fields import DynamicModelMultipleChoiceField, ExpandableNameField from .object_create import ComponentCreateForm __all__ = ( diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 1a52571650f..5966588fa8f 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -10,10 +10,9 @@ from extras.models import ConfigTemplate from ipam.models import ASN, VLAN, VLANGroup, VRF from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant -from utilities.forms import ( - add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField, - DynamicModelMultipleChoiceField, form_from_model, SelectSpeedWidget, -) +from utilities.forms import BulkEditForm, add_blank_choice, form_from_model +from utilities.forms.fields import ColorField, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.widgets import BulkEditNullBooleanSelect, SelectSpeedWidget __all__ = ( 'CableBulkEditForm', diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index d596542afa1..73eda38fe89 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -12,8 +12,9 @@ from extras.models import ConfigTemplate from ipam.models import VRF from netbox.forms import NetBoxModelImportForm from tenancy.models import Tenant -from utilities.forms import ( - CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField, CSVModelMultipleChoiceField +from utilities.forms.fields import ( + CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVModelMultipleChoiceField, CSVTypedChoiceField, + SlugField, ) from virtualization.models import Cluster from wireless.choices import WirelessRoleChoices diff --git a/netbox/dcim/forms/common.py b/netbox/dcim/forms/common.py index f047d621b6f..064a9a80be5 100644 --- a/netbox/dcim/forms/common.py +++ b/netbox/dcim/forms/common.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext as _ from dcim.choices import * from dcim.constants import * -from utilities.forms.utils import get_field_value +from utilities.forms import get_field_value __all__ = ( 'InterfaceCommonForm', diff --git a/netbox/dcim/forms/connections.py b/netbox/dcim/forms/connections.py index 443dc9143b7..8e3dcdc685c 100644 --- a/netbox/dcim/forms/connections.py +++ b/netbox/dcim/forms/connections.py @@ -1,9 +1,9 @@ from django import forms from django.utils.translation import gettext as _ -from circuits.models import Circuit, CircuitTermination, Provider +from circuits.models import Circuit, CircuitTermination from dcim.models import * -from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField from .model_forms import CableForm diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 4ccc2fe5480..727064e8fa7 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -10,10 +10,9 @@ from extras.models import ConfigTemplate from ipam.models import ASN, L2VPN, VRF from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import ContactModelFilterForm, TenancyFilterForm -from utilities.forms import ( - APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, - TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, SelectSpeedWidget, -) +from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice +from utilities.forms.fields import ColorField, DynamicModelMultipleChoiceField, TagFilterField +from utilities.forms.widgets import APISelectMultiple, SelectSpeedWidget from wireless.choices import * __all__ = ( diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 6c8ca7566c4..f899c31e11b 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -11,11 +11,12 @@ 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 ( - add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, ContentTypeChoiceField, - DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, NumericArrayField, SlugField, +from utilities.forms import BootstrapMixin, add_blank_choice +from utilities.forms.fields import ( + CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, JSONField, + NumericArrayField, SlugField, ) -from utilities.forms.widgets import APISelect, HTMXSelect, SelectSpeedWidget, SelectWithPK +from utilities.forms.widgets import APISelect, ClearableFileInput, HTMXSelect, SelectSpeedWidget, SelectWithPK from virtualization.models import Cluster from wireless.models import WirelessLAN, WirelessLANGroup from .common import InterfaceCommonForm, ModuleCommonForm diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py index 46f783cb70e..3507faf3b2b 100644 --- a/netbox/dcim/forms/object_create.py +++ b/netbox/dcim/forms/object_create.py @@ -3,7 +3,7 @@ from django.utils.translation import gettext as _ from dcim.models import * from netbox.forms import NetBoxModelForm -from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField +from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField from . import model_forms __all__ = ( diff --git a/netbox/dcim/tables/template_code.py b/netbox/dcim/tables/template_code.py index d390871c462..e0f38afefe3 100644 --- a/netbox/dcim/tables/template_code.py +++ b/netbox/dcim/tables/template_code.py @@ -12,12 +12,12 @@ LINKTERMINATION = """ CABLE_LENGTH = """ {% load helpers %} -{% if record.length %}{{ record.length|simplify_decimal }} {{ record.length_unit }}{% endif %} +{% if record.length %}{{ record.length|floatformat:"-2" }} {{ record.length_unit }}{% endif %} """ WEIGHT = """ {% load helpers %} -{% if value %}{{ value|simplify_decimal }} {{ record.weight_unit }}{% endif %} +{% if value %}{{ value|floatformat:"-2" }} {{ record.weight_unit }}{% endif %} """ DEVICE_LINK = """ diff --git a/netbox/extras/forms/bulk_edit.py b/netbox/extras/forms/bulk_edit.py index a4e0cabbabc..7c838be20a6 100644 --- a/netbox/extras/forms/bulk_edit.py +++ b/netbox/extras/forms/bulk_edit.py @@ -3,9 +3,9 @@ from django.utils.translation import gettext as _ from extras.choices import * from extras.models import * -from utilities.forms import ( - add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, -) +from utilities.forms import BulkEditForm, add_blank_choice +from utilities.forms.fields import ColorField +from utilities.forms.widgets import BulkEditNullBooleanSelect __all__ = ( 'ConfigContextBulkEditForm', diff --git a/netbox/extras/forms/bulk_import.py b/netbox/extras/forms/bulk_import.py index 28791ceb858..c344a32148a 100644 --- a/netbox/extras/forms/bulk_import.py +++ b/netbox/extras/forms/bulk_import.py @@ -7,7 +7,8 @@ from django.utils.translation import gettext as _ from extras.choices import CustomFieldVisibilityChoices, CustomFieldTypeChoices from extras.models import * from extras.utils import FeatureQuery -from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelForm, CSVMultipleContentTypeField, SlugField +from utilities.forms import CSVModelForm +from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVMultipleContentTypeField, SlugField __all__ = ( 'ConfigTemplateImportForm', diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 3a6c25cc788..0563023436f 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -10,10 +10,9 @@ from extras.models import * from extras.utils import FeatureQuery from netbox.forms.base import NetBoxModelFilterSetForm from tenancy.models import Tenant, TenantGroup -from utilities.forms import ( - add_blank_choice, APISelectMultiple, BOOLEAN_WITH_BLANK_CHOICES, ContentTypeMultipleChoiceField, DateTimePicker, - DynamicModelMultipleChoiceField, FilterForm, TagFilterField, -) +from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, FilterForm, add_blank_choice +from utilities.forms.fields import ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, TagFilterField +from utilities.forms.widgets import APISelectMultiple, DateTimePicker from virtualization.models import Cluster, ClusterGroup, ClusterType from .mixins import SavedFiltersMixin diff --git a/netbox/extras/forms/model_forms.py b/netbox/extras/forms/model_forms.py index c199d2b531b..c7c55e282a3 100644 --- a/netbox/extras/forms/model_forms.py +++ b/netbox/extras/forms/model_forms.py @@ -12,9 +12,10 @@ from extras.models import * from extras.utils import FeatureQuery from netbox.forms import NetBoxModelForm from tenancy.models import Tenant, TenantGroup -from utilities.forms import ( - add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, - DynamicModelMultipleChoiceField, JSONField, SlugField, +from utilities.forms import BootstrapMixin, add_blank_choice +from utilities.forms.fields import ( + CommentField, ContentTypeChoiceField, ContentTypeMultipleChoiceField, DynamicModelMultipleChoiceField, JSONField, + SlugField, ) from virtualization.models import Cluster, ClusterGroup, ClusterType diff --git a/netbox/extras/forms/reports.py b/netbox/extras/forms/reports.py index ed7f49304c7..6a0b99eec96 100644 --- a/netbox/extras/forms/reports.py +++ b/netbox/extras/forms/reports.py @@ -1,8 +1,8 @@ from django import forms -from django.utils import timezone from django.utils.translation import gettext as _ -from utilities.forms import BootstrapMixin, DateTimePicker, SelectDurationWidget +from utilities.forms import BootstrapMixin +from utilities.forms.widgets import DateTimePicker, SelectDurationWidget from utilities.utils import local_now __all__ = ( diff --git a/netbox/extras/forms/scripts.py b/netbox/extras/forms/scripts.py index ca739813295..29e5f47ab72 100644 --- a/netbox/extras/forms/scripts.py +++ b/netbox/extras/forms/scripts.py @@ -1,7 +1,8 @@ from django import forms from django.utils.translation import gettext as _ -from utilities.forms import BootstrapMixin, DateTimePicker, SelectDurationWidget +from utilities.forms import BootstrapMixin +from utilities.forms.widgets import DateTimePicker, SelectDurationWidget from utilities.utils import local_now __all__ = ( diff --git a/netbox/extras/scripts.py b/netbox/extras/scripts.py index b5be917f369..db94b6cbf8a 100644 --- a/netbox/extras/scripts.py +++ b/netbox/extras/scripts.py @@ -21,7 +21,8 @@ from extras.signals import clear_webhooks from ipam.formfields import IPAddressFormField, IPNetworkFormField from ipam.validators import MaxPrefixLengthValidator, MinPrefixLengthValidator, prefix_validator from utilities.exceptions import AbortScript, AbortTransaction -from utilities.forms import add_blank_choice, DynamicModelChoiceField, DynamicModelMultipleChoiceField +from utilities.forms import add_blank_choice +from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField from .context_managers import change_logging from .forms import ScriptForm diff --git a/netbox/ipam/api/views.py b/netbox/ipam/api/views.py index 5263b049aba..f432e0e6b06 100644 --- a/netbox/ipam/api/views.py +++ b/netbox/ipam/api/views.py @@ -2,7 +2,7 @@ from django.core.exceptions import ObjectDoesNotExist, PermissionDenied from django.db import transaction from django.shortcuts import get_object_or_404 from django_pglocks import advisory_lock -from drf_spectacular.utils import extend_schema, extend_schema_view +from drf_spectacular.utils import extend_schema from rest_framework import status from rest_framework.response import Response from rest_framework.routers import APIRootView @@ -15,7 +15,7 @@ from ipam.models import * from netbox.api.viewsets import NetBoxModelViewSet from netbox.api.viewsets.mixins import ObjectValidationMixin from netbox.config import get_config -from utilities.constants import ADVISORY_LOCK_KEYS +from netbox.constants import ADVISORY_LOCK_KEYS from utilities.utils import count_related from . import serializers from ipam.models import L2VPN, L2VPNTermination diff --git a/netbox/ipam/forms/bulk_create.py b/netbox/ipam/forms/bulk_create.py index 6d07951a337..1ba786aae6e 100644 --- a/netbox/ipam/forms/bulk_create.py +++ b/netbox/ipam/forms/bulk_create.py @@ -1,7 +1,8 @@ from django import forms from django.utils.translation import gettext as _ -from utilities.forms import BootstrapMixin, ExpandableIPAddressField +from utilities.forms import BootstrapMixin +from utilities.forms.fields import ExpandableIPAddressField __all__ = ( 'IPAddressBulkCreateForm', diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index cd8ead81ad4..71ce1404062 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -8,10 +8,11 @@ from ipam.models import * from ipam.models import ASN from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant -from utilities.forms import ( - add_blank_choice, BulkEditNullBooleanSelect, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, - NumericArrayField, +from utilities.forms import add_blank_choice +from utilities.forms.fields import ( + CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, ) +from utilities.forms.widgets import BulkEditNullBooleanSelect __all__ = ( 'AggregateBulkEditForm', diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py index 67463ade808..fd0b315a0dd 100644 --- a/netbox/ipam/forms/bulk_import.py +++ b/netbox/ipam/forms/bulk_import.py @@ -9,7 +9,7 @@ from ipam.constants import * from ipam.models import * from netbox.forms import NetBoxModelImportForm from tenancy.models import Tenant -from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField +from utilities.forms.fields import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField from virtualization.models import VirtualMachine, VMInterface __all__ = ( diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index 83fe84cd2cc..53fecfe2f7c 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -8,9 +8,9 @@ from ipam.constants import * from ipam.models import * from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import TenancyFilterForm -from utilities.forms import ( - add_blank_choice, ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, - TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, +from utilities.forms import BOOLEAN_WITH_BLANK_CHOICES, add_blank_choice +from utilities.forms.fields import ( + ContentTypeMultipleChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, TagFilterField, ) from virtualization.models import VirtualMachine diff --git a/netbox/ipam/forms/model_forms.py b/netbox/ipam/forms/model_forms.py index 3904281a83e..9951b72e428 100644 --- a/netbox/ipam/forms/model_forms.py +++ b/netbox/ipam/forms/model_forms.py @@ -11,10 +11,12 @@ from ipam.models import * from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.exceptions import PermissionsViolation -from utilities.forms import ( - add_blank_choice, BootstrapMixin, CommentField, ContentTypeChoiceField, DatePicker, DynamicModelChoiceField, - DynamicModelMultipleChoiceField, NumericArrayField, SlugField, +from utilities.forms import BootstrapMixin, add_blank_choice +from utilities.forms.fields import ( + CommentField, ContentTypeChoiceField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, NumericArrayField, + SlugField, ) +from utilities.forms.widgets import DatePicker from virtualization.models import Cluster, ClusterGroup, VirtualMachine, VMInterface __all__ = ( diff --git a/netbox/netbox/constants.py b/netbox/netbox/constants.py index 0889f6a5c43..d69edc69c38 100644 --- a/netbox/netbox/constants.py +++ b/netbox/netbox/constants.py @@ -5,3 +5,14 @@ NESTED_SERIALIZER_PREFIX = 'Nested' RQ_QUEUE_DEFAULT = 'default' RQ_QUEUE_HIGH = 'high' RQ_QUEUE_LOW = 'low' + +# Keys for PostgreSQL advisory locks. These are arbitrary bigints used by the advisory_lock +# context manager. When a lock is acquired, one of these keys will be used to identify said lock. +# When adding a new key, pick something arbitrary and unique so that it is easily searchable in +# query logs. +ADVISORY_LOCK_KEYS = { + 'available-prefixes': 100100, + 'available-ips': 100200, + 'available-vlans': 100300, + 'available-asns': 100400, +} diff --git a/netbox/netbox/filtersets.py b/netbox/netbox/filtersets.py index 3ac9174de57..a0c1edee88d 100644 --- a/netbox/netbox/filtersets.py +++ b/netbox/netbox/filtersets.py @@ -14,7 +14,7 @@ from utilities.constants import ( FILTER_CHAR_BASED_LOOKUP_MAP, FILTER_NEGATION_LOOKUP_MAP, FILTER_TREENODE_NEGATION_LOOKUP_MAP, FILTER_NUMERIC_BASED_LOOKUP_MAP ) -from utilities.forms import MACAddressField +from utilities.forms.fields import MACAddressField from utilities import filters __all__ = ( diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 1053f60f368..db0fd7dfdfa 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -242,7 +242,7 @@ Channel Frequency {% if object.rf_channel_frequency %} - {{ object.rf_channel_frequency|simplify_decimal }} MHz + {{ object.rf_channel_frequency|floatformat:"-2" }} MHz {% else %} {{ ''|placeholder }} {% endif %} @@ -250,7 +250,7 @@ {% if peer %} {% if peer.rf_channel_frequency %} - {{ peer.rf_channel_frequency|simplify_decimal }} MHz + {{ peer.rf_channel_frequency|floatformat:"-2" }} MHz {% else %} {{ ''|placeholder }} {% endif %} @@ -261,7 +261,7 @@ Channel Width {% if object.rf_channel_width %} - {{ object.rf_channel_width|simplify_decimal }} MHz + {{ object.rf_channel_width|floatformat:"-3" }} MHz {% else %} {{ ''|placeholder }} {% endif %} @@ -269,7 +269,7 @@ {% if peer %} {% if peer.rf_channel_width %} - {{ peer.rf_channel_width|simplify_decimal }} MHz + {{ peer.rf_channel_width|floatformat:"-3" }} MHz {% else %} {{ ''|placeholder }} {% endif %} diff --git a/netbox/templates/wireless/inc/wirelesslink_interface.html b/netbox/templates/wireless/inc/wirelesslink_interface.html index 7732816a757..b2ad55adf81 100644 --- a/netbox/templates/wireless/inc/wirelesslink_interface.html +++ b/netbox/templates/wireless/inc/wirelesslink_interface.html @@ -31,7 +31,7 @@ Channel Frequency {% if interface.rf_channel_frequency %} - {{ interface.rf_channel_frequency|simplify_decimal }} MHz + {{ interface.rf_channel_frequency|floatformat:"-2" }} MHz {% else %} {{ ''|placeholder }} {% endif %} @@ -41,7 +41,7 @@ Channel Width {% if interface.rf_channel_width %} - {{ interface.rf_channel_width|simplify_decimal }} MHz + {{ interface.rf_channel_width|floatformat:"-3" }} MHz {% else %} {{ ''|placeholder }} {% endif %} diff --git a/netbox/tenancy/forms/bulk_edit.py b/netbox/tenancy/forms/bulk_edit.py index ae30d93ff64..34ca352397b 100644 --- a/netbox/tenancy/forms/bulk_edit.py +++ b/netbox/tenancy/forms/bulk_edit.py @@ -3,7 +3,8 @@ from django import forms from netbox.forms import NetBoxModelBulkEditForm from tenancy.choices import ContactPriorityChoices from tenancy.models import * -from utilities.forms import CommentField, DynamicModelChoiceField, add_blank_choice +from utilities.forms import add_blank_choice +from utilities.forms.fields import CommentField, DynamicModelChoiceField __all__ = ( 'ContactAssignmentBulkEditForm', diff --git a/netbox/tenancy/forms/bulk_import.py b/netbox/tenancy/forms/bulk_import.py index 8a251a3167a..f9b8accd9ac 100644 --- a/netbox/tenancy/forms/bulk_import.py +++ b/netbox/tenancy/forms/bulk_import.py @@ -1,7 +1,7 @@ from django.utils.translation import gettext as _ from netbox.forms import NetBoxModelImportForm from tenancy.models import * -from utilities.forms import CSVModelChoiceField, SlugField +from utilities.forms.fields import CSVModelChoiceField, SlugField __all__ = ( 'ContactImportForm', diff --git a/netbox/tenancy/forms/forms.py b/netbox/tenancy/forms/forms.py index 5e78bc5405e..789566e94b4 100644 --- a/netbox/tenancy/forms/forms.py +++ b/netbox/tenancy/forms/forms.py @@ -2,7 +2,7 @@ from django import forms from django.utils.translation import gettext as _ from tenancy.models import * -from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField +from utilities.forms.fields import DynamicModelChoiceField, DynamicModelMultipleChoiceField __all__ = ( 'ContactModelFilterForm', diff --git a/netbox/tenancy/forms/model_forms.py b/netbox/tenancy/forms/model_forms.py index a27e41f7427..6d6534d40e4 100644 --- a/netbox/tenancy/forms/model_forms.py +++ b/netbox/tenancy/forms/model_forms.py @@ -2,9 +2,8 @@ from django import forms from netbox.forms import NetBoxModelForm from tenancy.models import * -from utilities.forms import ( - BootstrapMixin, CommentField, DynamicModelChoiceField, SlugField, -) +from utilities.forms import BootstrapMixin +from utilities.forms.fields import CommentField, DynamicModelChoiceField, SlugField __all__ = ( 'ContactAssignmentForm', diff --git a/netbox/users/forms.py b/netbox/users/forms.py index 0c7d7ea19dc..3dd5cb32f81 100644 --- a/netbox/users/forms.py +++ b/netbox/users/forms.py @@ -7,7 +7,8 @@ from django.utils.translation import gettext as _ from ipam.formfields import IPNetworkFormField from netbox.preferences import PREFERENCES -from utilities.forms import BootstrapMixin, DateTimePicker +from utilities.forms import BootstrapMixin +from utilities.forms.widgets import DateTimePicker from utilities.utils import flatten_dict from .models import Token, UserConfig diff --git a/netbox/utilities/api.py b/netbox/utilities/api.py index e3fc3c8d436..50bb033e481 100644 --- a/netbox/utilities/api.py +++ b/netbox/utilities/api.py @@ -10,6 +10,14 @@ from rest_framework.utils import formatting from netbox.api.exceptions import GraphQLTypeNotFound, SerializerNotFound from .utils import dynamic_import +__all__ = ( + 'get_graphql_type_for_model', + 'get_serializer_for_model', + 'get_view_name', + 'is_api_request', + 'rest_api_server_error', +) + def get_serializer_for_model(model, prefix=''): """ diff --git a/netbox/utilities/constants.py b/netbox/utilities/constants.py index 096b60a7006..366d8f7967e 100644 --- a/netbox/utilities/constants.py +++ b/netbox/utilities/constants.py @@ -31,21 +31,6 @@ FILTER_TREENODE_NEGATION_LOOKUP_MAP = dict( n='in' ) - -# Keys for PostgreSQL advisory locks. These are arbitrary bigints used by -# the advisory_lock contextmanager. When a lock is acquired, -# one of these keys will be used to identify said lock. -# -# When adding a new key, pick something arbitrary and unique so -# that it is easily searchable in query logs. - -ADVISORY_LOCK_KEYS = { - 'available-prefixes': 100100, - 'available-ips': 100200, - 'available-vlans': 100300, - 'available-asns': 100400, -} - # # HTTP Request META safe copy # diff --git a/netbox/utilities/exceptions.py b/netbox/utilities/exceptions.py index d7418d0cb5f..512bb4b6081 100644 --- a/netbox/utilities/exceptions.py +++ b/netbox/utilities/exceptions.py @@ -3,6 +3,7 @@ from rest_framework.exceptions import APIException __all__ = ( 'AbortRequest', + 'AbortScript', 'AbortTransaction', 'PermissionsViolation', 'RQWorkerNotRunningException', diff --git a/netbox/utilities/fields.py b/netbox/utilities/fields.py index b2bc4d2cd07..8934e4ad64d 100644 --- a/netbox/utilities/fields.py +++ b/netbox/utilities/fields.py @@ -1,21 +1,23 @@ from collections import defaultdict from django.contrib.contenttypes.fields import GenericForeignKey -from django.core.validators import RegexValidator from django.db import models from utilities.ordering import naturalize -from .forms import ColorSelect +from .forms.widgets import ColorSelect +from .validators import ColorValidator -ColorValidator = RegexValidator( - regex='^[0-9a-f]{6}$', - message='Enter a valid hexadecimal RGB color code.', - code='invalid' +__all__ = ( + 'ColorField', + 'NaturalOrderingField', + 'NullableCharField', + 'RestrictedGenericForeignKey', ) # Deprecated: Retained only to ensure successful migration from early releases # Use models.CharField(null=True) instead +# TODO: Remove in v4.0 class NullableCharField(models.CharField): description = "Stores empty values as NULL rather than ''" diff --git a/netbox/utilities/files.py b/netbox/utilities/files.py index 68afe2962a4..09ed2c90b5a 100644 --- a/netbox/utilities/files.py +++ b/netbox/utilities/files.py @@ -1,5 +1,9 @@ import hashlib +__all__ = ( + 'sha256_hash', +) + def sha256_hash(filepath): """ diff --git a/netbox/utilities/filters.py b/netbox/utilities/filters.py index cfe21063b4c..1bf17beae32 100644 --- a/netbox/utilities/filters.py +++ b/netbox/utilities/filters.py @@ -6,6 +6,22 @@ from django_filters.constants import EMPTY_VALUES from drf_spectacular.utils import extend_schema_field from drf_spectacular.types import OpenApiTypes +__all__ = ( + 'ContentTypeFilter', + 'MACAddressFilter', + 'MultiValueCharFilter', + 'MultiValueDateFilter', + 'MultiValueDateTimeFilter', + 'MultiValueDecimalFilter', + 'MultiValueMACAddressFilter', + 'MultiValueNumberFilter', + 'MultiValueTimeFilter', + 'MultiValueWWNFilter', + 'NullableCharFieldFilter', + 'NumericArrayFilter', + 'TreeNodeMultipleChoiceFilter', +) + def multivalue_field_factory(field_class): """ diff --git a/netbox/utilities/forms/__init__.py b/netbox/utilities/forms/__init__.py index ce958a99e31..94f7d48c9d1 100644 --- a/netbox/utilities/forms/__init__.py +++ b/netbox/utilities/forms/__init__.py @@ -1,5 +1,4 @@ from .constants import * -from .fields import * from .forms import * +from .mixins import * from .utils import * -from .widgets import * diff --git a/netbox/utilities/forms/fields/__init__.py b/netbox/utilities/forms/fields/__init__.py index eacde0040a9..7f9f4b40940 100644 --- a/netbox/utilities/forms/fields/__init__.py +++ b/netbox/utilities/forms/fields/__init__.py @@ -1,3 +1,4 @@ +from .array import * from .content_types import * from .csv import * from .dynamic import * diff --git a/netbox/utilities/forms/fields/array.py b/netbox/utilities/forms/fields/array.py new file mode 100644 index 00000000000..6e1a40988d3 --- /dev/null +++ b/netbox/utilities/forms/fields/array.py @@ -0,0 +1,24 @@ +from django import forms +from django.contrib.postgres.forms import SimpleArrayField + +from ..utils import parse_numeric_range + +__all__ = ( + 'NumericArrayField', +) + + +class NumericArrayField(SimpleArrayField): + + def clean(self, value): + if value and not self.to_python(value): + raise forms.ValidationError(f'Invalid list ({value}). ' + f'Must be numeric and ranges must be in ascending order') + return super().clean(value) + + def to_python(self, value): + if not value: + return [] + if isinstance(value, str): + value = ','.join([str(n) for n in parse_numeric_range(value)]) + return super().to_python(value) diff --git a/netbox/utilities/forms/fields/content_types.py b/netbox/utilities/forms/fields/content_types.py index 76efe9a7bd8..0223ab05a10 100644 --- a/netbox/utilities/forms/fields/content_types.py +++ b/netbox/utilities/forms/fields/content_types.py @@ -1,6 +1,5 @@ from django import forms -from utilities.forms import widgets from utilities.utils import content_type_name __all__ = ( diff --git a/netbox/utilities/forms/fields/csv.py b/netbox/utilities/forms/fields/csv.py index f89f5f2ef73..5d625819397 100644 --- a/netbox/utilities/forms/fields/csv.py +++ b/netbox/utilities/forms/fields/csv.py @@ -1,14 +1,9 @@ -import csv -from io import StringIO - from django import forms from django.contrib.contenttypes.models import ContentType from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist from django.db.models import Q -from django.utils.translation import gettext as _ from utilities.choices import unpack_grouped_choices -from utilities.forms.utils import parse_csv, validate_csv from utilities.utils import content_type_identifier __all__ = ( diff --git a/netbox/utilities/forms/forms.py b/netbox/utilities/forms/forms.py index b06fb2e48ae..9f84e100f67 100644 --- a/netbox/utilities/forms/forms.py +++ b/netbox/utilities/forms/forms.py @@ -2,96 +2,31 @@ import re from django import forms from django.utils.translation import gettext as _ - -from .widgets import APISelect, APISelectMultiple, ClearableFileInput +from .mixins import BootstrapMixin __all__ = ( - 'BootstrapMixin', 'BulkEditForm', 'BulkRenameForm', 'ConfirmationForm', 'CSVModelForm', 'FilterForm', - 'ReturnURLForm', 'TableConfigForm', ) -# -# Mixins -# - -class BootstrapMixin: +class ConfirmationForm(BootstrapMixin, forms.Form): """ - Add the base Bootstrap CSS classes to form elements. + A generic confirmation form. The form is not valid unless the `confirm` field is checked. """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - exempt_widgets = [ - forms.FileInput, - forms.RadioSelect, - APISelect, - APISelectMultiple, - ClearableFileInput, - ] - - for field_name, field in self.fields.items(): - css = field.widget.attrs.get('class', '') - - if field.widget.__class__ in exempt_widgets: - continue - - elif isinstance(field.widget, forms.CheckboxInput): - field.widget.attrs['class'] = f'{css} form-check-input' - - elif isinstance(field.widget, forms.SelectMultiple): - if 'size' not in field.widget.attrs: - field.widget.attrs['class'] = f'{css} netbox-static-select' - - elif isinstance(field.widget, forms.Select): - field.widget.attrs['class'] = f'{css} netbox-static-select' - - else: - field.widget.attrs['class'] = f'{css} form-control' - - if field.required and not isinstance(field.widget, forms.FileInput): - field.widget.attrs['required'] = 'required' - - if 'placeholder' not in field.widget.attrs and field.label is not None: - field.widget.attrs['placeholder'] = field.label - - def is_valid(self): - is_valid = super().is_valid() - - # Apply is-invalid CSS class to fields with errors - if not is_valid: - for field_name in self.errors: - # Ignore e.g. __all__ - if field := self.fields.get(field_name): - css = field.widget.attrs.get('class', '') - field.widget.attrs['class'] = f'{css} is-invalid' - - return is_valid - - -# -# Form classes -# - -class ReturnURLForm(forms.Form): - """ - Provides a hidden return URL field to control where the user is directed after the form is submitted. - """ - return_url = forms.CharField(required=False, widget=forms.HiddenInput()) - - -class ConfirmationForm(BootstrapMixin, ReturnURLForm): - """ - A generic confirmation form. The form is not valid unless the confirm field is checked. - """ - confirm = forms.BooleanField(required=True, widget=forms.HiddenInput(), initial=True) + return_url = forms.CharField( + required=False, + widget=forms.HiddenInput() + ) + confirm = forms.BooleanField( + required=True, + widget=forms.HiddenInput(), + initial=True + ) class BulkEditForm(BootstrapMixin, forms.Form): diff --git a/netbox/utilities/forms/mixins.py b/netbox/utilities/forms/mixins.py new file mode 100644 index 00000000000..dc9c3eb803b --- /dev/null +++ b/netbox/utilities/forms/mixins.py @@ -0,0 +1,62 @@ +from django import forms + +from .widgets import APISelect, APISelectMultiple, ClearableFileInput + +__all__ = ( + 'BootstrapMixin', +) + + +class BootstrapMixin: + """ + Add the base Bootstrap CSS classes to form elements. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + exempt_widgets = [ + forms.FileInput, + forms.RadioSelect, + APISelect, + APISelectMultiple, + ClearableFileInput, + ] + + for field_name, field in self.fields.items(): + css = field.widget.attrs.get('class', '') + + if field.widget.__class__ in exempt_widgets: + continue + + elif isinstance(field.widget, forms.CheckboxInput): + field.widget.attrs['class'] = f'{css} form-check-input' + + elif isinstance(field.widget, forms.SelectMultiple): + if 'size' not in field.widget.attrs: + field.widget.attrs['class'] = f'{css} netbox-static-select' + + elif isinstance(field.widget, forms.Select): + field.widget.attrs['class'] = f'{css} netbox-static-select' + + else: + field.widget.attrs['class'] = f'{css} form-control' + + if field.required and not isinstance(field.widget, forms.FileInput): + field.widget.attrs['required'] = 'required' + + if 'placeholder' not in field.widget.attrs and field.label is not None: + field.widget.attrs['placeholder'] = field.label + + def is_valid(self): + is_valid = super().is_valid() + + # Apply is-invalid CSS class to fields with errors + if not is_valid: + for field_name in self.errors: + # Ignore e.g. __all__ + if field := self.fields.get(field_name): + css = field.widget.attrs.get('class', '') + field.widget.attrs['class'] = f'{css} is-invalid' + + return is_valid diff --git a/netbox/utilities/forms/widgets/__init__.py b/netbox/utilities/forms/widgets/__init__.py new file mode 100644 index 00000000000..9bd9f4faa0b --- /dev/null +++ b/netbox/utilities/forms/widgets/__init__.py @@ -0,0 +1,4 @@ +from .apiselect import * +from .datetime import * +from .misc import * +from .select import * diff --git a/netbox/utilities/forms/widgets.py b/netbox/utilities/forms/widgets/apiselect.py similarity index 56% rename from netbox/utilities/forms/widgets.py rename to netbox/utilities/forms/widgets/apiselect.py index 7b20d00c916..e4b02cb1dac 100644 --- a/netbox/utilities/forms/widgets.py +++ b/netbox/utilities/forms/widgets/apiselect.py @@ -1,120 +1,14 @@ import json -from typing import Dict, Sequence, List, Tuple, Union +from typing import Dict, List, Tuple from django import forms from django.conf import settings -from django.contrib.postgres.forms import SimpleArrayField - -from utilities.choices import ColorChoices -from .utils import add_blank_choice, parse_numeric_range __all__ = ( 'APISelect', 'APISelectMultiple', - 'BulkEditNullBooleanSelect', - 'ClearableFileInput', - 'ColorSelect', - 'DatePicker', - 'DateTimePicker', - 'HTMXSelect', - 'MarkdownWidget', - 'NumericArrayField', - 'SelectDurationWidget', - 'SelectSpeedWidget', - 'SelectWithPK', - 'SlugWidget', - 'TimePicker', ) -JSONPrimitive = Union[str, bool, int, float, None] -QueryParamValue = Union[JSONPrimitive, Sequence[JSONPrimitive]] -QueryParam = Dict[str, QueryParamValue] -ProcessedParams = Sequence[Dict[str, Sequence[JSONPrimitive]]] - - -class SlugWidget(forms.TextInput): - """ - Subclass TextInput and add a slug regeneration button next to the form field. - """ - template_name = 'widgets/sluginput.html' - - -class ColorSelect(forms.Select): - """ - Extends the built-in Select widget to colorize each