Просмотр исходного кода

drf-yasg updates for rack elevations

John Anderson 6 лет назад
Родитель
Сommit
b4d724b5ae

+ 10 - 0
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
 #

+ 19 - 37
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:

+ 1 - 1
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'
         )
     )

+ 30 - 20
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)

+ 1 - 1
netbox/templates/dcim/inc/rack_elevation.html

@@ -8,6 +8,6 @@
 
 <div class="rack_frame">
 
-  <object data="{% url 'dcim-api:rack-elevation-detail' pk=rack.pk %}?face={{face}}"></object>
+  <object data="{% url 'dcim-api:rack-elevation' pk=rack.pk %}?face={{face}}&render_format=svg"></object>
 
 </div>