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']
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
#

View File

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

View File

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

View File

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

View File

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