diff --git a/docs/plugins/development/forms.md b/docs/plugins/development/forms.md
index 976997bea..51f6c70de 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 39cdd85d0..efc9b5f3a 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 690cea828..d55831008 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 aeeddfd36..075855f3b 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 8aeaa9619..2925efec1 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 5a24ba90f..de8727643 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 ee8faa125..7c3f2ab09 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 67732d5be..304bc346a 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 4127aa3ea..179ff9b67 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 1a5257165..5966588fa 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 d596542af..73eda38fe 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 f047d621b..064a9a80b 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 443dc9143..8e3dcdc68 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 4ccc2fe54..727064e8f 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 6c8ca7566..f899c31e1 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 46f783cb7..3507faf3b 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 d390871c4..e0f38afef 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 a4e0cabba..7c838be20 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 28791ceb8..c344a3214 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 3a6c25cc7..056302343 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 c199d2b53..c7c55e282 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 ed7f49304..6a0b99eec 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 ca7398132..29e5f47ab 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 b5be917f3..db94b6cbf 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 5263b049a..f432e0e6b 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 6d07951a3..1ba786aae 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 cd8ead81a..71ce14040 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 67463ade8..fd0b315a0 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 83fe84cd2..53fecfe2f 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 3904281a8..9951b72e4 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 0889f6a5c..d69edc69c 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 3ac9174de..a0c1edee8 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 1053f60f3..db0fd7dfd 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 7732816a7..b2ad55adf 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 ae30d93ff..34ca35239 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 8a251a316..f9b8accd9 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 5e78bc540..789566e94 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 a27e41f74..6d6534d40 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 0c7d7ea19..3dd5cb32f 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 e3fc3c8d4..50bb033e4 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 096b60a70..366d8f796 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 d7418d0cb..512bb4b60 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 b2bc4d2cd..8934e4ad6 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 68afe2962..09ed2c90b 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 cfe21063b..1bf17beae 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 ce958a99e..94f7d48c9 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 eacde0040..7f9f4b409 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 000000000..6e1a40988
--- /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 76efe9a7b..0223ab05a 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 f89f5f2ef..5d6258193 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 b06fb2e48..9f84e100f 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 000000000..dc9c3eb80
--- /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 000000000..9bd9f4faa
--- /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 7b20d00c9..e4b02cb1d 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 |