فهرست منبع

drf-yasg updates for rack elevations

John Anderson 6 سال پیش
والد
کامیت
b4d724b5ae
5فایلهای تغییر یافته به همراه61 افزوده شده و 59 حذف شده
  1. 10 0
      netbox/dcim/api/serializers.py
  2. 19 37
      netbox/dcim/api/views.py
  3. 1 1
      netbox/dcim/forms.py
  4. 30 20
      netbox/dcim/models.py
  5. 1 1
      netbox/templates/dcim/inc/rack_elevation.html

+ 10 - 0
netbox/dcim/api/serializers.py

@@ -171,6 +171,16 @@ class RackReservationSerializer(ValidatedModelSerializer):
         fields = ['id', 'rack', 'units', 'created', 'user', 'tenant', 'description']
         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
 # Device types
 #
 #

+ 19 - 37
netbox/dcim/api/views.py

@@ -176,11 +176,13 @@ class RackViewSet(CustomFieldModelViewSet):
     serializer_class = serializers.RackSerializer
     serializer_class = serializers.RackSerializer
     filterset_class = filters.RackFilter
     filterset_class = filters.RackFilter
 
 
+    @swagger_auto_schema(deprecated=True)
     @action(detail=True)
     @action(detail=True)
     def units(self, request, pk=None):
     def units(self, request, pk=None):
         """
         """
         List rack units (by rack)
         List rack units (by rack)
         """
         """
+        # TODO: Remove this action detail route in v2.8
         rack = get_object_or_404(Rack, pk=pk)
         rack = get_object_or_404(Rack, pk=pk)
         face = request.GET.get('face', 'front')
         face = request.GET.get('face', 'front')
         exclude_pk = request.GET.get('exclude', None)
         exclude_pk = request.GET.get('exclude', None)
@@ -201,53 +203,33 @@ class RackViewSet(CustomFieldModelViewSet):
             rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
             rack_units = serializers.RackUnitSerializer(page, many=True, context={'request': request})
             return self.get_paginated_response(rack_units.data)
             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)
     @action(detail=True)
     def elevation(self, request, pk=None):
     def elevation(self, request, pk=None):
         """
         """
         Rack elevation representing the list of rack units. Also supports rendering the elevation as an SVG.
         Rack elevation representing the list of rack units. Also supports rendering the elevation as an SVG.
         """
         """
         rack = get_object_or_404(Rack, pk=pk)
         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')
             return HttpResponse(drawing.tostring(), content_type='image/svg+xml')
 
 
         else:
         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)
             page = self.paginate_queryset(elevation)
             if page is not None:
             if page is not None:

+ 1 - 1
netbox/dcim/forms.py

@@ -1475,7 +1475,7 @@ class DeviceForm(BootstrapMixin, TenancyForm, CustomFieldForm):
         empty_value=None,
         empty_value=None,
         help_text="The lowest-numbered unit occupied by the device",
         help_text="The lowest-numbered unit occupied by the device",
         widget=APISelect(
         widget=APISelect(
-            api_url='/api/dcim/racks/{{rack}}/units/',
+            api_url='/api/dcim/racks/{{rack}}/elevation/',
             disabled_indicator='device'
             disabled_indicator='device'
         )
         )
     )
     )

+ 30 - 20
netbox/dcim/models.py

@@ -526,17 +526,8 @@ class RackElevationHelperMixin:
             # Loop through all units in the elevation
             # Loop through all units in the elevation
             unit = elevation[unit_cursor]
             unit = elevation[unit_cursor]
             device = unit['device']
             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
             # Setup drawing cordinates
             start_y = unit_cursor * unit_height
             start_y = unit_cursor * unit_height
             end_y = unit_height * height
             end_y = unit_height * height
@@ -567,13 +558,16 @@ class RackElevationHelperMixin:
 
 
         return drawing
         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'}
         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.
         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 face: Rack face (front or rear)
         :param exclude: PK of a Device to exclude (optional); helpful when relocating a Device within a Rack
         :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()
         elevation = OrderedDict()
@@ -582,13 +576,29 @@ class RackElevationHelperMixin:
 
 
         # Add devices to rack units list
         # Add devices to rack units list
         if self.pk:
         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()]
         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
         :param unit_height: Height of each rack unit for the rendered drawing. Note this is not the total
             height of the elevation
             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()
         reserved_units = self.get_reserved_units().keys()
 
 
         return self._draw_elevations(elevation, reserved_units, face, width, unit_height)
         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">
 <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>
 </div>