From 31d76965509fe065f1f6bcc5e47f81cc42aaba6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20St=C3=A9phenne?= Date: Tue, 24 Mar 2026 14:12:32 -0400 Subject: [PATCH 1/7] Added device information in rack position select --- netbox/dcim/api/serializers_/rackunits.py | 13 +++++++++++- netbox/dcim/tests/test_api.py | 26 +++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/netbox/dcim/api/serializers_/rackunits.py b/netbox/dcim/api/serializers_/rackunits.py index 1f53067185a..c074e3647f8 100644 --- a/netbox/dcim/api/serializers_/rackunits.py +++ b/netbox/dcim/api/serializers_/rackunits.py @@ -1,6 +1,7 @@ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework import serializers +from django.utils.translation import gettext as _ from dcim.choices import * from netbox.api.fields import ChoiceField @@ -25,7 +26,17 @@ class RackUnitSerializer(serializers.Serializer): device = DeviceSerializer(nested=True, read_only=True) occupied = serializers.BooleanField(read_only=True) display = serializers.SerializerMethodField(read_only=True) + description = serializers.SerializerMethodField(read_only=True) @extend_schema_field(OpenApiTypes.STR) def get_display(self, obj): - return obj['name'] + display = obj['name'] + if obj['occupied'] and (device := obj['device']): + return _('{rack_unit} - {device}').format(rack_unit=display, device=device) + + return display + + @extend_schema_field(OpenApiTypes.STR) + def get_description(self, obj): + if obj['occupied'] and (device := obj['device']): + return str(device.device_type) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 1c4497e9abf..4290a9affaa 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -451,6 +451,32 @@ def test_get_rack_elevation(self): response = self.client.get(f'{url}?q=U10', **self.header) self.assertEqual(response.data['count'], 2) + def test_get_rack_elevation_display_includes_occupying_device(self): + """ + Verify occupied rack units include the occupying device in their display label. + """ + rack = Rack.objects.first() + device = create_test_device( + name='Device A', + site=rack.site, + rack=rack, + position=10, + face=DeviceFaceChoices.FACE_FRONT, + ) + + self.add_permissions('dcim.view_rack', 'dcim.view_device') + url = reverse('dcim-api:rack-elevation', kwargs={'pk': rack.pk}) + response = self.client.get(url, **self.header) + self.assertHttpStatus(response, status.HTTP_200_OK) + + occupied_unit = next(unit for unit in response.data['results'] if unit['name'] == 'U10') + unoccupied_unit = next(unit for unit in response.data['results'] if unit['name'] == 'U11') + + self.assertEqual(occupied_unit['device']['id'], device.pk) + self.assertEqual(occupied_unit['display'], f'U10 - {device}') + self.assertEqual(occupied_unit['description'], str(device.device_type)) + self.assertEqual(unoccupied_unit['display'], 'U11') + def test_get_rack_elevation_svg(self): """ GET a single rack elevation in SVG format. From 0f7b174ae7d6475cd82f6f24d4e63e4592ff46da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20St=C3=A9phenne?= Date: Thu, 9 Apr 2026 11:24:58 -0400 Subject: [PATCH 2/7] Added device information to additionnal fields on the serializer --- netbox/dcim/api/serializers_/rackunits.py | 16 +++++++++------- netbox/dcim/forms/model_forms.py | 2 ++ netbox/dcim/tests/test_api.py | 8 ++++++-- 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/netbox/dcim/api/serializers_/rackunits.py b/netbox/dcim/api/serializers_/rackunits.py index c074e3647f8..e48b111a524 100644 --- a/netbox/dcim/api/serializers_/rackunits.py +++ b/netbox/dcim/api/serializers_/rackunits.py @@ -1,7 +1,7 @@ +from django.utils.translation import gettext as _ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework import serializers -from django.utils.translation import gettext as _ from dcim.choices import * from netbox.api.fields import ChoiceField @@ -26,17 +26,19 @@ class RackUnitSerializer(serializers.Serializer): device = DeviceSerializer(nested=True, read_only=True) occupied = serializers.BooleanField(read_only=True) display = serializers.SerializerMethodField(read_only=True) - description = serializers.SerializerMethodField(read_only=True) + device_display = serializers.SerializerMethodField(read_only=True) + device_type = serializers.SerializerMethodField(read_only=True) @extend_schema_field(OpenApiTypes.STR) def get_display(self, obj): - display = obj['name'] - if obj['occupied'] and (device := obj['device']): - return _('{rack_unit} - {device}').format(rack_unit=display, device=device) + return obj['name'] - return display + @extend_schema_field(OpenApiTypes.STR) + def get_device_display(self, obj): + if obj['occupied'] and (device := obj['device']): + return _('{rack_unit} - {device}').format(rack_unit=obj['name'], device=device) @extend_schema_field(OpenApiTypes.STR) - def get_description(self, obj): + def get_device_type(self, obj): if obj['occupied'] and (device := obj['device']): return str(device.device_type) diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index 13d5f512b01..a0e82b22c82 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -590,6 +590,8 @@ class DeviceForm(TenancyForm, PrimaryModelForm): api_url='/api/dcim/racks/{{rack}}/elevation/', attrs={ 'ts-disabled-field': 'device', + 'ts-label-field': 'device_display', + 'ts-description-field': 'device_type', 'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]' }, ) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 4290a9affaa..5a4c9396da3 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -473,9 +473,13 @@ def test_get_rack_elevation_display_includes_occupying_device(self): unoccupied_unit = next(unit for unit in response.data['results'] if unit['name'] == 'U11') self.assertEqual(occupied_unit['device']['id'], device.pk) - self.assertEqual(occupied_unit['display'], f'U10 - {device}') - self.assertEqual(occupied_unit['description'], str(device.device_type)) + self.assertEqual(occupied_unit['display'], 'U10') + self.assertEqual(occupied_unit['device_display'], f'U10 - {device}') + self.assertEqual(occupied_unit['device_type'], str(device.device_type)) + self.assertEqual(unoccupied_unit['device'], None) self.assertEqual(unoccupied_unit['display'], 'U11') + self.assertEqual(unoccupied_unit['device_display'], None) + self.assertEqual(unoccupied_unit['device_type'], None) def test_get_rack_elevation_svg(self): """ From 2db290fa402802635009e147a291223f9b3a4b23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20St=C3=A9phenne?= Date: Thu, 9 Apr 2026 11:26:21 -0400 Subject: [PATCH 3/7] Added missing name fallback --- netbox/dcim/api/serializers_/rackunits.py | 1 + 1 file changed, 1 insertion(+) diff --git a/netbox/dcim/api/serializers_/rackunits.py b/netbox/dcim/api/serializers_/rackunits.py index e48b111a524..2ccefb8c33d 100644 --- a/netbox/dcim/api/serializers_/rackunits.py +++ b/netbox/dcim/api/serializers_/rackunits.py @@ -37,6 +37,7 @@ def get_display(self, obj): def get_device_display(self, obj): if obj['occupied'] and (device := obj['device']): return _('{rack_unit} - {device}').format(rack_unit=obj['name'], device=device) + return obj['name'] @extend_schema_field(OpenApiTypes.STR) def get_device_type(self, obj): From 53ae71d8e5e2320d03d32c7678f12861e2b164c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20St=C3=A9phenne?= Date: Mon, 11 May 2026 22:20:13 -0400 Subject: [PATCH 4/7] Applied pheus' recommanded implementation! --- netbox/dcim/api/serializers_/rackunits.py | 17 ++++------------- netbox/dcim/forms/model_forms.py | 2 -- netbox/dcim/tests/test_api.py | 6 ++---- 3 files changed, 6 insertions(+), 19 deletions(-) diff --git a/netbox/dcim/api/serializers_/rackunits.py b/netbox/dcim/api/serializers_/rackunits.py index 2ccefb8c33d..55aec349d92 100644 --- a/netbox/dcim/api/serializers_/rackunits.py +++ b/netbox/dcim/api/serializers_/rackunits.py @@ -1,4 +1,3 @@ -from django.utils.translation import gettext as _ from drf_spectacular.types import OpenApiTypes from drf_spectacular.utils import extend_schema_field from rest_framework import serializers @@ -26,20 +25,12 @@ class RackUnitSerializer(serializers.Serializer): device = DeviceSerializer(nested=True, read_only=True) occupied = serializers.BooleanField(read_only=True) display = serializers.SerializerMethodField(read_only=True) - device_display = serializers.SerializerMethodField(read_only=True) - device_type = serializers.SerializerMethodField(read_only=True) + description = serializers.SerializerMethodField(read_only=True) @extend_schema_field(OpenApiTypes.STR) def get_display(self, obj): return obj['name'] - @extend_schema_field(OpenApiTypes.STR) - def get_device_display(self, obj): - if obj['occupied'] and (device := obj['device']): - return _('{rack_unit} - {device}').format(rack_unit=obj['name'], device=device) - return obj['name'] - - @extend_schema_field(OpenApiTypes.STR) - def get_device_type(self, obj): - if obj['occupied'] and (device := obj['device']): - return str(device.device_type) + @extend_schema_field(OpenApiTypes.STR) + def get_description(self, obj): + return f'{obj["device"]}' if obj['device'] else None diff --git a/netbox/dcim/forms/model_forms.py b/netbox/dcim/forms/model_forms.py index a0e82b22c82..13d5f512b01 100644 --- a/netbox/dcim/forms/model_forms.py +++ b/netbox/dcim/forms/model_forms.py @@ -590,8 +590,6 @@ class DeviceForm(TenancyForm, PrimaryModelForm): api_url='/api/dcim/racks/{{rack}}/elevation/', attrs={ 'ts-disabled-field': 'device', - 'ts-label-field': 'device_display', - 'ts-description-field': 'device_type', 'data-dynamic-params': '[{"fieldName":"face","queryParam":"face"}]' }, ) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 5a4c9396da3..656aef9efa5 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -474,12 +474,10 @@ def test_get_rack_elevation_display_includes_occupying_device(self): self.assertEqual(occupied_unit['device']['id'], device.pk) self.assertEqual(occupied_unit['display'], 'U10') - self.assertEqual(occupied_unit['device_display'], f'U10 - {device}') - self.assertEqual(occupied_unit['device_type'], str(device.device_type)) + self.assertEqual(occupied_unit['description'], f'{device}') self.assertEqual(unoccupied_unit['device'], None) self.assertEqual(unoccupied_unit['display'], 'U11') - self.assertEqual(unoccupied_unit['device_display'], None) - self.assertEqual(unoccupied_unit['device_type'], None) + self.assertEqual(unoccupied_unit['description'], None) def test_get_rack_elevation_svg(self): """ From a58f96b35c4d6c66e43a122d6757082954409f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20St=C3=A9phenne?= Date: Wed, 13 May 2026 10:45:24 -0400 Subject: [PATCH 5/7] Fix: test is working --- netbox/dcim/api/serializers_/rackunits.py | 6 +++--- netbox/dcim/tests/test_api.py | 20 ++++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/netbox/dcim/api/serializers_/rackunits.py b/netbox/dcim/api/serializers_/rackunits.py index 55aec349d92..e65e04915d1 100644 --- a/netbox/dcim/api/serializers_/rackunits.py +++ b/netbox/dcim/api/serializers_/rackunits.py @@ -31,6 +31,6 @@ class RackUnitSerializer(serializers.Serializer): def get_display(self, obj): return obj['name'] - @extend_schema_field(OpenApiTypes.STR) - def get_description(self, obj): - return f'{obj["device"]}' if obj['device'] else None + @extend_schema_field(OpenApiTypes.STR) + def get_description(self, obj): + return f'{obj["device"]}' if obj['device'] else None diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 656aef9efa5..2376d966b5d 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -451,32 +451,32 @@ def test_get_rack_elevation(self): response = self.client.get(f'{url}?q=U10', **self.header) self.assertEqual(response.data['count'], 2) - def test_get_rack_elevation_display_includes_occupying_device(self): + def test_get_rack_elevation_description_is_occupying_device_name(self): """ Verify occupied rack units include the occupying device in their display label. """ rack = Rack.objects.first() + self.add_permissions('dcim.view_rack', 'dcim.view_device') + url = reverse('dcim-api:rack-elevation', kwargs={'pk': rack.pk}) + device = create_test_device( name='Device A', site=rack.site, rack=rack, - position=10, + position=40, face=DeviceFaceChoices.FACE_FRONT, ) - self.add_permissions('dcim.view_rack', 'dcim.view_device') - url = reverse('dcim-api:rack-elevation', kwargs={'pk': rack.pk}) + # Retrieve all units response = self.client.get(url, **self.header) self.assertHttpStatus(response, status.HTTP_200_OK) - occupied_unit = next(unit for unit in response.data['results'] if unit['name'] == 'U10') - unoccupied_unit = next(unit for unit in response.data['results'] if unit['name'] == 'U11') - + occupied_unit = next(unit for unit in response.data['results'] if unit['name'] == 'U40') self.assertEqual(occupied_unit['device']['id'], device.pk) - self.assertEqual(occupied_unit['display'], 'U10') self.assertEqual(occupied_unit['description'], f'{device}') - self.assertEqual(unoccupied_unit['device'], None) - self.assertEqual(unoccupied_unit['display'], 'U11') + + unoccupied_unit = next(unit for unit in response.data['results'] if unit['name'] == 'U39') + self.assertEqual(occupied_unit['device'], None) self.assertEqual(unoccupied_unit['description'], None) def test_get_rack_elevation_svg(self): From 360fb4d2cecc7de6b0753109711feb24960f738b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20St=C3=A9phenne?= Date: Wed, 13 May 2026 14:54:11 -0400 Subject: [PATCH 6/7] Fix: typo --- netbox/dcim/tests/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 2376d966b5d..8659daa98d3 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -476,7 +476,7 @@ def test_get_rack_elevation_description_is_occupying_device_name(self): self.assertEqual(occupied_unit['description'], f'{device}') unoccupied_unit = next(unit for unit in response.data['results'] if unit['name'] == 'U39') - self.assertEqual(occupied_unit['device'], None) + self.assertEqual(unoccupied_unit['device'], None) self.assertEqual(unoccupied_unit['description'], None) def test_get_rack_elevation_svg(self): From d1f5001132df4a7fc70bacf94dae7ad17f1341bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Laurent=20St=C3=A9phenne?= Date: Wed, 13 May 2026 15:42:14 -0400 Subject: [PATCH 7/7] Fix: Docstring typo Co-authored-by: Martin Hauser --- netbox/dcim/tests/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netbox/dcim/tests/test_api.py b/netbox/dcim/tests/test_api.py index 8659daa98d3..d57ea88aaf0 100644 --- a/netbox/dcim/tests/test_api.py +++ b/netbox/dcim/tests/test_api.py @@ -453,7 +453,7 @@ def test_get_rack_elevation(self): def test_get_rack_elevation_description_is_occupying_device_name(self): """ - Verify occupied rack units include the occupying device in their display label. + Verify occupied rack units include the occupying device in their description. """ rack = Rack.objects.first() self.add_permissions('dcim.view_rack', 'dcim.view_device')