From 0030fe1779e86eed6313f8afeb93fb21e10c21bb Mon Sep 17 00:00:00 2001 From: Jeremy Stretch Date: Thu, 17 Sep 2020 14:22:14 -0400 Subject: [PATCH] Fixes #5146: Add custom fields support for cables, power panels, rack reservations, and virtual chassis --- docs/release-notes/version-2.10.md | 4 ++++ netbox/dcim/api/serializers.py | 15 ++++++------ netbox/dcim/forms.py | 10 ++++---- .../dcim/migrations/0116_custom_field_data.py | 23 +++++++++++++++++++ netbox/dcim/models/devices.py | 8 +++---- netbox/dcim/models/power.py | 4 ++-- netbox/dcim/models/racks.py | 4 ++-- netbox/templates/dcim/cable.html | 1 + netbox/templates/dcim/inc/cable_form.html | 8 +++++++ netbox/templates/dcim/powerpanel.html | 1 + netbox/templates/dcim/rackreservation.html | 1 + .../templates/dcim/rackreservation_edit.html | 8 +++++++ netbox/templates/dcim/virtualchassis.html | 1 + .../templates/dcim/virtualchassis_edit.html | 13 ++++++++++- 14 files changed, 80 insertions(+), 21 deletions(-) diff --git a/docs/release-notes/version-2.10.md b/docs/release-notes/version-2.10.md index 45c192148..e9dfbc0c3 100644 --- a/docs/release-notes/version-2.10.md +++ b/docs/release-notes/version-2.10.md @@ -4,6 +4,10 @@ **NOTE:** This release completely removes support for embedded graphs. +### New Features + +* [#5146](https://github.com/netbox-community/netbox/issues/5146) - Add custom fields support for cables, power panels, rack reservations, and virtual chassis + ### Other Changes * [#4349](https://github.com/netbox-community/netbox/issues/4349) - Dropped support for embedded graphs diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 50c1f99ff..1b2f75201 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -168,7 +168,7 @@ class RackUnitSerializer(serializers.Serializer): occupied = serializers.BooleanField(read_only=True) -class RackReservationSerializer(TaggedObjectSerializer, ValidatedModelSerializer): +class RackReservationSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail') rack = NestedRackSerializer() user = NestedUserSerializer() @@ -176,7 +176,7 @@ class RackReservationSerializer(TaggedObjectSerializer, ValidatedModelSerializer class Meta: model = RackReservation - fields = ['id', 'url', 'rack', 'units', 'created', 'user', 'tenant', 'description', 'tags'] + fields = ['id', 'url', 'rack', 'units', 'created', 'user', 'tenant', 'description', 'tags', 'custom_fields'] class RackElevationDetailFilterSerializer(serializers.Serializer): @@ -649,7 +649,7 @@ class InventoryItemSerializer(TaggedObjectSerializer, ValidatedModelSerializer): # Cables # -class CableSerializer(TaggedObjectSerializer, ValidatedModelSerializer): +class CableSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail') termination_a_type = ContentTypeField( queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS) @@ -667,6 +667,7 @@ class CableSerializer(TaggedObjectSerializer, ValidatedModelSerializer): fields = [ 'id', 'url', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type', 'termination_b_id', 'termination_b', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags', + 'custom_fields', ] def _get_termination(self, obj, side): @@ -729,21 +730,21 @@ class InterfaceConnectionSerializer(ValidatedModelSerializer): # Virtual chassis # -class VirtualChassisSerializer(TaggedObjectSerializer, ValidatedModelSerializer): +class VirtualChassisSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail') master = NestedDeviceSerializer(required=False) member_count = serializers.IntegerField(read_only=True) class Meta: model = VirtualChassis - fields = ['id', 'url', 'name', 'domain', 'master', 'tags', 'member_count'] + fields = ['id', 'url', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count'] # # Power panels # -class PowerPanelSerializer(TaggedObjectSerializer, ValidatedModelSerializer): +class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail') site = NestedSiteSerializer() rack_group = NestedRackGroupSerializer( @@ -755,7 +756,7 @@ class PowerPanelSerializer(TaggedObjectSerializer, ValidatedModelSerializer): class Meta: model = PowerPanel - fields = ['id', 'url', 'site', 'rack_group', 'name', 'tags', 'powerfeed_count'] + fields = ['id', 'url', 'site', 'rack_group', 'name', 'tags', 'custom_fields', 'powerfeed_count'] class PowerFeedSerializer(TaggedObjectSerializer, CustomFieldModelSerializer): diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 43f77de51..1599b343d 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -690,7 +690,7 @@ class RackElevationFilterForm(RackFilterForm): # Rack reservations # -class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm): +class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm): site = DynamicModelChoiceField( queryset=Site.objects.all(), required=False @@ -3608,7 +3608,7 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, forms.ModelForm): return getattr(self.cleaned_data['termination_b_id'], 'pk', None) -class CableForm(BootstrapMixin, forms.ModelForm): +class CableForm(BootstrapMixin, CustomFieldModelForm): tags = DynamicModelMultipleChoiceField( queryset=Tag.objects.all(), required=False @@ -3919,7 +3919,7 @@ class DeviceSelectionForm(forms.Form): ) -class VirtualChassisCreateForm(BootstrapMixin, forms.ModelForm): +class VirtualChassisCreateForm(BootstrapMixin, CustomFieldModelForm): site = DynamicModelChoiceField( queryset=Site.objects.all(), required=False @@ -3972,7 +3972,7 @@ class VirtualChassisCreateForm(BootstrapMixin, forms.ModelForm): return instance -class VirtualChassisForm(BootstrapMixin, forms.ModelForm): +class VirtualChassisForm(BootstrapMixin, CustomFieldModelForm): master = forms.ModelChoiceField( queryset=Device.objects.all(), required=False, @@ -4152,7 +4152,7 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm): # Power panels # -class PowerPanelForm(BootstrapMixin, forms.ModelForm): +class PowerPanelForm(BootstrapMixin, CustomFieldModelForm): site = DynamicModelChoiceField( queryset=Site.objects.all() ) diff --git a/netbox/dcim/migrations/0116_custom_field_data.py b/netbox/dcim/migrations/0116_custom_field_data.py index cc7b8cc7f..34148bd0a 100644 --- a/netbox/dcim/migrations/0116_custom_field_data.py +++ b/netbox/dcim/migrations/0116_custom_field_data.py @@ -9,6 +9,7 @@ class Migration(migrations.Migration): ] operations = [ + # Original CustomFieldModels migrations.AddField( model_name='device', name='custom_field_data', @@ -34,4 +35,26 @@ class Migration(migrations.Migration): name='custom_field_data', field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), ), + + # Added under #5146 + migrations.AddField( + model_name='cable', + name='custom_field_data', + field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AddField( + model_name='powerpanel', + name='custom_field_data', + field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AddField( + model_name='rackreservation', + name='custom_field_data', + field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), + migrations.AddField( + model_name='virtualchassis', + name='custom_field_data', + field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), + ), ] diff --git a/netbox/dcim/models/devices.py b/netbox/dcim/models/devices.py index 8b4f06108..38ed84370 100644 --- a/netbox/dcim/models/devices.py +++ b/netbox/dcim/models/devices.py @@ -882,8 +882,8 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): # Cables # -@extras_features('custom_links', 'export_templates', 'webhooks') -class Cable(ChangeLoggedModel): +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +class Cable(ChangeLoggedModel, CustomFieldModel): """ A physical connection between two endpoints. """ @@ -1168,8 +1168,8 @@ class Cable(ChangeLoggedModel): # Virtual chassis # -@extras_features('custom_links', 'export_templates', 'webhooks') -class VirtualChassis(ChangeLoggedModel): +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +class VirtualChassis(ChangeLoggedModel, CustomFieldModel): """ A collection of Devices which operate with a shared control plane (e.g. a switch stack). """ diff --git a/netbox/dcim/models/power.py b/netbox/dcim/models/power.py index 08ae194ae..1b226586f 100644 --- a/netbox/dcim/models/power.py +++ b/netbox/dcim/models/power.py @@ -22,8 +22,8 @@ __all__ = ( # Power # -@extras_features('custom_links', 'export_templates', 'webhooks') -class PowerPanel(ChangeLoggedModel): +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +class PowerPanel(ChangeLoggedModel, CustomFieldModel): """ A distribution point for electrical power; e.g. a data center RPP. """ diff --git a/netbox/dcim/models/racks.py b/netbox/dcim/models/racks.py index 4a6b56298..f09f8c828 100644 --- a/netbox/dcim/models/racks.py +++ b/netbox/dcim/models/racks.py @@ -561,8 +561,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel): return 0 -@extras_features('custom_links', 'export_templates', 'webhooks') -class RackReservation(ChangeLoggedModel): +@extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks') +class RackReservation(ChangeLoggedModel, CustomFieldModel): """ One or more reserved units within a Rack. """ diff --git a/netbox/templates/dcim/cable.html b/netbox/templates/dcim/cable.html index 03188163d..bdae87c48 100644 --- a/netbox/templates/dcim/cable.html +++ b/netbox/templates/dcim/cable.html @@ -83,6 +83,7 @@ + {% include 'inc/custom_fields_panel.html' with obj=cable %} {% include 'extras/inc/tags_panel.html' with tags=cable.tags.all url='dcim:cable_list' %} {% plugin_left_page cable %} diff --git a/netbox/templates/dcim/inc/cable_form.html b/netbox/templates/dcim/inc/cable_form.html index 98eca17d2..c0ade9aec 100644 --- a/netbox/templates/dcim/inc/cable_form.html +++ b/netbox/templates/dcim/inc/cable_form.html @@ -32,3 +32,11 @@ {% render_field form.tags %} +{% if form.custom_fields %} +
+
Custom Fields
+
+ {% render_custom_fields form %} +
+
+{% endif %} diff --git a/netbox/templates/dcim/powerpanel.html b/netbox/templates/dcim/powerpanel.html index 90956d2a3..3cad8b5b3 100644 --- a/netbox/templates/dcim/powerpanel.html +++ b/netbox/templates/dcim/powerpanel.html @@ -82,6 +82,7 @@ + {% include 'inc/custom_fields_panel.html' with obj=powerpanel %} {% include 'extras/inc/tags_panel.html' with tags=powerpanel.tags.all url='dcim:powerpanel_list' %} {% plugin_left_page powerpanel %} diff --git a/netbox/templates/dcim/rackreservation.html b/netbox/templates/dcim/rackreservation.html index ab0fc0bba..5e607bcab 100644 --- a/netbox/templates/dcim/rackreservation.html +++ b/netbox/templates/dcim/rackreservation.html @@ -124,6 +124,7 @@ + {% include 'inc/custom_fields_panel.html' with obj=rackreservation %} {% include 'extras/inc/tags_panel.html' with tags=rackreservation.tags.all url='dcim:rackreservation_list' %} {% plugin_left_page rackreservation %} diff --git a/netbox/templates/dcim/rackreservation_edit.html b/netbox/templates/dcim/rackreservation_edit.html index d6fa9cfcb..fd030d1fe 100644 --- a/netbox/templates/dcim/rackreservation_edit.html +++ b/netbox/templates/dcim/rackreservation_edit.html @@ -21,4 +21,12 @@ {% render_field form.tenant %} + {% if form.custom_fields %} +
+
Custom Fields
+
+ {% render_custom_fields form %} +
+
+ {% endif %} {% endblock %} diff --git a/netbox/templates/dcim/virtualchassis.html b/netbox/templates/dcim/virtualchassis.html index 18ec8c4e7..c38761d68 100644 --- a/netbox/templates/dcim/virtualchassis.html +++ b/netbox/templates/dcim/virtualchassis.html @@ -78,6 +78,7 @@ + {% include 'inc/custom_fields_panel.html' with obj=virtualchassis %} {% include 'extras/inc/tags_panel.html' with tags=virtualchassis.tags.all url='dcim:virtualchassis_list' %} {% plugin_left_page virtualchassis %} diff --git a/netbox/templates/dcim/virtualchassis_edit.html b/netbox/templates/dcim/virtualchassis_edit.html index 54bdc9fe8..3b24cce20 100644 --- a/netbox/templates/dcim/virtualchassis_edit.html +++ b/netbox/templates/dcim/virtualchassis_edit.html @@ -21,9 +21,20 @@
Virtual Chassis
- {% render_form vc_form %} + {% render_field vc_form.name %} + {% render_field vc_form.domain %} + {% render_field vc_form.master %} + {% render_field vc_form.tags %}
+ {% if vc_form.custom_fields %} +
+
Custom Fields
+
+ {% render_custom_fields vc_form %} +
+
+ {% endif %}
Members