mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
#9102: Enable creating terminations in conjunction with cables via REST API
This commit is contained in:
@ -6,12 +6,12 @@
|
|||||||
|
|
||||||
* Device position and rack unit values are now reported as decimals (e.g. `1.0` or `1.5`) to support modeling half-height rack units.
|
* Device position and rack unit values are now reported as decimals (e.g. `1.0` or `1.5`) to support modeling half-height rack units.
|
||||||
* The `nat_outside` relation on the IP address model now returns a list of zero or more related IP addresses, rather than a single instance (or None).
|
* The `nat_outside` relation on the IP address model now returns a list of zero or more related IP addresses, rather than a single instance (or None).
|
||||||
* Several fields on the cable API serializers have been altered to support multiple-object cable terminations:
|
* Several fields on the cable API serializers have been altered or removed to support multiple-object cable terminations:
|
||||||
|
|
||||||
| Old Name | Old Type | New Name | New Type |
|
| Old Name | Old Type | New Name | New Type |
|
||||||
|----------------------|----------|-----------------------|----------|
|
|----------------------|----------|-----------------------|----------|
|
||||||
| `termination_a_type` | string | `a_terminations_type` | string |
|
| `termination_a_type` | string | _Removed_ | - |
|
||||||
| `termination_b_type` | string | `b_terminations_type` | string |
|
| `termination_b_type` | string | _Removed_ | - |
|
||||||
| `termination_a_id` | integer | _Removed_ | - |
|
| `termination_a_id` | integer | _Removed_ | - |
|
||||||
| `termination_b_id` | integer | _Removed_ | - |
|
| `termination_b_id` | integer | _Removed_ | - |
|
||||||
| `termination_a` | object | `a_terminations` | list |
|
| `termination_a` | object | `a_terminations` | list |
|
||||||
|
@ -15,7 +15,8 @@ from ipam.api.nested_serializers import (
|
|||||||
from ipam.models import ASN, VLAN
|
from ipam.models import ASN, VLAN
|
||||||
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
from netbox.api import ChoiceField, ContentTypeField, SerializedPKRelatedField
|
||||||
from netbox.api.serializers import (
|
from netbox.api.serializers import (
|
||||||
NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer, WritableNestedSerializer,
|
GenericObjectSerializer, NestedGroupModelSerializer, NetBoxModelSerializer, ValidatedModelSerializer,
|
||||||
|
WritableNestedSerializer,
|
||||||
)
|
)
|
||||||
from netbox.config import ConfigItem
|
from netbox.config import ConfigItem
|
||||||
from tenancy.api.nested_serializers import NestedTenantSerializer
|
from tenancy.api.nested_serializers import NestedTenantSerializer
|
||||||
@ -994,10 +995,8 @@ class InventoryItemRoleSerializer(NetBoxModelSerializer):
|
|||||||
|
|
||||||
class CableSerializer(NetBoxModelSerializer):
|
class CableSerializer(NetBoxModelSerializer):
|
||||||
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
url = serializers.HyperlinkedIdentityField(view_name='dcim-api:cable-detail')
|
||||||
a_terminations_type = serializers.SerializerMethodField(read_only=True)
|
a_terminations = GenericObjectSerializer(many=True, required=False)
|
||||||
b_terminations_type = serializers.SerializerMethodField(read_only=True)
|
b_terminations = GenericObjectSerializer(many=True, required=False)
|
||||||
a_terminations = serializers.SerializerMethodField(read_only=True)
|
|
||||||
b_terminations = serializers.SerializerMethodField(read_only=True)
|
|
||||||
status = ChoiceField(choices=LinkStatusChoices, required=False)
|
status = ChoiceField(choices=LinkStatusChoices, required=False)
|
||||||
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
tenant = NestedTenantSerializer(required=False, allow_null=True)
|
||||||
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False)
|
length_unit = ChoiceField(choices=CableLengthUnitChoices, allow_blank=True, required=False)
|
||||||
@ -1005,47 +1004,10 @@ class CableSerializer(NetBoxModelSerializer):
|
|||||||
class Meta:
|
class Meta:
|
||||||
model = Cable
|
model = Cable
|
||||||
fields = [
|
fields = [
|
||||||
'id', 'url', 'display', 'type', 'a_terminations_type', 'a_terminations', 'b_terminations_type',
|
'id', 'url', 'display', 'type', 'a_terminations', 'b_terminations', 'status', 'tenant', 'label', 'color',
|
||||||
'b_terminations', 'status', 'tenant', 'label', 'color', 'length', 'length_unit', 'tags', 'custom_fields',
|
'length', 'length_unit', 'tags', 'custom_fields', 'created', 'last_updated',
|
||||||
'created', 'last_updated',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def _get_terminations_type(self, obj, side):
|
|
||||||
assert side in CableEndChoices.values()
|
|
||||||
terms = getattr(obj, f'get_{side.lower()}_terminations')()
|
|
||||||
if terms:
|
|
||||||
ct = ContentType.objects.get_for_model(terms[0])
|
|
||||||
return f"{ct.app_label}.{ct.model}"
|
|
||||||
|
|
||||||
def _get_terminations(self, obj, side):
|
|
||||||
assert side in CableEndChoices.values()
|
|
||||||
terms = getattr(obj, f'get_{side.lower()}_terminations')()
|
|
||||||
if not terms:
|
|
||||||
return []
|
|
||||||
|
|
||||||
termination_type = ContentType.objects.get_for_model(terms[0])
|
|
||||||
serializer = get_serializer_for_model(termination_type.model_class(), prefix='Nested')
|
|
||||||
context = {'request': self.context['request']}
|
|
||||||
data = serializer(terms, context=context, many=True).data
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.CharField)
|
|
||||||
def get_a_terminations_type(self, obj):
|
|
||||||
return self._get_terminations_type(obj, CableEndChoices.SIDE_A)
|
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.CharField)
|
|
||||||
def get_b_terminations_type(self, obj):
|
|
||||||
return self._get_terminations_type(obj, CableEndChoices.SIDE_B)
|
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
|
||||||
def get_a_terminations(self, obj):
|
|
||||||
return self._get_terminations(obj, CableEndChoices.SIDE_A)
|
|
||||||
|
|
||||||
@swagger_serializer_method(serializer_or_field=serializers.DictField)
|
|
||||||
def get_b_terminations(self, obj):
|
|
||||||
return self._get_terminations(obj, CableEndChoices.SIDE_B)
|
|
||||||
|
|
||||||
|
|
||||||
class TracedCableSerializer(serializers.ModelSerializer):
|
class TracedCableSerializer(serializers.ModelSerializer):
|
||||||
"""
|
"""
|
||||||
|
@ -955,7 +955,7 @@ class CableCSVForm(NetBoxModelCSVForm):
|
|||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise forms.ValidationError(f"{side.upper()} side termination not found: {device} {name}")
|
raise forms.ValidationError(f"{side.upper()} side termination not found: {device} {name}")
|
||||||
|
|
||||||
setattr(self.instance, f'termination_{side}', termination_object)
|
setattr(self.instance, f'{side}_terminations', [termination_object])
|
||||||
return termination_object
|
return termination_object
|
||||||
|
|
||||||
def clean_side_a_name(self):
|
def clean_side_a_name(self):
|
||||||
|
@ -157,8 +157,8 @@ def get_cable_form(a_type, b_type):
|
|||||||
|
|
||||||
if self.instance and self.instance.pk:
|
if self.instance and self.instance.pk:
|
||||||
# Initialize A/B terminations when modifying an existing Cable instance
|
# Initialize A/B terminations when modifying an existing Cable instance
|
||||||
self.initial['a_terminations'] = self.instance.get_a_terminations()
|
self.initial['a_terminations'] = self.instance.a_terminations
|
||||||
self.initial['b_terminations'] = self.instance.get_b_terminations()
|
self.initial['b_terminations'] = self.instance.b_terminations
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
|
|
||||||
|
@ -93,11 +93,12 @@ class Cable(NetBoxModel):
|
|||||||
# Cache the original status so we can check later if it's been changed
|
# Cache the original status so we can check later if it's been changed
|
||||||
self._orig_status = self.status
|
self._orig_status = self.status
|
||||||
|
|
||||||
# Assign any *new* CableTerminations for the instance. These will replace any existing
|
self._terminations_modified = False
|
||||||
# terminations on save().
|
|
||||||
if a_terminations is not None:
|
# Assign or retrieve A/B terminations
|
||||||
|
if a_terminations:
|
||||||
self.a_terminations = a_terminations
|
self.a_terminations = a_terminations
|
||||||
if b_terminations is not None:
|
if b_terminations:
|
||||||
self.b_terminations = b_terminations
|
self.b_terminations = b_terminations
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -107,6 +108,34 @@ class Cable(NetBoxModel):
|
|||||||
def get_absolute_url(self):
|
def get_absolute_url(self):
|
||||||
return reverse('dcim:cable', args=[self.pk])
|
return reverse('dcim:cable', args=[self.pk])
|
||||||
|
|
||||||
|
@property
|
||||||
|
def a_terminations(self):
|
||||||
|
if hasattr(self, '_a_terminations'):
|
||||||
|
return self._a_terminations
|
||||||
|
# Query self.terminations.all() to leverage cached results
|
||||||
|
return [
|
||||||
|
ct.termination for ct in self.terminations.all() if ct.cable_end == CableEndChoices.SIDE_A
|
||||||
|
]
|
||||||
|
|
||||||
|
@a_terminations.setter
|
||||||
|
def a_terminations(self, value):
|
||||||
|
self._terminations_modified = True
|
||||||
|
self._a_terminations = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def b_terminations(self):
|
||||||
|
if hasattr(self, '_b_terminations'):
|
||||||
|
return self._b_terminations
|
||||||
|
# Query self.terminations.all() to leverage cached results
|
||||||
|
return [
|
||||||
|
ct.termination for ct in self.terminations.all() if ct.cable_end == CableEndChoices.SIDE_B
|
||||||
|
]
|
||||||
|
|
||||||
|
@b_terminations.setter
|
||||||
|
def b_terminations(self, value):
|
||||||
|
self._terminations_modified = True
|
||||||
|
self._b_terminations = value
|
||||||
|
|
||||||
def clean(self):
|
def clean(self):
|
||||||
super().clean()
|
super().clean()
|
||||||
|
|
||||||
@ -116,30 +145,28 @@ class Cable(NetBoxModel):
|
|||||||
elif self.length is None:
|
elif self.length is None:
|
||||||
self.length_unit = ''
|
self.length_unit = ''
|
||||||
|
|
||||||
a_terminations = [
|
if self.pk is None and (not self.a_terminations or not self.b_terminations):
|
||||||
CableTermination(cable=self, cable_end='A', termination=t)
|
raise ValidationError("Must define A and B terminations when creating a new cable.")
|
||||||
for t in getattr(self, 'a_terminations', [])
|
|
||||||
]
|
|
||||||
b_terminations = [
|
|
||||||
CableTermination(cable=self, cable_end='B', termination=t)
|
|
||||||
for t in getattr(self, 'b_terminations', [])
|
|
||||||
]
|
|
||||||
|
|
||||||
# Check that all termination objects for either end are of the same type
|
if self._terminations_modified:
|
||||||
for terms in (a_terminations, b_terminations):
|
|
||||||
if len(terms) > 1 and not all(t.termination_type == terms[0].termination_type for t in terms[1:]):
|
|
||||||
raise ValidationError("Cannot connect different termination types to same end of cable.")
|
|
||||||
|
|
||||||
# Check that termination types are compatible
|
# Check that all termination objects for either end are of the same type
|
||||||
if a_terminations and b_terminations:
|
for terms in (self.a_terminations, self.b_terminations):
|
||||||
a_type = a_terminations[0].termination_type.model
|
if len(terms) > 1 and not all(isinstance(t, type(terms[0])) for t in terms[1:]):
|
||||||
b_type = b_terminations[0].termination_type.model
|
raise ValidationError("Cannot connect different termination types to same end of cable.")
|
||||||
if b_type not in COMPATIBLE_TERMINATION_TYPES.get(a_type):
|
|
||||||
raise ValidationError(f"Incompatible termination types: {a_type} and {b_type}")
|
|
||||||
|
|
||||||
# Run clean() on any new CableTerminations
|
# Check that termination types are compatible
|
||||||
for cabletermination in [*a_terminations, *b_terminations]:
|
if self.a_terminations and self.b_terminations:
|
||||||
cabletermination.clean()
|
a_type = self.a_terminations[0]._meta.model_name
|
||||||
|
b_type = self.b_terminations[0]._meta.model_name
|
||||||
|
if b_type not in COMPATIBLE_TERMINATION_TYPES.get(a_type):
|
||||||
|
raise ValidationError(f"Incompatible termination types: {a_type} and {b_type}")
|
||||||
|
|
||||||
|
# Run clean() on any new CableTerminations
|
||||||
|
for termination in self.a_terminations:
|
||||||
|
CableTermination(cable=self, cable_end='A', termination=termination).clean()
|
||||||
|
for termination in self.b_terminations:
|
||||||
|
CableTermination(cable=self, cable_end='B', termination=termination).clean()
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
_created = self.pk is None
|
_created = self.pk is None
|
||||||
@ -160,23 +187,21 @@ class Cable(NetBoxModel):
|
|||||||
b_terminations = {ct.termination: ct for ct in self.terminations.filter(cable_end='B')}
|
b_terminations = {ct.termination: ct for ct in self.terminations.filter(cable_end='B')}
|
||||||
|
|
||||||
# Delete stale CableTerminations
|
# Delete stale CableTerminations
|
||||||
if hasattr(self, 'a_terminations'):
|
if self._terminations_modified:
|
||||||
for termination, ct in a_terminations.items():
|
for termination, ct in a_terminations.items():
|
||||||
if termination not in self.a_terminations:
|
if termination.pk and termination not in self.a_terminations:
|
||||||
ct.delete()
|
ct.delete()
|
||||||
if hasattr(self, 'b_terminations'):
|
|
||||||
for termination, ct in b_terminations.items():
|
for termination, ct in b_terminations.items():
|
||||||
if termination not in self.b_terminations:
|
if termination.pk and termination not in self.b_terminations:
|
||||||
ct.delete()
|
ct.delete()
|
||||||
|
|
||||||
# Save new CableTerminations (if any)
|
# Save new CableTerminations (if any)
|
||||||
if hasattr(self, 'a_terminations'):
|
if self._terminations_modified:
|
||||||
for termination in self.a_terminations:
|
for termination in self.a_terminations:
|
||||||
if termination not in a_terminations:
|
if not termination.pk or termination not in a_terminations:
|
||||||
CableTermination(cable=self, cable_end='A', termination=termination).save()
|
CableTermination(cable=self, cable_end='A', termination=termination).save()
|
||||||
if hasattr(self, 'b_terminations'):
|
|
||||||
for termination in self.b_terminations:
|
for termination in self.b_terminations:
|
||||||
if termination not in b_terminations:
|
if not termination.pk or termination not in b_terminations:
|
||||||
CableTermination(cable=self, cable_end='B', termination=termination).save()
|
CableTermination(cable=self, cable_end='B', termination=termination).save()
|
||||||
|
|
||||||
trace_paths.send(Cable, instance=self, created=_created)
|
trace_paths.send(Cable, instance=self, created=_created)
|
||||||
@ -184,18 +209,6 @@ class Cable(NetBoxModel):
|
|||||||
def get_status_color(self):
|
def get_status_color(self):
|
||||||
return LinkStatusChoices.colors.get(self.status)
|
return LinkStatusChoices.colors.get(self.status)
|
||||||
|
|
||||||
def get_a_terminations(self):
|
|
||||||
# Query self.terminations.all() to leverage cached results
|
|
||||||
return [
|
|
||||||
ct.termination for ct in self.terminations.all() if ct.cable_end == CableEndChoices.SIDE_A
|
|
||||||
]
|
|
||||||
|
|
||||||
def get_b_terminations(self):
|
|
||||||
# Query self.terminations.all() to leverage cached results
|
|
||||||
return [
|
|
||||||
ct.termination for ct in self.terminations.all() if ct.cable_end == CableEndChoices.SIDE_B
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
class CableTermination(models.Model):
|
class CableTermination(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -79,7 +79,7 @@ def update_connected_endpoints(instance, created, raw=False, **kwargs):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# Update cable paths if new terminations have been set
|
# Update cable paths if new terminations have been set
|
||||||
if hasattr(instance, 'a_terminations') or hasattr(instance, 'b_terminations'):
|
if instance._terminations_modified:
|
||||||
a_terminations = []
|
a_terminations = []
|
||||||
b_terminations = []
|
b_terminations = []
|
||||||
for t in instance.terminations.all():
|
for t in instance.terminations.all():
|
||||||
|
@ -7,6 +7,7 @@ from dcim.choices import *
|
|||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from ipam.models import ASN, RIR, VLAN, VRF
|
from ipam.models import ASN, RIR, VLAN, VRF
|
||||||
|
from netbox.api.serializers import GenericObjectSerializer
|
||||||
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
|
from utilities.testing import APITestCase, APIViewTestCases, create_test_device
|
||||||
from virtualization.models import Cluster, ClusterType
|
from virtualization.models import Cluster, ClusterType
|
||||||
from wireless.choices import WirelessChannelChoices
|
from wireless.choices import WirelessChannelChoices
|
||||||
@ -1864,6 +1865,17 @@ class CableTest(APIViewTestCases.APIViewTestCase):
|
|||||||
# TODO: Allow updating cable terminations
|
# TODO: Allow updating cable terminations
|
||||||
test_update_object = None
|
test_update_object = None
|
||||||
|
|
||||||
|
def model_to_dict(self, *args, **kwargs):
|
||||||
|
data = super().model_to_dict(*args, **kwargs)
|
||||||
|
|
||||||
|
# Serialize termination objects
|
||||||
|
if 'a_terminations' in data:
|
||||||
|
data['a_terminations'] = GenericObjectSerializer(data['a_terminations'], many=True).data
|
||||||
|
if 'b_terminations' in data:
|
||||||
|
data['b_terminations'] = GenericObjectSerializer(data['b_terminations'], many=True).data
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpTestData(cls):
|
def setUpTestData(cls):
|
||||||
site = Site.objects.create(name='Site 1', slug='site-1')
|
site = Site.objects.create(name='Site 1', slug='site-1')
|
||||||
@ -1893,24 +1905,36 @@ class CableTest(APIViewTestCases.APIViewTestCase):
|
|||||||
|
|
||||||
cls.create_data = [
|
cls.create_data = [
|
||||||
{
|
{
|
||||||
'a_terminations_type': 'dcim.interface',
|
'a_terminations': [{
|
||||||
'a_terminations': [interfaces[4].pk],
|
'object_type': 'dcim.interface',
|
||||||
'b_terminations_type': 'dcim.interface',
|
'object_id': interfaces[4].pk,
|
||||||
'b_terminations': [interfaces[14].pk],
|
}],
|
||||||
|
'b_terminations': [{
|
||||||
|
'object_type': 'dcim.interface',
|
||||||
|
'object_id': interfaces[14].pk,
|
||||||
|
}],
|
||||||
'label': 'Cable 4',
|
'label': 'Cable 4',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'a_terminations_type': 'dcim.interface',
|
'a_terminations': [{
|
||||||
'a_terminations': [interfaces[5].pk],
|
'object_type': 'dcim.interface',
|
||||||
'b_terminations_type': 'dcim.interface',
|
'object_id': interfaces[5].pk,
|
||||||
'b_terminations': [interfaces[15].pk],
|
}],
|
||||||
|
'b_terminations': [{
|
||||||
|
'object_type': 'dcim.interface',
|
||||||
|
'object_id': interfaces[15].pk,
|
||||||
|
}],
|
||||||
'label': 'Cable 5',
|
'label': 'Cable 5',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
'a_terminations_type': 'dcim.interface',
|
'a_terminations': [{
|
||||||
'a_terminations': [interfaces[6].pk],
|
'object_type': 'dcim.interface',
|
||||||
'b_terminations_type': 'dcim.interface',
|
'object_id': interfaces[6].pk,
|
||||||
'b_terminations': [interfaces[16].pk],
|
}],
|
||||||
|
'b_terminations': [{
|
||||||
|
'object_type': 'dcim.interface',
|
||||||
|
'object_id': interfaces[16].pk,
|
||||||
|
}],
|
||||||
'label': 'Cable 6',
|
'label': 'Cable 6',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
@ -12,6 +12,7 @@ from dcim.choices import *
|
|||||||
from dcim.constants import *
|
from dcim.constants import *
|
||||||
from dcim.models import *
|
from dcim.models import *
|
||||||
from ipam.models import ASN, RIR, VLAN, VRF
|
from ipam.models import ASN, RIR, VLAN, VRF
|
||||||
|
from netbox.api.serializers import GenericObjectSerializer
|
||||||
from tenancy.models import Tenant
|
from tenancy.models import Tenant
|
||||||
from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data
|
from utilities.testing import ViewTestCases, create_tags, create_test_device, post_data
|
||||||
from wireless.models import WirelessLAN
|
from wireless.models import WirelessLAN
|
||||||
@ -2640,8 +2641,8 @@ class CableTestCase(
|
|||||||
cls.form_data = {
|
cls.form_data = {
|
||||||
# TODO: Revisit this limitation
|
# TODO: Revisit this limitation
|
||||||
# Changing terminations not supported when editing an existing Cable
|
# Changing terminations not supported when editing an existing Cable
|
||||||
'a_terminations': interfaces[0].pk,
|
'a_terminations': [interfaces[0].pk],
|
||||||
'b_terminations': interfaces[3].pk,
|
'b_terminations': [interfaces[3].pk],
|
||||||
'type': CableTypeChoices.TYPE_CAT6,
|
'type': CableTypeChoices.TYPE_CAT6,
|
||||||
'status': LinkStatusChoices.STATUS_PLANNED,
|
'status': LinkStatusChoices.STATUS_PLANNED,
|
||||||
'label': 'Label',
|
'label': 'Label',
|
||||||
@ -2667,6 +2668,17 @@ class CableTestCase(
|
|||||||
'length_unit': CableLengthUnitChoices.UNIT_METER,
|
'length_unit': CableLengthUnitChoices.UNIT_METER,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def model_to_dict(self, *args, **kwargs):
|
||||||
|
data = super().model_to_dict(*args, **kwargs)
|
||||||
|
|
||||||
|
# Serialize termination objects
|
||||||
|
if 'a_terminations' in data:
|
||||||
|
data['a_terminations'] = [obj.pk for obj in data['a_terminations']]
|
||||||
|
if 'b_terminations' in data:
|
||||||
|
data['b_terminations'] = [obj.pk for obj in data['b_terminations']]
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
class VirtualChassisTestCase(ViewTestCases.PrimaryObjectViewTestCase):
|
||||||
model = VirtualChassis
|
model = VirtualChassis
|
||||||
|
@ -63,13 +63,13 @@
|
|||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">Termination A</h5>
|
<h5 class="card-header">Termination A</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% include 'dcim/inc/cable_termination.html' with terminations=object.get_a_terminations %}
|
{% include 'dcim/inc/cable_termination.html' with terminations=object.a_terminations %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h5 class="card-header">Termination B</h5>
|
<h5 class="card-header">Termination B</h5>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% include 'dcim/inc/cable_termination.html' with terminations=object.get_b_terminations %}
|
{% include 'dcim/inc/cable_termination.html' with terminations=object.b_terminations %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% plugin_right_page object %}
|
{% plugin_right_page object %}
|
||||||
|
Reference in New Issue
Block a user