From 0fe72376b158ebcfca02f29a92c2c0ce29be91c5 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 28 Jan 2022 10:00:36 -0500 Subject: [PATCH 1/8] Remove unused form attribute from BulkDeleteView --- netbox/netbox/views/generic/bulk_views.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/netbox/netbox/views/generic/bulk_views.py b/netbox/netbox/views/generic/bulk_views.py index 9c834d76f..a042a3cff 100644 --- a/netbox/netbox/views/generic/bulk_views.py +++ b/netbox/netbox/views/generic/bulk_views.py @@ -675,14 +675,13 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView): """ Delete objects in bulk. - filterset: FilterSet to apply when deleting by QuerySet - table: The table used to display devices being deleted - form: The form class used to delete objects in bulk + Attributes: + filterset: FilterSet to apply when deleting by QuerySet + table: The table used to display devices being deleted """ template_name = 'generic/object_bulk_delete.html' filterset = None table = None - form = None def get_required_permission(self): return get_permission_for_model(self.queryset.model, 'delete') @@ -694,9 +693,6 @@ class BulkDeleteView(GetReturnURLMixin, BaseMultiObjectView): class BulkDeleteForm(ConfirmationForm): pk = ModelMultipleChoiceField(queryset=self.queryset, widget=MultipleHiddenInput) - if self.form: - return self.form - return BulkDeleteForm # From f4776731ecc3d815c8002e9bbc50b81299e34403 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 28 Jan 2022 15:48:15 -0500 Subject: [PATCH 2/8] Establish 4 core forms in netbox.forms.base --- docs/plugins/development/forms.md | 10 ++ mkdocs.yml | 1 + netbox/circuits/forms/bulk_edit.py | 10 +- netbox/circuits/forms/bulk_import.py | 10 +- netbox/circuits/forms/filtersets.py | 10 +- netbox/circuits/forms/models.py | 10 +- netbox/dcim/forms/bulk_edit.py | 70 +++++------- netbox/dcim/forms/bulk_import.py | 56 ++++----- netbox/dcim/forms/connections.py | 8 +- netbox/dcim/forms/filtersets.py | 43 +++---- netbox/dcim/forms/models.py | 60 +++++----- netbox/dcim/forms/object_create.py | 4 +- netbox/extras/forms/customfields.py | 81 ------------- netbox/extras/forms/models.py | 17 --- netbox/ipam/forms/bulk_edit.py | 28 ++--- netbox/ipam/forms/bulk_import.py | 30 ++--- netbox/ipam/forms/filtersets.py | 28 ++--- netbox/ipam/forms/models.py | 32 +++--- netbox/netbox/{forms.py => forms/__init__.py} | 1 + netbox/netbox/forms/base.py | 108 ++++++++++++++++++ netbox/tenancy/forms/bulk_edit.py | 12 +- netbox/tenancy/forms/bulk_import.py | 12 +- netbox/tenancy/forms/filtersets.py | 12 +- netbox/tenancy/forms/models.py | 12 +- netbox/virtualization/forms/bulk_edit.py | 12 +- netbox/virtualization/forms/bulk_import.py | 12 +- netbox/virtualization/forms/filtersets.py | 13 ++- netbox/virtualization/forms/models.py | 12 +- netbox/wireless/forms/bulk_edit.py | 8 +- netbox/wireless/forms/bulk_import.py | 8 +- netbox/wireless/forms/filtersets.py | 8 +- netbox/wireless/forms/models.py | 8 +- 32 files changed, 380 insertions(+), 366 deletions(-) create mode 100644 docs/plugins/development/forms.md rename netbox/netbox/{forms.py => forms/__init__.py} (98%) create mode 100644 netbox/netbox/forms/base.py diff --git a/docs/plugins/development/forms.md b/docs/plugins/development/forms.md new file mode 100644 index 000000000..5af178194 --- /dev/null +++ b/docs/plugins/development/forms.md @@ -0,0 +1,10 @@ +# Forms + +NetBox provides several base form classes for use by plugins. These are documented below. + +* `NetBoxModelForm` +* `NetBoxModelCSVForm` +* `NetBoxModelBulkEditForm` +* `NetBoxModelFilterSetForm` + +### TODO: Include forms reference diff --git a/mkdocs.yml b/mkdocs.yml index 3b1e52f50..004f21c5e 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -105,6 +105,7 @@ nav: - Models: 'plugins/development/models.md' - Views: 'plugins/development/views.md' - Tables: 'plugins/development/tables.md' + - Forms: 'plugins/development/forms.md' - Filter Sets: 'plugins/development/filtersets.md' - REST API: 'plugins/development/rest-api.md' - Background Tasks: 'plugins/development/background-tasks.md' diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index af6bca91f..3e54cf711 100644 --- a/netbox/circuits/forms/bulk_edit.py +++ b/netbox/circuits/forms/bulk_edit.py @@ -2,7 +2,7 @@ from django import forms from circuits.choices import CircuitStatusChoices from circuits.models import * -from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm +from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import add_blank_choice, CommentField, DynamicModelChoiceField, SmallTextarea, StaticSelect @@ -14,7 +14,7 @@ __all__ = ( ) -class ProviderBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class ProviderBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Provider.objects.all(), widget=forms.MultipleHiddenInput @@ -53,7 +53,7 @@ class ProviderBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): ] -class ProviderNetworkBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ProviderNetwork.objects.all(), widget=forms.MultipleHiddenInput @@ -81,7 +81,7 @@ class ProviderNetworkBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditFor ] -class CircuitTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=CircuitType.objects.all(), widget=forms.MultipleHiddenInput @@ -95,7 +95,7 @@ class CircuitTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['description'] -class CircuitBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class CircuitBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Circuit.objects.all(), widget=forms.MultipleHiddenInput diff --git a/netbox/circuits/forms/bulk_import.py b/netbox/circuits/forms/bulk_import.py index fe1b927e5..6da79f75c 100644 --- a/netbox/circuits/forms/bulk_import.py +++ b/netbox/circuits/forms/bulk_import.py @@ -1,6 +1,6 @@ from circuits.choices import CircuitStatusChoices from circuits.models import * -from extras.forms import CustomFieldModelCSVForm +from netbox.forms import NetBoxModelCSVForm from tenancy.models import Tenant from utilities.forms import CSVChoiceField, CSVModelChoiceField, SlugField @@ -12,7 +12,7 @@ __all__ = ( ) -class ProviderCSVForm(CustomFieldModelCSVForm): +class ProviderCSVForm(NetBoxModelCSVForm): slug = SlugField() class Meta: @@ -22,7 +22,7 @@ class ProviderCSVForm(CustomFieldModelCSVForm): ) -class ProviderNetworkCSVForm(CustomFieldModelCSVForm): +class ProviderNetworkCSVForm(NetBoxModelCSVForm): provider = CSVModelChoiceField( queryset=Provider.objects.all(), to_field_name='name', @@ -36,7 +36,7 @@ class ProviderNetworkCSVForm(CustomFieldModelCSVForm): ] -class CircuitTypeCSVForm(CustomFieldModelCSVForm): +class CircuitTypeCSVForm(NetBoxModelCSVForm): slug = SlugField() class Meta: @@ -47,7 +47,7 @@ class CircuitTypeCSVForm(CustomFieldModelCSVForm): } -class CircuitCSVForm(CustomFieldModelCSVForm): +class CircuitCSVForm(NetBoxModelCSVForm): provider = CSVModelChoiceField( queryset=Provider.objects.all(), to_field_name='name', diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py index f5ff65088..18f914b58 100644 --- a/netbox/circuits/forms/filtersets.py +++ b/netbox/circuits/forms/filtersets.py @@ -4,7 +4,7 @@ from django.utils.translation import gettext as _ from circuits.choices import CircuitStatusChoices from circuits.models import * from dcim.models import Region, Site, SiteGroup -from extras.forms import CustomFieldModelFilterForm +from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import TenancyFilterForm from utilities.forms import DynamicModelMultipleChoiceField, StaticSelectMultiple, TagFilterField @@ -16,7 +16,7 @@ __all__ = ( ) -class ProviderFilterForm(CustomFieldModelFilterForm): +class ProviderFilterForm(NetBoxModelFilterSetForm): model = Provider field_groups = [ ['q', 'tag'], @@ -49,7 +49,7 @@ class ProviderFilterForm(CustomFieldModelFilterForm): tag = TagFilterField(model) -class ProviderNetworkFilterForm(CustomFieldModelFilterForm): +class ProviderNetworkFilterForm(NetBoxModelFilterSetForm): model = ProviderNetwork field_groups = ( ('q', 'tag'), @@ -67,12 +67,12 @@ class ProviderNetworkFilterForm(CustomFieldModelFilterForm): tag = TagFilterField(model) -class CircuitTypeFilterForm(CustomFieldModelFilterForm): +class CircuitTypeFilterForm(NetBoxModelFilterSetForm): model = CircuitType tag = TagFilterField(model) -class CircuitFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): +class CircuitFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Circuit field_groups = [ ['q', 'tag'], diff --git a/netbox/circuits/forms/models.py b/netbox/circuits/forms/models.py index f67114828..bf5d92e85 100644 --- a/netbox/circuits/forms/models.py +++ b/netbox/circuits/forms/models.py @@ -2,8 +2,8 @@ from django import forms from circuits.models import * from dcim.models import Region, Site, SiteGroup -from extras.forms import CustomFieldModelForm from extras.models import Tag +from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms import ( BootstrapMixin, CommentField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField, @@ -19,7 +19,7 @@ __all__ = ( ) -class ProviderForm(CustomFieldModelForm): +class ProviderForm(NetBoxModelForm): slug = SlugField() comments = CommentField() tags = DynamicModelMultipleChoiceField( @@ -53,7 +53,7 @@ class ProviderForm(CustomFieldModelForm): } -class ProviderNetworkForm(CustomFieldModelForm): +class ProviderNetworkForm(NetBoxModelForm): provider = DynamicModelChoiceField( queryset=Provider.objects.all() ) @@ -73,7 +73,7 @@ class ProviderNetworkForm(CustomFieldModelForm): ) -class CircuitTypeForm(CustomFieldModelForm): +class CircuitTypeForm(NetBoxModelForm): slug = SlugField() tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), @@ -87,7 +87,7 @@ class CircuitTypeForm(CustomFieldModelForm): ] -class CircuitForm(TenancyForm, CustomFieldModelForm): +class CircuitForm(TenancyForm, NetBoxModelForm): provider = DynamicModelChoiceField( queryset=Provider.objects.all() ) diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 3d73ada47..f5eb179fa 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -6,8 +6,8 @@ from timezone_field import TimeZoneFormField from dcim.choices import * from dcim.constants import * from dcim.models import * -from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm from ipam.models import ASN, VLAN, VRF +from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, BulkEditForm, BulkEditNullBooleanSelect, ColorField, CommentField, DynamicModelChoiceField, @@ -57,7 +57,7 @@ __all__ = ( ) -class RegionBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class RegionBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Region.objects.all(), widget=forms.MultipleHiddenInput @@ -75,7 +75,7 @@ class RegionBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['parent', 'description'] -class SiteGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class SiteGroupBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=SiteGroup.objects.all(), widget=forms.MultipleHiddenInput @@ -93,7 +93,7 @@ class SiteGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['parent', 'description'] -class SiteBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class SiteBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Site.objects.all(), widget=forms.MultipleHiddenInput @@ -137,7 +137,7 @@ class SiteBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): ] -class LocationBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class LocationBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Location.objects.all(), widget=forms.MultipleHiddenInput @@ -166,7 +166,7 @@ class LocationBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['parent', 'tenant', 'description'] -class RackRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class RackRoleBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=RackRole.objects.all(), widget=forms.MultipleHiddenInput @@ -183,7 +183,7 @@ class RackRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['color', 'description'] -class RackBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class RackBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Rack.objects.all(), widget=forms.MultipleHiddenInput @@ -283,7 +283,7 @@ class RackBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): ] -class RackReservationBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class RackReservationBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=RackReservation.objects.all(), widget=forms.MultipleHiddenInput() @@ -308,7 +308,7 @@ class RackReservationBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditFor nullable_fields = [] -class ManufacturerBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class ManufacturerBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Manufacturer.objects.all(), widget=forms.MultipleHiddenInput @@ -322,7 +322,7 @@ class ManufacturerBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['description'] -class DeviceTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=DeviceType.objects.all(), widget=forms.MultipleHiddenInput() @@ -353,7 +353,7 @@ class DeviceTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['part_number', 'airflow'] -class ModuleTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ModuleType.objects.all(), widget=forms.MultipleHiddenInput() @@ -370,7 +370,7 @@ class ModuleTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['part_number'] -class DeviceRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=DeviceRole.objects.all(), widget=forms.MultipleHiddenInput @@ -392,7 +392,7 @@ class DeviceRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['color', 'description'] -class PlatformBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class PlatformBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Platform.objects.all(), widget=forms.MultipleHiddenInput @@ -415,7 +415,7 @@ class PlatformBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['manufacturer', 'napalm_driver', 'description'] -class DeviceBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class DeviceBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Device.objects.all(), widget=forms.MultipleHiddenInput() @@ -476,7 +476,7 @@ class DeviceBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): ] -class ModuleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class ModuleBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Module.objects.all(), widget=forms.MultipleHiddenInput() @@ -502,7 +502,7 @@ class ModuleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['serial'] -class CableBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class CableBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Cable.objects.all(), widget=forms.MultipleHiddenInput @@ -558,7 +558,7 @@ class CableBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): }) -class VirtualChassisBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=VirtualChassis.objects.all(), widget=forms.MultipleHiddenInput() @@ -572,7 +572,7 @@ class VirtualChassisBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm nullable_fields = ['domain'] -class PowerPanelBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class PowerPanelBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=PowerPanel.objects.all(), widget=forms.MultipleHiddenInput @@ -611,7 +611,7 @@ class PowerPanelBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['location'] -class PowerFeedBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class PowerFeedBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=PowerFeed.objects.all(), widget=forms.MultipleHiddenInput @@ -939,8 +939,7 @@ class InventoryItemTemplateBulkEditForm(BulkEditForm): class ConsolePortBulkEditForm( form_from_model(ConsolePort, ['label', 'type', 'speed', 'mark_connected', 'description']), - AddRemoveTagsForm, - CustomFieldModelBulkEditForm + NetBoxModelBulkEditForm ): pk = forms.ModelMultipleChoiceField( queryset=ConsolePort.objects.all(), @@ -957,8 +956,7 @@ class ConsolePortBulkEditForm( class ConsoleServerPortBulkEditForm( form_from_model(ConsoleServerPort, ['label', 'type', 'speed', 'mark_connected', 'description']), - AddRemoveTagsForm, - CustomFieldModelBulkEditForm + NetBoxModelBulkEditForm ): pk = forms.ModelMultipleChoiceField( queryset=ConsoleServerPort.objects.all(), @@ -975,8 +973,7 @@ class ConsoleServerPortBulkEditForm( class PowerPortBulkEditForm( form_from_model(PowerPort, ['label', 'type', 'maximum_draw', 'allocated_draw', 'mark_connected', 'description']), - AddRemoveTagsForm, - CustomFieldModelBulkEditForm + NetBoxModelBulkEditForm ): pk = forms.ModelMultipleChoiceField( queryset=PowerPort.objects.all(), @@ -993,8 +990,7 @@ class PowerPortBulkEditForm( class PowerOutletBulkEditForm( form_from_model(PowerOutlet, ['label', 'type', 'feed_leg', 'power_port', 'mark_connected', 'description']), - AddRemoveTagsForm, - CustomFieldModelBulkEditForm + NetBoxModelBulkEditForm ): pk = forms.ModelMultipleChoiceField( queryset=PowerOutlet.objects.all(), @@ -1031,8 +1027,7 @@ class InterfaceBulkEditForm( 'label', 'type', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'mgmt_only', 'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', ]), - AddRemoveTagsForm, - CustomFieldModelBulkEditForm + NetBoxModelBulkEditForm ): pk = forms.ModelMultipleChoiceField( queryset=Interface.objects.all(), @@ -1154,8 +1149,7 @@ class InterfaceBulkEditForm( class FrontPortBulkEditForm( form_from_model(FrontPort, ['label', 'type', 'color', 'mark_connected', 'description']), - AddRemoveTagsForm, - CustomFieldModelBulkEditForm + NetBoxModelBulkEditForm ): pk = forms.ModelMultipleChoiceField( queryset=FrontPort.objects.all(), @@ -1168,8 +1162,7 @@ class FrontPortBulkEditForm( class RearPortBulkEditForm( form_from_model(RearPort, ['label', 'type', 'color', 'mark_connected', 'description']), - AddRemoveTagsForm, - CustomFieldModelBulkEditForm + NetBoxModelBulkEditForm ): pk = forms.ModelMultipleChoiceField( queryset=RearPort.objects.all(), @@ -1182,8 +1175,7 @@ class RearPortBulkEditForm( class ModuleBayBulkEditForm( form_from_model(DeviceBay, ['label', 'description']), - AddRemoveTagsForm, - CustomFieldModelBulkEditForm + NetBoxModelBulkEditForm ): pk = forms.ModelMultipleChoiceField( queryset=ModuleBay.objects.all(), @@ -1196,8 +1188,7 @@ class ModuleBayBulkEditForm( class DeviceBayBulkEditForm( form_from_model(DeviceBay, ['label', 'description']), - AddRemoveTagsForm, - CustomFieldModelBulkEditForm + NetBoxModelBulkEditForm ): pk = forms.ModelMultipleChoiceField( queryset=DeviceBay.objects.all(), @@ -1210,8 +1201,7 @@ class DeviceBayBulkEditForm( class InventoryItemBulkEditForm( form_from_model(InventoryItem, ['label', 'role', 'manufacturer', 'part_id', 'description']), - AddRemoveTagsForm, - CustomFieldModelBulkEditForm + NetBoxModelBulkEditForm ): pk = forms.ModelMultipleChoiceField( queryset=InventoryItem.objects.all(), @@ -1234,7 +1224,7 @@ class InventoryItemBulkEditForm( # Device component roles # -class InventoryItemRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class InventoryItemRoleBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=InventoryItemRole.objects.all(), widget=forms.MultipleHiddenInput diff --git a/netbox/dcim/forms/bulk_import.py b/netbox/dcim/forms/bulk_import.py index 1aec329eb..3974c4d54 100644 --- a/netbox/dcim/forms/bulk_import.py +++ b/netbox/dcim/forms/bulk_import.py @@ -7,8 +7,8 @@ from django.utils.safestring import mark_safe from dcim.choices import * from dcim.constants import * from dcim.models import * -from extras.forms import CustomFieldModelCSVForm from ipam.models import VRF +from netbox.forms import NetBoxModelCSVForm from tenancy.models import Tenant from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, CSVTypedChoiceField, SlugField from virtualization.models import Cluster @@ -46,7 +46,7 @@ __all__ = ( ) -class RegionCSVForm(CustomFieldModelCSVForm): +class RegionCSVForm(NetBoxModelCSVForm): parent = CSVModelChoiceField( queryset=Region.objects.all(), required=False, @@ -59,7 +59,7 @@ class RegionCSVForm(CustomFieldModelCSVForm): fields = ('name', 'slug', 'parent', 'description') -class SiteGroupCSVForm(CustomFieldModelCSVForm): +class SiteGroupCSVForm(NetBoxModelCSVForm): parent = CSVModelChoiceField( queryset=SiteGroup.objects.all(), required=False, @@ -72,7 +72,7 @@ class SiteGroupCSVForm(CustomFieldModelCSVForm): fields = ('name', 'slug', 'parent', 'description') -class SiteCSVForm(CustomFieldModelCSVForm): +class SiteCSVForm(NetBoxModelCSVForm): status = CSVChoiceField( choices=SiteStatusChoices, help_text='Operational status' @@ -109,7 +109,7 @@ class SiteCSVForm(CustomFieldModelCSVForm): } -class LocationCSVForm(CustomFieldModelCSVForm): +class LocationCSVForm(NetBoxModelCSVForm): site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', @@ -136,7 +136,7 @@ class LocationCSVForm(CustomFieldModelCSVForm): fields = ('site', 'parent', 'name', 'slug', 'tenant', 'description') -class RackRoleCSVForm(CustomFieldModelCSVForm): +class RackRoleCSVForm(NetBoxModelCSVForm): slug = SlugField() class Meta: @@ -147,7 +147,7 @@ class RackRoleCSVForm(CustomFieldModelCSVForm): } -class RackCSVForm(CustomFieldModelCSVForm): +class RackCSVForm(NetBoxModelCSVForm): site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name' @@ -205,7 +205,7 @@ class RackCSVForm(CustomFieldModelCSVForm): self.fields['location'].queryset = self.fields['location'].queryset.filter(**params) -class RackReservationCSVForm(CustomFieldModelCSVForm): +class RackReservationCSVForm(NetBoxModelCSVForm): site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', @@ -255,14 +255,14 @@ class RackReservationCSVForm(CustomFieldModelCSVForm): self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) -class ManufacturerCSVForm(CustomFieldModelCSVForm): +class ManufacturerCSVForm(NetBoxModelCSVForm): class Meta: model = Manufacturer fields = ('name', 'slug', 'description') -class DeviceRoleCSVForm(CustomFieldModelCSVForm): +class DeviceRoleCSVForm(NetBoxModelCSVForm): slug = SlugField() class Meta: @@ -273,7 +273,7 @@ class DeviceRoleCSVForm(CustomFieldModelCSVForm): } -class PlatformCSVForm(CustomFieldModelCSVForm): +class PlatformCSVForm(NetBoxModelCSVForm): slug = SlugField() manufacturer = CSVModelChoiceField( queryset=Manufacturer.objects.all(), @@ -287,7 +287,7 @@ class PlatformCSVForm(CustomFieldModelCSVForm): fields = ('name', 'slug', 'manufacturer', 'napalm_driver', 'napalm_args', 'description') -class BaseDeviceCSVForm(CustomFieldModelCSVForm): +class BaseDeviceCSVForm(NetBoxModelCSVForm): device_role = CSVModelChoiceField( queryset=DeviceRole.objects.all(), to_field_name='name', @@ -403,7 +403,7 @@ class DeviceCSVForm(BaseDeviceCSVForm): self.fields['rack'].queryset = self.fields['rack'].queryset.filter(**params) -class ModuleCSVForm(CustomFieldModelCSVForm): +class ModuleCSVForm(NetBoxModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name' @@ -478,7 +478,7 @@ class ChildDeviceCSVForm(BaseDeviceCSVForm): # Device components # -class ConsolePortCSVForm(CustomFieldModelCSVForm): +class ConsolePortCSVForm(NetBoxModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name' @@ -501,7 +501,7 @@ class ConsolePortCSVForm(CustomFieldModelCSVForm): fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description') -class ConsoleServerPortCSVForm(CustomFieldModelCSVForm): +class ConsoleServerPortCSVForm(NetBoxModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name' @@ -524,7 +524,7 @@ class ConsoleServerPortCSVForm(CustomFieldModelCSVForm): fields = ('device', 'name', 'label', 'type', 'speed', 'mark_connected', 'description') -class PowerPortCSVForm(CustomFieldModelCSVForm): +class PowerPortCSVForm(NetBoxModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name' @@ -542,7 +542,7 @@ class PowerPortCSVForm(CustomFieldModelCSVForm): ) -class PowerOutletCSVForm(CustomFieldModelCSVForm): +class PowerOutletCSVForm(NetBoxModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name' @@ -591,7 +591,7 @@ class PowerOutletCSVForm(CustomFieldModelCSVForm): self.fields['power_port'].queryset = PowerPort.objects.none() -class InterfaceCSVForm(CustomFieldModelCSVForm): +class InterfaceCSVForm(NetBoxModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name' @@ -655,7 +655,7 @@ class InterfaceCSVForm(CustomFieldModelCSVForm): return self.cleaned_data['enabled'] -class FrontPortCSVForm(CustomFieldModelCSVForm): +class FrontPortCSVForm(NetBoxModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name' @@ -703,7 +703,7 @@ class FrontPortCSVForm(CustomFieldModelCSVForm): self.fields['rear_port'].queryset = RearPort.objects.none() -class RearPortCSVForm(CustomFieldModelCSVForm): +class RearPortCSVForm(NetBoxModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name' @@ -721,7 +721,7 @@ class RearPortCSVForm(CustomFieldModelCSVForm): } -class ModuleBayCSVForm(CustomFieldModelCSVForm): +class ModuleBayCSVForm(NetBoxModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name' @@ -732,7 +732,7 @@ class ModuleBayCSVForm(CustomFieldModelCSVForm): fields = ('device', 'name', 'label', 'position', 'description') -class DeviceBayCSVForm(CustomFieldModelCSVForm): +class DeviceBayCSVForm(NetBoxModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name' @@ -778,7 +778,7 @@ class DeviceBayCSVForm(CustomFieldModelCSVForm): self.fields['installed_device'].queryset = Interface.objects.none() -class InventoryItemCSVForm(CustomFieldModelCSVForm): +class InventoryItemCSVForm(NetBoxModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name' @@ -827,7 +827,7 @@ class InventoryItemCSVForm(CustomFieldModelCSVForm): # Device component roles # -class InventoryItemRoleCSVForm(CustomFieldModelCSVForm): +class InventoryItemRoleCSVForm(NetBoxModelCSVForm): slug = SlugField() class Meta: @@ -842,7 +842,7 @@ class InventoryItemRoleCSVForm(CustomFieldModelCSVForm): # Cables # -class CableCSVForm(CustomFieldModelCSVForm): +class CableCSVForm(NetBoxModelCSVForm): # Termination A side_a_device = CSVModelChoiceField( queryset=Device.objects.all(), @@ -947,7 +947,7 @@ class CableCSVForm(CustomFieldModelCSVForm): # Virtual chassis # -class VirtualChassisCSVForm(CustomFieldModelCSVForm): +class VirtualChassisCSVForm(NetBoxModelCSVForm): master = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name', @@ -964,7 +964,7 @@ class VirtualChassisCSVForm(CustomFieldModelCSVForm): # Power # -class PowerPanelCSVForm(CustomFieldModelCSVForm): +class PowerPanelCSVForm(NetBoxModelCSVForm): site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', @@ -990,7 +990,7 @@ class PowerPanelCSVForm(CustomFieldModelCSVForm): self.fields['location'].queryset = self.fields['location'].queryset.filter(**params) -class PowerFeedCSVForm(CustomFieldModelCSVForm): +class PowerFeedCSVForm(NetBoxModelCSVForm): site = CSVModelChoiceField( queryset=Site.objects.all(), to_field_name='name', diff --git a/netbox/dcim/forms/connections.py b/netbox/dcim/forms/connections.py index 6a7a09023..7ae6a898a 100644 --- a/netbox/dcim/forms/connections.py +++ b/netbox/dcim/forms/connections.py @@ -1,7 +1,7 @@ from circuits.models import Circuit, CircuitTermination, Provider from dcim.models import * -from extras.forms import CustomFieldModelForm from extras.models import Tag +from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect @@ -18,7 +18,7 @@ __all__ = ( ) -class ConnectCableToDeviceForm(TenancyForm, CustomFieldModelForm): +class ConnectCableToDeviceForm(TenancyForm, NetBoxModelForm): """ Base form for connecting a Cable to a Device component """ @@ -171,7 +171,7 @@ class ConnectCableToRearPortForm(ConnectCableToDeviceForm): ) -class ConnectCableToCircuitTerminationForm(TenancyForm, CustomFieldModelForm): +class ConnectCableToCircuitTerminationForm(TenancyForm, NetBoxModelForm): termination_b_provider = DynamicModelChoiceField( queryset=Provider.objects.all(), label='Provider', @@ -229,7 +229,7 @@ class ConnectCableToCircuitTerminationForm(TenancyForm, CustomFieldModelForm): return getattr(self.cleaned_data['termination_b_id'], 'pk', None) -class ConnectCableToPowerFeedForm(TenancyForm, CustomFieldModelForm): +class ConnectCableToPowerFeedForm(TenancyForm, NetBoxModelForm): termination_b_region = DynamicModelChoiceField( queryset=Region.objects.all(), label='Region', diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index 8868cdf78..e9aa3ec3f 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -5,8 +5,9 @@ from django.utils.translation import gettext as _ from dcim.choices import * from dcim.constants import * from dcim.models import * -from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm +from extras.forms import LocalConfigContextFilterForm from ipam.models import ASN, VRF +from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import TenancyFilterForm from utilities.forms import ( APISelectMultiple, add_blank_choice, ColorField, DynamicModelMultipleChoiceField, FilterForm, StaticSelect, @@ -52,7 +53,7 @@ __all__ = ( ) -class DeviceComponentFilterForm(CustomFieldModelFilterForm): +class DeviceComponentFilterForm(NetBoxModelFilterSetForm): name = forms.CharField( required=False ) @@ -103,7 +104,7 @@ class DeviceComponentFilterForm(CustomFieldModelFilterForm): ) -class RegionFilterForm(CustomFieldModelFilterForm): +class RegionFilterForm(NetBoxModelFilterSetForm): model = Region parent_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), @@ -113,7 +114,7 @@ class RegionFilterForm(CustomFieldModelFilterForm): tag = TagFilterField(model) -class SiteGroupFilterForm(CustomFieldModelFilterForm): +class SiteGroupFilterForm(NetBoxModelFilterSetForm): model = SiteGroup parent_id = DynamicModelMultipleChoiceField( queryset=SiteGroup.objects.all(), @@ -123,7 +124,7 @@ class SiteGroupFilterForm(CustomFieldModelFilterForm): tag = TagFilterField(model) -class SiteFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): +class SiteFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Site field_groups = [ ['q', 'tag'], @@ -154,7 +155,7 @@ class SiteFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): tag = TagFilterField(model) -class LocationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): +class LocationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Location field_groups = [ ['q', 'tag'], @@ -192,12 +193,12 @@ class LocationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): tag = TagFilterField(model) -class RackRoleFilterForm(CustomFieldModelFilterForm): +class RackRoleFilterForm(NetBoxModelFilterSetForm): model = RackRole tag = TagFilterField(model) -class RackFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): +class RackFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Rack field_groups = [ ['q', 'tag'], @@ -270,7 +271,7 @@ class RackElevationFilterForm(RackFilterForm): ) -class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): +class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = RackReservation field_groups = [ ['q', 'tag'], @@ -308,12 +309,12 @@ class RackReservationFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): tag = TagFilterField(model) -class ManufacturerFilterForm(CustomFieldModelFilterForm): +class ManufacturerFilterForm(NetBoxModelFilterSetForm): model = Manufacturer tag = TagFilterField(model) -class DeviceTypeFilterForm(CustomFieldModelFilterForm): +class DeviceTypeFilterForm(NetBoxModelFilterSetForm): model = DeviceType field_groups = [ ['q', 'tag'], @@ -383,7 +384,7 @@ class DeviceTypeFilterForm(CustomFieldModelFilterForm): tag = TagFilterField(model) -class ModuleTypeFilterForm(CustomFieldModelFilterForm): +class ModuleTypeFilterForm(NetBoxModelFilterSetForm): model = ModuleType field_groups = [ ['q', 'tag'], @@ -444,12 +445,12 @@ class ModuleTypeFilterForm(CustomFieldModelFilterForm): tag = TagFilterField(model) -class DeviceRoleFilterForm(CustomFieldModelFilterForm): +class DeviceRoleFilterForm(NetBoxModelFilterSetForm): model = DeviceRole tag = TagFilterField(model) -class PlatformFilterForm(CustomFieldModelFilterForm): +class PlatformFilterForm(NetBoxModelFilterSetForm): model = Platform manufacturer_id = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), @@ -459,7 +460,7 @@ class PlatformFilterForm(CustomFieldModelFilterForm): tag = TagFilterField(model) -class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm): +class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): model = Device field_groups = [ ['q', 'tag'], @@ -613,7 +614,7 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi tag = TagFilterField(model) -class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm): +class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): model = Module field_groups = [ ['q', 'tag'], @@ -644,7 +645,7 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFi tag = TagFilterField(model) -class VirtualChassisFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): +class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = VirtualChassis field_groups = [ ['q', 'tag'], @@ -673,7 +674,7 @@ class VirtualChassisFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): tag = TagFilterField(model) -class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): +class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Cable field_groups = [ ['q', 'tag'], @@ -736,7 +737,7 @@ class CableFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): tag = TagFilterField(model) -class PowerPanelFilterForm(CustomFieldModelFilterForm): +class PowerPanelFilterForm(NetBoxModelFilterSetForm): model = PowerPanel field_groups = ( ('q', 'tag'), @@ -773,7 +774,7 @@ class PowerPanelFilterForm(CustomFieldModelFilterForm): tag = TagFilterField(model) -class PowerFeedFilterForm(CustomFieldModelFilterForm): +class PowerFeedFilterForm(NetBoxModelFilterSetForm): model = PowerFeed field_groups = [ ['q', 'tag'], @@ -1103,7 +1104,7 @@ class InventoryItemFilterForm(DeviceComponentFilterForm): # Device component roles # -class InventoryItemRoleFilterForm(CustomFieldModelFilterForm): +class InventoryItemRoleFilterForm(NetBoxModelFilterSetForm): model = InventoryItemRole tag = TagFilterField(model) diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index 378a567fc..80e785940 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -7,9 +7,9 @@ from timezone_field import TimeZoneFormField from dcim.choices import * from dcim.constants import * from dcim.models import * -from extras.forms import CustomFieldModelForm from extras.models import Tag from ipam.models import ASN, IPAddress, VLAN, VLANGroup, VRF +from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms import ( APISelect, add_blank_choice, BootstrapMixin, ClearableFileInput, CommentField, ContentTypeChoiceField, @@ -72,7 +72,7 @@ Tagged (All): Implies all VLANs are available (w/optional untagged VLAN) """ -class RegionForm(CustomFieldModelForm): +class RegionForm(NetBoxModelForm): parent = DynamicModelChoiceField( queryset=Region.objects.all(), required=False @@ -90,7 +90,7 @@ class RegionForm(CustomFieldModelForm): ) -class SiteGroupForm(CustomFieldModelForm): +class SiteGroupForm(NetBoxModelForm): parent = DynamicModelChoiceField( queryset=SiteGroup.objects.all(), required=False @@ -108,7 +108,7 @@ class SiteGroupForm(CustomFieldModelForm): ) -class SiteForm(TenancyForm, CustomFieldModelForm): +class SiteForm(TenancyForm, NetBoxModelForm): region = DynamicModelChoiceField( queryset=Region.objects.all(), required=False @@ -173,7 +173,7 @@ class SiteForm(TenancyForm, CustomFieldModelForm): } -class LocationForm(TenancyForm, CustomFieldModelForm): +class LocationForm(TenancyForm, NetBoxModelForm): region = DynamicModelChoiceField( queryset=Region.objects.all(), required=False, @@ -221,7 +221,7 @@ class LocationForm(TenancyForm, CustomFieldModelForm): ) -class RackRoleForm(CustomFieldModelForm): +class RackRoleForm(NetBoxModelForm): slug = SlugField() tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), @@ -235,7 +235,7 @@ class RackRoleForm(CustomFieldModelForm): ] -class RackForm(TenancyForm, CustomFieldModelForm): +class RackForm(TenancyForm, NetBoxModelForm): region = DynamicModelChoiceField( queryset=Region.objects.all(), required=False, @@ -295,7 +295,7 @@ class RackForm(TenancyForm, CustomFieldModelForm): } -class RackReservationForm(TenancyForm, CustomFieldModelForm): +class RackReservationForm(TenancyForm, NetBoxModelForm): region = DynamicModelChoiceField( queryset=Region.objects.all(), required=False, @@ -359,7 +359,7 @@ class RackReservationForm(TenancyForm, CustomFieldModelForm): ) -class ManufacturerForm(CustomFieldModelForm): +class ManufacturerForm(NetBoxModelForm): slug = SlugField() tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), @@ -373,7 +373,7 @@ class ManufacturerForm(CustomFieldModelForm): ] -class DeviceTypeForm(CustomFieldModelForm): +class DeviceTypeForm(NetBoxModelForm): manufacturer = DynamicModelChoiceField( queryset=Manufacturer.objects.all() ) @@ -412,7 +412,7 @@ class DeviceTypeForm(CustomFieldModelForm): } -class ModuleTypeForm(CustomFieldModelForm): +class ModuleTypeForm(NetBoxModelForm): manufacturer = DynamicModelChoiceField( queryset=Manufacturer.objects.all() ) @@ -429,7 +429,7 @@ class ModuleTypeForm(CustomFieldModelForm): ] -class DeviceRoleForm(CustomFieldModelForm): +class DeviceRoleForm(NetBoxModelForm): slug = SlugField() tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), @@ -443,7 +443,7 @@ class DeviceRoleForm(CustomFieldModelForm): ] -class PlatformForm(CustomFieldModelForm): +class PlatformForm(NetBoxModelForm): manufacturer = DynamicModelChoiceField( queryset=Manufacturer.objects.all(), required=False @@ -466,7 +466,7 @@ class PlatformForm(CustomFieldModelForm): } -class DeviceForm(TenancyForm, CustomFieldModelForm): +class DeviceForm(TenancyForm, NetBoxModelForm): region = DynamicModelChoiceField( queryset=Region.objects.all(), required=False, @@ -648,7 +648,7 @@ class DeviceForm(TenancyForm, CustomFieldModelForm): self.fields['position'].widget.choices = [(position, f'U{position}')] -class ModuleForm(CustomFieldModelForm): +class ModuleForm(NetBoxModelForm): device = DynamicModelChoiceField( queryset=Device.objects.all(), required=False, @@ -688,7 +688,7 @@ class ModuleForm(CustomFieldModelForm): ] -class CableForm(TenancyForm, CustomFieldModelForm): +class CableForm(TenancyForm, NetBoxModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False @@ -711,7 +711,7 @@ class CableForm(TenancyForm, CustomFieldModelForm): } -class PowerPanelForm(CustomFieldModelForm): +class PowerPanelForm(NetBoxModelForm): region = DynamicModelChoiceField( queryset=Region.objects.all(), required=False, @@ -755,7 +755,7 @@ class PowerPanelForm(CustomFieldModelForm): ) -class PowerFeedForm(CustomFieldModelForm): +class PowerFeedForm(NetBoxModelForm): region = DynamicModelChoiceField( queryset=Region.objects.all(), required=False, @@ -823,7 +823,7 @@ class PowerFeedForm(CustomFieldModelForm): # Virtual chassis # -class VirtualChassisForm(CustomFieldModelForm): +class VirtualChassisForm(NetBoxModelForm): master = forms.ModelChoiceField( queryset=Device.objects.all(), required=False, @@ -1120,7 +1120,7 @@ class InventoryItemTemplateForm(BootstrapMixin, forms.ModelForm): # Device components # -class ConsolePortForm(CustomFieldModelForm): +class ConsolePortForm(NetBoxModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False @@ -1138,7 +1138,7 @@ class ConsolePortForm(CustomFieldModelForm): } -class ConsoleServerPortForm(CustomFieldModelForm): +class ConsoleServerPortForm(NetBoxModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False @@ -1156,7 +1156,7 @@ class ConsoleServerPortForm(CustomFieldModelForm): } -class PowerPortForm(CustomFieldModelForm): +class PowerPortForm(NetBoxModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False @@ -1174,7 +1174,7 @@ class PowerPortForm(CustomFieldModelForm): } -class PowerOutletForm(CustomFieldModelForm): +class PowerOutletForm(NetBoxModelForm): power_port = DynamicModelChoiceField( queryset=PowerPort.objects.all(), required=False, @@ -1199,7 +1199,7 @@ class PowerOutletForm(CustomFieldModelForm): } -class InterfaceForm(InterfaceCommonForm, CustomFieldModelForm): +class InterfaceForm(InterfaceCommonForm, NetBoxModelForm): parent = DynamicModelChoiceField( queryset=Interface.objects.all(), required=False, @@ -1308,7 +1308,7 @@ class InterfaceForm(InterfaceCommonForm, CustomFieldModelForm): } -class FrontPortForm(CustomFieldModelForm): +class FrontPortForm(NetBoxModelForm): rear_port = DynamicModelChoiceField( queryset=RearPort.objects.all(), query_params={ @@ -1332,7 +1332,7 @@ class FrontPortForm(CustomFieldModelForm): } -class RearPortForm(CustomFieldModelForm): +class RearPortForm(NetBoxModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False @@ -1349,7 +1349,7 @@ class RearPortForm(CustomFieldModelForm): } -class ModuleBayForm(CustomFieldModelForm): +class ModuleBayForm(NetBoxModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False @@ -1365,7 +1365,7 @@ class ModuleBayForm(CustomFieldModelForm): } -class DeviceBayForm(CustomFieldModelForm): +class DeviceBayForm(NetBoxModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False @@ -1401,7 +1401,7 @@ class PopulateDeviceBayForm(BootstrapMixin, forms.Form): ).exclude(pk=device_bay.device.pk) -class InventoryItemForm(CustomFieldModelForm): +class InventoryItemForm(NetBoxModelForm): parent = DynamicModelChoiceField( queryset=InventoryItem.objects.all(), required=False, @@ -1451,7 +1451,7 @@ class InventoryItemForm(CustomFieldModelForm): # Device component roles # -class InventoryItemRoleForm(CustomFieldModelForm): +class InventoryItemRoleForm(NetBoxModelForm): slug = SlugField() tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), diff --git a/netbox/dcim/forms/object_create.py b/netbox/dcim/forms/object_create.py index 1fea886ea..73bb621fc 100644 --- a/netbox/dcim/forms/object_create.py +++ b/netbox/dcim/forms/object_create.py @@ -1,8 +1,8 @@ from django import forms from dcim.models import * -from extras.forms import CustomFieldModelForm from extras.models import Tag +from netbox.forms import NetBoxModelForm from utilities.forms import ( BootstrapMixin, DynamicModelChoiceField, DynamicModelMultipleChoiceField, ExpandableNameField, ) @@ -149,7 +149,7 @@ class FrontPortCreateForm(DeviceComponentCreateForm): } -class VirtualChassisCreateForm(CustomFieldModelForm): +class VirtualChassisCreateForm(NetBoxModelForm): region = DynamicModelChoiceField( queryset=Region.objects.all(), required=False, diff --git a/netbox/extras/forms/customfields.py b/netbox/extras/forms/customfields.py index 8912d0365..c3a44f47b 100644 --- a/netbox/extras/forms/customfields.py +++ b/netbox/extras/forms/customfields.py @@ -1,16 +1,8 @@ -from django import forms from django.contrib.contenttypes.models import ContentType -from django.db.models import Q -from extras.choices import * from extras.models import * -from utilities.forms import BootstrapMixin, BulkEditBaseForm, CSVModelForm __all__ = ( - 'CustomFieldModelCSVForm', - 'CustomFieldModelBulkEditForm', - 'CustomFieldModelFilterForm', - 'CustomFieldModelForm', 'CustomFieldsMixin', ) @@ -50,76 +42,3 @@ class CustomFieldsMixin: # Annotate the field in the list of CustomField form fields self.custom_fields[field_name] = customfield - - -class CustomFieldModelForm(BootstrapMixin, CustomFieldsMixin, forms.ModelForm): - """ - Extend ModelForm to include custom field support. - """ - def _get_content_type(self): - return ContentType.objects.get_for_model(self._meta.model) - - def _get_form_field(self, customfield): - if self.instance.pk: - form_field = customfield.to_form_field(set_initial=False) - form_field.initial = self.instance.custom_field_data.get(customfield.name, None) - return form_field - - return customfield.to_form_field() - - def clean(self): - - # Save custom field data on instance - for cf_name, customfield in self.custom_fields.items(): - key = cf_name[3:] # Strip "cf_" from field name - value = self.cleaned_data.get(cf_name) - - # Convert "empty" values to null - if value in self.fields[cf_name].empty_values: - self.instance.custom_field_data[key] = None - else: - self.instance.custom_field_data[key] = customfield.serialize(value) - - return super().clean() - - -class CustomFieldModelCSVForm(CSVModelForm, CustomFieldModelForm): - - def _get_form_field(self, customfield): - return customfield.to_form_field(for_csv_import=True) - - -class CustomFieldModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, BulkEditBaseForm): - - def _get_form_field(self, customfield): - return customfield.to_form_field(set_initial=False, enforce_required=False) - - def _append_customfield_fields(self): - """ - Append form fields for all CustomFields assigned to this object type. - """ - for customfield in self._get_custom_fields(self._get_content_type()): - # Annotate non-required custom fields as nullable - if not customfield.required: - self.nullable_fields.append(customfield.name) - - self.fields[customfield.name] = self._get_form_field(customfield) - - # Annotate the field in the list of CustomField form fields - self.custom_fields[customfield.name] = customfield - - -class CustomFieldModelFilterForm(BootstrapMixin, CustomFieldsMixin, forms.Form): - q = forms.CharField( - required=False, - label='Search' - ) - - def _get_custom_fields(self, content_type): - return CustomField.objects.filter(content_types=content_type).exclude( - Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) | - Q(type=CustomFieldTypeChoices.TYPE_JSON) - ) - - def _get_form_field(self, customfield): - return customfield.to_form_field(set_initial=False, enforce_required=False) diff --git a/netbox/extras/forms/models.py b/netbox/extras/forms/models.py index 9aac8454b..5c29a8381 100644 --- a/netbox/extras/forms/models.py +++ b/netbox/extras/forms/models.py @@ -13,7 +13,6 @@ from utilities.forms import ( from virtualization.models import Cluster, ClusterGroup, ClusterType __all__ = ( - 'AddRemoveTagsForm', 'ConfigContextForm', 'CustomFieldForm', 'CustomLinkForm', @@ -134,22 +133,6 @@ class TagForm(BootstrapMixin, forms.ModelForm): ) -class AddRemoveTagsForm(forms.Form): - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Add add/remove tags fields - self.fields['add_tags'] = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - self.fields['remove_tags'] = DynamicModelMultipleChoiceField( - queryset=Tag.objects.all(), - required=False - ) - - class ConfigContextForm(BootstrapMixin, forms.ModelForm): regions = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index 308a467d1..3da03dd41 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -1,11 +1,11 @@ from django import forms from dcim.models import Region, Site, SiteGroup -from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm from ipam.choices import * from ipam.constants import * 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, DatePicker, DynamicModelChoiceField, NumericArrayField, StaticSelect, @@ -30,7 +30,7 @@ __all__ = ( ) -class VRFBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class VRFBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=VRF.objects.all(), widget=forms.MultipleHiddenInput() @@ -55,7 +55,7 @@ class VRFBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): ] -class RouteTargetBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class RouteTargetBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=RouteTarget.objects.all(), widget=forms.MultipleHiddenInput() @@ -75,7 +75,7 @@ class RouteTargetBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): ] -class RIRBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class RIRBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=RIR.objects.all(), widget=forms.MultipleHiddenInput @@ -93,7 +93,7 @@ class RIRBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['is_private', 'description'] -class ASNBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class ASNBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ASN.objects.all(), widget=forms.MultipleHiddenInput() @@ -125,7 +125,7 @@ class ASNBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): } -class AggregateBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class AggregateBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Aggregate.objects.all(), widget=forms.MultipleHiddenInput() @@ -156,7 +156,7 @@ class AggregateBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): } -class RoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class RoleBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Role.objects.all(), widget=forms.MultipleHiddenInput @@ -173,7 +173,7 @@ class RoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['description'] -class PrefixBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class PrefixBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Prefix.objects.all(), widget=forms.MultipleHiddenInput() @@ -238,7 +238,7 @@ class PrefixBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): ] -class IPRangeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class IPRangeBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=IPRange.objects.all(), widget=forms.MultipleHiddenInput() @@ -272,7 +272,7 @@ class IPRangeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): ] -class IPAddressBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class IPAddressBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=IPAddress.objects.all(), widget=forms.MultipleHiddenInput() @@ -317,7 +317,7 @@ class IPAddressBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): ] -class FHRPGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=FHRPGroup.objects.all(), widget=forms.MultipleHiddenInput() @@ -352,7 +352,7 @@ class FHRPGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['auth_type', 'auth_key', 'description'] -class VLANGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class VLANGroupBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=VLANGroup.objects.all(), widget=forms.MultipleHiddenInput @@ -382,7 +382,7 @@ class VLANGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['site', 'description'] -class VLANBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class VLANBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=VLAN.objects.all(), widget=forms.MultipleHiddenInput() @@ -434,7 +434,7 @@ class VLANBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): ] -class ServiceTemplateBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ServiceTemplate.objects.all(), widget=forms.MultipleHiddenInput() diff --git a/netbox/ipam/forms/bulk_import.py b/netbox/ipam/forms/bulk_import.py index 1ae977fe5..365f82858 100644 --- a/netbox/ipam/forms/bulk_import.py +++ b/netbox/ipam/forms/bulk_import.py @@ -2,10 +2,10 @@ from django import forms from django.contrib.contenttypes.models import ContentType from dcim.models import Device, Interface, Site -from extras.forms import CustomFieldModelCSVForm from ipam.choices import * from ipam.constants import * from ipam.models import * +from netbox.forms import NetBoxModelCSVForm from tenancy.models import Tenant from utilities.forms import CSVChoiceField, CSVContentTypeField, CSVModelChoiceField, SlugField from virtualization.models import VirtualMachine, VMInterface @@ -28,7 +28,7 @@ __all__ = ( ) -class VRFCSVForm(CustomFieldModelCSVForm): +class VRFCSVForm(NetBoxModelCSVForm): tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, @@ -41,7 +41,7 @@ class VRFCSVForm(CustomFieldModelCSVForm): fields = ('name', 'rd', 'tenant', 'enforce_unique', 'description') -class RouteTargetCSVForm(CustomFieldModelCSVForm): +class RouteTargetCSVForm(NetBoxModelCSVForm): tenant = CSVModelChoiceField( queryset=Tenant.objects.all(), required=False, @@ -54,7 +54,7 @@ class RouteTargetCSVForm(CustomFieldModelCSVForm): fields = ('name', 'description', 'tenant') -class RIRCSVForm(CustomFieldModelCSVForm): +class RIRCSVForm(NetBoxModelCSVForm): slug = SlugField() class Meta: @@ -65,7 +65,7 @@ class RIRCSVForm(CustomFieldModelCSVForm): } -class AggregateCSVForm(CustomFieldModelCSVForm): +class AggregateCSVForm(NetBoxModelCSVForm): rir = CSVModelChoiceField( queryset=RIR.objects.all(), to_field_name='name', @@ -83,7 +83,7 @@ class AggregateCSVForm(CustomFieldModelCSVForm): fields = ('prefix', 'rir', 'tenant', 'date_added', 'description') -class ASNCSVForm(CustomFieldModelCSVForm): +class ASNCSVForm(NetBoxModelCSVForm): rir = CSVModelChoiceField( queryset=RIR.objects.all(), to_field_name='name', @@ -102,7 +102,7 @@ class ASNCSVForm(CustomFieldModelCSVForm): help_texts = {} -class RoleCSVForm(CustomFieldModelCSVForm): +class RoleCSVForm(NetBoxModelCSVForm): slug = SlugField() class Meta: @@ -110,7 +110,7 @@ class RoleCSVForm(CustomFieldModelCSVForm): fields = ('name', 'slug', 'weight', 'description') -class PrefixCSVForm(CustomFieldModelCSVForm): +class PrefixCSVForm(NetBoxModelCSVForm): vrf = CSVModelChoiceField( queryset=VRF.objects.all(), to_field_name='name', @@ -174,7 +174,7 @@ class PrefixCSVForm(CustomFieldModelCSVForm): self.fields['vlan'].queryset = self.fields['vlan'].queryset.filter(**params) -class IPRangeCSVForm(CustomFieldModelCSVForm): +class IPRangeCSVForm(NetBoxModelCSVForm): vrf = CSVModelChoiceField( queryset=VRF.objects.all(), to_field_name='name', @@ -205,7 +205,7 @@ class IPRangeCSVForm(CustomFieldModelCSVForm): ) -class IPAddressCSVForm(CustomFieldModelCSVForm): +class IPAddressCSVForm(NetBoxModelCSVForm): vrf = CSVModelChoiceField( queryset=VRF.objects.all(), to_field_name='name', @@ -312,7 +312,7 @@ class IPAddressCSVForm(CustomFieldModelCSVForm): return ipaddress -class FHRPGroupCSVForm(CustomFieldModelCSVForm): +class FHRPGroupCSVForm(NetBoxModelCSVForm): protocol = CSVChoiceField( choices=FHRPGroupProtocolChoices ) @@ -326,7 +326,7 @@ class FHRPGroupCSVForm(CustomFieldModelCSVForm): fields = ('protocol', 'group_id', 'auth_type', 'auth_key', 'description') -class VLANGroupCSVForm(CustomFieldModelCSVForm): +class VLANGroupCSVForm(NetBoxModelCSVForm): slug = SlugField() scope_type = CSVContentTypeField( queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), @@ -354,7 +354,7 @@ class VLANGroupCSVForm(CustomFieldModelCSVForm): } -class VLANCSVForm(CustomFieldModelCSVForm): +class VLANCSVForm(NetBoxModelCSVForm): site = CSVModelChoiceField( queryset=Site.objects.all(), required=False, @@ -393,7 +393,7 @@ class VLANCSVForm(CustomFieldModelCSVForm): } -class ServiceTemplateCSVForm(CustomFieldModelCSVForm): +class ServiceTemplateCSVForm(NetBoxModelCSVForm): protocol = CSVChoiceField( choices=ServiceProtocolChoices, help_text='IP protocol' @@ -404,7 +404,7 @@ class ServiceTemplateCSVForm(CustomFieldModelCSVForm): fields = ('name', 'protocol', 'ports', 'description') -class ServiceCSVForm(CustomFieldModelCSVForm): +class ServiceCSVForm(NetBoxModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), required=False, diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index 9bfb1df10..4301a1810 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -2,11 +2,11 @@ from django import forms from django.utils.translation import gettext as _ from dcim.models import Location, Rack, Region, Site, SiteGroup -from extras.forms import CustomFieldModelFilterForm from ipam.choices import * from ipam.constants import * from ipam.models import * from ipam.models import ASN +from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import TenancyFilterForm from utilities.forms import ( add_blank_choice, DynamicModelChoiceField, DynamicModelMultipleChoiceField, StaticSelect, StaticSelectMultiple, @@ -39,7 +39,7 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([ ]) -class VRFFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): +class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = VRF field_groups = [ ['q', 'tag'], @@ -59,7 +59,7 @@ class VRFFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): tag = TagFilterField(model) -class RouteTargetFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): +class RouteTargetFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = RouteTarget field_groups = [ ['q', 'tag'], @@ -79,7 +79,7 @@ class RouteTargetFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): tag = TagFilterField(model) -class RIRFilterForm(CustomFieldModelFilterForm): +class RIRFilterForm(NetBoxModelFilterSetForm): model = RIR is_private = forms.NullBooleanField( required=False, @@ -91,7 +91,7 @@ class RIRFilterForm(CustomFieldModelFilterForm): tag = TagFilterField(model) -class AggregateFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): +class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Aggregate field_groups = [ ['q', 'tag'], @@ -112,7 +112,7 @@ class AggregateFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): tag = TagFilterField(model) -class ASNFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): +class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = ASN field_groups = [ ['q'], @@ -132,12 +132,12 @@ class ASNFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): ) -class RoleFilterForm(CustomFieldModelFilterForm): +class RoleFilterForm(NetBoxModelFilterSetForm): model = Role tag = TagFilterField(model) -class PrefixFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): +class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Prefix field_groups = [ ['q', 'tag'], @@ -228,7 +228,7 @@ class PrefixFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): tag = TagFilterField(model) -class IPRangeFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): +class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = IPRange field_groups = [ ['q', 'tag'], @@ -261,7 +261,7 @@ class IPRangeFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): tag = TagFilterField(model) -class IPAddressFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): +class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = IPAddress field_groups = [ ['q', 'tag'], @@ -321,7 +321,7 @@ class IPAddressFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): tag = TagFilterField(model) -class FHRPGroupFilterForm(CustomFieldModelFilterForm): +class FHRPGroupFilterForm(NetBoxModelFilterSetForm): model = FHRPGroup field_groups = ( ('q', 'tag'), @@ -351,7 +351,7 @@ class FHRPGroupFilterForm(CustomFieldModelFilterForm): tag = TagFilterField(model) -class VLANGroupFilterForm(CustomFieldModelFilterForm): +class VLANGroupFilterForm(NetBoxModelFilterSetForm): field_groups = [ ['q', 'tag'], ['region', 'sitegroup', 'site', 'location', 'rack'], @@ -394,7 +394,7 @@ class VLANGroupFilterForm(CustomFieldModelFilterForm): tag = TagFilterField(model) -class VLANFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): +class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = VLAN field_groups = [ ['q', 'tag'], @@ -448,7 +448,7 @@ class VLANFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): tag = TagFilterField(model) -class ServiceTemplateFilterForm(CustomFieldModelFilterForm): +class ServiceTemplateFilterForm(NetBoxModelFilterSetForm): model = ServiceTemplate field_groups = ( ('q', 'tag'), diff --git a/netbox/ipam/forms/models.py b/netbox/ipam/forms/models.py index 34c67773f..e86fe1dab 100644 --- a/netbox/ipam/forms/models.py +++ b/netbox/ipam/forms/models.py @@ -2,13 +2,13 @@ from django import forms from django.contrib.contenttypes.models import ContentType from dcim.models import Device, Interface, Location, Rack, Region, Site, SiteGroup -from extras.forms import CustomFieldModelForm from extras.models import Tag from ipam.choices import * from ipam.constants import * from ipam.formfields import IPNetworkFormField from ipam.models import * from ipam.models import ASN +from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.exceptions import PermissionsViolation from utilities.forms import ( @@ -39,7 +39,7 @@ __all__ = ( ) -class VRFForm(TenancyForm, CustomFieldModelForm): +class VRFForm(TenancyForm, NetBoxModelForm): import_targets = DynamicModelMultipleChoiceField( queryset=RouteTarget.objects.all(), required=False @@ -72,7 +72,7 @@ class VRFForm(TenancyForm, CustomFieldModelForm): } -class RouteTargetForm(TenancyForm, CustomFieldModelForm): +class RouteTargetForm(TenancyForm, NetBoxModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False @@ -89,7 +89,7 @@ class RouteTargetForm(TenancyForm, CustomFieldModelForm): ) -class RIRForm(CustomFieldModelForm): +class RIRForm(NetBoxModelForm): slug = SlugField() tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), @@ -103,7 +103,7 @@ class RIRForm(CustomFieldModelForm): ] -class AggregateForm(TenancyForm, CustomFieldModelForm): +class AggregateForm(TenancyForm, NetBoxModelForm): rir = DynamicModelChoiceField( queryset=RIR.objects.all(), label='RIR' @@ -131,7 +131,7 @@ class AggregateForm(TenancyForm, CustomFieldModelForm): } -class ASNForm(TenancyForm, CustomFieldModelForm): +class ASNForm(TenancyForm, NetBoxModelForm): rir = DynamicModelChoiceField( queryset=RIR.objects.all(), label='RIR', @@ -175,7 +175,7 @@ class ASNForm(TenancyForm, CustomFieldModelForm): return instance -class RoleForm(CustomFieldModelForm): +class RoleForm(NetBoxModelForm): slug = SlugField() tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), @@ -189,7 +189,7 @@ class RoleForm(CustomFieldModelForm): ] -class PrefixForm(TenancyForm, CustomFieldModelForm): +class PrefixForm(TenancyForm, NetBoxModelForm): vrf = DynamicModelChoiceField( queryset=VRF.objects.all(), required=False, @@ -264,7 +264,7 @@ class PrefixForm(TenancyForm, CustomFieldModelForm): } -class IPRangeForm(TenancyForm, CustomFieldModelForm): +class IPRangeForm(TenancyForm, NetBoxModelForm): vrf = DynamicModelChoiceField( queryset=VRF.objects.all(), required=False, @@ -293,7 +293,7 @@ class IPRangeForm(TenancyForm, CustomFieldModelForm): } -class IPAddressForm(TenancyForm, CustomFieldModelForm): +class IPAddressForm(TenancyForm, NetBoxModelForm): device = DynamicModelChoiceField( queryset=Device.objects.all(), required=False, @@ -506,7 +506,7 @@ class IPAddressForm(TenancyForm, CustomFieldModelForm): return ipaddress -class IPAddressBulkAddForm(TenancyForm, CustomFieldModelForm): +class IPAddressBulkAddForm(TenancyForm, NetBoxModelForm): vrf = DynamicModelChoiceField( queryset=VRF.objects.all(), required=False, @@ -540,7 +540,7 @@ class IPAddressAssignForm(BootstrapMixin, forms.Form): ) -class FHRPGroupForm(CustomFieldModelForm): +class FHRPGroupForm(NetBoxModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False @@ -629,7 +629,7 @@ class FHRPGroupAssignmentForm(BootstrapMixin, forms.ModelForm): self.fields['group'].widget.add_query_param('related_ip', ipaddress.pk) -class VLANGroupForm(CustomFieldModelForm): +class VLANGroupForm(NetBoxModelForm): scope_type = ContentTypeChoiceField( queryset=ContentType.objects.filter(model__in=VLANGROUP_SCOPE_TYPES), required=False @@ -736,7 +736,7 @@ class VLANGroupForm(CustomFieldModelForm): self.instance.scope_id = None -class VLANForm(TenancyForm, CustomFieldModelForm): +class VLANForm(TenancyForm, NetBoxModelForm): # VLANGroup assignment fields scope_type = forms.ChoiceField( choices=( @@ -817,7 +817,7 @@ class VLANForm(TenancyForm, CustomFieldModelForm): } -class ServiceTemplateForm(CustomFieldModelForm): +class ServiceTemplateForm(NetBoxModelForm): ports = NumericArrayField( base_field=forms.IntegerField( min_value=SERVICE_PORT_MIN, @@ -838,7 +838,7 @@ class ServiceTemplateForm(CustomFieldModelForm): } -class ServiceForm(CustomFieldModelForm): +class ServiceForm(NetBoxModelForm): device = DynamicModelChoiceField( queryset=Device.objects.all(), required=False diff --git a/netbox/netbox/forms.py b/netbox/netbox/forms/__init__.py similarity index 98% rename from netbox/netbox/forms.py rename to netbox/netbox/forms/__init__.py index b5d68c1fc..9984a4461 100644 --- a/netbox/netbox/forms.py +++ b/netbox/netbox/forms/__init__.py @@ -1,6 +1,7 @@ from django import forms from utilities.forms import BootstrapMixin +from .base import * OBJ_TYPE_CHOICES = ( ('', 'All Objects'), diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py new file mode 100644 index 000000000..601b0062a --- /dev/null +++ b/netbox/netbox/forms/base.py @@ -0,0 +1,108 @@ +from django import forms +from django.contrib.contenttypes.models import ContentType +from django.db.models import Q + +from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices +from extras.forms.customfields import CustomFieldsMixin +from extras.models import CustomField, Tag +from utilities.forms import BootstrapMixin, BulkEditBaseForm, CSVModelForm +from utilities.forms.fields import DynamicModelMultipleChoiceField + +__all__ = ( + 'NetBoxModelForm', + 'NetBoxModelCSVForm', + 'NetBoxModelBulkEditForm', + 'NetBoxModelFilterSetForm', +) + + +class NetBoxModelForm(BootstrapMixin, CustomFieldsMixin, forms.ModelForm): + """ + Base form for creating & editing NetBox models. Adds support for custom fields. + """ + def _get_content_type(self): + return ContentType.objects.get_for_model(self._meta.model) + + def _get_form_field(self, customfield): + if self.instance.pk: + form_field = customfield.to_form_field(set_initial=False) + form_field.initial = self.instance.custom_field_data.get(customfield.name, None) + return form_field + + return customfield.to_form_field() + + def clean(self): + + # Save custom field data on instance + for cf_name, customfield in self.custom_fields.items(): + key = cf_name[3:] # Strip "cf_" from field name + value = self.cleaned_data.get(cf_name) + + # Convert "empty" values to null + if value in self.fields[cf_name].empty_values: + self.instance.custom_field_data[key] = None + else: + self.instance.custom_field_data[key] = customfield.serialize(value) + + return super().clean() + + +class NetBoxModelCSVForm(CSVModelForm, NetBoxModelForm): + """ + Base form for creating a NetBox objects from CSV data. Used for bulk importing. + """ + def _get_form_field(self, customfield): + return customfield.to_form_field(for_csv_import=True) + + +class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, BulkEditBaseForm): + """ + Base form for modifying multiple NetBox objects (of the same type) in bulk via the UI. Adds support for custom + fields and adding/removing tags. + """ + add_tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + remove_tags = DynamicModelMultipleChoiceField( + queryset=Tag.objects.all(), + required=False + ) + + def _get_form_field(self, customfield): + return customfield.to_form_field(set_initial=False, enforce_required=False) + + def _append_customfield_fields(self): + """ + Append form fields for all CustomFields assigned to this object type. + """ + for customfield in self._get_custom_fields(self._get_content_type()): + # Annotate non-required custom fields as nullable + if not customfield.required: + self.nullable_fields.append(customfield.name) + + self.fields[customfield.name] = self._get_form_field(customfield) + + # Annotate the field in the list of CustomField form fields + self.custom_fields[customfield.name] = customfield + + +class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, forms.Form): + """ + Base form for FilerSet forms. These are used to filter object lists in the NetBox UI. + + The corresponding FilterSet *must* provide a `q` filter. + """ + q = forms.CharField( + required=False, + label='Search' + ) + + def _get_custom_fields(self, content_type): + return CustomField.objects.filter(content_types=content_type).exclude( + Q(filter_logic=CustomFieldFilterLogicChoices.FILTER_DISABLED) | + Q(type=CustomFieldTypeChoices.TYPE_JSON) + ) + + def _get_form_field(self, customfield): + return customfield.to_form_field(set_initial=False, enforce_required=False) diff --git a/netbox/tenancy/forms/bulk_edit.py b/netbox/tenancy/forms/bulk_edit.py index 9dc1b8ec5..9b14a167b 100644 --- a/netbox/tenancy/forms/bulk_edit.py +++ b/netbox/tenancy/forms/bulk_edit.py @@ -1,6 +1,6 @@ from django import forms -from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm +from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import * from utilities.forms import DynamicModelChoiceField @@ -17,7 +17,7 @@ __all__ = ( # Tenants # -class TenantGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class TenantGroupBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=TenantGroup.objects.all(), widget=forms.MultipleHiddenInput @@ -35,7 +35,7 @@ class TenantGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['parent', 'description'] -class TenantBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class TenantBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Tenant.objects.all(), widget=forms.MultipleHiddenInput() @@ -55,7 +55,7 @@ class TenantBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): # Contacts # -class ContactGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class ContactGroupBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ContactGroup.objects.all(), widget=forms.MultipleHiddenInput @@ -73,7 +73,7 @@ class ContactGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['parent', 'description'] -class ContactRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class ContactRoleBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ContactRole.objects.all(), widget=forms.MultipleHiddenInput @@ -87,7 +87,7 @@ class ContactRoleBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['description'] -class ContactBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class ContactBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Contact.objects.all(), widget=forms.MultipleHiddenInput() diff --git a/netbox/tenancy/forms/bulk_import.py b/netbox/tenancy/forms/bulk_import.py index 51b863cac..409590c28 100644 --- a/netbox/tenancy/forms/bulk_import.py +++ b/netbox/tenancy/forms/bulk_import.py @@ -1,4 +1,4 @@ -from extras.forms import CustomFieldModelCSVForm +from netbox.forms import NetBoxModelCSVForm from tenancy.models import * from utilities.forms import CSVModelChoiceField, SlugField @@ -15,7 +15,7 @@ __all__ = ( # Tenants # -class TenantGroupCSVForm(CustomFieldModelCSVForm): +class TenantGroupCSVForm(NetBoxModelCSVForm): parent = CSVModelChoiceField( queryset=TenantGroup.objects.all(), required=False, @@ -29,7 +29,7 @@ class TenantGroupCSVForm(CustomFieldModelCSVForm): fields = ('name', 'slug', 'parent', 'description') -class TenantCSVForm(CustomFieldModelCSVForm): +class TenantCSVForm(NetBoxModelCSVForm): slug = SlugField() group = CSVModelChoiceField( queryset=TenantGroup.objects.all(), @@ -47,7 +47,7 @@ class TenantCSVForm(CustomFieldModelCSVForm): # Contacts # -class ContactGroupCSVForm(CustomFieldModelCSVForm): +class ContactGroupCSVForm(NetBoxModelCSVForm): parent = CSVModelChoiceField( queryset=ContactGroup.objects.all(), required=False, @@ -61,7 +61,7 @@ class ContactGroupCSVForm(CustomFieldModelCSVForm): fields = ('name', 'slug', 'parent', 'description') -class ContactRoleCSVForm(CustomFieldModelCSVForm): +class ContactRoleCSVForm(NetBoxModelCSVForm): slug = SlugField() class Meta: @@ -69,7 +69,7 @@ class ContactRoleCSVForm(CustomFieldModelCSVForm): fields = ('name', 'slug', 'description') -class ContactCSVForm(CustomFieldModelCSVForm): +class ContactCSVForm(NetBoxModelCSVForm): group = CSVModelChoiceField( queryset=ContactGroup.objects.all(), required=False, diff --git a/netbox/tenancy/forms/filtersets.py b/netbox/tenancy/forms/filtersets.py index 7849e2171..8fb4b50ff 100644 --- a/netbox/tenancy/forms/filtersets.py +++ b/netbox/tenancy/forms/filtersets.py @@ -1,6 +1,6 @@ from django.utils.translation import gettext as _ -from extras.forms import CustomFieldModelFilterForm +from netbox.forms import NetBoxModelFilterSetForm from tenancy.models import * from utilities.forms import DynamicModelMultipleChoiceField, TagFilterField @@ -17,7 +17,7 @@ __all__ = ( # Tenants # -class TenantGroupFilterForm(CustomFieldModelFilterForm): +class TenantGroupFilterForm(NetBoxModelFilterSetForm): model = TenantGroup parent_id = DynamicModelMultipleChoiceField( queryset=TenantGroup.objects.all(), @@ -27,7 +27,7 @@ class TenantGroupFilterForm(CustomFieldModelFilterForm): tag = TagFilterField(model) -class TenantFilterForm(CustomFieldModelFilterForm): +class TenantFilterForm(NetBoxModelFilterSetForm): model = Tenant field_groups = ( ('q', 'tag'), @@ -46,7 +46,7 @@ class TenantFilterForm(CustomFieldModelFilterForm): # Contacts # -class ContactGroupFilterForm(CustomFieldModelFilterForm): +class ContactGroupFilterForm(NetBoxModelFilterSetForm): model = ContactGroup parent_id = DynamicModelMultipleChoiceField( queryset=ContactGroup.objects.all(), @@ -56,12 +56,12 @@ class ContactGroupFilterForm(CustomFieldModelFilterForm): tag = TagFilterField(model) -class ContactRoleFilterForm(CustomFieldModelFilterForm): +class ContactRoleFilterForm(NetBoxModelFilterSetForm): model = ContactRole tag = TagFilterField(model) -class ContactFilterForm(CustomFieldModelFilterForm): +class ContactFilterForm(NetBoxModelFilterSetForm): model = Contact field_groups = ( ('q', 'tag'), diff --git a/netbox/tenancy/forms/models.py b/netbox/tenancy/forms/models.py index 398a44c9b..313b55417 100644 --- a/netbox/tenancy/forms/models.py +++ b/netbox/tenancy/forms/models.py @@ -1,7 +1,7 @@ from django import forms -from extras.forms import CustomFieldModelForm from extras.models import Tag +from netbox.forms import NetBoxModelForm from tenancy.models import * from utilities.forms import ( BootstrapMixin, CommentField, DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, SmallTextarea, @@ -22,7 +22,7 @@ __all__ = ( # Tenants # -class TenantGroupForm(CustomFieldModelForm): +class TenantGroupForm(NetBoxModelForm): parent = DynamicModelChoiceField( queryset=TenantGroup.objects.all(), required=False @@ -40,7 +40,7 @@ class TenantGroupForm(CustomFieldModelForm): ] -class TenantForm(CustomFieldModelForm): +class TenantForm(NetBoxModelForm): slug = SlugField() group = DynamicModelChoiceField( queryset=TenantGroup.objects.all(), @@ -66,7 +66,7 @@ class TenantForm(CustomFieldModelForm): # Contacts # -class ContactGroupForm(CustomFieldModelForm): +class ContactGroupForm(NetBoxModelForm): parent = DynamicModelChoiceField( queryset=ContactGroup.objects.all(), required=False @@ -82,7 +82,7 @@ class ContactGroupForm(CustomFieldModelForm): fields = ('parent', 'name', 'slug', 'description', 'tags') -class ContactRoleForm(CustomFieldModelForm): +class ContactRoleForm(NetBoxModelForm): slug = SlugField() tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), @@ -94,7 +94,7 @@ class ContactRoleForm(CustomFieldModelForm): fields = ('name', 'slug', 'description', 'tags') -class ContactForm(CustomFieldModelForm): +class ContactForm(NetBoxModelForm): group = DynamicModelChoiceField( queryset=ContactGroup.objects.all(), required=False diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py index 1e80e88e5..4232a87cd 100644 --- a/netbox/virtualization/forms/bulk_edit.py +++ b/netbox/virtualization/forms/bulk_edit.py @@ -3,8 +3,8 @@ from django import forms from dcim.choices import InterfaceModeChoices from dcim.constants import INTERFACE_MTU_MAX, INTERFACE_MTU_MIN from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup -from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm from ipam.models import VLAN +from netbox.forms import NetBoxModelBulkEditForm from tenancy.models import Tenant from utilities.forms import ( add_blank_choice, BulkEditNullBooleanSelect, BulkRenameForm, CommentField, DynamicModelChoiceField, @@ -23,7 +23,7 @@ __all__ = ( ) -class ClusterTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class ClusterTypeBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ClusterType.objects.all(), widget=forms.MultipleHiddenInput @@ -37,7 +37,7 @@ class ClusterTypeBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['description'] -class ClusterGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class ClusterGroupBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=ClusterGroup.objects.all(), widget=forms.MultipleHiddenInput @@ -51,7 +51,7 @@ class ClusterGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['description'] -class ClusterBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class ClusterBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=Cluster.objects.all(), widget=forms.MultipleHiddenInput() @@ -95,7 +95,7 @@ class ClusterBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): ] -class VirtualMachineBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=VirtualMachine.objects.all(), widget=forms.MultipleHiddenInput() @@ -150,7 +150,7 @@ class VirtualMachineBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm ] -class VMInterfaceBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=VMInterface.objects.all(), widget=forms.MultipleHiddenInput() diff --git a/netbox/virtualization/forms/bulk_import.py b/netbox/virtualization/forms/bulk_import.py index bd3279959..cefc2219d 100644 --- a/netbox/virtualization/forms/bulk_import.py +++ b/netbox/virtualization/forms/bulk_import.py @@ -1,6 +1,6 @@ from dcim.choices import InterfaceModeChoices from dcim.models import DeviceRole, Platform, Site -from extras.forms import CustomFieldModelCSVForm +from netbox.forms import NetBoxModelCSVForm from tenancy.models import Tenant from utilities.forms import CSVChoiceField, CSVModelChoiceField, SlugField from virtualization.choices import * @@ -15,7 +15,7 @@ __all__ = ( ) -class ClusterTypeCSVForm(CustomFieldModelCSVForm): +class ClusterTypeCSVForm(NetBoxModelCSVForm): slug = SlugField() class Meta: @@ -23,7 +23,7 @@ class ClusterTypeCSVForm(CustomFieldModelCSVForm): fields = ('name', 'slug', 'description') -class ClusterGroupCSVForm(CustomFieldModelCSVForm): +class ClusterGroupCSVForm(NetBoxModelCSVForm): slug = SlugField() class Meta: @@ -31,7 +31,7 @@ class ClusterGroupCSVForm(CustomFieldModelCSVForm): fields = ('name', 'slug', 'description') -class ClusterCSVForm(CustomFieldModelCSVForm): +class ClusterCSVForm(NetBoxModelCSVForm): type = CSVModelChoiceField( queryset=ClusterType.objects.all(), to_field_name='name', @@ -61,7 +61,7 @@ class ClusterCSVForm(CustomFieldModelCSVForm): fields = ('name', 'type', 'group', 'site', 'comments') -class VirtualMachineCSVForm(CustomFieldModelCSVForm): +class VirtualMachineCSVForm(NetBoxModelCSVForm): status = CSVChoiceField( choices=VirtualMachineStatusChoices, help_text='Operational status of device' @@ -99,7 +99,7 @@ class VirtualMachineCSVForm(CustomFieldModelCSVForm): ) -class VMInterfaceCSVForm(CustomFieldModelCSVForm): +class VMInterfaceCSVForm(NetBoxModelCSVForm): virtual_machine = CSVModelChoiceField( queryset=VirtualMachine.objects.all(), to_field_name='name' diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py index 9ca8eba6e..292cd661d 100644 --- a/netbox/virtualization/forms/filtersets.py +++ b/netbox/virtualization/forms/filtersets.py @@ -2,7 +2,8 @@ from django import forms from django.utils.translation import gettext as _ from dcim.models import DeviceRole, Platform, Region, Site, SiteGroup -from extras.forms import CustomFieldModelFilterForm, LocalConfigContextFilterForm +from extras.forms import LocalConfigContextFilterForm +from netbox.forms import NetBoxModelFilterSetForm from tenancy.forms import TenancyFilterForm from utilities.forms import ( DynamicModelMultipleChoiceField, StaticSelect, StaticSelectMultiple, TagFilterField, BOOLEAN_WITH_BLANK_CHOICES, @@ -19,17 +20,17 @@ __all__ = ( ) -class ClusterTypeFilterForm(CustomFieldModelFilterForm): +class ClusterTypeFilterForm(NetBoxModelFilterSetForm): model = ClusterType tag = TagFilterField(model) -class ClusterGroupFilterForm(CustomFieldModelFilterForm): +class ClusterGroupFilterForm(NetBoxModelFilterSetForm): model = ClusterGroup tag = TagFilterField(model) -class ClusterFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): +class ClusterFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Cluster field_groups = [ ['q', 'tag'], @@ -71,7 +72,7 @@ class ClusterFilterForm(TenancyFilterForm, CustomFieldModelFilterForm): tag = TagFilterField(model) -class VirtualMachineFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, CustomFieldModelFilterForm): +class VirtualMachineFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): model = VirtualMachine field_groups = [ ['q', 'tag'], @@ -151,7 +152,7 @@ class VirtualMachineFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, tag = TagFilterField(model) -class VMInterfaceFilterForm(CustomFieldModelFilterForm): +class VMInterfaceFilterForm(NetBoxModelFilterSetForm): model = VMInterface field_groups = [ ['q', 'tag'], diff --git a/netbox/virtualization/forms/models.py b/netbox/virtualization/forms/models.py index 624c9e87f..883fcd363 100644 --- a/netbox/virtualization/forms/models.py +++ b/netbox/virtualization/forms/models.py @@ -5,9 +5,9 @@ from django.core.exceptions import ValidationError from dcim.forms.common import InterfaceCommonForm from dcim.forms.models import INTERFACE_MODE_HELP_TEXT from dcim.models import Device, DeviceRole, Platform, Rack, Region, Site, SiteGroup -from extras.forms import CustomFieldModelForm from extras.models import Tag from ipam.models import IPAddress, VLAN, VLANGroup +from netbox.forms import NetBoxModelForm from tenancy.forms import TenancyForm from utilities.forms import ( BootstrapMixin, CommentField, ConfirmationForm, DynamicModelChoiceField, DynamicModelMultipleChoiceField, @@ -26,7 +26,7 @@ __all__ = ( ) -class ClusterTypeForm(CustomFieldModelForm): +class ClusterTypeForm(NetBoxModelForm): slug = SlugField() tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), @@ -40,7 +40,7 @@ class ClusterTypeForm(CustomFieldModelForm): ) -class ClusterGroupForm(CustomFieldModelForm): +class ClusterGroupForm(NetBoxModelForm): slug = SlugField() tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), @@ -54,7 +54,7 @@ class ClusterGroupForm(CustomFieldModelForm): ) -class ClusterForm(TenancyForm, CustomFieldModelForm): +class ClusterForm(TenancyForm, NetBoxModelForm): type = DynamicModelChoiceField( queryset=ClusterType.objects.all() ) @@ -171,7 +171,7 @@ class ClusterRemoveDevicesForm(ConfirmationForm): ) -class VirtualMachineForm(TenancyForm, CustomFieldModelForm): +class VirtualMachineForm(TenancyForm, NetBoxModelForm): cluster_group = DynamicModelChoiceField( queryset=ClusterGroup.objects.all(), required=False, @@ -271,7 +271,7 @@ class VirtualMachineForm(TenancyForm, CustomFieldModelForm): self.fields['primary_ip6'].widget.attrs['readonly'] = True -class VMInterfaceForm(InterfaceCommonForm, CustomFieldModelForm): +class VMInterfaceForm(InterfaceCommonForm, NetBoxModelForm): parent = DynamicModelChoiceField( queryset=VMInterface.objects.all(), required=False, diff --git a/netbox/wireless/forms/bulk_edit.py b/netbox/wireless/forms/bulk_edit.py index 9d07d09f0..147108d77 100644 --- a/netbox/wireless/forms/bulk_edit.py +++ b/netbox/wireless/forms/bulk_edit.py @@ -1,8 +1,8 @@ from django import forms from dcim.choices import LinkStatusChoices -from extras.forms import AddRemoveTagsForm, CustomFieldModelBulkEditForm from ipam.models import VLAN +from netbox.forms import NetBoxModelBulkEditForm from utilities.forms import add_blank_choice, DynamicModelChoiceField from wireless.choices import * from wireless.constants import SSID_MAX_LENGTH @@ -15,7 +15,7 @@ __all__ = ( ) -class WirelessLANGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class WirelessLANGroupBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=WirelessLANGroup.objects.all(), widget=forms.MultipleHiddenInput @@ -33,7 +33,7 @@ class WirelessLANGroupBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditFo nullable_fields = ['parent', 'description'] -class WirelessLANBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class WirelessLANBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=WirelessLAN.objects.all(), widget=forms.MultipleHiddenInput @@ -72,7 +72,7 @@ class WirelessLANBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): nullable_fields = ['ssid', 'group', 'vlan', 'description', 'auth_type', 'auth_cipher', 'auth_psk'] -class WirelessLinkBulkEditForm(AddRemoveTagsForm, CustomFieldModelBulkEditForm): +class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( queryset=WirelessLink.objects.all(), widget=forms.MultipleHiddenInput diff --git a/netbox/wireless/forms/bulk_import.py b/netbox/wireless/forms/bulk_import.py index aa79e1fc7..4b8acb385 100644 --- a/netbox/wireless/forms/bulk_import.py +++ b/netbox/wireless/forms/bulk_import.py @@ -1,7 +1,7 @@ from dcim.choices import LinkStatusChoices from dcim.models import Interface -from extras.forms import CustomFieldModelCSVForm from ipam.models import VLAN +from netbox.forms import NetBoxModelCSVForm from utilities.forms import CSVChoiceField, CSVModelChoiceField, SlugField from wireless.choices import * from wireless.models import * @@ -13,7 +13,7 @@ __all__ = ( ) -class WirelessLANGroupCSVForm(CustomFieldModelCSVForm): +class WirelessLANGroupCSVForm(NetBoxModelCSVForm): parent = CSVModelChoiceField( queryset=WirelessLANGroup.objects.all(), required=False, @@ -27,7 +27,7 @@ class WirelessLANGroupCSVForm(CustomFieldModelCSVForm): fields = ('name', 'slug', 'parent', 'description') -class WirelessLANCSVForm(CustomFieldModelCSVForm): +class WirelessLANCSVForm(NetBoxModelCSVForm): group = CSVModelChoiceField( queryset=WirelessLANGroup.objects.all(), required=False, @@ -56,7 +56,7 @@ class WirelessLANCSVForm(CustomFieldModelCSVForm): fields = ('ssid', 'group', 'description', 'vlan', 'auth_type', 'auth_cipher', 'auth_psk') -class WirelessLinkCSVForm(CustomFieldModelCSVForm): +class WirelessLinkCSVForm(NetBoxModelCSVForm): status = CSVChoiceField( choices=LinkStatusChoices, help_text='Connection status' diff --git a/netbox/wireless/forms/filtersets.py b/netbox/wireless/forms/filtersets.py index 82df93e6c..3c46caf21 100644 --- a/netbox/wireless/forms/filtersets.py +++ b/netbox/wireless/forms/filtersets.py @@ -2,7 +2,7 @@ from django import forms from django.utils.translation import gettext as _ from dcim.choices import LinkStatusChoices -from extras.forms import CustomFieldModelFilterForm +from netbox.forms import NetBoxModelFilterSetForm from utilities.forms import add_blank_choice, DynamicModelMultipleChoiceField, StaticSelect, TagFilterField from wireless.choices import * from wireless.models import * @@ -14,7 +14,7 @@ __all__ = ( ) -class WirelessLANGroupFilterForm(CustomFieldModelFilterForm): +class WirelessLANGroupFilterForm(NetBoxModelFilterSetForm): model = WirelessLANGroup parent_id = DynamicModelMultipleChoiceField( queryset=WirelessLANGroup.objects.all(), @@ -24,7 +24,7 @@ class WirelessLANGroupFilterForm(CustomFieldModelFilterForm): tag = TagFilterField(model) -class WirelessLANFilterForm(CustomFieldModelFilterForm): +class WirelessLANFilterForm(NetBoxModelFilterSetForm): model = WirelessLAN field_groups = [ ('q', 'tag'), @@ -56,7 +56,7 @@ class WirelessLANFilterForm(CustomFieldModelFilterForm): tag = TagFilterField(model) -class WirelessLinkFilterForm(CustomFieldModelFilterForm): +class WirelessLinkFilterForm(NetBoxModelFilterSetForm): model = WirelessLink ssid = forms.CharField( required=False, diff --git a/netbox/wireless/forms/models.py b/netbox/wireless/forms/models.py index 7687cb372..30a4a2352 100644 --- a/netbox/wireless/forms/models.py +++ b/netbox/wireless/forms/models.py @@ -1,7 +1,7 @@ from dcim.models import Device, Interface, Location, Site -from extras.forms import CustomFieldModelForm from extras.models import Tag from ipam.models import VLAN +from netbox.forms import NetBoxModelForm from utilities.forms import DynamicModelChoiceField, DynamicModelMultipleChoiceField, SlugField, StaticSelect from wireless.models import * @@ -12,7 +12,7 @@ __all__ = ( ) -class WirelessLANGroupForm(CustomFieldModelForm): +class WirelessLANGroupForm(NetBoxModelForm): parent = DynamicModelChoiceField( queryset=WirelessLANGroup.objects.all(), required=False @@ -30,7 +30,7 @@ class WirelessLANGroupForm(CustomFieldModelForm): ] -class WirelessLANForm(CustomFieldModelForm): +class WirelessLANForm(NetBoxModelForm): group = DynamicModelChoiceField( queryset=WirelessLANGroup.objects.all(), required=False @@ -61,7 +61,7 @@ class WirelessLANForm(CustomFieldModelForm): } -class WirelessLinkForm(CustomFieldModelForm): +class WirelessLinkForm(NetBoxModelForm): site_a = DynamicModelChoiceField( queryset=Site.objects.all(), required=False, From e4eee1cdfc74519e1494102506655063ac8b0264 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Fri, 28 Jan 2022 16:47:54 -0500 Subject: [PATCH 3/8] Clean up nullable fields declaration for bulk edit forms --- netbox/circuits/forms/bulk_edit.py | 14 ++--- netbox/dcim/forms/bulk_edit.py | 78 ++++++++++++------------ netbox/extras/forms/bulk_edit.py | 18 ++---- netbox/ipam/forms/bulk_edit.py | 44 +++++++------ netbox/netbox/forms/base.py | 13 ++-- netbox/tenancy/forms/bulk_edit.py | 12 ++-- netbox/utilities/forms/forms.py | 40 +++++++----- netbox/virtualization/forms/bulk_edit.py | 16 ++--- netbox/wireless/forms/bulk_edit.py | 6 +- 9 files changed, 120 insertions(+), 121 deletions(-) diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index 3e54cf711..f17df1302 100644 --- a/netbox/circuits/forms/bulk_edit.py +++ b/netbox/circuits/forms/bulk_edit.py @@ -48,9 +48,9 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ + nullable_fields = ( 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', - ] + ) class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm): @@ -76,9 +76,9 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ + nullable_fields = ( 'service_id', 'description', 'comments', - ] + ) class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm): @@ -92,7 +92,7 @@ class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['description'] + nullable_fields = ('description',) class CircuitBulkEditForm(NetBoxModelBulkEditForm): @@ -132,6 +132,6 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ + nullable_fields = ( 'tenant', 'commit_rate', 'description', 'comments', - ] + ) diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index f5eb179fa..13e9d945b 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -72,7 +72,7 @@ class RegionBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['parent', 'description'] + nullable_fields = ('parent', 'description') class SiteGroupBulkEditForm(NetBoxModelBulkEditForm): @@ -90,7 +90,7 @@ class SiteGroupBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['parent', 'description'] + nullable_fields = ('parent', 'description') class SiteBulkEditForm(NetBoxModelBulkEditForm): @@ -132,9 +132,9 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ + nullable_fields = ( 'region', 'group', 'tenant', 'asns', 'description', 'time_zone', - ] + ) class LocationBulkEditForm(NetBoxModelBulkEditForm): @@ -163,7 +163,7 @@ class LocationBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['parent', 'tenant', 'description'] + nullable_fields = ('parent', 'tenant', 'description') class RackRoleBulkEditForm(NetBoxModelBulkEditForm): @@ -180,7 +180,7 @@ class RackRoleBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['color', 'description'] + nullable_fields = ('color', 'description') class RackBulkEditForm(NetBoxModelBulkEditForm): @@ -278,9 +278,9 @@ class RackBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ + nullable_fields = ( 'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments', - ] + ) class RackReservationBulkEditForm(NetBoxModelBulkEditForm): @@ -304,9 +304,6 @@ class RackReservationBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = [] - class ManufacturerBulkEditForm(NetBoxModelBulkEditForm): pk = forms.ModelMultipleChoiceField( @@ -319,7 +316,7 @@ class ManufacturerBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['description'] + nullable_fields = ('description',) class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm): @@ -350,7 +347,7 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['part_number', 'airflow'] + nullable_fields = ('part_number', 'airflow') class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm): @@ -367,7 +364,7 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['part_number'] + nullable_fields = ('part_number',) class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm): @@ -389,7 +386,7 @@ class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['color', 'description'] + nullable_fields = ('color', 'description') class PlatformBulkEditForm(NetBoxModelBulkEditForm): @@ -412,7 +409,7 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['manufacturer', 'napalm_driver', 'description'] + nullable_fields = ('manufacturer', 'napalm_driver', 'description') class DeviceBulkEditForm(NetBoxModelBulkEditForm): @@ -471,9 +468,9 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ + nullable_fields = ( 'tenant', 'platform', 'serial', 'airflow', - ] + ) class ModuleBulkEditForm(NetBoxModelBulkEditForm): @@ -499,7 +496,7 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['serial'] + nullable_fields = ('serial',) class CableBulkEditForm(NetBoxModelBulkEditForm): @@ -542,9 +539,9 @@ class CableBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ + nullable_fields = ( 'type', 'status', 'tenant', 'label', 'color', 'length', - ] + ) def clean(self): super().clean() @@ -569,7 +566,7 @@ class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['domain'] + nullable_fields = ('domain',) class PowerPanelBulkEditForm(NetBoxModelBulkEditForm): @@ -608,7 +605,7 @@ class PowerPanelBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['location'] + nullable_fields = ('location',) class PowerFeedBulkEditForm(NetBoxModelBulkEditForm): @@ -667,9 +664,9 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ + nullable_fields = ( 'location', 'comments', - ] + ) # @@ -930,7 +927,7 @@ class InventoryItemTemplateBulkEditForm(BulkEditForm): ) class Meta: - nullable_fields = ['label', 'role', 'manufacturer', 'part_id', 'description'] + nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description') # @@ -951,7 +948,7 @@ class ConsolePortBulkEditForm( ) class Meta: - nullable_fields = ['label', 'description'] + nullable_fields = ('label', 'description') class ConsoleServerPortBulkEditForm( @@ -968,7 +965,7 @@ class ConsoleServerPortBulkEditForm( ) class Meta: - nullable_fields = ['label', 'description'] + nullable_fields = ('label', 'description') class PowerPortBulkEditForm( @@ -985,7 +982,7 @@ class PowerPortBulkEditForm( ) class Meta: - nullable_fields = ['label', 'description'] + nullable_fields = ('label', 'description') class PowerOutletBulkEditForm( @@ -1008,7 +1005,7 @@ class PowerOutletBulkEditForm( ) class Meta: - nullable_fields = ['label', 'type', 'feed_leg', 'power_port', 'description'] + nullable_fields = ('label', 'type', 'feed_leg', 'power_port', 'description') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -1088,10 +1085,11 @@ class InterfaceBulkEditForm( ) class Meta: - nullable_fields = [ - 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'description', 'mode', 'rf_channel', - 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'vrf', - ] + nullable_fields = ( + 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'description', 'mode', + 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', + 'vrf', + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -1157,7 +1155,7 @@ class FrontPortBulkEditForm( ) class Meta: - nullable_fields = ['label', 'description'] + nullable_fields = ('label', 'description') class RearPortBulkEditForm( @@ -1170,7 +1168,7 @@ class RearPortBulkEditForm( ) class Meta: - nullable_fields = ['label', 'description'] + nullable_fields = ('label', 'description') class ModuleBayBulkEditForm( @@ -1183,7 +1181,7 @@ class ModuleBayBulkEditForm( ) class Meta: - nullable_fields = ['label', 'position', 'description'] + nullable_fields = ('label', 'position', 'description') class DeviceBayBulkEditForm( @@ -1196,7 +1194,7 @@ class DeviceBayBulkEditForm( ) class Meta: - nullable_fields = ['label', 'description'] + nullable_fields = ('label', 'description') class InventoryItemBulkEditForm( @@ -1217,7 +1215,7 @@ class InventoryItemBulkEditForm( ) class Meta: - nullable_fields = ['label', 'role', 'manufacturer', 'part_id', 'description'] + nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description') # @@ -1238,4 +1236,4 @@ class InventoryItemRoleBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['color', 'description'] + nullable_fields = ('color', 'description') diff --git a/netbox/extras/forms/bulk_edit.py b/netbox/extras/forms/bulk_edit.py index 56b51c894..362592ace 100644 --- a/netbox/extras/forms/bulk_edit.py +++ b/netbox/extras/forms/bulk_edit.py @@ -34,7 +34,7 @@ class CustomFieldBulkEditForm(BulkEditForm): ) class Meta: - nullable_fields = [] + nullable_fields = ('description',) class CustomLinkBulkEditForm(BulkEditForm): @@ -64,9 +64,6 @@ class CustomLinkBulkEditForm(BulkEditForm): widget=StaticSelect() ) - class Meta: - nullable_fields = [] - class ExportTemplateBulkEditForm(BulkEditForm): pk = forms.ModelMultipleChoiceField( @@ -96,7 +93,7 @@ class ExportTemplateBulkEditForm(BulkEditForm): ) class Meta: - nullable_fields = ['description', 'mime_type', 'file_extension'] + nullable_fields = ('description', 'mime_type', 'file_extension') class WebhookBulkEditForm(BulkEditForm): @@ -139,7 +136,7 @@ class WebhookBulkEditForm(BulkEditForm): ) class Meta: - nullable_fields = ['secret', 'conditions', 'ca_file_path'] + nullable_fields = ('secret', 'conditions', 'ca_file_path') class TagBulkEditForm(BulkEditForm): @@ -156,7 +153,7 @@ class TagBulkEditForm(BulkEditForm): ) class Meta: - nullable_fields = ['description'] + nullable_fields = ('description',) class ConfigContextBulkEditForm(BulkEditForm): @@ -178,9 +175,7 @@ class ConfigContextBulkEditForm(BulkEditForm): ) class Meta: - nullable_fields = [ - 'description', - ] + nullable_fields = ('description',) class JournalEntryBulkEditForm(BulkEditForm): @@ -196,6 +191,3 @@ class JournalEntryBulkEditForm(BulkEditForm): required=False, widget=forms.Textarea() ) - - class Meta: - nullable_fields = [] diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index 3da03dd41..637051318 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -50,9 +50,9 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ + nullable_fields = ( 'tenant', 'description', - ] + ) class RouteTargetBulkEditForm(NetBoxModelBulkEditForm): @@ -70,9 +70,9 @@ class RouteTargetBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ + nullable_fields = ( 'tenant', 'description', - ] + ) class RIRBulkEditForm(NetBoxModelBulkEditForm): @@ -90,7 +90,7 @@ class RIRBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['is_private', 'description'] + nullable_fields = ('is_private', 'description') class ASNBulkEditForm(NetBoxModelBulkEditForm): @@ -117,9 +117,9 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ + nullable_fields = ( 'date_added', 'description', - ] + ) widgets = { 'date_added': DatePicker(), } @@ -148,9 +148,9 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ + nullable_fields = ( 'date_added', 'description', - ] + ) widgets = { 'date_added': DatePicker(), } @@ -170,7 +170,7 @@ class RoleBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['description'] + nullable_fields = ('description',) class PrefixBulkEditForm(NetBoxModelBulkEditForm): @@ -233,9 +233,9 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ + nullable_fields = ( 'site', 'vrf', 'tenant', 'role', 'description', - ] + ) class IPRangeBulkEditForm(NetBoxModelBulkEditForm): @@ -267,9 +267,9 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ + nullable_fields = ( 'vrf', 'tenant', 'role', 'description', - ] + ) class IPAddressBulkEditForm(NetBoxModelBulkEditForm): @@ -312,9 +312,9 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ + nullable_fields = ( 'vrf', 'role', 'tenant', 'dns_name', 'description', - ] + ) class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm): @@ -349,7 +349,7 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['auth_type', 'auth_key', 'description'] + nullable_fields = ('auth_type', 'auth_key', 'description') class VLANGroupBulkEditForm(NetBoxModelBulkEditForm): @@ -379,7 +379,7 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['site', 'description'] + nullable_fields = ('site', 'description') class VLANBulkEditForm(NetBoxModelBulkEditForm): @@ -429,9 +429,9 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ + nullable_fields = ( 'site', 'group', 'tenant', 'role', 'description', - ] + ) class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm): @@ -457,9 +457,7 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ - 'description', - ] + nullable_fields = ('description',) class ServiceBulkEditForm(ServiceTemplateBulkEditForm): diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index 601b0062a..63582a2d9 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -5,7 +5,7 @@ from django.db.models import Q from extras.choices import CustomFieldFilterLogicChoices, CustomFieldTypeChoices from extras.forms.customfields import CustomFieldsMixin from extras.models import CustomField, Tag -from utilities.forms import BootstrapMixin, BulkEditBaseForm, CSVModelForm +from utilities.forms import BootstrapMixin, BulkEditMixin, CSVModelForm from utilities.forms.fields import DynamicModelMultipleChoiceField __all__ = ( @@ -55,7 +55,7 @@ class NetBoxModelCSVForm(CSVModelForm, NetBoxModelForm): return customfield.to_form_field(for_csv_import=True) -class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, BulkEditBaseForm): +class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, BulkEditMixin, forms.Form): """ Base form for modifying multiple NetBox objects (of the same type) in bulk via the UI. Adds support for custom fields and adding/removing tags. @@ -76,16 +76,21 @@ class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, BulkEditBaseFor """ Append form fields for all CustomFields assigned to this object type. """ + nullable_custom_fields = [] for customfield in self._get_custom_fields(self._get_content_type()): - # Annotate non-required custom fields as nullable + # Record non-required custom fields as nullable if not customfield.required: - self.nullable_fields.append(customfield.name) + nullable_custom_fields.append(customfield.name) self.fields[customfield.name] = self._get_form_field(customfield) # Annotate the field in the list of CustomField form fields self.custom_fields[customfield.name] = customfield + # Annotate nullable custom fields (if any) on the form instance + if nullable_custom_fields: + self.custom_fields = (*self.custom_fields, *nullable_custom_fields) + class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, forms.Form): """ diff --git a/netbox/tenancy/forms/bulk_edit.py b/netbox/tenancy/forms/bulk_edit.py index 9b14a167b..f3cc2c33d 100644 --- a/netbox/tenancy/forms/bulk_edit.py +++ b/netbox/tenancy/forms/bulk_edit.py @@ -32,7 +32,7 @@ class TenantGroupBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['parent', 'description'] + nullable_fields = ('parent', 'description') class TenantBulkEditForm(NetBoxModelBulkEditForm): @@ -46,9 +46,7 @@ class TenantBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ - 'group', - ] + nullable_fields = ('group',) # @@ -70,7 +68,7 @@ class ContactGroupBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['parent', 'description'] + nullable_fields = ('parent', 'description') class ContactRoleBulkEditForm(NetBoxModelBulkEditForm): @@ -84,7 +82,7 @@ class ContactRoleBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['description'] + nullable_fields = ('description',) class ContactBulkEditForm(NetBoxModelBulkEditForm): @@ -113,4 +111,4 @@ class ContactBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['group', 'title', 'phone', 'email', 'address', 'comments'] + nullable_fields = ('group', 'title', 'phone', 'email', 'address', 'comments') diff --git a/netbox/utilities/forms/forms.py b/netbox/utilities/forms/forms.py index 88f837b2b..d9a6c2b29 100644 --- a/netbox/utilities/forms/forms.py +++ b/netbox/utilities/forms/forms.py @@ -10,7 +10,7 @@ from .widgets import APISelect, APISelectMultiple, ClearableFileInput, StaticSel __all__ = ( 'BootstrapMixin', 'BulkEditForm', - 'BulkEditBaseForm', + 'BulkEditMixin', 'BulkRenameForm', 'ConfirmationForm', 'CSVModelForm', @@ -21,6 +21,10 @@ __all__ = ( ) +# +# Mixins +# + class BootstrapMixin: """ Add the base Bootstrap CSS classes to form elements. @@ -61,6 +65,24 @@ class BootstrapMixin: field.widget.attrs['class'] = ' '.join((css, 'form-select')).strip() +class BulkEditMixin: + """ + Base form for editing multiple objects in bulk + """ + def __init__(self, model, *args, **kwargs): + super().__init__(*args, **kwargs) + self.model = model + self.nullable_fields = () + + # Copy any nullable fields defined in Meta + if hasattr(self, 'Meta') and hasattr(self.Meta, 'nullable_fields'): + self.nullable_fields = self.Meta.nullable_fields + + +# +# Form classes +# + class ReturnURLForm(forms.Form): """ Provides a hidden return URL field to control where the user is directed after the form is submitted. @@ -75,21 +97,7 @@ class ConfirmationForm(BootstrapMixin, ReturnURLForm): confirm = forms.BooleanField(required=True, widget=forms.HiddenInput(), initial=True) -class BulkEditBaseForm(forms.Form): - """ - Base form for editing multiple objects in bulk - """ - def __init__(self, model, *args, **kwargs): - super().__init__(*args, **kwargs) - self.model = model - self.nullable_fields = [] - - # Copy any nullable fields defined in Meta - if hasattr(self.Meta, 'nullable_fields'): - self.nullable_fields = self.Meta.nullable_fields - - -class BulkEditForm(BootstrapMixin, BulkEditBaseForm): +class BulkEditForm(BootstrapMixin, BulkEditMixin, forms.Form): pass diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py index 4232a87cd..dd846029a 100644 --- a/netbox/virtualization/forms/bulk_edit.py +++ b/netbox/virtualization/forms/bulk_edit.py @@ -34,7 +34,7 @@ class ClusterTypeBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['description'] + nullable_fields = ('description',) class ClusterGroupBulkEditForm(NetBoxModelBulkEditForm): @@ -48,7 +48,7 @@ class ClusterGroupBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['description'] + nullable_fields = ('description',) class ClusterBulkEditForm(NetBoxModelBulkEditForm): @@ -90,9 +90,9 @@ class ClusterBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ + nullable_fields = ( 'group', 'site', 'comments', 'tenant', - ] + ) class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm): @@ -145,9 +145,9 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ + nullable_fields = ( 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments', - ] + ) class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm): @@ -198,9 +198,9 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = [ + nullable_fields = ( 'parent', 'bridge', 'mtu', 'description', - ] + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/netbox/wireless/forms/bulk_edit.py b/netbox/wireless/forms/bulk_edit.py index 147108d77..2d2a3ff14 100644 --- a/netbox/wireless/forms/bulk_edit.py +++ b/netbox/wireless/forms/bulk_edit.py @@ -30,7 +30,7 @@ class WirelessLANGroupBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['parent', 'description'] + nullable_fields = ('parent', 'description') class WirelessLANBulkEditForm(NetBoxModelBulkEditForm): @@ -69,7 +69,7 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['ssid', 'group', 'vlan', 'description', 'auth_type', 'auth_cipher', 'auth_psk'] + nullable_fields = ('ssid', 'group', 'vlan', 'description', 'auth_type', 'auth_cipher', 'auth_psk') class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm): @@ -103,4 +103,4 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm): ) class Meta: - nullable_fields = ['ssid', 'description', 'auth_type', 'auth_cipher', 'auth_psk'] + nullable_fields = ('ssid', 'description', 'auth_type', 'auth_cipher', 'auth_psk') From cf3ca5a661cc015baf4ef462be07e91c09db0ede Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 31 Jan 2022 14:10:13 -0500 Subject: [PATCH 4/8] Refactor & document supported form fields --- docs/plugins/development/forms.md | 68 +++ netbox/utilities/forms/fields.py | 526 ------------------ netbox/utilities/forms/fields/__init__.py | 5 + .../utilities/forms/fields/content_types.py | 37 ++ netbox/utilities/forms/fields/csv.py | 193 +++++++ netbox/utilities/forms/fields/dynamic.py | 141 +++++ netbox/utilities/forms/fields/expandable.py | 54 ++ netbox/utilities/forms/fields/fields.py | 127 +++++ 8 files changed, 625 insertions(+), 526 deletions(-) delete mode 100644 netbox/utilities/forms/fields.py create mode 100644 netbox/utilities/forms/fields/__init__.py create mode 100644 netbox/utilities/forms/fields/content_types.py create mode 100644 netbox/utilities/forms/fields/csv.py create mode 100644 netbox/utilities/forms/fields/dynamic.py create mode 100644 netbox/utilities/forms/fields/expandable.py create mode 100644 netbox/utilities/forms/fields/fields.py diff --git a/docs/plugins/development/forms.md b/docs/plugins/development/forms.md index 5af178194..c9c6cbde6 100644 --- a/docs/plugins/development/forms.md +++ b/docs/plugins/development/forms.md @@ -1,5 +1,7 @@ # Forms +## Form Classes + NetBox provides several base form classes for use by plugins. These are documented below. * `NetBoxModelForm` @@ -8,3 +10,69 @@ NetBox provides several base form classes for use by plugins. These are document * `NetBoxModelFilterSetForm` ### TODO: Include forms reference + +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. + +## General Purpose Fields + +::: utilities.forms.ColorField + selection: + members: false + +::: utilities.forms.CommentField + selection: + members: false + +::: utilities.forms.JSONField + selection: + members: false + +::: utilities.forms.MACAddressField + selection: + members: false + +::: utilities.forms.SlugField + selection: + members: false + +## Dynamic Object Fields + +::: utilities.forms.DynamicModelChoiceField + selection: + members: false + +::: utilities.forms.DynamicModelMultipleChoiceField + selection: + members: false + +## Content Type Fields + +::: utilities.forms.ContentTypeChoiceField + selection: + members: false + +::: utilities.forms.ContentTypeMultipleChoiceField + selection: + members: false + +## CSV Import Fields + +::: utilities.forms.CSVChoiceField + selection: + members: false + +::: utilities.forms.CSVMultipleChoiceField + selection: + members: false + +::: utilities.forms.CSVModelChoiceField + selection: + members: false + +::: utilities.forms.CSVContentTypeField + selection: + members: false + +::: utilities.forms.CSVMultipleContentTypeField + selection: + members: false diff --git a/netbox/utilities/forms/fields.py b/netbox/utilities/forms/fields.py deleted file mode 100644 index ceca895c0..000000000 --- a/netbox/utilities/forms/fields.py +++ /dev/null @@ -1,526 +0,0 @@ -import csv -import json -import re -from io import StringIO -from netaddr import AddrFormatError, EUI - -import django_filters -from django import forms -from django.conf import settings -from django.contrib.contenttypes.models import ContentType -from django.core.exceptions import MultipleObjectsReturned, ObjectDoesNotExist -from django.db.models import Count, Q -from django.forms import BoundField -from django.forms.fields import JSONField as _JSONField, InvalidJSONInput -from django.urls import reverse - -from utilities.choices import unpack_grouped_choices -from utilities.utils import content_type_identifier, content_type_name -from utilities.validators import EnhancedURLValidator -from . import widgets -from .constants import * -from .utils import expand_alphanumeric_pattern, expand_ipaddress_pattern, parse_csv, validate_csv - -__all__ = ( - 'ColorField', - 'CommentField', - 'ContentTypeChoiceField', - 'ContentTypeMultipleChoiceField', - 'CSVChoiceField', - 'CSVContentTypeField', - 'CSVDataField', - 'CSVFileField', - 'CSVModelChoiceField', - 'CSVMultipleChoiceField', - 'CSVMultipleContentTypeField', - 'CSVTypedChoiceField', - 'DynamicModelChoiceField', - 'DynamicModelMultipleChoiceField', - 'ExpandableIPAddressField', - 'ExpandableNameField', - 'JSONField', - 'LaxURLField', - 'MACAddressField', - 'SlugField', - 'TagFilterField', -) - - -class CommentField(forms.CharField): - """ - A textarea with support for Markdown rendering. Exists mostly just to add a standard help_text. - """ - widget = forms.Textarea - default_label = '' - # TODO: Port Markdown cheat sheet to internal documentation - default_helptext = ' '\ - ''\ - 'Markdown syntax is supported' - - def __init__(self, *args, **kwargs): - required = kwargs.pop('required', False) - label = kwargs.pop('label', self.default_label) - help_text = kwargs.pop('help_text', self.default_helptext) - super().__init__(required=required, label=label, help_text=help_text, *args, **kwargs) - - -class SlugField(forms.SlugField): - """ - Extend the built-in SlugField to automatically populate from a field called `name` unless otherwise specified. - """ - - def __init__(self, slug_source='name', *args, **kwargs): - label = kwargs.pop('label', "Slug") - help_text = kwargs.pop('help_text', "URL-friendly unique shorthand") - widget = kwargs.pop('widget', widgets.SlugWidget) - super().__init__(label=label, help_text=help_text, widget=widget, *args, **kwargs) - self.widget.attrs['slug-source'] = slug_source - - -class ColorField(forms.CharField): - """ - A field which represents a color in hexadecimal RRGGBB format. - """ - widget = widgets.ColorSelect - - -class TagFilterField(forms.MultipleChoiceField): - """ - A filter field for the tags of a model. Only the tags used by a model are displayed. - - :param model: The model of the filter - """ - widget = widgets.StaticSelectMultiple - - def __init__(self, model, *args, **kwargs): - def get_choices(): - tags = model.tags.annotate( - count=Count('extras_taggeditem_items') - ).order_by('name') - return [ - (str(tag.slug), '{} ({})'.format(tag.name, tag.count)) for tag in tags - ] - - # Choices are fetched each time the form is initialized - super().__init__(label='Tags', choices=get_choices, required=False, *args, **kwargs) - - -class LaxURLField(forms.URLField): - """ - Modifies Django's built-in URLField to remove the requirement for fully-qualified domain names - (e.g. http://myserver/ is valid) - """ - default_validators = [EnhancedURLValidator()] - - -class JSONField(_JSONField): - """ - Custom wrapper around Django's built-in JSONField to avoid presenting "null" as the default text. - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if not self.help_text: - self.help_text = 'Enter context data in JSON format.' - self.widget.attrs['placeholder'] = '' - - def prepare_value(self, value): - if isinstance(value, InvalidJSONInput): - return value - if value is None: - return '' - return json.dumps(value, sort_keys=True, indent=4) - - -class MACAddressField(forms.Field): - widget = forms.CharField - default_error_messages = { - 'invalid': 'MAC address must be in EUI-48 format', - } - - def to_python(self, value): - value = super().to_python(value) - - # Validate MAC address format - try: - value = EUI(value.strip()) - except AddrFormatError: - raise forms.ValidationError(self.error_messages['invalid'], code='invalid') - - return value - - -# -# Content type fields -# - -class ContentTypeChoiceMixin: - - def __init__(self, queryset, *args, **kwargs): - # Order ContentTypes by app_label - queryset = queryset.order_by('app_label', 'model') - super().__init__(queryset, *args, **kwargs) - - def label_from_instance(self, obj): - try: - return content_type_name(obj) - except AttributeError: - return super().label_from_instance(obj) - - -class ContentTypeChoiceField(ContentTypeChoiceMixin, forms.ModelChoiceField): - widget = widgets.StaticSelect - - -class ContentTypeMultipleChoiceField(ContentTypeChoiceMixin, forms.ModelMultipleChoiceField): - widget = widgets.StaticSelectMultiple - - -# -# CSV fields -# - -class CSVDataField(forms.CharField): - """ - A CharField (rendered as a Textarea) which accepts CSV-formatted data. It returns data as a two-tuple: The first - item is a dictionary of column headers, mapping field names to the attribute by which they match a related object - (where applicable). The second item is a list of dictionaries, each representing a discrete row of CSV data. - - :param from_form: The form from which the field derives its validation rules. - """ - widget = forms.Textarea - - def __init__(self, from_form, *args, **kwargs): - - form = from_form() - self.model = form.Meta.model - self.fields = form.fields - self.required_fields = [ - name for name, field in form.fields.items() if field.required - ] - - super().__init__(*args, **kwargs) - - self.strip = False - if not self.label: - self.label = '' - if not self.initial: - self.initial = ','.join(self.required_fields) + '\n' - if not self.help_text: - self.help_text = 'Enter the list of column headers followed by one line per record to be imported, using ' \ - 'commas to separate values. Multi-line data and values containing commas may be wrapped ' \ - 'in double quotes.' - - def to_python(self, value): - reader = csv.reader(StringIO(value.strip())) - - return parse_csv(reader) - - def validate(self, value): - headers, records = value - validate_csv(headers, self.fields, self.required_fields) - - return value - - -class CSVFileField(forms.FileField): - """ - A FileField (rendered as a file input button) which accepts a file containing CSV-formatted data. It returns - data as a two-tuple: The first item is a dictionary of column headers, mapping field names to the attribute - by which they match a related object (where applicable). The second item is a list of dictionaries, each - representing a discrete row of CSV data. - - :param from_form: The form from which the field derives its validation rules. - """ - - def __init__(self, from_form, *args, **kwargs): - - form = from_form() - self.model = form.Meta.model - self.fields = form.fields - self.required_fields = [ - name for name, field in form.fields.items() if field.required - ] - - super().__init__(*args, **kwargs) - - def to_python(self, file): - if file is None: - return None - - csv_str = file.read().decode('utf-8').strip() - reader = csv.reader(StringIO(csv_str)) - headers, records = parse_csv(reader) - - return headers, records - - def validate(self, value): - if value is None: - return None - - headers, records = value - validate_csv(headers, self.fields, self.required_fields) - - return value - - -class CSVChoicesMixin: - STATIC_CHOICES = True - - def __init__(self, *, choices=(), **kwargs): - super().__init__(choices=choices, **kwargs) - self.choices = unpack_grouped_choices(choices) - - -class CSVChoiceField(CSVChoicesMixin, forms.ChoiceField): - """ - A CSV field which accepts a single selection value. - """ - pass - - -class CSVMultipleChoiceField(CSVChoicesMixin, forms.MultipleChoiceField): - """ - A CSV field which accepts multiple selection values. - """ - def to_python(self, value): - if not value: - return [] - if not isinstance(value, str): - raise forms.ValidationError(f"Invalid value for a multiple choice field: {value}") - return value.split(',') - - -class CSVTypedChoiceField(forms.TypedChoiceField): - STATIC_CHOICES = True - - -class CSVModelChoiceField(forms.ModelChoiceField): - """ - Provides additional validation for model choices entered as CSV data. - """ - default_error_messages = { - 'invalid_choice': 'Object not found.', - } - - def to_python(self, value): - try: - return super().to_python(value) - except MultipleObjectsReturned: - raise forms.ValidationError( - f'"{value}" is not a unique value for this field; multiple objects were found' - ) - - -class CSVContentTypeField(CSVModelChoiceField): - """ - Reference a ContentType in the form . - """ - STATIC_CHOICES = True - - def prepare_value(self, value): - return content_type_identifier(value) - - def to_python(self, value): - if not value: - return None - try: - app_label, model = value.split('.') - except ValueError: - raise forms.ValidationError(f'Object type must be specified as "."') - try: - return self.queryset.get(app_label=app_label, model=model) - except ObjectDoesNotExist: - raise forms.ValidationError(f'Invalid object type') - - -class CSVMultipleContentTypeField(forms.ModelMultipleChoiceField): - STATIC_CHOICES = True - - # TODO: Improve validation of selected ContentTypes - def prepare_value(self, value): - if type(value) is str: - ct_filter = Q() - for name in value.split(','): - app_label, model = name.split('.') - ct_filter |= Q(app_label=app_label, model=model) - return list(ContentType.objects.filter(ct_filter).values_list('pk', flat=True)) - return content_type_identifier(value) - - -# -# Expansion fields -# - -class ExpandableNameField(forms.CharField): - """ - A field which allows for numeric range expansion - Example: 'Gi0/[1-3]' => ['Gi0/1', 'Gi0/2', 'Gi0/3'] - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if not self.help_text: - self.help_text = """ - Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range - are not supported. Example: [ge,xe]-0/0/[0-9] - """ - - def to_python(self, value): - if not value: - return '' - if re.search(ALPHANUMERIC_EXPANSION_PATTERN, value): - return list(expand_alphanumeric_pattern(value)) - return [value] - - -class ExpandableIPAddressField(forms.CharField): - """ - A field which allows for expansion of IP address ranges - Example: '192.0.2.[1-254]/24' => ['192.0.2.1/24', '192.0.2.2/24', '192.0.2.3/24' ... '192.0.2.254/24'] - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if not self.help_text: - self.help_text = 'Specify a numeric range to create multiple IPs.
'\ - 'Example: 192.0.2.[1,5,100-254]/24' - - def to_python(self, value): - # Hackish address family detection but it's all we have to work with - if '.' in value and re.search(IP4_EXPANSION_PATTERN, value): - return list(expand_ipaddress_pattern(value, 4)) - elif ':' in value and re.search(IP6_EXPANSION_PATTERN, value): - return list(expand_ipaddress_pattern(value, 6)) - return [value] - - -# -# Dynamic fields -# - -class DynamicModelChoiceMixin: - """ - :param query_params: A dictionary of additional key/value pairs to attach to the API request - :param initial_params: A dictionary of child field references to use for selecting a parent field's initial value - :param null_option: The string used to represent a null selection (if any) - :param disabled_indicator: The name of the field which, if populated, will disable selection of the - choice (optional) - :param str fetch_trigger: The event type which will cause the select element to - fetch data from the API. Must be 'load', 'open', or 'collapse'. (optional) - """ - filter = django_filters.ModelChoiceFilter - widget = widgets.APISelect - - def __init__(self, query_params=None, initial_params=None, null_option=None, disabled_indicator=None, - fetch_trigger=None, empty_label=None, *args, **kwargs): - self.query_params = query_params or {} - self.initial_params = initial_params or {} - self.null_option = null_option - self.disabled_indicator = disabled_indicator - self.fetch_trigger = fetch_trigger - - # to_field_name is set by ModelChoiceField.__init__(), but we need to set it early for reference - # by widget_attrs() - self.to_field_name = kwargs.get('to_field_name') - self.empty_option = empty_label or "" - - super().__init__(*args, **kwargs) - - def widget_attrs(self, widget): - attrs = { - 'data-empty-option': self.empty_option - } - - # Set value-field attribute if the field specifies to_field_name - if self.to_field_name: - attrs['value-field'] = self.to_field_name - - # Set the string used to represent a null option - if self.null_option is not None: - attrs['data-null-option'] = self.null_option - - # Set the disabled indicator, if any - if self.disabled_indicator is not None: - attrs['disabled-indicator'] = self.disabled_indicator - - # Set the fetch trigger, if any. - if self.fetch_trigger is not None: - attrs['data-fetch-trigger'] = self.fetch_trigger - - # Attach any static query parameters - if (len(self.query_params) > 0): - widget.add_query_params(self.query_params) - - return attrs - - def get_bound_field(self, form, field_name): - bound_field = BoundField(form, self, field_name) - - # Set initial value based on prescribed child fields (if not already set) - if not self.initial and self.initial_params: - filter_kwargs = {} - for kwarg, child_field in self.initial_params.items(): - value = form.initial.get(child_field.lstrip('$')) - if value: - filter_kwargs[kwarg] = value - if filter_kwargs: - self.initial = self.queryset.filter(**filter_kwargs).first() - - # Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options - # will be populated on-demand via the APISelect widget. - data = bound_field.value() - if data: - field_name = getattr(self, 'to_field_name') or 'pk' - filter = self.filter(field_name=field_name) - try: - self.queryset = filter.filter(self.queryset, data) - except (TypeError, ValueError): - # Catch any error caused by invalid initial data passed from the user - self.queryset = self.queryset.none() - else: - self.queryset = self.queryset.none() - - # Set the data URL on the APISelect widget (if not already set) - widget = bound_field.field.widget - if not widget.attrs.get('data-url'): - app_label = self.queryset.model._meta.app_label - model_name = self.queryset.model._meta.model_name - data_url = reverse('{}-api:{}-list'.format(app_label, model_name)) - widget.attrs['data-url'] = data_url - - return bound_field - - -class DynamicModelChoiceField(DynamicModelChoiceMixin, forms.ModelChoiceField): - """ - Override get_bound_field() to avoid pre-populating field choices with a SQL query. The field will be - rendered only with choices set via bound data. Choices are populated on-demand via the APISelect widget. - """ - - def clean(self, value): - """ - When null option is enabled and "None" is sent as part of a form to be submitted, it is sent as the - string 'null'. This will check for that condition and gracefully handle the conversion to a NoneType. - """ - if self.null_option is not None and value == settings.FILTERS_NULL_CHOICE_VALUE: - return None - return super().clean(value) - - -class DynamicModelMultipleChoiceField(DynamicModelChoiceMixin, forms.ModelMultipleChoiceField): - """ - A multiple-choice version of DynamicModelChoiceField. - """ - filter = django_filters.ModelMultipleChoiceFilter - widget = widgets.APISelectMultiple - - def clean(self, value): - """ - When null option is enabled and "None" is sent as part of a form to be submitted, it is sent as the - string 'null'. This will check for that condition and gracefully handle the conversion to a NoneType. - """ - if self.null_option is not None and settings.FILTERS_NULL_CHOICE_VALUE in value: - value = [v for v in value if v != settings.FILTERS_NULL_CHOICE_VALUE] - return [None, *value] - return super().clean(value) diff --git a/netbox/utilities/forms/fields/__init__.py b/netbox/utilities/forms/fields/__init__.py new file mode 100644 index 000000000..eacde0040 --- /dev/null +++ b/netbox/utilities/forms/fields/__init__.py @@ -0,0 +1,5 @@ +from .content_types import * +from .csv import * +from .dynamic import * +from .expandable import * +from .fields import * diff --git a/netbox/utilities/forms/fields/content_types.py b/netbox/utilities/forms/fields/content_types.py new file mode 100644 index 000000000..80861166c --- /dev/null +++ b/netbox/utilities/forms/fields/content_types.py @@ -0,0 +1,37 @@ +from django import forms + +from utilities.forms import widgets +from utilities.utils import content_type_name + +__all__ = ( + 'ContentTypeChoiceField', + 'ContentTypeMultipleChoiceField', +) + + +class ContentTypeChoiceMixin: + + def __init__(self, queryset, *args, **kwargs): + # Order ContentTypes by app_label + queryset = queryset.order_by('app_label', 'model') + super().__init__(queryset, *args, **kwargs) + + def label_from_instance(self, obj): + try: + return content_type_name(obj) + except AttributeError: + return super().label_from_instance(obj) + + +class ContentTypeChoiceField(ContentTypeChoiceMixin, forms.ModelChoiceField): + """ + Selection field for a single content type. + """ + widget = widgets.StaticSelect + + +class ContentTypeMultipleChoiceField(ContentTypeChoiceMixin, forms.ModelMultipleChoiceField): + """ + Selection field for one or more content types. + """ + widget = widgets.StaticSelectMultiple diff --git a/netbox/utilities/forms/fields/csv.py b/netbox/utilities/forms/fields/csv.py new file mode 100644 index 000000000..275c8084c --- /dev/null +++ b/netbox/utilities/forms/fields/csv.py @@ -0,0 +1,193 @@ +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 utilities.choices import unpack_grouped_choices +from utilities.forms.utils import parse_csv, validate_csv +from utilities.utils import content_type_identifier + +__all__ = ( + 'CSVChoiceField', + 'CSVContentTypeField', + 'CSVDataField', + 'CSVFileField', + 'CSVModelChoiceField', + 'CSVMultipleChoiceField', + 'CSVMultipleContentTypeField', + 'CSVTypedChoiceField', +) + + +class CSVDataField(forms.CharField): + """ + A CharField (rendered as a Textarea) which accepts CSV-formatted data. It returns data as a two-tuple: The first + item is a dictionary of column headers, mapping field names to the attribute by which they match a related object + (where applicable). The second item is a list of dictionaries, each representing a discrete row of CSV data. + + :param from_form: The form from which the field derives its validation rules. + """ + widget = forms.Textarea + + def __init__(self, from_form, *args, **kwargs): + + form = from_form() + self.model = form.Meta.model + self.fields = form.fields + self.required_fields = [ + name for name, field in form.fields.items() if field.required + ] + + super().__init__(*args, **kwargs) + + self.strip = False + if not self.label: + self.label = '' + if not self.initial: + self.initial = ','.join(self.required_fields) + '\n' + if not self.help_text: + self.help_text = 'Enter the list of column headers followed by one line per record to be imported, using ' \ + 'commas to separate values. Multi-line data and values containing commas may be wrapped ' \ + 'in double quotes.' + + def to_python(self, value): + reader = csv.reader(StringIO(value.strip())) + + return parse_csv(reader) + + def validate(self, value): + headers, records = value + validate_csv(headers, self.fields, self.required_fields) + + return value + + +class CSVFileField(forms.FileField): + """ + A FileField (rendered as a file input button) which accepts a file containing CSV-formatted data. It returns + data as a two-tuple: The first item is a dictionary of column headers, mapping field names to the attribute + by which they match a related object (where applicable). The second item is a list of dictionaries, each + representing a discrete row of CSV data. + + :param from_form: The form from which the field derives its validation rules. + """ + + def __init__(self, from_form, *args, **kwargs): + + form = from_form() + self.model = form.Meta.model + self.fields = form.fields + self.required_fields = [ + name for name, field in form.fields.items() if field.required + ] + + super().__init__(*args, **kwargs) + + def to_python(self, file): + if file is None: + return None + + csv_str = file.read().decode('utf-8').strip() + reader = csv.reader(StringIO(csv_str)) + headers, records = parse_csv(reader) + + return headers, records + + def validate(self, value): + if value is None: + return None + + headers, records = value + validate_csv(headers, self.fields, self.required_fields) + + return value + + +class CSVChoicesMixin: + STATIC_CHOICES = True + + def __init__(self, *, choices=(), **kwargs): + super().__init__(choices=choices, **kwargs) + self.choices = unpack_grouped_choices(choices) + + +class CSVChoiceField(CSVChoicesMixin, forms.ChoiceField): + """ + A CSV field which accepts a single selection value. + """ + pass + + +class CSVMultipleChoiceField(CSVChoicesMixin, forms.MultipleChoiceField): + """ + A CSV field which accepts multiple selection values. + """ + def to_python(self, value): + if not value: + return [] + if not isinstance(value, str): + raise forms.ValidationError(f"Invalid value for a multiple choice field: {value}") + return value.split(',') + + +class CSVTypedChoiceField(forms.TypedChoiceField): + STATIC_CHOICES = True + + +class CSVModelChoiceField(forms.ModelChoiceField): + """ + Extends Django's `ModelChoiceField` to provide additional validation for CSV values. + """ + default_error_messages = { + 'invalid_choice': 'Object not found.', + } + + def to_python(self, value): + try: + return super().to_python(value) + except MultipleObjectsReturned: + raise forms.ValidationError( + f'"{value}" is not a unique value for this field; multiple objects were found' + ) + + +class CSVContentTypeField(CSVModelChoiceField): + """ + CSV field for referencing a single content type, in the form `.`. + """ + STATIC_CHOICES = True + + def prepare_value(self, value): + return content_type_identifier(value) + + def to_python(self, value): + if not value: + return None + try: + app_label, model = value.split('.') + except ValueError: + raise forms.ValidationError(f'Object type must be specified as "."') + try: + return self.queryset.get(app_label=app_label, model=model) + except ObjectDoesNotExist: + raise forms.ValidationError(f'Invalid object type') + + +class CSVMultipleContentTypeField(forms.ModelMultipleChoiceField): + """ + CSV field for referencing one or more content types, in the form `.`. + """ + STATIC_CHOICES = True + + # TODO: Improve validation of selected ContentTypes + def prepare_value(self, value): + if type(value) is str: + ct_filter = Q() + for name in value.split(','): + app_label, model = name.split('.') + ct_filter |= Q(app_label=app_label, model=model) + return list(ContentType.objects.filter(ct_filter).values_list('pk', flat=True)) + return content_type_identifier(value) diff --git a/netbox/utilities/forms/fields/dynamic.py b/netbox/utilities/forms/fields/dynamic.py new file mode 100644 index 000000000..1bc8b9ec4 --- /dev/null +++ b/netbox/utilities/forms/fields/dynamic.py @@ -0,0 +1,141 @@ +import django_filters +from django import forms +from django.conf import settings +from django.forms import BoundField +from django.urls import reverse + +from utilities.forms import widgets + +__all__ = ( + 'DynamicModelChoiceField', + 'DynamicModelMultipleChoiceField', +) + + +class DynamicModelChoiceMixin: + """ + Override `get_bound_field()` to avoid pre-populating field choices with a SQL query. The field will be + rendered only with choices set via bound data. Choices are populated on-demand via the APISelect widget. + + Attributes: + query_params: A dictionary of additional key/value pairs to attach to the API request + initial_params: A dictionary of child field references to use for selecting a parent field's initial value + null_option: The string used to represent a null selection (if any) + disabled_indicator: The name of the field which, if populated, will disable selection of the + choice (optional) + fetch_trigger: The event type which will cause the select element to + fetch data from the API. Must be 'load', 'open', or 'collapse'. (optional) + """ + filter = django_filters.ModelChoiceFilter + widget = widgets.APISelect + + def __init__(self, query_params=None, initial_params=None, null_option=None, disabled_indicator=None, + fetch_trigger=None, empty_label=None, *args, **kwargs): + self.query_params = query_params or {} + self.initial_params = initial_params or {} + self.null_option = null_option + self.disabled_indicator = disabled_indicator + self.fetch_trigger = fetch_trigger + + # to_field_name is set by ModelChoiceField.__init__(), but we need to set it early for reference + # by widget_attrs() + self.to_field_name = kwargs.get('to_field_name') + self.empty_option = empty_label or "" + + super().__init__(*args, **kwargs) + + def widget_attrs(self, widget): + attrs = { + 'data-empty-option': self.empty_option + } + + # Set value-field attribute if the field specifies to_field_name + if self.to_field_name: + attrs['value-field'] = self.to_field_name + + # Set the string used to represent a null option + if self.null_option is not None: + attrs['data-null-option'] = self.null_option + + # Set the disabled indicator, if any + if self.disabled_indicator is not None: + attrs['disabled-indicator'] = self.disabled_indicator + + # Set the fetch trigger, if any. + if self.fetch_trigger is not None: + attrs['data-fetch-trigger'] = self.fetch_trigger + + # Attach any static query parameters + if (len(self.query_params) > 0): + widget.add_query_params(self.query_params) + + return attrs + + def get_bound_field(self, form, field_name): + bound_field = BoundField(form, self, field_name) + + # Set initial value based on prescribed child fields (if not already set) + if not self.initial and self.initial_params: + filter_kwargs = {} + for kwarg, child_field in self.initial_params.items(): + value = form.initial.get(child_field.lstrip('$')) + if value: + filter_kwargs[kwarg] = value + if filter_kwargs: + self.initial = self.queryset.filter(**filter_kwargs).first() + + # Modify the QuerySet of the field before we return it. Limit choices to any data already bound: Options + # will be populated on-demand via the APISelect widget. + data = bound_field.value() + if data: + field_name = getattr(self, 'to_field_name') or 'pk' + filter = self.filter(field_name=field_name) + try: + self.queryset = filter.filter(self.queryset, data) + except (TypeError, ValueError): + # Catch any error caused by invalid initial data passed from the user + self.queryset = self.queryset.none() + else: + self.queryset = self.queryset.none() + + # Set the data URL on the APISelect widget (if not already set) + widget = bound_field.field.widget + if not widget.attrs.get('data-url'): + app_label = self.queryset.model._meta.app_label + model_name = self.queryset.model._meta.model_name + data_url = reverse('{}-api:{}-list'.format(app_label, model_name)) + widget.attrs['data-url'] = data_url + + return bound_field + + +class DynamicModelChoiceField(DynamicModelChoiceMixin, forms.ModelChoiceField): + """ + Dynamic selection field for a single object, backed by NetBox's REST API. + """ + def clean(self, value): + """ + When null option is enabled and "None" is sent as part of a form to be submitted, it is sent as the + string 'null'. This will check for that condition and gracefully handle the conversion to a NoneType. + """ + if self.null_option is not None and value == settings.FILTERS_NULL_CHOICE_VALUE: + return None + return super().clean(value) + + +class DynamicModelMultipleChoiceField(DynamicModelChoiceMixin, forms.ModelMultipleChoiceField): + """ + A multiple-choice version of `DynamicModelChoiceField`. + """ + filter = django_filters.ModelMultipleChoiceFilter + widget = widgets.APISelectMultiple + + def clean(self, value): + """ + When null option is enabled and "None" is sent as part of a form to be submitted, it is sent as the + string 'null'. This will check for that condition and gracefully handle the conversion to a NoneType. + """ + if self.null_option is not None and settings.FILTERS_NULL_CHOICE_VALUE in value: + value = [v for v in value if v != settings.FILTERS_NULL_CHOICE_VALUE] + return [None, *value] + return super().clean(value) diff --git a/netbox/utilities/forms/fields/expandable.py b/netbox/utilities/forms/fields/expandable.py new file mode 100644 index 000000000..214775f03 --- /dev/null +++ b/netbox/utilities/forms/fields/expandable.py @@ -0,0 +1,54 @@ +import re + +from django import forms + +from utilities.forms.constants import * +from utilities.forms.utils import expand_alphanumeric_pattern, expand_ipaddress_pattern + +__all__ = ( + 'ExpandableIPAddressField', + 'ExpandableNameField', +) + + +class ExpandableNameField(forms.CharField): + """ + A field which allows for numeric range expansion + Example: 'Gi0/[1-3]' => ['Gi0/1', 'Gi0/2', 'Gi0/3'] + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not self.help_text: + self.help_text = """ + Alphanumeric ranges are supported for bulk creation. Mixed cases and types within a single range + are not supported. Example: [ge,xe]-0/0/[0-9] + """ + + def to_python(self, value): + if not value: + return '' + if re.search(ALPHANUMERIC_EXPANSION_PATTERN, value): + return list(expand_alphanumeric_pattern(value)) + return [value] + + +class ExpandableIPAddressField(forms.CharField): + """ + A field which allows for expansion of IP address ranges + Example: '192.0.2.[1-254]/24' => ['192.0.2.1/24', '192.0.2.2/24', '192.0.2.3/24' ... '192.0.2.254/24'] + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not self.help_text: + self.help_text = 'Specify a numeric range to create multiple IPs.
'\ + 'Example: 192.0.2.[1,5,100-254]/24' + + def to_python(self, value): + # Hackish address family detection but it's all we have to work with + if '.' in value and re.search(IP4_EXPANSION_PATTERN, value): + return list(expand_ipaddress_pattern(value, 4)) + elif ':' in value and re.search(IP6_EXPANSION_PATTERN, value): + return list(expand_ipaddress_pattern(value, 6)) + return [value] diff --git a/netbox/utilities/forms/fields/fields.py b/netbox/utilities/forms/fields/fields.py new file mode 100644 index 000000000..c2357a6e8 --- /dev/null +++ b/netbox/utilities/forms/fields/fields.py @@ -0,0 +1,127 @@ +import json + +from django import forms +from django.db.models import Count +from django.forms.fields import JSONField as _JSONField, InvalidJSONInput +from netaddr import AddrFormatError, EUI + +from utilities.forms import widgets +from utilities.validators import EnhancedURLValidator + +__all__ = ( + 'ColorField', + 'CommentField', + 'JSONField', + 'LaxURLField', + 'MACAddressField', + 'SlugField', + 'TagFilterField', +) + + +class CommentField(forms.CharField): + """ + A textarea with support for Markdown rendering. Exists mostly just to add a standard `help_text`. + """ + widget = forms.Textarea + # TODO: Port Markdown cheat sheet to internal documentation + help_text = """ + + + Markdown syntax is supported + """ + + def __init__(self, *, help_text=help_text, required=False, **kwargs): + super().__init__(help_text=help_text, required=required, **kwargs) + + +class SlugField(forms.SlugField): + """ + Extend Django's built-in SlugField to automatically populate from a field called `name` unless otherwise specified. + + Parameters: + slug_source: Name of the form field from which the slug value will be derived + """ + widget = widgets.SlugWidget + help_text = "URL-friendly unique shorthand" + + def __init__(self, *, slug_source='name', help_text=help_text, **kwargs): + super().__init__(help_text=help_text, **kwargs) + + self.widget.attrs['slug-source'] = slug_source + + +class ColorField(forms.CharField): + """ + A field which represents a color value in hexadecimal `RRGGBB` format. Utilizes NetBox's `ColorSelect` widget to + render choices. + """ + widget = widgets.ColorSelect + + +class TagFilterField(forms.MultipleChoiceField): + """ + A filter field for the tags of a model. Only the tags used by a model are displayed. + + :param model: The model of the filter + """ + widget = widgets.StaticSelectMultiple + + def __init__(self, model, *args, **kwargs): + def get_choices(): + tags = model.tags.annotate( + count=Count('extras_taggeditem_items') + ).order_by('name') + return [ + (str(tag.slug), '{} ({})'.format(tag.name, tag.count)) for tag in tags + ] + + # Choices are fetched each time the form is initialized + super().__init__(label='Tags', choices=get_choices, required=False, *args, **kwargs) + + +class LaxURLField(forms.URLField): + """ + Modifies Django's built-in URLField to remove the requirement for fully-qualified domain names + (e.g. http://myserver/ is valid) + """ + default_validators = [EnhancedURLValidator()] + + +class JSONField(_JSONField): + """ + Custom wrapper around Django's built-in JSONField to avoid presenting "null" as the default text. + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if not self.help_text: + self.help_text = 'Enter context data in JSON format.' + self.widget.attrs['placeholder'] = '' + + def prepare_value(self, value): + if isinstance(value, InvalidJSONInput): + return value + if value is None: + return '' + return json.dumps(value, sort_keys=True, indent=4) + + +class MACAddressField(forms.Field): + """ + Validates a 48-bit MAC address. + """ + widget = forms.CharField + default_error_messages = { + 'invalid': 'MAC address must be in EUI-48 format', + } + + def to_python(self, value): + value = super().to_python(value) + + # Validate MAC address format + try: + value = EUI(value.strip()) + except AddrFormatError: + raise forms.ValidationError(self.error_messages['invalid'], code='invalid') + + return value From ccb3a7528114b9c42f59e579687585218522828e Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 31 Jan 2022 15:52:36 -0500 Subject: [PATCH 5/8] Move fieldsets out of Meta for model forms --- netbox/circuits/forms/models.py | 25 ++--- netbox/dcim/forms/models.py | 114 ++++++++++++---------- netbox/extras/forms/models.py | 61 ++++++------ netbox/ipam/forms/models.py | 80 ++++++++------- netbox/netbox/forms/base.py | 6 ++ netbox/templates/generic/object_edit.html | 4 +- netbox/templates/users/preferences.html | 2 +- netbox/tenancy/forms/models.py | 14 +-- netbox/users/forms.py | 20 ++-- netbox/virtualization/forms/models.py | 26 ++--- netbox/wireless/forms/models.py | 24 ++--- 11 files changed, 206 insertions(+), 170 deletions(-) diff --git a/netbox/circuits/forms/models.py b/netbox/circuits/forms/models.py index bf5d92e85..2246573ba 100644 --- a/netbox/circuits/forms/models.py +++ b/netbox/circuits/forms/models.py @@ -27,15 +27,16 @@ class ProviderForm(NetBoxModelForm): required=False ) + fieldsets = ( + ('Provider', ('name', 'slug', 'asn', 'tags')), + ('Support Info', ('account', 'portal_url', 'noc_contact', 'admin_contact')), + ) + class Meta: model = Provider fields = [ 'name', 'slug', 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', 'tags', ] - fieldsets = ( - ('Provider', ('name', 'slug', 'asn', 'tags')), - ('Support Info', ('account', 'portal_url', 'noc_contact', 'admin_contact')), - ) widgets = { 'noc_contact': SmallTextarea( attrs={'rows': 5} @@ -63,14 +64,15 @@ class ProviderNetworkForm(NetBoxModelForm): required=False ) + fieldsets = ( + ('Provider Network', ('provider', 'name', 'service_id', 'description', 'tags')), + ) + class Meta: model = ProviderNetwork fields = [ 'provider', 'name', 'service_id', 'description', 'comments', 'tags', ] - fieldsets = ( - ('Provider Network', ('provider', 'name', 'service_id', 'description', 'tags')), - ) class CircuitTypeForm(NetBoxModelForm): @@ -100,16 +102,17 @@ class CircuitForm(TenancyForm, NetBoxModelForm): required=False ) + fieldsets = ( + ('Circuit', ('provider', 'cid', 'type', 'status', 'install_date', 'commit_rate', 'description', 'tags')), + ('Tenancy', ('tenant_group', 'tenant')), + ) + class Meta: model = Circuit fields = [ 'cid', 'type', 'provider', 'status', 'install_date', 'commit_rate', 'description', 'tenant_group', 'tenant', 'comments', 'tags', ] - fieldsets = ( - ('Circuit', ('provider', 'cid', 'type', 'status', 'install_date', 'commit_rate', 'description', 'tags')), - ('Tenancy', ('tenant_group', 'tenant')), - ) help_texts = { 'cid': "Unique circuit ID", 'commit_rate': "Committed rate", diff --git a/netbox/dcim/forms/models.py b/netbox/dcim/forms/models.py index 80e785940..92f74036a 100644 --- a/netbox/dcim/forms/models.py +++ b/netbox/dcim/forms/models.py @@ -134,19 +134,20 @@ class SiteForm(TenancyForm, NetBoxModelForm): required=False ) + fieldsets = ( + ('Site', ( + 'name', 'slug', 'status', 'region', 'group', 'facility', 'asns', 'time_zone', 'description', 'tags', + )), + ('Tenancy', ('tenant_group', 'tenant')), + ('Contact Info', ('physical_address', 'shipping_address', 'latitude', 'longitude')), + ) + class Meta: model = Site fields = ( 'name', 'slug', 'status', 'region', 'group', 'tenant_group', 'tenant', 'facility', 'asns', 'time_zone', 'description', 'physical_address', 'shipping_address', 'latitude', 'longitude', 'comments', 'tags', ) - fieldsets = ( - ('Site', ( - 'name', 'slug', 'status', 'region', 'group', 'facility', 'asns', 'time_zone', 'description', 'tags', - )), - ('Tenancy', ('tenant_group', 'tenant')), - ('Contact Info', ('physical_address', 'shipping_address', 'latitude', 'longitude')), - ) widgets = { 'physical_address': SmallTextarea( attrs={ @@ -208,17 +209,18 @@ class LocationForm(TenancyForm, NetBoxModelForm): required=False ) + fieldsets = ( + ('Location', ( + 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tags', + )), + ('Tenancy', ('tenant_group', 'tenant')), + ) + class Meta: model = Location fields = ( 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tenant_group', 'tenant', 'tags', ) - fieldsets = ( - ('Location', ( - 'region', 'site_group', 'site', 'parent', 'name', 'slug', 'description', 'tags', - )), - ('Tenancy', ('tenant_group', 'tenant')), - ) class RackRoleForm(NetBoxModelForm): @@ -347,16 +349,17 @@ class RackReservationForm(TenancyForm, NetBoxModelForm): required=False ) + fieldsets = ( + ('Reservation', ('region', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')), + ('Tenancy', ('tenant_group', 'tenant')), + ) + class Meta: model = RackReservation fields = [ 'region', 'site_group', 'site', 'location', 'rack', 'units', 'user', 'tenant_group', 'tenant', 'description', 'tags', ] - fieldsets = ( - ('Reservation', ('region', 'site', 'location', 'rack', 'units', 'user', 'description', 'tags')), - ('Tenancy', ('tenant_group', 'tenant')), - ) class ManufacturerForm(NetBoxModelForm): @@ -386,21 +389,22 @@ class DeviceTypeForm(NetBoxModelForm): required=False ) + fieldsets = ( + ('Device Type', ( + 'manufacturer', 'model', 'slug', 'part_number', 'tags', + )), + ('Chassis', ( + 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', + )), + ('Images', ('front_image', 'rear_image')), + ) + class Meta: model = DeviceType fields = [ 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', 'front_image', 'rear_image', 'comments', 'tags', ] - fieldsets = ( - ('Device Type', ( - 'manufacturer', 'model', 'slug', 'part_number', 'tags', - )), - ('Chassis', ( - 'u_height', 'is_full_depth', 'subdevice_role', 'airflow', - )), - ('Images', ('front_image', 'rear_image')), - ) widgets = { 'subdevice_role': StaticSelect(), 'front_image': ClearableFileInput(attrs={ @@ -745,14 +749,15 @@ class PowerPanelForm(NetBoxModelForm): required=False ) + fieldsets = ( + ('Power Panel', ('region', 'site_group', 'site', 'location', 'name', 'tags')), + ) + class Meta: model = PowerPanel fields = [ 'region', 'site_group', 'site', 'location', 'name', 'tags', ] - fieldsets = ( - ('Power Panel', ('region', 'site_group', 'site', 'location', 'name', 'tags')), - ) class PowerFeedForm(NetBoxModelForm): @@ -800,17 +805,18 @@ class PowerFeedForm(NetBoxModelForm): required=False ) + fieldsets = ( + ('Power Panel', ('region', 'site', 'power_panel')), + ('Power Feed', ('rack', 'name', 'status', 'type', 'mark_connected', 'tags')), + ('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')), + ) + class Meta: model = PowerFeed fields = [ 'region', 'site_group', 'site', 'power_panel', 'rack', 'name', 'status', 'type', 'mark_connected', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization', 'comments', 'tags', ] - fieldsets = ( - ('Power Panel', ('region', 'site', 'power_panel')), - ('Power Feed', ('rack', 'name', 'status', 'type', 'mark_connected', 'tags')), - ('Characteristics', ('supply', 'voltage', 'amperage', 'phase', 'max_utilization')), - ) widgets = { 'status': StaticSelect(), 'type': StaticSelect(), @@ -1101,16 +1107,17 @@ class InventoryItemTemplateForm(BootstrapMixin, forms.ModelForm): widget=forms.HiddenInput ) + fieldsets = ( + ('Inventory Item', ('device_type', 'parent', 'name', 'label', 'role', 'description')), + ('Hardware', ('manufacturer', 'part_id')), + ) + class Meta: model = InventoryItemTemplate fields = [ 'device_type', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'description', 'component_type', 'component_id', ] - fieldsets = ( - ('Inventory Item', ('device_type', 'parent', 'name', 'label', 'role', 'description')), - ('Hardware', ('manufacturer', 'part_id')), - ) widgets = { 'device_type': forms.HiddenInput(), } @@ -1271,6 +1278,17 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm): required=False ) + fieldsets = ( + ('Interface', ('device', 'name', 'type', 'speed', 'duplex', 'label', 'description', 'tags')), + ('Addressing', ('vrf', 'mac_address', 'wwn')), + ('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')), + ('Related Interfaces', ('parent', 'bridge', 'lag')), + ('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')), + ('Wireless', ( + 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', 'wireless_lans', + )), + ) + class Meta: model = Interface fields = [ @@ -1278,17 +1296,6 @@ class InterfaceForm(InterfaceCommonForm, NetBoxModelForm): 'mgmt_only', 'mark_connected', 'description', 'mode', 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'wireless_lans', 'untagged_vlan', 'tagged_vlans', 'vrf', 'tags', ] - fieldsets = ( - ('Interface', ('device', 'name', 'type', 'speed', 'duplex', 'label', 'description', 'tags')), - ('Addressing', ('vrf', 'mac_address', 'wwn')), - ('Operation', ('mtu', 'tx_power', 'enabled', 'mgmt_only', 'mark_connected')), - ('Related Interfaces', ('parent', 'bridge', 'lag')), - ('802.1Q Switching', ('mode', 'vlan_group', 'untagged_vlan', 'tagged_vlans')), - ('Wireless', ( - 'rf_role', 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'wireless_lan_group', - 'wireless_lans', - )), - ) widgets = { 'device': forms.HiddenInput(), 'type': StaticSelect(), @@ -1432,16 +1439,17 @@ class InventoryItemForm(NetBoxModelForm): required=False ) + fieldsets = ( + ('Inventory Item', ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')), + ('Hardware', ('manufacturer', 'part_id', 'serial', 'asset_tag')), + ) + class Meta: model = InventoryItem fields = [ 'device', 'parent', 'name', 'label', 'role', 'manufacturer', 'part_id', 'serial', 'asset_tag', 'description', 'component_type', 'component_id', 'tags', ] - fieldsets = ( - ('Inventory Item', ('device', 'parent', 'name', 'label', 'role', 'description', 'tags')), - ('Hardware', ('manufacturer', 'part_id', 'serial', 'asset_tag')), - ) widgets = { 'device': forms.HiddenInput(), } diff --git a/netbox/extras/forms/models.py b/netbox/extras/forms/models.py index 5c29a8381..c391665b3 100644 --- a/netbox/extras/forms/models.py +++ b/netbox/extras/forms/models.py @@ -30,16 +30,17 @@ class CustomFieldForm(BootstrapMixin, forms.ModelForm): limit_choices_to=FeatureQuery('custom_fields') ) + fieldsets = ( + ('Custom Field', ('name', 'label', 'type', 'object_type', 'weight', 'required', 'description')), + ('Assigned Models', ('content_types',)), + ('Behavior', ('filter_logic',)), + ('Values', ('default', 'choices')), + ('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')), + ) + class Meta: model = CustomField fields = '__all__' - fieldsets = ( - ('Custom Field', ('name', 'label', 'type', 'object_type', 'weight', 'required', 'description')), - ('Assigned Models', ('content_types',)), - ('Behavior', ('filter_logic',)), - ('Values', ('default', 'choices')), - ('Validation', ('validation_minimum', 'validation_maximum', 'validation_regex')), - ) widgets = { 'type': StaticSelect(), 'filter_logic': StaticSelect(), @@ -52,13 +53,14 @@ class CustomLinkForm(BootstrapMixin, forms.ModelForm): limit_choices_to=FeatureQuery('custom_links') ) + fieldsets = ( + ('Custom Link', ('name', 'content_type', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')), + ('Templates', ('link_text', 'link_url')), + ) + class Meta: model = CustomLink fields = '__all__' - fieldsets = ( - ('Custom Link', ('name', 'content_type', 'weight', 'group_name', 'button_class', 'enabled', 'new_window')), - ('Templates', ('link_text', 'link_url')), - ) widgets = { 'button_class': StaticSelect(), 'link_text': forms.Textarea(attrs={'class': 'font-monospace'}), @@ -77,14 +79,15 @@ class ExportTemplateForm(BootstrapMixin, forms.ModelForm): limit_choices_to=FeatureQuery('export_templates') ) + fieldsets = ( + ('Export Template', ('name', 'content_type', 'description')), + ('Template', ('template_code',)), + ('Rendering', ('mime_type', 'file_extension', 'as_attachment')), + ) + class Meta: model = ExportTemplate fields = '__all__' - fieldsets = ( - ('Export Template', ('name', 'content_type', 'description')), - ('Template', ('template_code',)), - ('Rendering', ('mime_type', 'file_extension', 'as_attachment')), - ) widgets = { 'template_code': forms.Textarea(attrs={'class': 'font-monospace'}), } @@ -96,18 +99,19 @@ class WebhookForm(BootstrapMixin, forms.ModelForm): limit_choices_to=FeatureQuery('webhooks') ) + fieldsets = ( + ('Webhook', ('name', 'content_types', 'enabled')), + ('Events', ('type_create', 'type_update', 'type_delete')), + ('HTTP Request', ( + 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', + )), + ('Conditions', ('conditions',)), + ('SSL', ('ssl_verification', 'ca_file_path')), + ) + class Meta: model = Webhook fields = '__all__' - fieldsets = ( - ('Webhook', ('name', 'content_types', 'enabled')), - ('Events', ('type_create', 'type_update', 'type_delete')), - ('HTTP Request', ( - 'payload_url', 'http_method', 'http_content_type', 'additional_headers', 'body_template', 'secret', - )), - ('Conditions', ('conditions',)), - ('SSL', ('ssl_verification', 'ca_file_path')), - ) labels = { 'type_create': 'Creations', 'type_update': 'Updates', @@ -123,14 +127,15 @@ class WebhookForm(BootstrapMixin, forms.ModelForm): class TagForm(BootstrapMixin, forms.ModelForm): slug = SlugField() + fieldsets = ( + ('Tag', ('name', 'slug', 'color', 'description')), + ) + class Meta: model = Tag fields = [ 'name', 'slug', 'color', 'description' ] - fieldsets = ( - ('Tag', ('name', 'slug', 'color', 'description')), - ) class ConfigContextForm(BootstrapMixin, forms.ModelForm): diff --git a/netbox/ipam/forms/models.py b/netbox/ipam/forms/models.py index e86fe1dab..68016a0e5 100644 --- a/netbox/ipam/forms/models.py +++ b/netbox/ipam/forms/models.py @@ -53,17 +53,18 @@ class VRFForm(TenancyForm, NetBoxModelForm): required=False ) + fieldsets = ( + ('VRF', ('name', 'rd', 'enforce_unique', 'description', 'tags')), + ('Route Targets', ('import_targets', 'export_targets')), + ('Tenancy', ('tenant_group', 'tenant')), + ) + class Meta: model = VRF fields = [ 'name', 'rd', 'enforce_unique', 'description', 'import_targets', 'export_targets', 'tenant_group', 'tenant', 'tags', ] - fieldsets = ( - ('VRF', ('name', 'rd', 'enforce_unique', 'description', 'tags')), - ('Route Targets', ('import_targets', 'export_targets')), - ('Tenancy', ('tenant_group', 'tenant')), - ) labels = { 'rd': "RD", } @@ -78,15 +79,16 @@ class RouteTargetForm(TenancyForm, NetBoxModelForm): required=False ) + fieldsets = ( + ('Route Target', ('name', 'description', 'tags')), + ('Tenancy', ('tenant_group', 'tenant')), + ) + class Meta: model = RouteTarget fields = [ 'name', 'description', 'tenant_group', 'tenant', 'tags', ] - fieldsets = ( - ('Route Target', ('name', 'description', 'tags')), - ('Tenancy', ('tenant_group', 'tenant')), - ) class RIRForm(NetBoxModelForm): @@ -113,15 +115,16 @@ class AggregateForm(TenancyForm, NetBoxModelForm): required=False ) + fieldsets = ( + ('Aggregate', ('prefix', 'rir', 'date_added', 'description', 'tags')), + ('Tenancy', ('tenant_group', 'tenant')), + ) + class Meta: model = Aggregate fields = [ 'prefix', 'rir', 'date_added', 'description', 'tenant_group', 'tenant', 'tags', ] - fieldsets = ( - ('Aggregate', ('prefix', 'rir', 'date_added', 'description', 'tags')), - ('Tenancy', ('tenant_group', 'tenant')), - ) help_texts = { 'prefix': "IPv4 or IPv6 network", 'rir': "Regional Internet Registry responsible for this prefix", @@ -146,15 +149,16 @@ class ASNForm(TenancyForm, NetBoxModelForm): required=False ) + fieldsets = ( + ('ASN', ('asn', 'rir', 'sites', 'description', 'tags')), + ('Tenancy', ('tenant_group', 'tenant')), + ) + class Meta: model = ASN fields = [ 'asn', 'rir', 'sites', 'tenant_group', 'tenant', 'description', 'tags' ] - fieldsets = ( - ('ASN', ('asn', 'rir', 'sites', 'description', 'tags')), - ('Tenancy', ('tenant_group', 'tenant')), - ) help_texts = { 'asn': "AS number", 'rir': "Regional Internet Registry responsible for this prefix", @@ -248,17 +252,18 @@ class PrefixForm(TenancyForm, NetBoxModelForm): required=False ) + fieldsets = ( + ('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')), + ('Site/VLAN Assignment', ('region', 'site_group', 'site', 'vlan_group', 'vlan')), + ('Tenancy', ('tenant_group', 'tenant')), + ) + class Meta: model = Prefix fields = [ 'prefix', 'vrf', 'site', 'vlan', 'status', 'role', 'is_pool', 'mark_utilized', 'description', 'tenant_group', 'tenant', 'tags', ] - fieldsets = ( - ('Prefix', ('prefix', 'status', 'vrf', 'role', 'is_pool', 'mark_utilized', 'description', 'tags')), - ('Site/VLAN Assignment', ('region', 'site_group', 'site', 'vlan_group', 'vlan')), - ('Tenancy', ('tenant_group', 'tenant')), - ) widgets = { 'status': StaticSelect(), } @@ -279,15 +284,16 @@ class IPRangeForm(TenancyForm, NetBoxModelForm): required=False ) + fieldsets = ( + ('IP Range', ('vrf', 'start_address', 'end_address', 'role', 'status', 'description', 'tags')), + ('Tenancy', ('tenant_group', 'tenant')), + ) + class Meta: model = IPRange fields = [ 'vrf', 'start_address', 'end_address', 'status', 'role', 'description', 'tenant_group', 'tenant', 'tags', ] - fieldsets = ( - ('IP Range', ('vrf', 'start_address', 'end_address', 'role', 'status', 'description', 'tags')), - ('Tenancy', ('tenant_group', 'tenant')), - ) widgets = { 'status': StaticSelect(), } @@ -562,16 +568,17 @@ class FHRPGroupForm(NetBoxModelForm): label='Status' ) + fieldsets = ( + ('FHRP Group', ('protocol', 'group_id', 'description', 'tags')), + ('Authentication', ('auth_type', 'auth_key')), + ('Virtual IP Address', ('ip_vrf', 'ip_address', 'ip_status')) + ) + class Meta: model = FHRPGroup fields = ( 'protocol', 'group_id', 'auth_type', 'auth_key', 'description', 'ip_vrf', 'ip_address', 'ip_status', 'tags', ) - fieldsets = ( - ('FHRP Group', ('protocol', 'group_id', 'description', 'tags')), - ('Authentication', ('auth_type', 'auth_key')), - ('Virtual IP Address', ('ip_vrf', 'ip_address', 'ip_status')) - ) def save(self, *args, **kwargs): instance = super().save(*args, **kwargs) @@ -699,17 +706,18 @@ class VLANGroupForm(NetBoxModelForm): required=False ) + fieldsets = ( + ('VLAN Group', ('name', 'slug', 'description', 'tags')), + ('Child VLANs', ('min_vid', 'max_vid')), + ('Scope', ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')), + ) + class Meta: model = VLANGroup fields = [ 'name', 'slug', 'description', 'scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster', 'min_vid', 'max_vid', 'tags', ] - fieldsets = ( - ('VLAN Group', ('name', 'slug', 'description', 'tags')), - ('Child VLANs', ('min_vid', 'max_vid')), - ('Scope', ('scope_type', 'region', 'sitegroup', 'site', 'location', 'rack', 'clustergroup', 'cluster')), - ) widgets = { 'scope_type': StaticSelect, } diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index 63582a2d9..3fa85f1f7 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -19,7 +19,13 @@ __all__ = ( class NetBoxModelForm(BootstrapMixin, CustomFieldsMixin, forms.ModelForm): """ Base form for creating & editing NetBox models. Adds support for custom fields. + + Attributes: + fieldsets: An iterable of two-tuples which define a heading and field set to display per section of + the rendered form (optional). If not defined, the all fields will be rendered as a single section. """ + fieldsets = () + def _get_content_type(self): return ContentType.objects.get_for_model(self._meta.model) diff --git a/netbox/templates/generic/object_edit.html b/netbox/templates/generic/object_edit.html index 5dc8f995d..fbf7fd394 100644 --- a/netbox/templates/generic/object_edit.html +++ b/netbox/templates/generic/object_edit.html @@ -33,7 +33,7 @@ {% csrf_token %} {% block form %} - {% if form.Meta.fieldsets %} + {% if form.fieldsets %} {# Render hidden fields #} {% for field in form.hidden_fields %} @@ -41,7 +41,7 @@ {% endfor %} {# Render grouped fields according to Form #} - {% for group, fields in form.Meta.fieldsets %} + {% for group, fields in form.fieldsets %}
{{ group }}
diff --git a/netbox/templates/users/preferences.html b/netbox/templates/users/preferences.html index 2a34f1b3f..b9b0be665 100644 --- a/netbox/templates/users/preferences.html +++ b/netbox/templates/users/preferences.html @@ -8,7 +8,7 @@
{% csrf_token %} - {% for group, fields in form.Meta.fieldsets %} + {% for group, fields in form.fieldsets %}
{{ group }}
diff --git a/netbox/tenancy/forms/models.py b/netbox/tenancy/forms/models.py index 313b55417..5fe299cad 100644 --- a/netbox/tenancy/forms/models.py +++ b/netbox/tenancy/forms/models.py @@ -52,14 +52,15 @@ class TenantForm(NetBoxModelForm): required=False ) + fieldsets = ( + ('Tenant', ('name', 'slug', 'group', 'description', 'tags')), + ) + class Meta: model = Tenant fields = ( 'name', 'slug', 'group', 'description', 'comments', 'tags', ) - fieldsets = ( - ('Tenant', ('name', 'slug', 'group', 'description', 'tags')), - ) # @@ -105,14 +106,15 @@ class ContactForm(NetBoxModelForm): required=False ) + fieldsets = ( + ('Contact', ('group', 'name', 'title', 'phone', 'email', 'address', 'tags')), + ) + class Meta: model = Contact fields = ( 'group', 'name', 'title', 'phone', 'email', 'address', 'comments', 'tags', ) - fieldsets = ( - ('Contact', ('group', 'name', 'title', 'phone', 'email', 'address', 'tags')), - ) widgets = { 'address': SmallTextarea(attrs={'rows': 3}), } diff --git a/netbox/users/forms.py b/netbox/users/forms.py index 5a99adc5a..49c080853 100644 --- a/netbox/users/forms.py +++ b/netbox/users/forms.py @@ -40,20 +40,20 @@ class UserConfigFormMetaclass(forms.models.ModelFormMetaclass): class UserConfigForm(BootstrapMixin, forms.ModelForm, metaclass=UserConfigFormMetaclass): + fieldsets = ( + ('User Interface', ( + 'pagination.per_page', + 'pagination.placement', + 'ui.colormode', + )), + ('Miscellaneous', ( + 'data_format', + )), + ) class Meta: model = UserConfig fields = () - fieldsets = ( - ('User Interface', ( - 'pagination.per_page', - 'pagination.placement', - 'ui.colormode', - )), - ('Miscellaneous', ( - 'data_format', - )), - ) def __init__(self, *args, instance=None, **kwargs): diff --git a/netbox/virtualization/forms/models.py b/netbox/virtualization/forms/models.py index 883fcd363..ecd909ec2 100644 --- a/netbox/virtualization/forms/models.py +++ b/netbox/virtualization/forms/models.py @@ -90,15 +90,16 @@ class ClusterForm(TenancyForm, NetBoxModelForm): required=False ) + fieldsets = ( + ('Cluster', ('name', 'type', 'group', 'region', 'site_group', 'site', 'tags')), + ('Tenancy', ('tenant_group', 'tenant')), + ) + class Meta: model = Cluster fields = ( 'name', 'type', 'group', 'tenant', 'region', 'site_group', 'site', 'comments', 'tags', ) - fieldsets = ( - ('Cluster', ('name', 'type', 'group', 'region', 'site_group', 'site', 'tags')), - ('Tenancy', ('tenant_group', 'tenant')), - ) class ClusterAddDevicesForm(BootstrapMixin, forms.Form): @@ -206,20 +207,21 @@ class VirtualMachineForm(TenancyForm, NetBoxModelForm): required=False ) + fieldsets = ( + ('Virtual Machine', ('name', 'role', 'status', 'tags')), + ('Cluster', ('cluster_group', 'cluster')), + ('Tenancy', ('tenant_group', 'tenant')), + ('Management', ('platform', 'primary_ip4', 'primary_ip6')), + ('Resources', ('vcpus', 'memory', 'disk')), + ('Config Context', ('local_context_data',)), + ) + class Meta: model = VirtualMachine fields = [ 'name', 'status', 'cluster_group', 'cluster', 'role', 'tenant_group', 'tenant', 'platform', 'primary_ip4', 'primary_ip6', 'vcpus', 'memory', 'disk', 'comments', 'tags', 'local_context_data', ] - fieldsets = ( - ('Virtual Machine', ('name', 'role', 'status', 'tags')), - ('Cluster', ('cluster_group', 'cluster')), - ('Tenancy', ('tenant_group', 'tenant')), - ('Management', ('platform', 'primary_ip4', 'primary_ip6')), - ('Resources', ('vcpus', 'memory', 'disk')), - ('Config Context', ('local_context_data',)), - ) help_texts = { 'local_context_data': "Local config context data overwrites all sources contexts in the final rendered " "config context", diff --git a/netbox/wireless/forms/models.py b/netbox/wireless/forms/models.py index 30a4a2352..2ed4a5766 100644 --- a/netbox/wireless/forms/models.py +++ b/netbox/wireless/forms/models.py @@ -45,16 +45,17 @@ class WirelessLANForm(NetBoxModelForm): required=False ) + fieldsets = ( + ('Wireless LAN', ('ssid', 'group', 'description', 'tags')), + ('VLAN', ('vlan',)), + ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')), + ) + class Meta: model = WirelessLAN fields = [ 'ssid', 'group', 'description', 'vlan', 'auth_type', 'auth_cipher', 'auth_psk', 'tags', ] - fieldsets = ( - ('Wireless LAN', ('ssid', 'group', 'description', 'tags')), - ('VLAN', ('vlan',)), - ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')), - ) widgets = { 'auth_type': StaticSelect, 'auth_cipher': StaticSelect, @@ -141,18 +142,19 @@ class WirelessLinkForm(NetBoxModelForm): required=False ) + fieldsets = ( + ('Side A', ('site_a', 'location_a', 'device_a', 'interface_a')), + ('Side B', ('site_b', 'location_b', 'device_b', 'interface_b')), + ('Link', ('status', 'ssid', 'description', 'tags')), + ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')), + ) + class Meta: model = WirelessLink fields = [ 'site_a', 'location_a', 'device_a', 'interface_a', 'site_b', 'location_b', 'device_b', 'interface_b', 'status', 'ssid', 'description', 'auth_type', 'auth_cipher', 'auth_psk', 'tags', ] - fieldsets = ( - ('Side A', ('site_a', 'location_a', 'device_a', 'interface_a')), - ('Side B', ('site_b', 'location_b', 'device_b', 'interface_b')), - ('Link', ('status', 'ssid', 'description', 'tags')), - ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')), - ) widgets = { 'status': StaticSelect, 'auth_type': StaticSelect, From 353e132cf95b82b9d61cbadc7c34e054c3030b88 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 31 Jan 2022 16:03:26 -0500 Subject: [PATCH 6/8] Replace filter_groups with fieldsets on filter forms --- netbox/circuits/forms/filtersets.py | 30 +-- netbox/dcim/forms/filtersets.py | 247 +++++++++++----------- netbox/extras/forms/filtersets.py | 69 +++--- netbox/ipam/forms/filtersets.py | 114 +++++----- netbox/templates/inc/filter_list.html | 31 +-- netbox/tenancy/forms/filtersets.py | 8 - netbox/virtualization/forms/filtersets.py | 36 ++-- netbox/wireless/forms/filtersets.py | 9 +- 8 files changed, 270 insertions(+), 274 deletions(-) diff --git a/netbox/circuits/forms/filtersets.py b/netbox/circuits/forms/filtersets.py index 18f914b58..e7e5287a6 100644 --- a/netbox/circuits/forms/filtersets.py +++ b/netbox/circuits/forms/filtersets.py @@ -18,11 +18,11 @@ __all__ = ( class ProviderFilterForm(NetBoxModelFilterSetForm): model = Provider - field_groups = [ - ['q', 'tag'], - ['region_id', 'site_group_id', 'site_id'], - ['asn'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Location', ('region_id', 'site_group_id', 'site_id')), + ('ASN', ('asn',)), + ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, @@ -51,9 +51,9 @@ class ProviderFilterForm(NetBoxModelFilterSetForm): class ProviderNetworkFilterForm(NetBoxModelFilterSetForm): model = ProviderNetwork - field_groups = ( - ('q', 'tag'), - ('provider_id',), + fieldsets = ( + (None, ('q', 'tag')), + ('Attributes', ('provider_id', 'service_id')), ) provider_id = DynamicModelMultipleChoiceField( queryset=Provider.objects.all(), @@ -74,13 +74,13 @@ class CircuitTypeFilterForm(NetBoxModelFilterSetForm): class CircuitFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Circuit - field_groups = [ - ['q', 'tag'], - ['provider_id', 'provider_network_id'], - ['type_id', 'status', 'commit_rate'], - ['region_id', 'site_group_id', 'site_id'], - ['tenant_group_id', 'tenant_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Provider', ('provider_id', 'provider_network_id')), + ('Attributes', ('type_id', 'status', 'commit_rate')), + ('Location', ('region_id', 'site_group_id', 'site_id')), + ('Tenant', ('tenant_group_id', 'tenant_id')), + ) type_id = DynamicModelMultipleChoiceField( queryset=CircuitType.objects.all(), required=False, diff --git a/netbox/dcim/forms/filtersets.py b/netbox/dcim/forms/filtersets.py index e9aa3ec3f..180d0c4e7 100644 --- a/netbox/dcim/forms/filtersets.py +++ b/netbox/dcim/forms/filtersets.py @@ -126,12 +126,11 @@ class SiteGroupFilterForm(NetBoxModelFilterSetForm): class SiteFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Site - field_groups = [ - ['q', 'tag'], - ['status', 'region_id', 'group_id'], - ['tenant_group_id', 'tenant_id'], - ['asn_id'] - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Attributes', ('status', 'region_id', 'group_id', 'asn_id')), + ('Tenant', ('tenant_group_id', 'tenant_id')), + ) status = forms.MultipleChoiceField( choices=SiteStatusChoices, required=False, @@ -157,11 +156,11 @@ class SiteFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class LocationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Location - field_groups = [ - ['q', 'tag'], - ['region_id', 'site_group_id', 'site_id', 'parent_id'], - ['tenant_group_id', 'tenant_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Parent', ('region_id', 'site_group_id', 'site_id', 'parent_id')), + ('Tenant', ('tenant_group_id', 'tenant_id')), + ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, @@ -200,13 +199,13 @@ class RackRoleFilterForm(NetBoxModelFilterSetForm): class RackFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Rack - field_groups = [ - ['q', 'tag'], - ['region_id', 'site_id', 'location_id'], - ['status', 'role_id'], - ['type', 'width', 'serial', 'asset_tag'], - ['tenant_group_id', 'tenant_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Location', ('region_id', 'site_id', 'location_id')), + ('Function', ('status', 'role_id')), + ('Hardware', ('type', 'width', 'serial', 'asset_tag')), + ('Tenant', ('tenant_group_id', 'tenant_id')), + ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, @@ -273,12 +272,12 @@ class RackElevationFilterForm(RackFilterForm): class RackReservationFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = RackReservation - field_groups = [ - ['q', 'tag'], - ['user_id'], - ['region_id', 'site_id', 'location_id'], - ['tenant_group_id', 'tenant_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('User', ('user_id',)), + ('Rack', ('region_id', 'site_id', 'location_id')), + ('Tenant', ('tenant_group_id', 'tenant_id')), + ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, @@ -316,11 +315,14 @@ class ManufacturerFilterForm(NetBoxModelFilterSetForm): class DeviceTypeFilterForm(NetBoxModelFilterSetForm): model = DeviceType - field_groups = [ - ['q', 'tag'], - ['manufacturer_id', 'part_number', 'subdevice_role', 'airflow'], - ['console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Hardware', ('manufacturer_id', 'part_number', 'subdevice_role', 'airflow')), + ('Components', ( + 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', + 'pass_through_ports', + )), + ) manufacturer_id = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), required=False, @@ -386,11 +388,14 @@ class DeviceTypeFilterForm(NetBoxModelFilterSetForm): class ModuleTypeFilterForm(NetBoxModelFilterSetForm): model = ModuleType - field_groups = [ - ['q', 'tag'], - ['manufacturer_id', 'part_number'], - ['console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Hardware', ('manufacturer_id', 'part_number')), + ('Components', ( + 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', + 'pass_through_ports', + )), + ) manufacturer_id = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), required=False, @@ -462,17 +467,17 @@ class PlatformFilterForm(NetBoxModelFilterSetForm): class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): model = Device - field_groups = [ - ['q', 'tag'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id'], - ['status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address'], - ['manufacturer_id', 'device_type_id', 'platform_id'], - ['tenant_group_id', 'tenant_id'], - [ - 'has_primary_ip', 'virtual_chassis_member', 'console_ports', 'console_server_ports', 'power_ports', - 'power_outlets', 'interfaces', 'pass_through_ports', 'local_context_data', - ], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id', 'rack_id')), + ('Operation', ('status', 'role_id', 'airflow', 'serial', 'asset_tag', 'mac_address')), + ('Hardware', ('manufacturer_id', 'device_type_id', 'platform_id')), + ('Tenant', ('tenant_group_id', 'tenant_id')), + ('Components', ( + 'console_ports', 'console_server_ports', 'power_ports', 'power_outlets', 'interfaces', 'pass_through_ports', + )), + ('Miscellaneous', ('has_primary_ip', 'virtual_chassis_member', 'local_context_data')) + ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, @@ -616,11 +621,10 @@ class DeviceFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): model = Module - field_groups = [ - ['q', 'tag'], - ['manufacturer_id', 'module_type_id'], - ['serial', 'asset_tag'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Hardware', ('manufacturer_id', 'module_type_id', 'serial', 'asset_tag')), + ) manufacturer_id = DynamicModelMultipleChoiceField( queryset=Manufacturer.objects.all(), required=False, @@ -647,11 +651,11 @@ class ModuleFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxMo class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = VirtualChassis - field_groups = [ - ['q', 'tag'], - ['region_id', 'site_group_id', 'site_id'], - ['tenant_group_id', 'tenant_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Location', ('region_id', 'site_group_id', 'site_id')), + ('Tenant', ('tenant_group_id', 'tenant_id')), + ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, @@ -676,12 +680,12 @@ class VirtualChassisFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Cable - field_groups = [ - ['q', 'tag'], - ['site_id', 'rack_id', 'device_id'], - ['type', 'status', 'color', 'length', 'length_unit'], - ['tenant_group_id', 'tenant_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Location', ('site_id', 'rack_id', 'device_id')), + ('Attributes', ('type', 'status', 'color', 'length', 'length_unit')), + ('Tenant', ('tenant_group_id', 'tenant_id')), + ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, @@ -739,9 +743,9 @@ class CableFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class PowerPanelFilterForm(NetBoxModelFilterSetForm): model = PowerPanel - field_groups = ( - ('q', 'tag'), - ('region_id', 'site_group_id', 'site_id', 'location_id') + fieldsets = ( + (None, ('q', 'tag')), + ('Location', ('region_id', 'site_group_id', 'site_id', 'location_id')) ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), @@ -776,12 +780,11 @@ class PowerPanelFilterForm(NetBoxModelFilterSetForm): class PowerFeedFilterForm(NetBoxModelFilterSetForm): model = PowerFeed - field_groups = [ - ['q', 'tag'], - ['region_id', 'site_group_id', 'site_id'], - ['power_panel_id', 'rack_id'], - ['status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Location', ('region_id', 'site_group_id', 'site_id', 'power_panel_id', 'rack_id')), + ('Attributes', ('status', 'type', 'supply', 'phase', 'voltage', 'amperage', 'max_utilization')), + ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, @@ -856,11 +859,11 @@ class PowerFeedFilterForm(NetBoxModelFilterSetForm): class ConsolePortFilterForm(DeviceComponentFilterForm): model = ConsolePort - field_groups = [ - ['q', 'tag'], - ['name', 'label', 'type', 'speed'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Attributes', ('name', 'label', 'type', 'speed')), + ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')), + ) type = forms.MultipleChoiceField( choices=ConsolePortTypeChoices, required=False, @@ -876,11 +879,11 @@ class ConsolePortFilterForm(DeviceComponentFilterForm): class ConsoleServerPortFilterForm(DeviceComponentFilterForm): model = ConsoleServerPort - field_groups = [ - ['q', 'tag'], - ['name', 'label', 'type', 'speed'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Attributes', ('name', 'label', 'type', 'speed')), + ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')), + ) type = forms.MultipleChoiceField( choices=ConsolePortTypeChoices, required=False, @@ -896,11 +899,11 @@ class ConsoleServerPortFilterForm(DeviceComponentFilterForm): class PowerPortFilterForm(DeviceComponentFilterForm): model = PowerPort - field_groups = [ - ['q', 'tag'], - ['name', 'label', 'type'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Attributes', ('name', 'label', 'type')), + ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')), + ) type = forms.MultipleChoiceField( choices=PowerPortTypeChoices, required=False, @@ -911,11 +914,11 @@ class PowerPortFilterForm(DeviceComponentFilterForm): class PowerOutletFilterForm(DeviceComponentFilterForm): model = PowerOutlet - field_groups = [ - ['q', 'tag'], - ['name', 'label', 'type'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Attributes', ('name', 'label', 'type')), + ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')), + ) type = forms.MultipleChoiceField( choices=PowerOutletTypeChoices, required=False, @@ -926,13 +929,13 @@ class PowerOutletFilterForm(DeviceComponentFilterForm): class InterfaceFilterForm(DeviceComponentFilterForm): model = Interface - field_groups = [ - ['q', 'tag'], - ['name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only'], - ['vrf_id', 'mac_address', 'wwn'], - ['rf_role', 'rf_channel', 'rf_channel_width', 'tx_power'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Attributes', ('name', 'label', 'kind', 'type', 'speed', 'duplex', 'enabled', 'mgmt_only')), + ('Addressing', ('vrf_id', 'mac_address', 'wwn')), + ('Wireless', ('rf_role', 'rf_channel', 'rf_channel_width', 'tx_power')), + ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')), + ) kind = forms.MultipleChoiceField( choices=InterfaceKindChoices, required=False, @@ -1009,11 +1012,11 @@ class InterfaceFilterForm(DeviceComponentFilterForm): class FrontPortFilterForm(DeviceComponentFilterForm): - field_groups = [ - ['q', 'tag'], - ['name', 'label', 'type', 'color'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Attributes', ('name', 'label', 'type', 'color')), + ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')), + ) model = FrontPort type = forms.MultipleChoiceField( choices=PortTypeChoices, @@ -1028,11 +1031,11 @@ class FrontPortFilterForm(DeviceComponentFilterForm): class RearPortFilterForm(DeviceComponentFilterForm): model = RearPort - field_groups = [ - ['q', 'tag'], - ['name', 'label', 'type', 'color'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Attributes', ('name', 'label', 'type', 'color')), + ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')), + ) type = forms.MultipleChoiceField( choices=PortTypeChoices, required=False, @@ -1046,11 +1049,11 @@ class RearPortFilterForm(DeviceComponentFilterForm): class ModuleBayFilterForm(DeviceComponentFilterForm): model = ModuleBay - field_groups = [ - ['q', 'tag'], - ['name', 'label', 'position'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Attributes', ('name', 'label', 'position')), + ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')), + ) tag = TagFilterField(model) position = forms.CharField( required=False @@ -1059,21 +1062,21 @@ class ModuleBayFilterForm(DeviceComponentFilterForm): class DeviceBayFilterForm(DeviceComponentFilterForm): model = DeviceBay - field_groups = [ - ['q', 'tag'], - ['name', 'label'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Attributes', ('name', 'label')), + ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')), + ) tag = TagFilterField(model) class InventoryItemFilterForm(DeviceComponentFilterForm): model = InventoryItem - field_groups = [ - ['q', 'tag'], - ['name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered'], - ['region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Attributes', ('name', 'label', 'manufacturer_id', 'serial', 'asset_tag', 'discovered')), + ('Device', ('region_id', 'site_group_id', 'site_id', 'location_id', 'virtual_chassis_id', 'device_id')), + ) role_id = DynamicModelMultipleChoiceField( queryset=InventoryItemRole.objects.all(), required=False, diff --git a/netbox/extras/forms/filtersets.py b/netbox/extras/forms/filtersets.py index 330bb91e3..760f873c3 100644 --- a/netbox/extras/forms/filtersets.py +++ b/netbox/extras/forms/filtersets.py @@ -28,11 +28,10 @@ __all__ = ( class CustomFieldFilterForm(FilterForm): - field_groups = [ - ['q'], - ['type', 'content_types'], - ['weight', 'required'], - ] + fieldsets = ( + (None, ('q',)), + ('Attributes', ('type', 'content_types', 'weight', 'required')), + ) content_types = ContentTypeMultipleChoiceField( queryset=ContentType.objects.all(), limit_choices_to=FeatureQuery('custom_fields'), @@ -56,10 +55,10 @@ class CustomFieldFilterForm(FilterForm): class CustomLinkFilterForm(FilterForm): - field_groups = [ - ['q'], - ['content_type', 'enabled', 'new_window', 'weight'], - ] + fieldsets = ( + (None, ('q',)), + ('Attributes', ('content_type', 'enabled', 'new_window', 'weight')), + ) content_type = ContentTypeChoiceField( queryset=ContentType.objects.all(), limit_choices_to=FeatureQuery('custom_fields'), @@ -83,10 +82,10 @@ class CustomLinkFilterForm(FilterForm): class ExportTemplateFilterForm(FilterForm): - field_groups = [ - ['q'], - ['content_type', 'mime_type', 'file_extension', 'as_attachment'], - ] + fieldsets = ( + (None, ('q',)), + ('Attributes', ('content_type', 'mime_type', 'file_extension', 'as_attachment')), + ) content_type = ContentTypeChoiceField( queryset=ContentType.objects.all(), limit_choices_to=FeatureQuery('custom_fields'), @@ -108,11 +107,11 @@ class ExportTemplateFilterForm(FilterForm): class WebhookFilterForm(FilterForm): - field_groups = [ - ['q'], - ['content_types', 'http_method', 'enabled'], - ['type_create', 'type_update', 'type_delete'], - ] + fieldsets = ( + (None, ('q',)), + ('Attributes', ('content_types', 'http_method', 'enabled')), + ('Events', ('type_create', 'type_update', 'type_delete')), + ) content_types = ContentTypeMultipleChoiceField( queryset=ContentType.objects.all(), limit_choices_to=FeatureQuery('custom_fields'), @@ -160,13 +159,13 @@ class TagFilterForm(FilterForm): class ConfigContextFilterForm(FilterForm): - field_groups = [ - ['q', 'tag'], - ['region_id', 'site_group_id', 'site_id'], - ['device_type_id', 'platform_id', 'role_id'], - ['cluster_type_id', 'cluster_group_id', 'cluster_id'], - ['tenant_group_id', 'tenant_id'] - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Location', ('region_id', 'site_group_id', 'site_id')), + ('Device', ('device_type_id', 'platform_id', 'role_id')), + ('Cluster', ('cluster_type_id', 'cluster_group_id', 'cluster_id')), + ('Tenant', ('tenant_group_id', 'tenant_id')) + ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, @@ -243,11 +242,11 @@ class LocalConfigContextFilterForm(forms.Form): class JournalEntryFilterForm(FilterForm): model = JournalEntry - field_groups = [ - ['q'], - ['created_before', 'created_after', 'created_by_id'], - ['assigned_object_type_id', 'kind'] - ] + fieldsets = ( + (None, ('q',)), + ('Creation', ('created_before', 'created_after', 'created_by_id')), + ('Attributes', ('assigned_object_type_id', 'kind')) + ) created_after = forms.DateTimeField( required=False, label=_('After'), @@ -283,11 +282,11 @@ class JournalEntryFilterForm(FilterForm): class ObjectChangeFilterForm(FilterForm): model = ObjectChange - field_groups = [ - ['q'], - ['time_before', 'time_after', 'action'], - ['user_id', 'changed_object_type_id'], - ] + fieldsets = ( + (None, ('q',)), + ('Time', ('time_before', 'time_after')), + ('Attributes', ('action', 'user_id', 'changed_object_type_id')), + ) time_after = forms.DateTimeField( required=False, label=_('After'), diff --git a/netbox/ipam/forms/filtersets.py b/netbox/ipam/forms/filtersets.py index 4301a1810..bf780a7d0 100644 --- a/netbox/ipam/forms/filtersets.py +++ b/netbox/ipam/forms/filtersets.py @@ -41,11 +41,11 @@ IPADDRESS_MASK_LENGTH_CHOICES = add_blank_choice([ class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = VRF - field_groups = [ - ['q', 'tag'], - ['import_target_id', 'export_target_id'], - ['tenant_group_id', 'tenant_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Route Targets', ('import_target_id', 'export_target_id')), + ('Tenant', ('tenant_group_id', 'tenant_id')), + ) import_target_id = DynamicModelMultipleChoiceField( queryset=RouteTarget.objects.all(), required=False, @@ -61,11 +61,11 @@ class VRFFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class RouteTargetFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = RouteTarget - field_groups = [ - ['q', 'tag'], - ['importing_vrf_id', 'exporting_vrf_id'], - ['tenant_group_id', 'tenant_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('VRF', ('importing_vrf_id', 'exporting_vrf_id')), + ('Tenant', ('tenant_group_id', 'tenant_id')), + ) importing_vrf_id = DynamicModelMultipleChoiceField( queryset=VRF.objects.all(), required=False, @@ -93,11 +93,11 @@ class RIRFilterForm(NetBoxModelFilterSetForm): class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Aggregate - field_groups = [ - ['q', 'tag'], - ['family', 'rir_id'], - ['tenant_group_id', 'tenant_id'] - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Attributes', ('family', 'rir_id')), + ('Tenant', ('tenant_group_id', 'tenant_id')), + ) family = forms.ChoiceField( required=False, choices=add_blank_choice(IPAddressFamilyChoices), @@ -114,12 +114,11 @@ class AggregateFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = ASN - field_groups = [ - ['q'], - ['rir_id'], - ['tenant_group_id', 'tenant_id'], - ['site_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Assignment', ('rir_id', 'site_id')), + ('Tenant', ('tenant_group_id', 'tenant_id')), + ) rir_id = DynamicModelMultipleChoiceField( queryset=RIR.objects.all(), required=False, @@ -130,6 +129,7 @@ class ASNFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): required=False, label=_('Site') ) + tag = TagFilterField(model) class RoleFilterForm(NetBoxModelFilterSetForm): @@ -139,13 +139,13 @@ class RoleFilterForm(NetBoxModelFilterSetForm): class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Prefix - field_groups = [ - ['q', 'tag'], - ['within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized'], - ['vrf_id', 'present_in_vrf_id'], - ['region_id', 'site_group_id', 'site_id'], - ['tenant_group_id', 'tenant_id'] - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Addressing', ('within_include', 'family', 'status', 'role_id', 'mask_length', 'is_pool', 'mark_utilized')), + ('VRF', ('vrf_id', 'present_in_vrf_id')), + ('Location', ('region_id', 'site_group_id', 'site_id')), + ('Tenant', ('tenant_group_id', 'tenant_id')), + ) mask_length__lte = forms.IntegerField( widget=forms.HiddenInput() ) @@ -230,11 +230,11 @@ class PrefixFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = IPRange - field_groups = [ - ['q', 'tag'], - ['family', 'vrf_id', 'status', 'role_id'], - ['tenant_group_id', 'tenant_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Attriubtes', ('family', 'vrf_id', 'status', 'role_id')), + ('Tenant', ('tenant_group_id', 'tenant_id')), + ) family = forms.ChoiceField( required=False, choices=add_blank_choice(IPAddressFamilyChoices), @@ -263,12 +263,12 @@ class IPRangeFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = IPAddress - field_groups = [ - ['q', 'tag'], - ['parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface'], - ['vrf_id', 'present_in_vrf_id'], - ['tenant_group_id', 'tenant_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Attributes', ('parent', 'family', 'status', 'role', 'mask_length', 'assigned_to_interface')), + ('VRF', ('vrf_id', 'present_in_vrf_id')), + ('Tenant', ('tenant_group_id', 'tenant_id')), + ) parent = forms.CharField( required=False, widget=forms.TextInput( @@ -323,10 +323,10 @@ class IPAddressFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class FHRPGroupFilterForm(NetBoxModelFilterSetForm): model = FHRPGroup - field_groups = ( - ('q', 'tag'), - ('protocol', 'group_id'), - ('auth_type', 'auth_key'), + fieldsets = ( + (None, ('q', 'tag')), + ('Attributes', ('protocol', 'group_id')), + ('Authentication', ('auth_type', 'auth_key')), ) protocol = forms.MultipleChoiceField( choices=FHRPGroupProtocolChoices, @@ -352,11 +352,11 @@ class FHRPGroupFilterForm(NetBoxModelFilterSetForm): class VLANGroupFilterForm(NetBoxModelFilterSetForm): - field_groups = [ - ['q', 'tag'], - ['region', 'sitegroup', 'site', 'location', 'rack'], - ['min_vid', 'max_vid'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Location', ('region', 'sitegroup', 'site', 'location', 'rack')), + ('VLAN ID', ('min_vid', 'max_vid')), + ) model = VLANGroup region = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), @@ -396,12 +396,12 @@ class VLANGroupFilterForm(NetBoxModelFilterSetForm): class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = VLAN - field_groups = [ - ['q', 'tag'], - ['region_id', 'site_group_id', 'site_id'], - ['group_id', 'status', 'role_id', 'vid'], - ['tenant_group_id', 'tenant_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Location', ('region_id', 'site_group_id', 'site_id')), + ('Attributes', ('group_id', 'status', 'role_id', 'vid')), + ('Tenant', ('tenant_group_id', 'tenant_id')), + ) region_id = DynamicModelMultipleChoiceField( queryset=Region.objects.all(), required=False, @@ -450,9 +450,9 @@ class VLANFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class ServiceTemplateFilterForm(NetBoxModelFilterSetForm): model = ServiceTemplate - field_groups = ( - ('q', 'tag'), - ('protocol', 'port'), + fieldsets = ( + (None, ('q', 'tag')), + ('Attributes', ('protocol', 'port')), ) protocol = forms.ChoiceField( choices=add_blank_choice(ServiceProtocolChoices), diff --git a/netbox/templates/inc/filter_list.html b/netbox/templates/inc/filter_list.html index e6a1e6a28..4276764d1 100644 --- a/netbox/templates/inc/filter_list.html +++ b/netbox/templates/inc/filter_list.html @@ -7,21 +7,22 @@ {% for field in filter_form.hidden_fields %} {{ field }} {% endfor %} - {% if filter_form.field_groups %} - {# List filters by group #} - {% for group in filter_form.field_groups %} -
- {% for name in group %} - {% with field=filter_form|get_item:name %} - {% render_field field %} - {% endwith %} - {% endfor %} -
- {% if not forloop.last %} -
+ {# List filters by group #} + {% for heading, fields in filter_form.fieldsets %} +
+ {% if heading %} +
{{ heading }}
{% endif %} - {% endfor %} - {% else %} + {% for name in fields %} + {% with field=filter_form|get_item:name %} + {% render_field field %} + {% endwith %} + {% endfor %} +
+ {% if not forloop.last %} +
+ {% endif %} + {% empty %} {# List all non-customfield filters as declared in the form class #} {% for field in filter_form.visible_fields %} {% if not filter_form.custom_fields or field.name not in filter_form.custom_fields %} @@ -30,7 +31,7 @@
{% endif %} {% endfor %} - {% endif %} + {% endfor %} {% if filter_form.custom_fields %} {# List all custom field filters #}
diff --git a/netbox/tenancy/forms/filtersets.py b/netbox/tenancy/forms/filtersets.py index 8fb4b50ff..73e30cc77 100644 --- a/netbox/tenancy/forms/filtersets.py +++ b/netbox/tenancy/forms/filtersets.py @@ -29,10 +29,6 @@ class TenantGroupFilterForm(NetBoxModelFilterSetForm): class TenantFilterForm(NetBoxModelFilterSetForm): model = Tenant - field_groups = ( - ('q', 'tag'), - ('group_id',), - ) group_id = DynamicModelMultipleChoiceField( queryset=TenantGroup.objects.all(), required=False, @@ -63,10 +59,6 @@ class ContactRoleFilterForm(NetBoxModelFilterSetForm): class ContactFilterForm(NetBoxModelFilterSetForm): model = Contact - field_groups = ( - ('q', 'tag'), - ('group_id',), - ) group_id = DynamicModelMultipleChoiceField( queryset=ContactGroup.objects.all(), required=False, diff --git a/netbox/virtualization/forms/filtersets.py b/netbox/virtualization/forms/filtersets.py index 292cd661d..8e3dcd143 100644 --- a/netbox/virtualization/forms/filtersets.py +++ b/netbox/virtualization/forms/filtersets.py @@ -32,12 +32,12 @@ class ClusterGroupFilterForm(NetBoxModelFilterSetForm): class ClusterFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): model = Cluster - field_groups = [ - ['q', 'tag'], - ['group_id', 'type_id'], - ['region_id', 'site_group_id', 'site_id'], - ['tenant_group_id', 'tenant_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Attributes', ('group_id', 'type_id')), + ('Location', ('region_id', 'site_group_id', 'site_id')), + ('Tenant', ('tenant_group_id', 'tenant_id')), + ) type_id = DynamicModelMultipleChoiceField( queryset=ClusterType.objects.all(), required=False, @@ -74,13 +74,13 @@ class ClusterFilterForm(TenancyFilterForm, NetBoxModelFilterSetForm): class VirtualMachineFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, NetBoxModelFilterSetForm): model = VirtualMachine - field_groups = [ - ['q', 'tag'], - ['cluster_group_id', 'cluster_type_id', 'cluster_id'], - ['region_id', 'site_group_id', 'site_id'], - ['status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data'], - ['tenant_group_id', 'tenant_id'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Cluster', ('cluster_group_id', 'cluster_type_id', 'cluster_id')), + ('Location', ('region_id', 'site_group_id', 'site_id')), + ('Attriubtes', ('status', 'role_id', 'platform_id', 'mac_address', 'has_primary_ip', 'local_context_data')), + ('Tenant', ('tenant_group_id', 'tenant_id')), + ) cluster_group_id = DynamicModelMultipleChoiceField( queryset=ClusterGroup.objects.all(), required=False, @@ -154,11 +154,11 @@ class VirtualMachineFilterForm(LocalConfigContextFilterForm, TenancyFilterForm, class VMInterfaceFilterForm(NetBoxModelFilterSetForm): model = VMInterface - field_groups = [ - ['q', 'tag'], - ['cluster_id', 'virtual_machine_id'], - ['enabled', 'mac_address'], - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Virtual Machine', ('cluster_id', 'virtual_machine_id')), + ('Attributes', ('enabled', 'mac_address')), + ) cluster_id = DynamicModelMultipleChoiceField( queryset=Cluster.objects.all(), required=False, diff --git a/netbox/wireless/forms/filtersets.py b/netbox/wireless/forms/filtersets.py index 3c46caf21..8dcb48673 100644 --- a/netbox/wireless/forms/filtersets.py +++ b/netbox/wireless/forms/filtersets.py @@ -26,10 +26,11 @@ class WirelessLANGroupFilterForm(NetBoxModelFilterSetForm): class WirelessLANFilterForm(NetBoxModelFilterSetForm): model = WirelessLAN - field_groups = [ - ('q', 'tag'), - ('group_id',), - ] + fieldsets = ( + (None, ('q', 'tag')), + ('Attributes', ('ssid', 'group_id',)), + ('Authentication', ('auth_type', 'auth_cipher', 'auth_psk')), + ) ssid = forms.CharField( required=False, label='SSID' From d1672f8818de1f712854dd22011c8a7b99e9a193 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 31 Jan 2022 16:15:40 -0500 Subject: [PATCH 7/8] Move nullable_fields out of Meta for bulk edit forms --- netbox/circuits/forms/bulk_edit.py | 24 ++-- netbox/dcim/forms/bulk_edit.py | 139 ++++++++--------------- netbox/extras/forms/bulk_edit.py | 15 +-- netbox/ipam/forms/bulk_edit.py | 69 ++++------- netbox/tenancy/forms/bulk_edit.py | 15 +-- netbox/utilities/forms/forms.py | 7 +- netbox/virtualization/forms/bulk_edit.py | 27 ++--- netbox/wireless/forms/bulk_edit.py | 13 ++- 8 files changed, 110 insertions(+), 199 deletions(-) diff --git a/netbox/circuits/forms/bulk_edit.py b/netbox/circuits/forms/bulk_edit.py index f17df1302..9c20a2fe7 100644 --- a/netbox/circuits/forms/bulk_edit.py +++ b/netbox/circuits/forms/bulk_edit.py @@ -47,10 +47,9 @@ class ProviderBulkEditForm(NetBoxModelBulkEditForm): label='Comments' ) - class Meta: - nullable_fields = ( - 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', - ) + nullable_fields = ( + 'asn', 'account', 'portal_url', 'noc_contact', 'admin_contact', 'comments', + ) class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm): @@ -75,10 +74,9 @@ class ProviderNetworkBulkEditForm(NetBoxModelBulkEditForm): label='Comments' ) - class Meta: - nullable_fields = ( - 'service_id', 'description', 'comments', - ) + nullable_fields = ( + 'service_id', 'description', 'comments', + ) class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm): @@ -91,8 +89,7 @@ class CircuitTypeBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('description',) + nullable_fields = ('description',) class CircuitBulkEditForm(NetBoxModelBulkEditForm): @@ -131,7 +128,6 @@ class CircuitBulkEditForm(NetBoxModelBulkEditForm): label='Comments' ) - class Meta: - nullable_fields = ( - 'tenant', 'commit_rate', 'description', 'comments', - ) + nullable_fields = ( + 'tenant', 'commit_rate', 'description', 'comments', + ) diff --git a/netbox/dcim/forms/bulk_edit.py b/netbox/dcim/forms/bulk_edit.py index 13e9d945b..a126d22f9 100644 --- a/netbox/dcim/forms/bulk_edit.py +++ b/netbox/dcim/forms/bulk_edit.py @@ -71,8 +71,7 @@ class RegionBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('parent', 'description') + nullable_fields = ('parent', 'description') class SiteGroupBulkEditForm(NetBoxModelBulkEditForm): @@ -89,8 +88,7 @@ class SiteGroupBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('parent', 'description') + nullable_fields = ('parent', 'description') class SiteBulkEditForm(NetBoxModelBulkEditForm): @@ -131,10 +129,9 @@ class SiteBulkEditForm(NetBoxModelBulkEditForm): widget=StaticSelect() ) - class Meta: - nullable_fields = ( - 'region', 'group', 'tenant', 'asns', 'description', 'time_zone', - ) + nullable_fields = ( + 'region', 'group', 'tenant', 'asns', 'description', 'time_zone', + ) class LocationBulkEditForm(NetBoxModelBulkEditForm): @@ -162,8 +159,7 @@ class LocationBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('parent', 'tenant', 'description') + nullable_fields = ('parent', 'tenant', 'description') class RackRoleBulkEditForm(NetBoxModelBulkEditForm): @@ -179,8 +175,7 @@ class RackRoleBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('color', 'description') + nullable_fields = ('color', 'description') class RackBulkEditForm(NetBoxModelBulkEditForm): @@ -277,10 +272,9 @@ class RackBulkEditForm(NetBoxModelBulkEditForm): label='Comments' ) - class Meta: - nullable_fields = ( - 'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments', - ) + nullable_fields = ( + 'location', 'tenant', 'role', 'serial', 'asset_tag', 'outer_width', 'outer_depth', 'outer_unit', 'comments', + ) class RackReservationBulkEditForm(NetBoxModelBulkEditForm): @@ -315,8 +309,7 @@ class ManufacturerBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('description',) + nullable_fields = ('description',) class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm): @@ -346,8 +339,7 @@ class DeviceTypeBulkEditForm(NetBoxModelBulkEditForm): widget=StaticSelect() ) - class Meta: - nullable_fields = ('part_number', 'airflow') + nullable_fields = ('part_number', 'airflow') class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm): @@ -363,8 +355,7 @@ class ModuleTypeBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('part_number',) + nullable_fields = ('part_number',) class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm): @@ -385,8 +376,7 @@ class DeviceRoleBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('color', 'description') + nullable_fields = ('color', 'description') class PlatformBulkEditForm(NetBoxModelBulkEditForm): @@ -408,8 +398,7 @@ class PlatformBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('manufacturer', 'napalm_driver', 'description') + nullable_fields = ('manufacturer', 'napalm_driver', 'description') class DeviceBulkEditForm(NetBoxModelBulkEditForm): @@ -467,10 +456,9 @@ class DeviceBulkEditForm(NetBoxModelBulkEditForm): label='Serial Number' ) - class Meta: - nullable_fields = ( - 'tenant', 'platform', 'serial', 'airflow', - ) + nullable_fields = ( + 'tenant', 'platform', 'serial', 'airflow', + ) class ModuleBulkEditForm(NetBoxModelBulkEditForm): @@ -495,8 +483,7 @@ class ModuleBulkEditForm(NetBoxModelBulkEditForm): label='Serial Number' ) - class Meta: - nullable_fields = ('serial',) + nullable_fields = ('serial',) class CableBulkEditForm(NetBoxModelBulkEditForm): @@ -538,10 +525,9 @@ class CableBulkEditForm(NetBoxModelBulkEditForm): widget=StaticSelect() ) - class Meta: - nullable_fields = ( - 'type', 'status', 'tenant', 'label', 'color', 'length', - ) + nullable_fields = ( + 'type', 'status', 'tenant', 'label', 'color', 'length', + ) def clean(self): super().clean() @@ -565,8 +551,7 @@ class VirtualChassisBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('domain',) + nullable_fields = ('domain',) class PowerPanelBulkEditForm(NetBoxModelBulkEditForm): @@ -604,8 +589,7 @@ class PowerPanelBulkEditForm(NetBoxModelBulkEditForm): } ) - class Meta: - nullable_fields = ('location',) + nullable_fields = ('location',) class PowerFeedBulkEditForm(NetBoxModelBulkEditForm): @@ -663,10 +647,7 @@ class PowerFeedBulkEditForm(NetBoxModelBulkEditForm): label='Comments' ) - class Meta: - nullable_fields = ( - 'location', 'comments', - ) + nullable_fields = ('location', 'comments') # @@ -688,8 +669,7 @@ class ConsolePortTemplateBulkEditForm(BulkEditForm): widget=StaticSelect() ) - class Meta: - nullable_fields = ('label', 'type', 'description') + nullable_fields = ('label', 'type', 'description') class ConsoleServerPortTemplateBulkEditForm(BulkEditForm): @@ -710,8 +690,7 @@ class ConsoleServerPortTemplateBulkEditForm(BulkEditForm): required=False ) - class Meta: - nullable_fields = ('label', 'type', 'description') + nullable_fields = ('label', 'type', 'description') class PowerPortTemplateBulkEditForm(BulkEditForm): @@ -742,8 +721,7 @@ class PowerPortTemplateBulkEditForm(BulkEditForm): required=False ) - class Meta: - nullable_fields = ('label', 'type', 'maximum_draw', 'allocated_draw', 'description') + nullable_fields = ('label', 'type', 'maximum_draw', 'allocated_draw', 'description') class PowerOutletTemplateBulkEditForm(BulkEditForm): @@ -779,8 +757,7 @@ class PowerOutletTemplateBulkEditForm(BulkEditForm): required=False ) - class Meta: - nullable_fields = ('label', 'type', 'power_port', 'feed_leg', 'description') + nullable_fields = ('label', 'type', 'power_port', 'feed_leg', 'description') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -817,8 +794,7 @@ class InterfaceTemplateBulkEditForm(BulkEditForm): required=False ) - class Meta: - nullable_fields = ('label', 'description') + nullable_fields = ('label', 'description') class FrontPortTemplateBulkEditForm(BulkEditForm): @@ -842,8 +818,7 @@ class FrontPortTemplateBulkEditForm(BulkEditForm): required=False ) - class Meta: - nullable_fields = ('description',) + nullable_fields = ('description',) class RearPortTemplateBulkEditForm(BulkEditForm): @@ -867,8 +842,7 @@ class RearPortTemplateBulkEditForm(BulkEditForm): required=False ) - class Meta: - nullable_fields = ('description',) + nullable_fields = ('description',) class ModuleBayTemplateBulkEditForm(BulkEditForm): @@ -884,8 +858,7 @@ class ModuleBayTemplateBulkEditForm(BulkEditForm): required=False ) - class Meta: - nullable_fields = ('label', 'position', 'description') + nullable_fields = ('label', 'position', 'description') class DeviceBayTemplateBulkEditForm(BulkEditForm): @@ -901,8 +874,7 @@ class DeviceBayTemplateBulkEditForm(BulkEditForm): required=False ) - class Meta: - nullable_fields = ('label', 'description') + nullable_fields = ('label', 'description') class InventoryItemTemplateBulkEditForm(BulkEditForm): @@ -926,8 +898,7 @@ class InventoryItemTemplateBulkEditForm(BulkEditForm): required=False ) - class Meta: - nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description') + nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description') # @@ -947,8 +918,7 @@ class ConsolePortBulkEditForm( widget=BulkEditNullBooleanSelect ) - class Meta: - nullable_fields = ('label', 'description') + nullable_fields = ('label', 'description') class ConsoleServerPortBulkEditForm( @@ -964,8 +934,7 @@ class ConsoleServerPortBulkEditForm( widget=BulkEditNullBooleanSelect ) - class Meta: - nullable_fields = ('label', 'description') + nullable_fields = ('label', 'description') class PowerPortBulkEditForm( @@ -981,8 +950,7 @@ class PowerPortBulkEditForm( widget=BulkEditNullBooleanSelect ) - class Meta: - nullable_fields = ('label', 'description') + nullable_fields = ('label', 'description') class PowerOutletBulkEditForm( @@ -1004,8 +972,7 @@ class PowerOutletBulkEditForm( widget=BulkEditNullBooleanSelect ) - class Meta: - nullable_fields = ('label', 'type', 'feed_leg', 'power_port', 'description') + nullable_fields = ('label', 'type', 'feed_leg', 'power_port', 'description') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -1084,12 +1051,10 @@ class InterfaceBulkEditForm( label='VRF' ) - class Meta: - nullable_fields = ( - 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'description', 'mode', - 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', - 'vrf', - ) + nullable_fields = ( + 'label', 'parent', 'bridge', 'lag', 'speed', 'duplex', 'mac_address', 'wwn', 'mtu', 'description', 'mode', + 'rf_channel', 'rf_channel_frequency', 'rf_channel_width', 'tx_power', 'untagged_vlan', 'tagged_vlans', 'vrf', + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -1154,8 +1119,7 @@ class FrontPortBulkEditForm( widget=forms.MultipleHiddenInput() ) - class Meta: - nullable_fields = ('label', 'description') + nullable_fields = ('label', 'description') class RearPortBulkEditForm( @@ -1167,8 +1131,7 @@ class RearPortBulkEditForm( widget=forms.MultipleHiddenInput() ) - class Meta: - nullable_fields = ('label', 'description') + nullable_fields = ('label', 'description') class ModuleBayBulkEditForm( @@ -1180,8 +1143,7 @@ class ModuleBayBulkEditForm( widget=forms.MultipleHiddenInput() ) - class Meta: - nullable_fields = ('label', 'position', 'description') + nullable_fields = ('label', 'position', 'description') class DeviceBayBulkEditForm( @@ -1193,8 +1155,7 @@ class DeviceBayBulkEditForm( widget=forms.MultipleHiddenInput() ) - class Meta: - nullable_fields = ('label', 'description') + nullable_fields = ('label', 'description') class InventoryItemBulkEditForm( @@ -1214,8 +1175,7 @@ class InventoryItemBulkEditForm( required=False ) - class Meta: - nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description') + nullable_fields = ('label', 'role', 'manufacturer', 'part_id', 'description') # @@ -1235,5 +1195,4 @@ class InventoryItemRoleBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('color', 'description') + nullable_fields = ('color', 'description') diff --git a/netbox/extras/forms/bulk_edit.py b/netbox/extras/forms/bulk_edit.py index 362592ace..b09bfc612 100644 --- a/netbox/extras/forms/bulk_edit.py +++ b/netbox/extras/forms/bulk_edit.py @@ -33,8 +33,7 @@ class CustomFieldBulkEditForm(BulkEditForm): required=False ) - class Meta: - nullable_fields = ('description',) + nullable_fields = ('description',) class CustomLinkBulkEditForm(BulkEditForm): @@ -92,8 +91,7 @@ class ExportTemplateBulkEditForm(BulkEditForm): widget=BulkEditNullBooleanSelect() ) - class Meta: - nullable_fields = ('description', 'mime_type', 'file_extension') + nullable_fields = ('description', 'mime_type', 'file_extension') class WebhookBulkEditForm(BulkEditForm): @@ -135,8 +133,7 @@ class WebhookBulkEditForm(BulkEditForm): required=False ) - class Meta: - nullable_fields = ('secret', 'conditions', 'ca_file_path') + nullable_fields = ('secret', 'conditions', 'ca_file_path') class TagBulkEditForm(BulkEditForm): @@ -152,8 +149,7 @@ class TagBulkEditForm(BulkEditForm): required=False ) - class Meta: - nullable_fields = ('description',) + nullable_fields = ('description',) class ConfigContextBulkEditForm(BulkEditForm): @@ -174,8 +170,7 @@ class ConfigContextBulkEditForm(BulkEditForm): max_length=100 ) - class Meta: - nullable_fields = ('description',) + nullable_fields = ('description',) class JournalEntryBulkEditForm(BulkEditForm): diff --git a/netbox/ipam/forms/bulk_edit.py b/netbox/ipam/forms/bulk_edit.py index 637051318..9a56501d2 100644 --- a/netbox/ipam/forms/bulk_edit.py +++ b/netbox/ipam/forms/bulk_edit.py @@ -49,10 +49,7 @@ class VRFBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ( - 'tenant', 'description', - ) + nullable_fields = ('tenant', 'description') class RouteTargetBulkEditForm(NetBoxModelBulkEditForm): @@ -69,10 +66,7 @@ class RouteTargetBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ( - 'tenant', 'description', - ) + nullable_fields = ('tenant', 'description') class RIRBulkEditForm(NetBoxModelBulkEditForm): @@ -89,8 +83,7 @@ class RIRBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('is_private', 'description') + nullable_fields = ('is_private', 'description') class ASNBulkEditForm(NetBoxModelBulkEditForm): @@ -116,13 +109,7 @@ class ASNBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ( - 'date_added', 'description', - ) - widgets = { - 'date_added': DatePicker(), - } + nullable_fields = ('date_added', 'description') class AggregateBulkEditForm(NetBoxModelBulkEditForm): @@ -147,13 +134,7 @@ class AggregateBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ( - 'date_added', 'description', - ) - widgets = { - 'date_added': DatePicker(), - } + nullable_fields = ('date_added', 'description') class RoleBulkEditForm(NetBoxModelBulkEditForm): @@ -169,8 +150,7 @@ class RoleBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('description',) + nullable_fields = ('description',) class PrefixBulkEditForm(NetBoxModelBulkEditForm): @@ -232,10 +212,9 @@ class PrefixBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ( - 'site', 'vrf', 'tenant', 'role', 'description', - ) + nullable_fields = ( + 'site', 'vrf', 'tenant', 'role', 'description', + ) class IPRangeBulkEditForm(NetBoxModelBulkEditForm): @@ -266,10 +245,9 @@ class IPRangeBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ( - 'vrf', 'tenant', 'role', 'description', - ) + nullable_fields = ( + 'vrf', 'tenant', 'role', 'description', + ) class IPAddressBulkEditForm(NetBoxModelBulkEditForm): @@ -311,10 +289,9 @@ class IPAddressBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ( - 'vrf', 'role', 'tenant', 'dns_name', 'description', - ) + nullable_fields = ( + 'vrf', 'role', 'tenant', 'dns_name', 'description', + ) class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm): @@ -348,8 +325,7 @@ class FHRPGroupBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('auth_type', 'auth_key', 'description') + nullable_fields = ('auth_type', 'auth_key', 'description') class VLANGroupBulkEditForm(NetBoxModelBulkEditForm): @@ -378,8 +354,7 @@ class VLANGroupBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('site', 'description') + nullable_fields = ('site', 'description') class VLANBulkEditForm(NetBoxModelBulkEditForm): @@ -428,10 +403,9 @@ class VLANBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ( - 'site', 'group', 'tenant', 'role', 'description', - ) + nullable_fields = ( + 'site', 'group', 'tenant', 'role', 'description', + ) class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm): @@ -456,8 +430,7 @@ class ServiceTemplateBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('description',) + nullable_fields = ('description',) class ServiceBulkEditForm(ServiceTemplateBulkEditForm): diff --git a/netbox/tenancy/forms/bulk_edit.py b/netbox/tenancy/forms/bulk_edit.py index f3cc2c33d..a3695c10e 100644 --- a/netbox/tenancy/forms/bulk_edit.py +++ b/netbox/tenancy/forms/bulk_edit.py @@ -31,8 +31,7 @@ class TenantGroupBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('parent', 'description') + nullable_fields = ('parent', 'description') class TenantBulkEditForm(NetBoxModelBulkEditForm): @@ -45,8 +44,7 @@ class TenantBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('group',) + nullable_fields = ('group',) # @@ -67,8 +65,7 @@ class ContactGroupBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('parent', 'description') + nullable_fields = ('parent', 'description') class ContactRoleBulkEditForm(NetBoxModelBulkEditForm): @@ -81,8 +78,7 @@ class ContactRoleBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('description',) + nullable_fields = ('description',) class ContactBulkEditForm(NetBoxModelBulkEditForm): @@ -110,5 +106,4 @@ class ContactBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('group', 'title', 'phone', 'email', 'address', 'comments') + nullable_fields = ('group', 'title', 'phone', 'email', 'address', 'comments') diff --git a/netbox/utilities/forms/forms.py b/netbox/utilities/forms/forms.py index d9a6c2b29..67a2bcb74 100644 --- a/netbox/utilities/forms/forms.py +++ b/netbox/utilities/forms/forms.py @@ -69,14 +69,11 @@ class BulkEditMixin: """ Base form for editing multiple objects in bulk """ + nullable_fields = () + def __init__(self, model, *args, **kwargs): super().__init__(*args, **kwargs) self.model = model - self.nullable_fields = () - - # Copy any nullable fields defined in Meta - if hasattr(self, 'Meta') and hasattr(self.Meta, 'nullable_fields'): - self.nullable_fields = self.Meta.nullable_fields # diff --git a/netbox/virtualization/forms/bulk_edit.py b/netbox/virtualization/forms/bulk_edit.py index dd846029a..e4f6ab25c 100644 --- a/netbox/virtualization/forms/bulk_edit.py +++ b/netbox/virtualization/forms/bulk_edit.py @@ -33,8 +33,7 @@ class ClusterTypeBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('description',) + nullable_fields = ('description',) class ClusterGroupBulkEditForm(NetBoxModelBulkEditForm): @@ -47,8 +46,7 @@ class ClusterGroupBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('description',) + nullable_fields = ('description',) class ClusterBulkEditForm(NetBoxModelBulkEditForm): @@ -89,10 +87,9 @@ class ClusterBulkEditForm(NetBoxModelBulkEditForm): label='Comments' ) - class Meta: - nullable_fields = ( - 'group', 'site', 'comments', 'tenant', - ) + nullable_fields = ( + 'group', 'site', 'comments', 'tenant', + ) class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm): @@ -144,10 +141,9 @@ class VirtualMachineBulkEditForm(NetBoxModelBulkEditForm): label='Comments' ) - class Meta: - nullable_fields = ( - 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments', - ) + nullable_fields = ( + 'role', 'tenant', 'platform', 'vcpus', 'memory', 'disk', 'comments', + ) class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm): @@ -197,10 +193,9 @@ class VMInterfaceBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ( - 'parent', 'bridge', 'mtu', 'description', - ) + nullable_fields = ( + 'parent', 'bridge', 'mtu', 'description', + ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/netbox/wireless/forms/bulk_edit.py b/netbox/wireless/forms/bulk_edit.py index 2d2a3ff14..0a3f0364e 100644 --- a/netbox/wireless/forms/bulk_edit.py +++ b/netbox/wireless/forms/bulk_edit.py @@ -29,8 +29,7 @@ class WirelessLANGroupBulkEditForm(NetBoxModelBulkEditForm): required=False ) - class Meta: - nullable_fields = ('parent', 'description') + nullable_fields = ('parent', 'description') class WirelessLANBulkEditForm(NetBoxModelBulkEditForm): @@ -68,8 +67,9 @@ class WirelessLANBulkEditForm(NetBoxModelBulkEditForm): label='Pre-shared key' ) - class Meta: - nullable_fields = ('ssid', 'group', 'vlan', 'description', 'auth_type', 'auth_cipher', 'auth_psk') + nullable_fields = ( + 'ssid', 'group', 'vlan', 'description', 'auth_type', 'auth_cipher', 'auth_psk', + ) class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm): @@ -102,5 +102,6 @@ class WirelessLinkBulkEditForm(NetBoxModelBulkEditForm): label='Pre-shared key' ) - class Meta: - nullable_fields = ('ssid', 'description', 'auth_type', 'auth_cipher', 'auth_psk') + nullable_fields = ( + 'ssid', 'description', 'auth_type', 'auth_cipher', 'auth_psk', + ) From bfb1a82754cc66b223d4d0905451680eb7a01849 Mon Sep 17 00:00:00 2001 From: jeremystretch Date: Mon, 31 Jan 2022 16:23:23 -0500 Subject: [PATCH 8/8] Update docstrings for base form classes --- netbox/netbox/forms/base.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/netbox/netbox/forms/base.py b/netbox/netbox/forms/base.py index 3fa85f1f7..2de15c6b8 100644 --- a/netbox/netbox/forms/base.py +++ b/netbox/netbox/forms/base.py @@ -18,7 +18,7 @@ __all__ = ( class NetBoxModelForm(BootstrapMixin, CustomFieldsMixin, forms.ModelForm): """ - Base form for creating & editing NetBox models. Adds support for custom fields. + Base form for creating & editing NetBox models. Extends Django's ModelForm to add support for custom fields. Attributes: fieldsets: An iterable of two-tuples which define a heading and field set to display per section of @@ -65,6 +65,9 @@ class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, BulkEditMixin, """ Base form for modifying multiple NetBox objects (of the same type) in bulk via the UI. Adds support for custom fields and adding/removing tags. + + Attributes: + nullable_fields: A list of field names indicating which fields support being set to null/empty """ add_tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), @@ -100,9 +103,13 @@ class NetBoxModelBulkEditForm(BootstrapMixin, CustomFieldsMixin, BulkEditMixin, class NetBoxModelFilterSetForm(BootstrapMixin, CustomFieldsMixin, forms.Form): """ - Base form for FilerSet forms. These are used to filter object lists in the NetBox UI. + Base form for FilerSet forms. These are used to filter object lists in the NetBox UI. Note that the + corresponding FilterSet *must* provide a `q` filter. - The corresponding FilterSet *must* provide a `q` filter. + Attributes: + model: The model class associated with the form + fieldsets: An iterable of two-tuples which define a heading and field set to display per section of + the rendered form (optional). If not defined, the all fields will be rendered as a single section. """ q = forms.CharField( required=False,