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