From d6ee4d58ba61cbd9616b8684fd88c9a044585c8f Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Mon, 1 Mar 2021 13:07:25 -0500 Subject: [PATCH] Add custom field support for device component models --- netbox/dcim/api/serializers.py | 28 ++++++------ netbox/dcim/filters.py | 3 +- netbox/dcim/forms.py | 40 +++++++++-------- .../migrations/0123_standardize_models.py | 45 +++++++++++++++++++ netbox/dcim/models/device_components.py | 4 +- netbox/netbox/models.py | 1 + netbox/templates/dcim/consoleport.html | 1 + netbox/templates/dcim/consoleserverport.html | 1 + netbox/templates/dcim/devicebay.html | 1 + netbox/templates/dcim/frontport.html | 1 + netbox/templates/dcim/interface.html | 1 + netbox/templates/dcim/inventoryitem.html | 1 + netbox/templates/dcim/poweroutlet.html | 1 + netbox/templates/dcim/powerport.html | 1 + netbox/templates/dcim/rearport.html | 1 + 15 files changed, 93 insertions(+), 37 deletions(-) diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index b4ac77e2d..d94d3aa49 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -49,7 +49,7 @@ class CableTerminationSerializer(serializers.ModelSerializer): return None -class ConnectedEndpointSerializer(ValidatedModelSerializer): +class ConnectedEndpointSerializer(CustomFieldModelSerializer): connected_endpoint_type = serializers.SerializerMethodField(read_only=True) connected_endpoint = serializers.SerializerMethodField(read_only=True) connected_endpoint_reachable = serializers.SerializerMethodField(read_only=True) @@ -497,7 +497,7 @@ class ConsoleServerPortSerializer(TaggedObjectSerializer, CableTerminationSerial model = ConsoleServerPort fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'cable_peer_type', - 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', + 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', ] @@ -515,7 +515,7 @@ class ConsolePortSerializer(TaggedObjectSerializer, CableTerminationSerializer, model = ConsolePort fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'description', 'cable', 'cable_peer', 'cable_peer_type', - 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', + 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', ] @@ -544,7 +544,7 @@ class PowerOutletSerializer(TaggedObjectSerializer, CableTerminationSerializer, fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'power_port', 'feed_leg', 'description', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', - 'connected_endpoint_reachable', 'tags', + 'connected_endpoint_reachable', 'tags', 'custom_fields', ] @@ -563,7 +563,7 @@ class PowerPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'maximum_draw', 'allocated_draw', 'description', 'cable', 'cable_peer', 'cable_peer_type', 'connected_endpoint', 'connected_endpoint_type', - 'connected_endpoint_reachable', 'tags', + 'connected_endpoint_reachable', 'tags', 'custom_fields', ] @@ -588,7 +588,7 @@ class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'enabled', 'lag', 'mtu', 'mac_address', 'mgmt_only', 'description', 'mode', 'untagged_vlan', 'tagged_vlans', 'cable', 'cable_peer', 'cable_peer_type', - 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', + 'connected_endpoint', 'connected_endpoint_type', 'connected_endpoint_reachable', 'tags', 'custom_fields', 'count_ipaddresses', ] @@ -606,7 +606,7 @@ class InterfaceSerializer(TaggedObjectSerializer, CableTerminationSerializer, Co return super().validate(data) -class RearPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ValidatedModelSerializer): +class RearPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, CustomFieldModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rearport-detail') device = NestedDeviceSerializer() type = ChoiceField(choices=PortTypeChoices) @@ -616,7 +616,7 @@ class RearPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Val model = RearPort fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'positions', 'description', 'cable', 'cable_peer', - 'cable_peer_type', 'tags', + 'cable_peer_type', 'tags', 'custom_fields', ] @@ -631,7 +631,7 @@ class FrontPortRearPortSerializer(WritableNestedSerializer): fields = ['id', 'url', 'name', 'label'] -class FrontPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, ValidatedModelSerializer): +class FrontPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, CustomFieldModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:frontport-detail') device = NestedDeviceSerializer() type = ChoiceField(choices=PortTypeChoices) @@ -642,25 +642,25 @@ class FrontPortSerializer(TaggedObjectSerializer, CableTerminationSerializer, Va model = FrontPort fields = [ 'id', 'url', 'device', 'name', 'label', 'type', 'rear_port', 'rear_port_position', 'description', 'cable', - 'cable_peer', 'cable_peer_type', 'tags', + 'cable_peer', 'cable_peer_type', 'tags', 'custom_fields', ] -class DeviceBaySerializer(TaggedObjectSerializer, ValidatedModelSerializer): +class DeviceBaySerializer(TaggedObjectSerializer, CustomFieldModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:devicebay-detail') device = NestedDeviceSerializer() installed_device = NestedDeviceSerializer(required=False, allow_null=True) class Meta: model = DeviceBay - fields = ['id', 'url', 'device', 'name', 'label', 'description', 'installed_device', 'tags'] + fields = ['id', 'url', 'device', 'name', 'label', 'description', 'installed_device', 'tags', 'custom_fields'] # # Inventory items # -class InventoryItemSerializer(TaggedObjectSerializer, ValidatedModelSerializer): +class InventoryItemSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:inventoryitem-detail') device = NestedDeviceSerializer() # Provide a default value to satisfy UniqueTogetherValidator @@ -672,7 +672,7 @@ class InventoryItemSerializer(TaggedObjectSerializer, ValidatedModelSerializer): model = InventoryItem fields = [ 'id', 'url', 'device', 'parent', 'name', 'label', 'manufacturer', 'part_id', 'serial', 'asset_tag', - 'discovered', 'description', 'tags', '_depth', + 'discovered', 'description', 'tags', 'custom_fields', '_depth', ] diff --git a/netbox/dcim/filters.py b/netbox/dcim/filters.py index 41363c261..e89f156ac 100644 --- a/netbox/dcim/filters.py +++ b/netbox/dcim/filters.py @@ -1,6 +1,5 @@ import django_filters from django.contrib.auth.models import User -from django.db.models import Count from extras.filters import CustomFieldModelFilterSet, LocalConfigContextFilterSet, CreatedUpdatedFilterSet from tenancy.filters import TenancyFilterSet @@ -704,7 +703,7 @@ class DeviceFilterSet( return queryset.exclude(devicebays__isnull=value) -class DeviceComponentFilterSet(django_filters.FilterSet): +class DeviceComponentFilterSet(CustomFieldModelFilterSet): q = django_filters.CharFilter( method='search', label='Search', diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 01704ff5a..f55697f22 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -58,7 +58,7 @@ def get_device_by_name_or_pk(name): return device -class DeviceComponentFilterForm(BootstrapMixin, forms.Form): +class DeviceComponentFilterForm(BootstrapMixin, CustomFieldFilterForm): field_order = [ 'q', 'region', 'site' ] @@ -2274,6 +2274,7 @@ class ComponentCreateForm(ComponentForm): """ Base form for the creation of device components (models subclassed from ComponentModel). """ + # TODO: Enable custom field support device = DynamicModelChoiceField( queryset=Device.objects.all(), display_field='display_name' @@ -2289,6 +2290,7 @@ class ComponentCreateForm(ComponentForm): class DeviceBulkAddComponentForm(ComponentForm): + # TODO: Enable custom field support pk = forms.ModelMultipleChoiceField( queryset=Device.objects.all(), widget=forms.MultipleHiddenInput() @@ -2318,7 +2320,7 @@ class ConsolePortFilterForm(DeviceComponentFilterForm): tag = TagFilterField(model) -class ConsolePortForm(BootstrapMixin, forms.ModelForm): +class ConsolePortForm(BootstrapMixin, CustomFieldModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False @@ -2365,7 +2367,7 @@ class ConsolePortBulkEditForm( nullable_fields = ('label', 'description') -class ConsolePortCSVForm(CSVModelForm): +class ConsolePortCSVForm(CustomFieldModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name' @@ -2396,7 +2398,7 @@ class ConsoleServerPortFilterForm(DeviceComponentFilterForm): tag = TagFilterField(model) -class ConsoleServerPortForm(BootstrapMixin, forms.ModelForm): +class ConsoleServerPortForm(BootstrapMixin, CustomFieldModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False @@ -2443,7 +2445,7 @@ class ConsoleServerPortBulkEditForm( nullable_fields = ('label', 'description') -class ConsoleServerPortCSVForm(CSVModelForm): +class ConsoleServerPortCSVForm(CustomFieldModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name' @@ -2474,7 +2476,7 @@ class PowerPortFilterForm(DeviceComponentFilterForm): tag = TagFilterField(model) -class PowerPortForm(BootstrapMixin, forms.ModelForm): +class PowerPortForm(BootstrapMixin, CustomFieldModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False @@ -2533,7 +2535,7 @@ class PowerPortBulkEditForm( nullable_fields = ('label', 'description') -class PowerPortCSVForm(CSVModelForm): +class PowerPortCSVForm(CustomFieldModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name' @@ -2564,7 +2566,7 @@ class PowerOutletFilterForm(DeviceComponentFilterForm): tag = TagFilterField(model) -class PowerOutletForm(BootstrapMixin, forms.ModelForm): +class PowerOutletForm(BootstrapMixin, CustomFieldModelForm): power_port = forms.ModelChoiceField( queryset=PowerPort.objects.all(), required=False @@ -2658,7 +2660,7 @@ class PowerOutletBulkEditForm( self.fields['power_port'].widget.attrs['disabled'] = True -class PowerOutletCSVForm(CSVModelForm): +class PowerOutletCSVForm(CustomFieldModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name' @@ -2738,7 +2740,7 @@ class InterfaceFilterForm(DeviceComponentFilterForm): tag = TagFilterField(model) -class InterfaceForm(BootstrapMixin, InterfaceCommonForm, forms.ModelForm): +class InterfaceForm(BootstrapMixin, InterfaceCommonForm, CustomFieldModelForm): untagged_vlan = DynamicModelChoiceField( queryset=VLAN.objects.all(), required=False, @@ -2988,7 +2990,7 @@ class InterfaceBulkEditForm( self.cleaned_data['tagged_vlans'] = [] -class InterfaceCSVForm(CSVModelForm): +class InterfaceCSVForm(CustomFieldModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name' @@ -3058,7 +3060,7 @@ class FrontPortFilterForm(DeviceComponentFilterForm): tag = TagFilterField(model) -class FrontPortForm(BootstrapMixin, forms.ModelForm): +class FrontPortForm(BootstrapMixin, CustomFieldModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False @@ -3168,7 +3170,7 @@ class FrontPortBulkEditForm( nullable_fields = ('label', 'description') -class FrontPortCSVForm(CSVModelForm): +class FrontPortCSVForm(CustomFieldModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name' @@ -3227,7 +3229,7 @@ class RearPortFilterForm(DeviceComponentFilterForm): tag = TagFilterField(model) -class RearPortForm(BootstrapMixin, forms.ModelForm): +class RearPortForm(BootstrapMixin, CustomFieldModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False @@ -3280,7 +3282,7 @@ class RearPortBulkEditForm( nullable_fields = ('label', 'description') -class RearPortCSVForm(CSVModelForm): +class RearPortCSVForm(CustomFieldModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name' @@ -3307,7 +3309,7 @@ class DeviceBayFilterForm(DeviceComponentFilterForm): tag = TagFilterField(model) -class DeviceBayForm(BootstrapMixin, forms.ModelForm): +class DeviceBayForm(BootstrapMixin, CustomFieldModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False @@ -3367,7 +3369,7 @@ class DeviceBayBulkEditForm( nullable_fields = ('label', 'description') -class DeviceBayCSVForm(CSVModelForm): +class DeviceBayCSVForm(CustomFieldModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name' @@ -3417,7 +3419,7 @@ class DeviceBayCSVForm(CSVModelForm): # Inventory items # -class InventoryItemForm(BootstrapMixin, forms.ModelForm): +class InventoryItemForm(BootstrapMixin, CustomFieldModelForm): device = DynamicModelChoiceField( queryset=Device.objects.all(), display_field='display_name' @@ -3477,7 +3479,7 @@ class InventoryItemCreateForm(ComponentCreateForm): ) -class InventoryItemCSVForm(CSVModelForm): +class InventoryItemCSVForm(CustomFieldModelCSVForm): device = CSVModelChoiceField( queryset=Device.objects.all(), to_field_name='name' diff --git a/netbox/dcim/migrations/0123_standardize_models.py b/netbox/dcim/migrations/0123_standardize_models.py index 4a6fc87b6..ca8534559 100644 --- a/netbox/dcim/migrations/0123_standardize_models.py +++ b/netbox/dcim/migrations/0123_standardize_models.py @@ -9,11 +9,41 @@ class Migration(migrations.Migration): ] operations = [ + migrations.AddField( + model_name='consoleport', + name='custom_field_data', + field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AddField( + model_name='consoleserverport', + name='custom_field_data', + field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AddField( + model_name='devicebay', + name='custom_field_data', + field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), migrations.AddField( model_name='devicerole', name='custom_field_data', field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), ), + migrations.AddField( + model_name='frontport', + name='custom_field_data', + field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AddField( + model_name='interface', + name='custom_field_data', + field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AddField( + model_name='inventoryitem', + name='custom_field_data', + field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), migrations.AddField( model_name='manufacturer', name='custom_field_data', @@ -24,6 +54,16 @@ class Migration(migrations.Migration): name='custom_field_data', field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), ), + migrations.AddField( + model_name='poweroutlet', + name='custom_field_data', + field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AddField( + model_name='powerport', + name='custom_field_data', + field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), migrations.AddField( model_name='rackgroup', name='custom_field_data', @@ -34,6 +74,11 @@ class Migration(migrations.Migration): name='custom_field_data', field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), ), + migrations.AddField( + model_name='rearport', + name='custom_field_data', + field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), migrations.AddField( model_name='region', name='custom_field_data', diff --git a/netbox/dcim/models/device_components.py b/netbox/dcim/models/device_components.py index 01d2712a7..10422a59b 100644 --- a/netbox/dcim/models/device_components.py +++ b/netbox/dcim/models/device_components.py @@ -13,7 +13,7 @@ from dcim.constants import * from dcim.fields import MACAddressField from extras.models import ObjectChange, TaggedItem from extras.utils import extras_features -from netbox.models import BigIDModel +from netbox.models import BigIDModel, CustomFieldsMixin from utilities.fields import NaturalOrderingField from utilities.mptt import TreeManager from utilities.ordering import naturalize_interface @@ -38,7 +38,7 @@ __all__ = ( ) -class ComponentModel(BigIDModel): +class ComponentModel(CustomFieldsMixin, BigIDModel): """ An abstract model inherited by any model which has a parent Device. """ diff --git a/netbox/netbox/models.py b/netbox/netbox/models.py index 74d018420..3b97d4ef0 100644 --- a/netbox/netbox/models.py +++ b/netbox/netbox/models.py @@ -10,6 +10,7 @@ from utilities.utils import serialize_object __all__ = ( 'BigIDModel', + 'CustomFieldsMixin', 'NestedGroupModel', 'OrganizationalModel', 'PrimaryModel', diff --git a/netbox/templates/dcim/consoleport.html b/netbox/templates/dcim/consoleport.html index 5d113c86a..f1e020fd6 100644 --- a/netbox/templates/dcim/consoleport.html +++ b/netbox/templates/dcim/consoleport.html @@ -34,6 +34,7 @@ + {% include 'inc/custom_fields_panel.html' %} {% include 'extras/inc/tags_panel.html' with tags=object.tags.all %} {% plugin_left_page object %} diff --git a/netbox/templates/dcim/consoleserverport.html b/netbox/templates/dcim/consoleserverport.html index b64b4aff2..b609eedb2 100644 --- a/netbox/templates/dcim/consoleserverport.html +++ b/netbox/templates/dcim/consoleserverport.html @@ -34,6 +34,7 @@ + {% include 'inc/custom_fields_panel.html' %} {% include 'extras/inc/tags_panel.html' with tags=object.tags.all %} {% plugin_left_page object %} diff --git a/netbox/templates/dcim/devicebay.html b/netbox/templates/dcim/devicebay.html index 365625469..13b019812 100644 --- a/netbox/templates/dcim/devicebay.html +++ b/netbox/templates/dcim/devicebay.html @@ -30,6 +30,7 @@ + {% include 'inc/custom_fields_panel.html' %} {% include 'extras/inc/tags_panel.html' with tags=object.tags.all %} {% plugin_left_page object %} diff --git a/netbox/templates/dcim/frontport.html b/netbox/templates/dcim/frontport.html index 9dc0bc9f5..ba2ba9d19 100644 --- a/netbox/templates/dcim/frontport.html +++ b/netbox/templates/dcim/frontport.html @@ -44,6 +44,7 @@ + {% include 'inc/custom_fields_panel.html' %} {% include 'extras/inc/tags_panel.html' with tags=object.tags.all %} {% plugin_left_page object %} diff --git a/netbox/templates/dcim/interface.html b/netbox/templates/dcim/interface.html index 3ae0733a1..8f22d2f67 100644 --- a/netbox/templates/dcim/interface.html +++ b/netbox/templates/dcim/interface.html @@ -66,6 +66,7 @@ + {% include 'inc/custom_fields_panel.html' %} {% include 'extras/inc/tags_panel.html' with tags=object.tags.all %} {% plugin_left_page object %} diff --git a/netbox/templates/dcim/inventoryitem.html b/netbox/templates/dcim/inventoryitem.html index 4e0963f9d..6eec7f434 100644 --- a/netbox/templates/dcim/inventoryitem.html +++ b/netbox/templates/dcim/inventoryitem.html @@ -62,6 +62,7 @@ + {% include 'inc/custom_fields_panel.html' %} {% include 'extras/inc/tags_panel.html' with tags=object.tags.all %} {% plugin_left_page object %} diff --git a/netbox/templates/dcim/poweroutlet.html b/netbox/templates/dcim/poweroutlet.html index e5c9f8093..ae09afb31 100644 --- a/netbox/templates/dcim/poweroutlet.html +++ b/netbox/templates/dcim/poweroutlet.html @@ -42,6 +42,7 @@ + {% include 'inc/custom_fields_panel.html' %} {% include 'extras/inc/tags_panel.html' with tags=object.tags.all %} {% plugin_left_page object %} diff --git a/netbox/templates/dcim/powerport.html b/netbox/templates/dcim/powerport.html index 7356bafb9..aff32d494 100644 --- a/netbox/templates/dcim/powerport.html +++ b/netbox/templates/dcim/powerport.html @@ -42,6 +42,7 @@ + {% include 'inc/custom_fields_panel.html' %} {% include 'extras/inc/tags_panel.html' with tags=object.tags.all %} {% plugin_left_page object %} diff --git a/netbox/templates/dcim/rearport.html b/netbox/templates/dcim/rearport.html index 319850397..e4cada913 100644 --- a/netbox/templates/dcim/rearport.html +++ b/netbox/templates/dcim/rearport.html @@ -38,6 +38,7 @@ + {% include 'inc/custom_fields_panel.html' %} {% include 'extras/inc/tags_panel.html' with tags=object.tags.all %} {% plugin_left_page object %}