Fixes #5146: Add custom fields support for cables, power panels, rack reservations, and virtual chassis

This commit is contained in:
Jeremy Stretch 2020-09-17 14:22:14 -04:00
parent 91eca8cac9
commit 0030fe1779
14 changed files with 80 additions and 21 deletions

View File

@ -4,6 +4,10 @@
**NOTE:** This release completely removes support for embedded graphs. **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 ### Other Changes
* [#4349](https://github.com/netbox-community/netbox/issues/4349) - Dropped support for embedded graphs * [#4349](https://github.com/netbox-community/netbox/issues/4349) - Dropped support for embedded graphs

View File

@ -168,7 +168,7 @@ class RackUnitSerializer(serializers.Serializer):
occupied = serializers.BooleanField(read_only=True) occupied = serializers.BooleanField(read_only=True)
class RackReservationSerializer(TaggedObjectSerializer, ValidatedModelSerializer): class RackReservationSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:rackreservation-detail')
rack = NestedRackSerializer() rack = NestedRackSerializer()
user = NestedUserSerializer() user = NestedUserSerializer()
@ -176,7 +176,7 @@ class RackReservationSerializer(TaggedObjectSerializer, ValidatedModelSerializer
class Meta: class Meta:
model = RackReservation 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): class RackElevationDetailFilterSerializer(serializers.Serializer):
@ -649,7 +649,7 @@ class InventoryItemSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
# Cables # Cables
# #
class CableSerializer(TaggedObjectSerializer, ValidatedModelSerializer): class CableSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
termination_a_type = ContentTypeField( termination_a_type = ContentTypeField(
queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS) queryset=ContentType.objects.filter(CABLE_TERMINATION_MODELS)
@ -667,6 +667,7 @@ class CableSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
fields = [ fields = [
'id', 'url', 'termination_a_type', 'termination_a_id', 'termination_a', 'termination_b_type', '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', 'termination_b_id', 'termination_b', 'type', 'status', 'label', 'color', 'length', 'length_unit', 'tags',
'custom_fields',
] ]
def _get_termination(self, obj, side): def _get_termination(self, obj, side):
@ -729,21 +730,21 @@ class InterfaceConnectionSerializer(ValidatedModelSerializer):
# Virtual chassis # Virtual chassis
# #
class VirtualChassisSerializer(TaggedObjectSerializer, ValidatedModelSerializer): class VirtualChassisSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:virtualchassis-detail')
master = NestedDeviceSerializer(required=False) master = NestedDeviceSerializer(required=False)
member_count = serializers.IntegerField(read_only=True) member_count = serializers.IntegerField(read_only=True)
class Meta: class Meta:
model = VirtualChassis model = VirtualChassis
fields = ['id', 'url', 'name', 'domain', 'master', 'tags', 'member_count'] fields = ['id', 'url', 'name', 'domain', 'master', 'tags', 'custom_fields', 'member_count']
# #
# Power panels # Power panels
# #
class PowerPanelSerializer(TaggedObjectSerializer, ValidatedModelSerializer): class PowerPanelSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail') url = serializers.HyperlinkedIdentityField(view_name='dcim-api:powerpanel-detail')
site = NestedSiteSerializer() site = NestedSiteSerializer()
rack_group = NestedRackGroupSerializer( rack_group = NestedRackGroupSerializer(
@ -755,7 +756,7 @@ class PowerPanelSerializer(TaggedObjectSerializer, ValidatedModelSerializer):
class Meta: class Meta:
model = PowerPanel 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): class PowerFeedSerializer(TaggedObjectSerializer, CustomFieldModelSerializer):

View File

@ -690,7 +690,7 @@ class RackElevationFilterForm(RackFilterForm):
# Rack reservations # Rack reservations
# #
class RackReservationForm(BootstrapMixin, TenancyForm, forms.ModelForm): class RackReservationForm(BootstrapMixin, TenancyForm, CustomFieldModelForm):
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False required=False
@ -3608,7 +3608,7 @@ class ConnectCableToPowerFeedForm(BootstrapMixin, forms.ModelForm):
return getattr(self.cleaned_data['termination_b_id'], 'pk', None) return getattr(self.cleaned_data['termination_b_id'], 'pk', None)
class CableForm(BootstrapMixin, forms.ModelForm): class CableForm(BootstrapMixin, CustomFieldModelForm):
tags = DynamicModelMultipleChoiceField( tags = DynamicModelMultipleChoiceField(
queryset=Tag.objects.all(), queryset=Tag.objects.all(),
required=False required=False
@ -3919,7 +3919,7 @@ class DeviceSelectionForm(forms.Form):
) )
class VirtualChassisCreateForm(BootstrapMixin, forms.ModelForm): class VirtualChassisCreateForm(BootstrapMixin, CustomFieldModelForm):
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
queryset=Site.objects.all(), queryset=Site.objects.all(),
required=False required=False
@ -3972,7 +3972,7 @@ class VirtualChassisCreateForm(BootstrapMixin, forms.ModelForm):
return instance return instance
class VirtualChassisForm(BootstrapMixin, forms.ModelForm): class VirtualChassisForm(BootstrapMixin, CustomFieldModelForm):
master = forms.ModelChoiceField( master = forms.ModelChoiceField(
queryset=Device.objects.all(), queryset=Device.objects.all(),
required=False, required=False,
@ -4152,7 +4152,7 @@ class VirtualChassisFilterForm(BootstrapMixin, CustomFieldFilterForm):
# Power panels # Power panels
# #
class PowerPanelForm(BootstrapMixin, forms.ModelForm): class PowerPanelForm(BootstrapMixin, CustomFieldModelForm):
site = DynamicModelChoiceField( site = DynamicModelChoiceField(
queryset=Site.objects.all() queryset=Site.objects.all()
) )

View File

@ -9,6 +9,7 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
# Original CustomFieldModels
migrations.AddField( migrations.AddField(
model_name='device', model_name='device',
name='custom_field_data', name='custom_field_data',
@ -34,4 +35,26 @@ class Migration(migrations.Migration):
name='custom_field_data', name='custom_field_data',
field=models.JSONField(blank=True, default=dict, encoder=django.core.serializers.json.DjangoJSONEncoder), 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),
),
] ]

View File

@ -882,8 +882,8 @@ class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
# Cables # Cables
# #
@extras_features('custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class Cable(ChangeLoggedModel): class Cable(ChangeLoggedModel, CustomFieldModel):
""" """
A physical connection between two endpoints. A physical connection between two endpoints.
""" """
@ -1168,8 +1168,8 @@ class Cable(ChangeLoggedModel):
# Virtual chassis # Virtual chassis
# #
@extras_features('custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class VirtualChassis(ChangeLoggedModel): class VirtualChassis(ChangeLoggedModel, CustomFieldModel):
""" """
A collection of Devices which operate with a shared control plane (e.g. a switch stack). A collection of Devices which operate with a shared control plane (e.g. a switch stack).
""" """

View File

@ -22,8 +22,8 @@ __all__ = (
# Power # Power
# #
@extras_features('custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class PowerPanel(ChangeLoggedModel): class PowerPanel(ChangeLoggedModel, CustomFieldModel):
""" """
A distribution point for electrical power; e.g. a data center RPP. A distribution point for electrical power; e.g. a data center RPP.
""" """

View File

@ -561,8 +561,8 @@ class Rack(ChangeLoggedModel, CustomFieldModel):
return 0 return 0
@extras_features('custom_links', 'export_templates', 'webhooks') @extras_features('custom_fields', 'custom_links', 'export_templates', 'webhooks')
class RackReservation(ChangeLoggedModel): class RackReservation(ChangeLoggedModel, CustomFieldModel):
""" """
One or more reserved units within a Rack. One or more reserved units within a Rack.
""" """

View File

@ -83,6 +83,7 @@
</tr> </tr>
</table> </table>
</div> </div>
{% include 'inc/custom_fields_panel.html' with obj=cable %}
{% include 'extras/inc/tags_panel.html' with tags=cable.tags.all url='dcim:cable_list' %} {% include 'extras/inc/tags_panel.html' with tags=cable.tags.all url='dcim:cable_list' %}
{% plugin_left_page cable %} {% plugin_left_page cable %}
</div> </div>

View File

@ -32,3 +32,11 @@
{% render_field form.tags %} {% render_field form.tags %}
</div> </div>
</div> </div>
{% if form.custom_fields %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Custom Fields</strong></div>
<div class="panel-body">
{% render_custom_fields form %}
</div>
</div>
{% endif %}

View File

@ -82,6 +82,7 @@
</tr> </tr>
</table> </table>
</div> </div>
{% include 'inc/custom_fields_panel.html' with obj=powerpanel %}
{% include 'extras/inc/tags_panel.html' with tags=powerpanel.tags.all url='dcim:powerpanel_list' %} {% include 'extras/inc/tags_panel.html' with tags=powerpanel.tags.all url='dcim:powerpanel_list' %}
{% plugin_left_page powerpanel %} {% plugin_left_page powerpanel %}
</div> </div>

View File

@ -124,6 +124,7 @@
</tr> </tr>
</table> </table>
</div> </div>
{% include 'inc/custom_fields_panel.html' with obj=rackreservation %}
{% include 'extras/inc/tags_panel.html' with tags=rackreservation.tags.all url='dcim:rackreservation_list' %} {% include 'extras/inc/tags_panel.html' with tags=rackreservation.tags.all url='dcim:rackreservation_list' %}
{% plugin_left_page rackreservation %} {% plugin_left_page rackreservation %}
</div> </div>

View File

@ -21,4 +21,12 @@
{% render_field form.tenant %} {% render_field form.tenant %}
</div> </div>
</div> </div>
{% if form.custom_fields %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Custom Fields</strong></div>
<div class="panel-body">
{% render_custom_fields form %}
</div>
</div>
{% endif %}
{% endblock %} {% endblock %}

View File

@ -78,6 +78,7 @@
</tr> </tr>
</table> </table>
</div> </div>
{% include 'inc/custom_fields_panel.html' with obj=virtualchassis %}
{% include 'extras/inc/tags_panel.html' with tags=virtualchassis.tags.all url='dcim:virtualchassis_list' %} {% include 'extras/inc/tags_panel.html' with tags=virtualchassis.tags.all url='dcim:virtualchassis_list' %}
{% plugin_left_page virtualchassis %} {% plugin_left_page virtualchassis %}
</div> </div>

View File

@ -21,9 +21,20 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><strong>Virtual Chassis</strong></div> <div class="panel-heading"><strong>Virtual Chassis</strong></div>
<div class="table panel-body"> <div class="table panel-body">
{% render_form vc_form %} {% render_field vc_form.name %}
{% render_field vc_form.domain %}
{% render_field vc_form.master %}
{% render_field vc_form.tags %}
</div> </div>
</div> </div>
{% if vc_form.custom_fields %}
<div class="panel panel-default">
<div class="panel-heading"><strong>Custom Fields</strong></div>
<div class="panel-body">
{% render_custom_fields vc_form %}
</div>
</div>
{% endif %}
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading"><strong>Members</strong></div> <div class="panel-heading"><strong>Members</strong></div>
<table class="table panel-body"> <table class="table panel-body">