drf-yasg updates for rack elevations

This commit is contained in:
John Anderson 2019-12-11 09:45:08 -05:00
parent d8dd5f00c1
commit b4d724b5ae
5 changed files with 61 additions and 59 deletions

View File

@ -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
# #

View File

@ -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') serializer = serializers.RackElevationDetailFilterSerializer(data=request.GET)
if face not in ['front', 'rear']: if not serializer.is_valid():
face = 'front' return Response(serializer.errors, 400)
data = serializer.validated_data
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)
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 elevation = rack.get_rack_units(
exclude = request.GET.get('exclude', None) face=data['face'],
if exclude is not None: exclude=data['exclude'],
try: expand_devices=data['expand_devices']
if isinstance(exclude, list): )
exclude = [int(item) for item in exclude] if data['q']:
else: elevation = [u for u in elevation if data['q'] in str(u['id'])]
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'])]
page = self.paginate_queryset(elevation) page = self.paginate_queryset(elevation)
if page is not None: if page is not None:

View File

@ -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'
) )
) )

View File

@ -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: height = unit.get('height', 1)
# 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
# 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')\ queryset = Device.objects.prefetch_related(
.annotate(devicebay_count=Count('device_bays'))\ 'device_type',
.exclude(pk=exclude)\ 'device_type__manufacturer',
.filter(rack=self, position__gt=0)\ 'device_role'
.filter(Q(face=face) | Q(device_type__is_full_depth=True)): ).annotate(
for u in range(device.position, device.position + device.device_type.u_height): devicebay_count=Count('device_bays')
elevation[u]['device'] = device ).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)

View File

@ -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>