diff --git a/netbox/dcim/api/serializers.py b/netbox/dcim/api/serializers.py index 84a08107b..5c51b75e9 100644 --- a/netbox/dcim/api/serializers.py +++ b/netbox/dcim/api/serializers.py @@ -171,6 +171,16 @@ class RackReservationSerializer(ValidatedModelSerializer): fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description'] +class RackElevationDetailFilterSerializer(serializers.Serializer): + face = serializers.ChoiceField(choices=['front', 'rear'], default='front') + render_format = serializers.ChoiceField(choices=['json', 'svg'], default='json') + width = serializers.IntegerField(default=230) + unit_height = serializers.IntegerField(default=20) + exclude = serializers.IntegerField(required=False, default=None) + q = serializers.CharField(required=False, default=None) + expand_devices = serializers.BooleanField(required=False, default=True) + + # # Device types # diff --git a/netbox/dcim/api/views.py b/netbox/dcim/api/views.py index 268ff1804..13d12bab7 100644 --- a/netbox/dcim/api/views.py +++ b/netbox/dcim/api/views.py @@ -176,11 +176,13 @@ class RackViewSet(CustomFieldModelViewSet): serializer_class = serializers.RackSerializer filterset_class = filters.RackFilter + @swagger_auto_schema(deprecated=True) @action(detail=True) def units(self, request, pk=None): """ List rack units (by rack) """ + # TODO: Remove this action detail route in v2.8 rack = get_object_or_404(Rack, pk=pk) face = request.GET.get('face', 'front') exclude_pk = request.GET.get('exclude', None) @@ -201,53 +203,33 @@ class RackViewSet(CustomFieldModelViewSet): rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request}) return self.get_paginated_response(rack_units.data) - @swagger_auto_schema(responses={200: serializers.RackUnitSerializer(many=True)}) + @swagger_auto_schema( + responses={200: serializers.RackUnitSerializer(many=True)}, + query_serializer=serializers.RackElevationDetailFilterSerializer + ) @action(detail=True) def elevation(self, request, pk=None): """ Rack elevation representing the list of rack units. Also supports rendering the elevation as an SVG. """ rack = get_object_or_404(Rack, pk=pk) - face = request.GET.get('face') - if face not in ['front', 'rear']: - face = 'front' - - if request.GET.get('render_format', 'json') == 'svg': - # Render the elevantion as an SVG - width = request.GET.get('width', 230) - try: - width = int(width) - except ValueError: - return HttpResponseBadRequest('width must be an integer.') - - unit_height = request.GET.get('unit_height', 20) - try: - unit_height = int(unit_height) - except ValueError: - return HttpResponseBadRequest('unit_height must be numeric.') - - drawing = rack.get_elevation_svg(face, width, unit_height) + serializer = serializers.RackElevationDetailFilterSerializer(data=request.GET) + if not serializer.is_valid(): + return Response(serializer.errors, 400) + data = serializer.validated_data + if data['render_format'] == 'svg': + drawing = rack.get_elevation_svg(data['face'], data['width'], data['unit_height']) return HttpResponse(drawing.tostring(), content_type='image/svg+xml') else: - # Render a JSON response of the elevation - exclude = request.GET.get('exclude', None) - if exclude is not None: - try: - if isinstance(exclude, list): - exclude = [int(item) for item in exclude] - else: - exclude = int(exclude) - except ValueError: - exclude = None - - elevation = rack.get_rack_units(face, exclude) - - # Enable filtering rack units by ID - q = request.GET.get('q', None) - if q: - elevation = [u for u in elevation if q in str(u['id'])] + elevation = rack.get_rack_units( + face=data['face'], + exclude=data['exclude'], + expand_devices=data['expand_devices'] + ) + if data['q']: + elevation = [u for u in elevation if data['q'] in str(u['id'])] page = self.paginate_queryset(elevation) if page is not None: diff --git a/netbox/dcim/forms.py b/netbox/dcim/forms.py index 9a836326a..92247af29 100644 --- a/netbox/dcim/forms.py +++ b/netbox/dcim/forms.py @@ -1475,7 +1475,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm): empty_value=None, help_text="The lowest-numbered unit occupied by the device", widget=APISelect( - api_url='/api/dcim/racks/{{rack}}/units/', + api_url='/api/dcim/racks/{{rack}}/elevation/', disabled_indicator='device' ) ) diff --git a/netbox/dcim/models.py b/netbox/dcim/models.py index 0ef98d36e..f7f56af27 100644 --- a/netbox/dcim/models.py +++ b/netbox/dcim/models.py @@ -526,17 +526,8 @@ class RackElevationHelperMixin: # Loop through all units in the elevation unit = elevation[unit_cursor] device = unit['device'] - if device: - # Look ahead to get the total device height - height = 0 - look_ahead_unit_cursor = unit_cursor - while elevation[look_ahead_unit_cursor]['device'] == device and look_ahead_unit_cursor < total_units: - height += 1 - look_ahead_unit_cursor += 1 - else: - # Empty unit - height = 1 - + height = unit.get('height', 1) + # Setup drawing cordinates start_y = unit_cursor * unit_height end_y = unit_height * height @@ -567,13 +558,16 @@ class RackElevationHelperMixin: return drawing - def get_rack_units(self, face=DeviceFaceChoices.FACE_FRONT, exclude=None): + def get_rack_units(self, face=DeviceFaceChoices.FACE_FRONT, exclude=None, expand_devices=True): """ Return a list of rack units as dictionaries. Example: {'device': None, 'face': 0, 'id': 48, 'name': 'U48'} Each key 'device' is either a Device or None. By default, multi-U devices are repeated for each U they occupy. :param face: Rack face (front or rear) :param exclude: PK of a Device to exclude (optional); helpful when relocating a Device within a Rack + :param expand_devices: When True, all units that a device occupies will be listed with each containing a + reference to the device. When False, only the bottom most unit for a device is included and that unit + contains a height attribute for the device """ elevation = OrderedDict() @@ -582,13 +576,29 @@ class RackElevationHelperMixin: # Add devices to rack units list if self.pk: - for device in Device.objects.prefetch_related('device_type__manufacturer', 'device_role')\ - .annotate(devicebay_count=Count('device_bays'))\ - .exclude(pk=exclude)\ - .filter(rack=self, position__gt=0)\ - .filter(Q(face=face) | Q(device_type__is_full_depth=True)): - for u in range(device.position, device.position + device.device_type.u_height): - elevation[u]['device'] = device + queryset = Device.objects.prefetch_related( + 'device_type', + 'device_type__manufacturer', + 'device_role' + ).annotate( + devicebay_count=Count('device_bays') + ).exclude( + pk=exclude + ).filter( + rack=self, + position__gt=0 + ).filter( + Q(face=face) | Q(device_type__is_full_depth=True) + ) + for device in queryset: + if expand_devices: + for u in range(device.position, device.position + device.device_type.u_height): + elevation[u]['device'] = device + else: + elevation[device.position]['device'] = device + elevation[device.position]['height'] = device.device_type.u_height + for u in range(device.position + 1, device.position + device.device_type.u_height): + elevation.pop(u, None) return [u for u in elevation.values()] @@ -646,7 +656,7 @@ class RackElevationHelperMixin: :param unit_height: Height of each rack unit for the rendered drawing. Note this is not the total height of the elevation """ - elevation = self.get_rack_units(face=face) + elevation = self.get_rack_units(face=face, expand_devices=False) reserved_units = self.get_reserved_units().keys() return self._draw_elevations(elevation, reserved_units, face, width, unit_height) diff --git a/netbox/templates/dcim/inc/rack_elevation.html b/netbox/templates/dcim/inc/rack_elevation.html index 1334465b3..8f8204f9e 100644 --- a/netbox/templates/dcim/inc/rack_elevation.html +++ b/netbox/templates/dcim/inc/rack_elevation.html @@ -8,6 +8,6 @@