Closes #19977: Denormalize device relationships on component models (#19984)
Some checks failed
CodeQL / Analyze (${{ matrix.language }}) (none, python) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, actions) (push) Waiting to run
CodeQL / Analyze (${{ matrix.language }}) (none, javascript-typescript) (push) Waiting to run
CI / build (20.x, 3.10) (push) Has been cancelled
CI / build (20.x, 3.11) (push) Has been cancelled
CI / build (20.x, 3.12) (push) Has been cancelled

* Closes #19977: Denormalize site, location, and rack for device components

* Set blank=True on denormalized ForeignKeys

* Populate denormalized field in test data

* Ignore private fields when constructing test GraphQL requests
This commit is contained in:
Jeremy Stretch 2025-08-01 16:40:15 -04:00 committed by GitHub
parent 35b9d80819
commit aa9ee0e5c6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 546 additions and 25 deletions

View File

@ -1515,34 +1515,34 @@ class DeviceComponentFilterSet(django_filters.FilterSet):
label=_('Site group (slug)'), label=_('Site group (slug)'),
) )
site_id = django_filters.ModelMultipleChoiceFilter( site_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__site', field_name='_site',
queryset=Site.objects.all(), queryset=Site.objects.all(),
label=_('Site (ID)'), label=_('Site (ID)'),
) )
site = django_filters.ModelMultipleChoiceFilter( site = django_filters.ModelMultipleChoiceFilter(
field_name='device__site__slug', field_name='_site__slug',
queryset=Site.objects.all(), queryset=Site.objects.all(),
to_field_name='slug', to_field_name='slug',
label=_('Site name (slug)'), label=_('Site name (slug)'),
) )
location_id = django_filters.ModelMultipleChoiceFilter( location_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__location', field_name='_location',
queryset=Location.objects.all(), queryset=Location.objects.all(),
label=_('Location (ID)'), label=_('Location (ID)'),
) )
location = django_filters.ModelMultipleChoiceFilter( location = django_filters.ModelMultipleChoiceFilter(
field_name='device__location__slug', field_name='_location__slug',
queryset=Location.objects.all(), queryset=Location.objects.all(),
to_field_name='slug', to_field_name='slug',
label=_('Location (slug)'), label=_('Location (slug)'),
) )
rack_id = django_filters.ModelMultipleChoiceFilter( rack_id = django_filters.ModelMultipleChoiceFilter(
field_name='device__rack', field_name='_rack',
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
label=_('Rack (ID)'), label=_('Rack (ID)'),
) )
rack = django_filters.ModelMultipleChoiceFilter( rack = django_filters.ModelMultipleChoiceFilter(
field_name='device__rack__name', field_name='_rack__name',
queryset=Rack.objects.all(), queryset=Rack.objects.all(),
to_field_name='name', to_field_name='name',
label=_('Rack (name)'), label=_('Rack (name)'),

View File

@ -0,0 +1,287 @@
import django.db.models.deletion
from django.db import migrations, models
from django.db.models import OuterRef, Subquery
def populate_denormalized_data(apps, schema_editor):
Device = apps.get_model('dcim', 'Device')
component_models = (
apps.get_model('dcim', 'ConsolePort'),
apps.get_model('dcim', 'ConsoleServerPort'),
apps.get_model('dcim', 'PowerPort'),
apps.get_model('dcim', 'PowerOutlet'),
apps.get_model('dcim', 'Interface'),
apps.get_model('dcim', 'FrontPort'),
apps.get_model('dcim', 'RearPort'),
apps.get_model('dcim', 'DeviceBay'),
apps.get_model('dcim', 'ModuleBay'),
apps.get_model('dcim', 'InventoryItem'),
)
for model in component_models:
subquery = Device.objects.filter(pk=OuterRef('device_id'))
model.objects.update(
_site=Subquery(subquery.values('site_id')[:1]),
_location=Subquery(subquery.values('location_id')[:1]),
_rack=Subquery(subquery.values('rack_id')[:1]),
)
class Migration(migrations.Migration):
dependencies = [
('dcim', '0208_devicerole_uniqueness'),
]
operations = [
migrations.AddField(
model_name='consoleport',
name='_location',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+',
to='dcim.location',
),
),
migrations.AddField(
model_name='consoleport',
name='_rack',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
),
),
migrations.AddField(
model_name='consoleport',
name='_site',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
),
),
migrations.AddField(
model_name='consoleserverport',
name='_location',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+',
to='dcim.location',
),
),
migrations.AddField(
model_name='consoleserverport',
name='_rack',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
),
),
migrations.AddField(
model_name='consoleserverport',
name='_site',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
),
),
migrations.AddField(
model_name='devicebay',
name='_location',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+',
to='dcim.location',
),
),
migrations.AddField(
model_name='devicebay',
name='_rack',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
),
),
migrations.AddField(
model_name='devicebay',
name='_site',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
),
),
migrations.AddField(
model_name='frontport',
name='_location',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+',
to='dcim.location',
),
),
migrations.AddField(
model_name='frontport',
name='_rack',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
),
),
migrations.AddField(
model_name='frontport',
name='_site',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
),
),
migrations.AddField(
model_name='interface',
name='_location',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+',
to='dcim.location',
),
),
migrations.AddField(
model_name='interface',
name='_rack',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
),
),
migrations.AddField(
model_name='interface',
name='_site',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
),
),
migrations.AddField(
model_name='inventoryitem',
name='_location',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+',
to='dcim.location',
),
),
migrations.AddField(
model_name='inventoryitem',
name='_rack',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
),
),
migrations.AddField(
model_name='inventoryitem',
name='_site',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
),
),
migrations.AddField(
model_name='modulebay',
name='_location',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+',
to='dcim.location',
),
),
migrations.AddField(
model_name='modulebay',
name='_rack',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
),
),
migrations.AddField(
model_name='modulebay',
name='_site',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
),
),
migrations.AddField(
model_name='poweroutlet',
name='_location',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+',
to='dcim.location',
),
),
migrations.AddField(
model_name='poweroutlet',
name='_rack',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
),
),
migrations.AddField(
model_name='poweroutlet',
name='_site',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
),
),
migrations.AddField(
model_name='powerport',
name='_location',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+',
to='dcim.location',
),
),
migrations.AddField(
model_name='powerport',
name='_rack',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
),
),
migrations.AddField(
model_name='powerport',
name='_site',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
),
),
migrations.AddField(
model_name='rearport',
name='_location',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='+',
to='dcim.location',
),
),
migrations.AddField(
model_name='rearport',
name='_rack',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.rack'
),
),
migrations.AddField(
model_name='rearport',
name='_site',
field=models.ForeignKey(
blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='dcim.site'
),
),
migrations.RunPython(populate_denormalized_data),
]

View File

@ -65,6 +65,29 @@ class ComponentModel(NetBoxModel):
blank=True blank=True
) )
# Denormalized references replicated from the parent Device
_site = models.ForeignKey(
to='dcim.Site',
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True,
)
_location = models.ForeignKey(
to='dcim.Location',
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True,
)
_rack = models.ForeignKey(
to='dcim.Rack',
on_delete=models.SET_NULL,
related_name='+',
blank=True,
null=True,
)
class Meta: class Meta:
abstract = True abstract = True
ordering = ('device', 'name') ordering = ('device', 'name')
@ -100,6 +123,14 @@ class ComponentModel(NetBoxModel):
"device": _("Components cannot be moved to a different device.") "device": _("Components cannot be moved to a different device.")
}) })
def save(self, *args, **kwargs):
# Save denormalized references
self._site = self.device.site
self._location = self.device.location
self._rack = self.device.rack
super().save(*args, **kwargs)
@property @property
def parent_object(self): def parent_object(self):
return self.device return self.device

View File

@ -3,13 +3,28 @@ import logging
from django.db.models.signals import post_save, post_delete, pre_delete from django.db.models.signals import post_save, post_delete, pre_delete
from django.dispatch import receiver from django.dispatch import receiver
from .choices import CableEndChoices, LinkStatusChoices from dcim.choices import CableEndChoices, LinkStatusChoices
from .models import ( from .models import (
Cable, CablePath, CableTermination, Device, FrontPort, PathEndpoint, PowerPanel, Rack, Location, VirtualChassis, Cable, CablePath, CableTermination, ConsolePort, ConsoleServerPort, Device, DeviceBay, FrontPort, Interface,
InventoryItem, ModuleBay, PathEndpoint, PowerOutlet, PowerPanel, PowerPort, Rack, RearPort, Location,
VirtualChassis,
) )
from .models.cables import trace_paths from .models.cables import trace_paths
from .utils import create_cablepath, rebuild_paths from .utils import create_cablepath, rebuild_paths
COMPONENT_MODELS = (
ConsolePort,
ConsoleServerPort,
DeviceBay,
FrontPort,
Interface,
InventoryItem,
ModuleBay,
PowerOutlet,
PowerPort,
RearPort,
)
# #
# Location/rack/device assignment # Location/rack/device assignment
@ -39,6 +54,20 @@ def handle_rack_site_change(instance, created, **kwargs):
Device.objects.filter(rack=instance).update(site=instance.site, location=instance.location) Device.objects.filter(rack=instance).update(site=instance.site, location=instance.location)
@receiver(post_save, sender=Device)
def handle_device_site_change(instance, created, **kwargs):
"""
Update child components to update the parent Site, Location, and Rack when a Device is saved.
"""
if not created:
for model in COMPONENT_MODELS:
model.objects.filter(device=instance).update(
_site=instance.site,
_location=instance.location,
_rack=instance.rack,
)
# #
# Virtual chassis # Virtual chassis
# #

View File

@ -3367,9 +3367,36 @@ class ConsolePortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
ConsoleServerPort.objects.bulk_create(console_server_ports) ConsoleServerPort.objects.bulk_create(console_server_ports)
console_ports = ( console_ports = (
ConsolePort(device=devices[0], module=modules[0], name='Console Port 1', label='A', description='First'), ConsolePort(
ConsolePort(device=devices[1], module=modules[1], name='Console Port 2', label='B', description='Second'), device=devices[0],
ConsolePort(device=devices[2], module=modules[2], name='Console Port 3', label='C', description='Third'), module=modules[0],
name='Console Port 1',
label='A',
description='First',
_site=devices[0].site,
_location=devices[0].location,
_rack=devices[0].rack,
),
ConsolePort(
device=devices[1],
module=modules[1],
name='Console Port 2',
label='B',
description='Second',
_site=devices[1].site,
_location=devices[1].location,
_rack=devices[1].rack,
),
ConsolePort(
device=devices[2],
module=modules[2],
name='Console Port 3',
label='C',
description='Third',
_site=devices[2].site,
_location=devices[2].location,
_rack=devices[2].rack,
),
) )
ConsolePort.objects.bulk_create(console_ports) ConsolePort.objects.bulk_create(console_ports)
@ -3581,13 +3608,34 @@ class ConsoleServerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeL
console_server_ports = ( console_server_ports = (
ConsoleServerPort( ConsoleServerPort(
device=devices[0], module=modules[0], name='Console Server Port 1', label='A', description='First' device=devices[0],
module=modules[0],
name='Console Server Port 1',
label='A',
description='First',
_site=devices[0].site,
_location=devices[0].location,
_rack=devices[0].rack,
), ),
ConsoleServerPort( ConsoleServerPort(
device=devices[1], module=modules[1], name='Console Server Port 2', label='B', description='Second' device=devices[1],
module=modules[1],
name='Console Server Port 2',
label='B',
description='Second',
_site=devices[1].site,
_location=devices[1].location,
_rack=devices[1].rack,
), ),
ConsoleServerPort( ConsoleServerPort(
device=devices[2], module=modules[2], name='Console Server Port 3', label='C', description='Third' device=devices[2],
module=modules[2],
name='Console Server Port 3',
label='C',
description='Third',
_site=devices[2].site,
_location=devices[2].location,
_rack=devices[2].rack,
), ),
) )
ConsoleServerPort.objects.bulk_create(console_server_ports) ConsoleServerPort.objects.bulk_create(console_server_ports)
@ -3807,6 +3855,9 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
maximum_draw=100, maximum_draw=100,
allocated_draw=50, allocated_draw=50,
description='First', description='First',
_site=devices[0].site,
_location=devices[0].location,
_rack=devices[0].rack,
), ),
PowerPort( PowerPort(
device=devices[1], device=devices[1],
@ -3816,6 +3867,9 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
maximum_draw=200, maximum_draw=200,
allocated_draw=100, allocated_draw=100,
description='Second', description='Second',
_site=devices[1].site,
_location=devices[1].location,
_rack=devices[1].rack,
), ),
PowerPort( PowerPort(
device=devices[2], device=devices[2],
@ -3825,6 +3879,9 @@ class PowerPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
maximum_draw=300, maximum_draw=300,
allocated_draw=150, allocated_draw=150,
description='Third', description='Third',
_site=devices[2].site,
_location=devices[2].location,
_rack=devices[2].rack,
), ),
) )
PowerPort.objects.bulk_create(power_ports) PowerPort.objects.bulk_create(power_ports)
@ -4053,6 +4110,9 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
description='First', description='First',
color='ff0000', color='ff0000',
status=PowerOutletStatusChoices.STATUS_ENABLED, status=PowerOutletStatusChoices.STATUS_ENABLED,
_site=devices[0].site,
_location=devices[0].location,
_rack=devices[0].rack,
), ),
PowerOutlet( PowerOutlet(
device=devices[1], device=devices[1],
@ -4063,6 +4123,9 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
description='Second', description='Second',
color='00ff00', color='00ff00',
status=PowerOutletStatusChoices.STATUS_DISABLED, status=PowerOutletStatusChoices.STATUS_DISABLED,
_site=devices[1].site,
_location=devices[1].location,
_rack=devices[1].rack,
), ),
PowerOutlet( PowerOutlet(
device=devices[2], device=devices[2],
@ -4073,6 +4136,9 @@ class PowerOutletTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedF
description='Third', description='Third',
color='0000ff', color='0000ff',
status=PowerOutletStatusChoices.STATUS_FAULTY, status=PowerOutletStatusChoices.STATUS_FAULTY,
_site=devices[2].site,
_location=devices[2].location,
_rack=devices[2].rack,
), ),
) )
PowerOutlet.objects.bulk_create(power_outlets) PowerOutlet.objects.bulk_create(power_outlets)
@ -4381,13 +4447,19 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
poe_mode=InterfacePoEModeChoices.MODE_PSE, poe_mode=InterfacePoEModeChoices.MODE_PSE,
poe_type=InterfacePoETypeChoices.TYPE_1_8023AF, poe_type=InterfacePoETypeChoices.TYPE_1_8023AF,
vlan_translation_policy=vlan_translation_policies[0], vlan_translation_policy=vlan_translation_policies[0],
_site=devices[0].site,
_location=devices[0].location,
_rack=devices[0].rack,
), ),
Interface( Interface(
device=devices[1], device=devices[1],
module=modules[1], module=modules[1],
name='VC Chassis Interface', name='VC Chassis Interface',
type=InterfaceTypeChoices.TYPE_1GE_SFP, type=InterfaceTypeChoices.TYPE_1GE_SFP,
enabled=True enabled=True,
_site=devices[1].site,
_location=devices[1].location,
_rack=devices[1].rack,
), ),
Interface( Interface(
device=devices[2], device=devices[2],
@ -4406,6 +4478,9 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
poe_mode=InterfacePoEModeChoices.MODE_PD, poe_mode=InterfacePoEModeChoices.MODE_PD,
poe_type=InterfacePoETypeChoices.TYPE_1_8023AF, poe_type=InterfacePoETypeChoices.TYPE_1_8023AF,
vlan_translation_policy=vlan_translation_policies[0], vlan_translation_policy=vlan_translation_policies[0],
_site=devices[2].site,
_location=devices[2].location,
_rack=devices[2].rack,
), ),
Interface( Interface(
device=devices[3], device=devices[3],
@ -4424,6 +4499,9 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
poe_mode=InterfacePoEModeChoices.MODE_PSE, poe_mode=InterfacePoEModeChoices.MODE_PSE,
poe_type=InterfacePoETypeChoices.TYPE_2_8023AT, poe_type=InterfacePoETypeChoices.TYPE_2_8023AT,
vlan_translation_policy=vlan_translation_policies[1], vlan_translation_policy=vlan_translation_policies[1],
_site=devices[3].site,
_location=devices[3].location,
_rack=devices[3].rack,
), ),
Interface( Interface(
device=devices[4], device=devices[4],
@ -4440,6 +4518,9 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
mode=InterfaceModeChoices.MODE_Q_IN_Q, mode=InterfaceModeChoices.MODE_Q_IN_Q,
qinq_svlan=vlans[0], qinq_svlan=vlans[0],
vlan_translation_policy=vlan_translation_policies[1], vlan_translation_policy=vlan_translation_policies[1],
_site=devices[4].site,
_location=devices[4].location,
_rack=devices[4].rack,
), ),
Interface( Interface(
device=devices[4], device=devices[4],
@ -4450,7 +4531,10 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
mgmt_only=True, mgmt_only=True,
tx_power=40, tx_power=40,
mode=InterfaceModeChoices.MODE_Q_IN_Q, mode=InterfaceModeChoices.MODE_Q_IN_Q,
qinq_svlan=vlans[1] qinq_svlan=vlans[1],
_site=devices[4].site,
_location=devices[4].location,
_rack=devices[4].rack,
), ),
Interface( Interface(
device=devices[4], device=devices[4],
@ -4461,7 +4545,10 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
mgmt_only=False, mgmt_only=False,
tx_power=40, tx_power=40,
mode=InterfaceModeChoices.MODE_Q_IN_Q, mode=InterfaceModeChoices.MODE_Q_IN_Q,
qinq_svlan=vlans[2] qinq_svlan=vlans[2],
_site=devices[4].site,
_location=devices[4].location,
_rack=devices[4].rack,
), ),
Interface( Interface(
device=devices[4], device=devices[4],
@ -4470,7 +4557,10 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
rf_role=WirelessRoleChoices.ROLE_AP, rf_role=WirelessRoleChoices.ROLE_AP,
rf_channel=WirelessChannelChoices.CHANNEL_24G_1, rf_channel=WirelessChannelChoices.CHANNEL_24G_1,
rf_channel_frequency=2412, rf_channel_frequency=2412,
rf_channel_width=22 rf_channel_width=22,
_site=devices[4].site,
_location=devices[4].location,
_rack=devices[4].rack,
), ),
Interface( Interface(
device=devices[4], device=devices[4],
@ -4479,7 +4569,10 @@ class InterfaceTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
rf_role=WirelessRoleChoices.ROLE_STATION, rf_role=WirelessRoleChoices.ROLE_STATION,
rf_channel=WirelessChannelChoices.CHANNEL_5G_32, rf_channel=WirelessChannelChoices.CHANNEL_5G_32,
rf_channel_frequency=5160, rf_channel_frequency=5160,
rf_channel_width=20 rf_channel_width=20,
_site=devices[4].site,
_location=devices[4].location,
_rack=devices[4].rack,
), ),
) )
Interface.objects.bulk_create(interfaces) Interface.objects.bulk_create(interfaces)
@ -4906,6 +4999,9 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
rear_port=rear_ports[0], rear_port=rear_ports[0],
rear_port_position=1, rear_port_position=1,
description='First', description='First',
_site=devices[0].site,
_location=devices[0].location,
_rack=devices[0].rack,
), ),
FrontPort( FrontPort(
device=devices[1], device=devices[1],
@ -4917,6 +5013,9 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
rear_port=rear_ports[1], rear_port=rear_ports[1],
rear_port_position=2, rear_port_position=2,
description='Second', description='Second',
_site=devices[1].site,
_location=devices[1].location,
_rack=devices[1].rack,
), ),
FrontPort( FrontPort(
device=devices[2], device=devices[2],
@ -4928,6 +5027,9 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
rear_port=rear_ports[2], rear_port=rear_ports[2],
rear_port_position=3, rear_port_position=3,
description='Third', description='Third',
_site=devices[2].site,
_location=devices[2].location,
_rack=devices[2].rack,
), ),
FrontPort( FrontPort(
device=devices[3], device=devices[3],
@ -4936,6 +5038,9 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
type=PortTypeChoices.TYPE_FC, type=PortTypeChoices.TYPE_FC,
rear_port=rear_ports[3], rear_port=rear_ports[3],
rear_port_position=1, rear_port_position=1,
_site=devices[3].site,
_location=devices[3].location,
_rack=devices[3].rack,
), ),
FrontPort( FrontPort(
device=devices[3], device=devices[3],
@ -4944,6 +5049,9 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
type=PortTypeChoices.TYPE_FC, type=PortTypeChoices.TYPE_FC,
rear_port=rear_ports[4], rear_port=rear_ports[4],
rear_port_position=1, rear_port_position=1,
_site=devices[3].site,
_location=devices[3].location,
_rack=devices[3].rack,
), ),
FrontPort( FrontPort(
device=devices[3], device=devices[3],
@ -4952,6 +5060,9 @@ class FrontPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
type=PortTypeChoices.TYPE_FC, type=PortTypeChoices.TYPE_FC,
rear_port=rear_ports[5], rear_port=rear_ports[5],
rear_port_position=1, rear_port_position=1,
_site=devices[3].site,
_location=devices[3].location,
_rack=devices[3].rack,
), ),
) )
FrontPort.objects.bulk_create(front_ports) FrontPort.objects.bulk_create(front_ports)
@ -5168,6 +5279,9 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt
color=ColorChoices.COLOR_RED, color=ColorChoices.COLOR_RED,
positions=1, positions=1,
description='First', description='First',
_site=devices[0].site,
_location=devices[0].location,
_rack=devices[0].rack,
), ),
RearPort( RearPort(
device=devices[1], device=devices[1],
@ -5178,6 +5292,9 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt
color=ColorChoices.COLOR_GREEN, color=ColorChoices.COLOR_GREEN,
positions=2, positions=2,
description='Second', description='Second',
_site=devices[1].site,
_location=devices[1].location,
_rack=devices[1].rack,
), ),
RearPort( RearPort(
device=devices[2], device=devices[2],
@ -5188,10 +5305,40 @@ class RearPortTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFilt
color=ColorChoices.COLOR_BLUE, color=ColorChoices.COLOR_BLUE,
positions=3, positions=3,
description='Third', description='Third',
_site=devices[2].site,
_location=devices[2].location,
_rack=devices[2].rack,
),
RearPort(
device=devices[3],
name='Rear Port 4',
label='D',
type=PortTypeChoices.TYPE_FC,
positions=4,
_site=devices[3].site,
_location=devices[3].location,
_rack=devices[3].rack,
),
RearPort(
device=devices[3],
name='Rear Port 5',
label='E',
type=PortTypeChoices.TYPE_FC,
positions=5,
_site=devices[3].site,
_location=devices[3].location,
_rack=devices[3].rack,
),
RearPort(
device=devices[3],
name='Rear Port 6',
label='F',
type=PortTypeChoices.TYPE_FC,
positions=6,
_site=devices[3].site,
_location=devices[3].location,
_rack=devices[3].rack,
), ),
RearPort(device=devices[3], name='Rear Port 4', label='D', type=PortTypeChoices.TYPE_FC, positions=4),
RearPort(device=devices[3], name='Rear Port 5', label='E', type=PortTypeChoices.TYPE_FC, positions=5),
RearPort(device=devices[3], name='Rear Port 6', label='F', type=PortTypeChoices.TYPE_FC, positions=6),
) )
RearPort.objects.bulk_create(rear_ports) RearPort.objects.bulk_create(rear_ports)
@ -5550,9 +5697,33 @@ class DeviceBayTestCase(TestCase, DeviceComponentFilterSetTests, ChangeLoggedFil
Device.objects.bulk_create(devices) Device.objects.bulk_create(devices)
device_bays = ( device_bays = (
DeviceBay(device=devices[0], name='Device Bay 1', label='A', description='First'), DeviceBay(
DeviceBay(device=devices[1], name='Device Bay 2', label='B', description='Second'), device=devices[0],
DeviceBay(device=devices[2], name='Device Bay 3', label='C', description='Third'), name='Device Bay 1',
label='A',
description='First',
_site=devices[0].site,
_location=devices[0].location,
_rack=devices[0].rack,
),
DeviceBay(
device=devices[1],
name='Device Bay 2',
label='B',
description='Second',
_site=devices[1].site,
_location=devices[1].location,
_rack=devices[1].rack,
),
DeviceBay(
device=devices[2],
name='Device Bay 3',
label='C',
description='Third',
_site=devices[2].site,
_location=devices[2].location,
_rack=devices[2].rack,
),
) )
DeviceBay.objects.bulk_create(device_bays) DeviceBay.objects.bulk_create(device_bays)

View File

@ -470,6 +470,9 @@ class APIViewTestCases:
elif type(field.type) is StrawberryOptional and type(field.type.of_type) is LazyType: elif type(field.type) is StrawberryOptional and type(field.type.of_type) is LazyType:
fields_string += f'{field.name} {{ id }}\n' fields_string += f'{field.name} {{ id }}\n'
elif hasattr(field, 'is_relation') and field.is_relation: elif hasattr(field, 'is_relation') and field.is_relation:
# Ignore private fields
if field.name.startswith('_'):
continue
# Note: StrawberryField types do not have is_relation # Note: StrawberryField types do not have is_relation
fields_string += f'{field.name} {{ id }}\n' fields_string += f'{field.name} {{ id }}\n'
elif inspect.isclass(field.type) and issubclass(field.type, IPAddressFamilyType): elif inspect.isclass(field.type) and issubclass(field.type, IPAddressFamilyType):