mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
review updates to svg rendering
This commit is contained in:
@ -54,9 +54,19 @@ By default, this endpoint returns a paginated JSON response representing each ra
|
|||||||
/api/dcim/racks/<id>/units/
|
/api/dcim/racks/<id>/units/
|
||||||
```
|
```
|
||||||
|
|
||||||
All internal use of the rack units endpoint has been updated to use the new rack elevation endpoint.
|
In order to render the elevation as an SVG, include the `render_format=svg` query parameter in the request. You may also control the width of the elevation drawing in pixels with `unit_width=<width in pixels>` and the height of each rack unit with `unit_height=<height in pixels>`. The `unit_width` defaults to `230` and the `unit_height` default to `20` which produces elevations the same size as those that appear in the NetBox Web UI. The query parameter `face` is used to request either the `front` or `rear` of the elevation and defaults to `front`.
|
||||||
|
|
||||||
In order to render the elevation as an SVG, include the `render_format=svg` query parameter in the request. You may also control the width of the elevation drawing in pixels with `width=<width in pixels>` and the height of each rack unit with `unit_height=<height in pixels>`. The `width` defaults to `230` and the `unit_height` default to `20` which produces elevations the same size as those that appear in the NetBox Web UI. The query parameter `face` is used to request either the `front` or `rear` of the elevation and defaults to `front`.
|
Here is an example of the request url for an SVG rendering using the default parameters to render the front of the elevation:
|
||||||
|
|
||||||
|
```
|
||||||
|
/api/dcim/racks/<id>/elevation/?render_format=svg
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is an example of the request url for an SVG rendering of the rear of the elevation having a width of 300 pixels and per unit height of 35 pixels:
|
||||||
|
|
||||||
|
```
|
||||||
|
/api/dcim/racks/<id>/elevation/?render_format=svg&face=rear&unit_width=300&unit_height=35
|
||||||
|
```
|
||||||
|
|
||||||
Thanks to [@hellerve](https://github.com/hellerve) for doing the heavy lifting on this!
|
Thanks to [@hellerve](https://github.com/hellerve) for doing the heavy lifting on this!
|
||||||
|
|
||||||
|
@ -157,7 +157,7 @@ class RackUnitSerializer(serializers.Serializer):
|
|||||||
"""
|
"""
|
||||||
id = serializers.IntegerField(read_only=True)
|
id = serializers.IntegerField(read_only=True)
|
||||||
name = serializers.CharField(read_only=True)
|
name = serializers.CharField(read_only=True)
|
||||||
face = ChoiceField(choices=DeviceFaceChoices, required=False, allow_null=True)
|
face = ChoiceField(choices=DeviceFaceChoices, read_only=True)
|
||||||
device = NestedDeviceSerializer(read_only=True)
|
device = NestedDeviceSerializer(read_only=True)
|
||||||
|
|
||||||
|
|
||||||
@ -172,12 +172,14 @@ class RackReservationSerializer(ValidatedModelSerializer):
|
|||||||
|
|
||||||
|
|
||||||
class RackElevationDetailFilterSerializer(serializers.Serializer):
|
class RackElevationDetailFilterSerializer(serializers.Serializer):
|
||||||
face = serializers.ChoiceField(choices=['front', 'rear'], default='front')
|
face = serializers.ChoiceField(choices=DeviceFaceChoices, default=DeviceFaceChoices.FACE_FRONT)
|
||||||
render_format = serializers.ChoiceField(choices=['json', 'svg'], default='json')
|
render_format = serializers.ChoiceField(
|
||||||
width = serializers.IntegerField(default=230)
|
choices=RackElecationDetailRenderFormatChoices,
|
||||||
unit_height = serializers.IntegerField(default=20)
|
default=RackElecationDetailRenderFormatChoices.RENDER_FORMAT_SVG
|
||||||
|
)
|
||||||
|
unit_width = serializers.IntegerField(default=RACK_ELEVATION_UNIT_WIDTH_DEFAULT)
|
||||||
|
unit_height = serializers.IntegerField(default=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT)
|
||||||
exclude = serializers.IntegerField(required=False, default=None)
|
exclude = serializers.IntegerField(required=False, default=None)
|
||||||
q = serializers.CharField(required=False, default=None)
|
|
||||||
expand_devices = serializers.BooleanField(required=False, default=True)
|
expand_devices = serializers.BooleanField(required=False, default=True)
|
||||||
|
|
||||||
|
|
||||||
|
@ -219,17 +219,17 @@ class RackViewSet(CustomFieldModelViewSet):
|
|||||||
data = serializer.validated_data
|
data = serializer.validated_data
|
||||||
|
|
||||||
if data['render_format'] == 'svg':
|
if data['render_format'] == 'svg':
|
||||||
drawing = rack.get_elevation_svg(data['face'], data['width'], data['unit_height'])
|
# Render and return the elevation as an SVG drawing with the correct content type
|
||||||
|
drawing = rack.get_elevation_svg(data['face'], data['unit_width'], data['unit_height'])
|
||||||
return HttpResponse(drawing.tostring(), content_type='image/svg+xml')
|
return HttpResponse(drawing.tostring(), content_type='image/svg+xml')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
# Return a JSON representation of the rack units in the elevation
|
||||||
elevation = rack.get_rack_units(
|
elevation = rack.get_rack_units(
|
||||||
face=data['face'],
|
face=data['face'],
|
||||||
exclude=data['exclude'],
|
exclude=data['exclude'],
|
||||||
expand_devices=data['expand_devices']
|
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:
|
||||||
|
@ -105,6 +105,17 @@ class RackDimensionUnitChoices(ChoiceSet):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RackElecationDetailRenderFormatChoices(ChoiceSet):
|
||||||
|
|
||||||
|
RENDER_FORMAT_JSON = 'json'
|
||||||
|
RENDER_FORMAT_SVG = 'svg'
|
||||||
|
|
||||||
|
CHOICES = (
|
||||||
|
(RENDER_FORMAT_JSON, 'json'),
|
||||||
|
(RENDER_FORMAT_SVG, 'svg')
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
# DeviceTypes
|
# DeviceTypes
|
||||||
#
|
#
|
||||||
|
@ -110,3 +110,8 @@ text {
|
|||||||
fill: none;
|
fill: none;
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Rack Elevation SVG Size
|
||||||
|
RACK_ELEVATION_UNIT_WIDTH_DEFAULT = 230
|
||||||
|
RACK_ELEVATION_UNIT_HEIGHT_DEFAULT = 20
|
||||||
|
@ -517,8 +517,8 @@ class RackElevationHelperMixin:
|
|||||||
link.add(drawing.rect(start, end, class_=class_))
|
link.add(drawing.rect(start, end, class_=class_))
|
||||||
link.add(drawing.text("add device", insert=text, class_='add-device'))
|
link.add(drawing.text("add device", insert=text, class_='add-device'))
|
||||||
|
|
||||||
def _draw_elevations(self, elevation, reserved_units, face, width, unit_height):
|
def _draw_elevations(self, elevation, reserved_units, face, unit_width, unit_height):
|
||||||
drawing = self._setup_drawing(width, unit_height * self.u_height)
|
drawing = self._setup_drawing(unit_width, unit_height * self.u_height)
|
||||||
|
|
||||||
unit_cursor = 0
|
unit_cursor = 0
|
||||||
total_units = len(elevation)
|
total_units = len(elevation)
|
||||||
@ -532,8 +532,8 @@ class RackElevationHelperMixin:
|
|||||||
start_y = unit_cursor * unit_height
|
start_y = unit_cursor * unit_height
|
||||||
end_y = unit_height * height
|
end_y = unit_height * height
|
||||||
start_cordinates = (0, start_y)
|
start_cordinates = (0, start_y)
|
||||||
end_cordinates = (width, end_y)
|
end_cordinates = (unit_width, end_y)
|
||||||
text_cordinates = (width / 2, start_y + end_y / 2)
|
text_cordinates = (unit_width / 2, start_y + end_y / 2)
|
||||||
|
|
||||||
# Draw the device
|
# Draw the device
|
||||||
if device and device.face == face:
|
if device and device.face == face:
|
||||||
@ -554,100 +554,11 @@ class RackElevationHelperMixin:
|
|||||||
unit_cursor += height
|
unit_cursor += height
|
||||||
|
|
||||||
# Wrap the drawing with a border
|
# Wrap the drawing with a border
|
||||||
drawing.add(drawing.rect((0, 0), (width, self.u_height * unit_height), class_='rack'))
|
drawing.add(drawing.rect((0, 0), (unit_width, self.u_height * unit_height), class_='rack'))
|
||||||
|
|
||||||
return drawing
|
return drawing
|
||||||
|
|
||||||
def get_rack_units(self, face=DeviceFaceChoices.FACE_FRONT, exclude=None, expand_devices=True):
|
def get_elevation_svg(self, face=DeviceFaceChoices.FACE_FRONT, unit_width=230, unit_height=20):
|
||||||
"""
|
|
||||||
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()
|
|
||||||
for u in self.units:
|
|
||||||
elevation[u] = {'id': u, 'name': 'U{}'.format(u), 'face': face, 'device': None}
|
|
||||||
|
|
||||||
# Add devices to rack units list
|
|
||||||
if self.pk:
|
|
||||||
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()]
|
|
||||||
|
|
||||||
def get_available_units(self, u_height=1, rack_face=None, exclude=list()):
|
|
||||||
"""
|
|
||||||
Return a list of units within the rack available to accommodate a device of a given U height (default 1).
|
|
||||||
Optionally exclude one or more devices when calculating empty units (needed when moving a device from one
|
|
||||||
position to another within a rack).
|
|
||||||
|
|
||||||
:param u_height: Minimum number of contiguous free units required
|
|
||||||
:param rack_face: The face of the rack (front or rear) required; 'None' if device is full depth
|
|
||||||
:param exclude: List of devices IDs to exclude (useful when moving a device within a rack)
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Gather all devices which consume U space within the rack
|
|
||||||
devices = self.devices.prefetch_related('device_type').filter(position__gte=1).exclude(pk__in=exclude)
|
|
||||||
|
|
||||||
# Initialize the rack unit skeleton
|
|
||||||
units = list(range(1, self.u_height + 1))
|
|
||||||
|
|
||||||
# Remove units consumed by installed devices
|
|
||||||
for d in devices:
|
|
||||||
if rack_face is None or d.face == rack_face or d.device_type.is_full_depth:
|
|
||||||
for u in range(d.position, d.position + d.device_type.u_height):
|
|
||||||
try:
|
|
||||||
units.remove(u)
|
|
||||||
except ValueError:
|
|
||||||
# Found overlapping devices in the rack!
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Remove units without enough space above them to accommodate a device of the specified height
|
|
||||||
available_units = []
|
|
||||||
for u in units:
|
|
||||||
if set(range(u, u + u_height)).issubset(units):
|
|
||||||
available_units.append(u)
|
|
||||||
|
|
||||||
return list(reversed(available_units))
|
|
||||||
|
|
||||||
def get_reserved_units(self):
|
|
||||||
"""
|
|
||||||
Return a dictionary mapping all reserved units within the rack to their reservation.
|
|
||||||
"""
|
|
||||||
reserved_units = {}
|
|
||||||
for r in self.reservations.all():
|
|
||||||
for u in r.units:
|
|
||||||
reserved_units[u] = r
|
|
||||||
return reserved_units
|
|
||||||
|
|
||||||
def get_elevation_svg(self, face=DeviceFaceChoices.FACE_FRONT, width=230, unit_height=20):
|
|
||||||
"""
|
"""
|
||||||
Return an SVG of the rack elevation
|
Return an SVG of the rack elevation
|
||||||
|
|
||||||
@ -659,7 +570,7 @@ class RackElevationHelperMixin:
|
|||||||
elevation = self.get_rack_units(face=face, expand_devices=False)
|
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, unit_width, unit_height)
|
||||||
|
|
||||||
|
|
||||||
class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin):
|
class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin):
|
||||||
@ -881,6 +792,95 @@ class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin):
|
|||||||
def get_status_class(self):
|
def get_status_class(self):
|
||||||
return self.STATUS_CLASS_MAP.get(self.status)
|
return self.STATUS_CLASS_MAP.get(self.status)
|
||||||
|
|
||||||
|
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()
|
||||||
|
for u in self.units:
|
||||||
|
elevation[u] = {'id': u, 'name': 'U{}'.format(u), 'face': face, 'device': None}
|
||||||
|
|
||||||
|
# Add devices to rack units list
|
||||||
|
if self.pk:
|
||||||
|
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()]
|
||||||
|
|
||||||
|
def get_available_units(self, u_height=1, rack_face=None, exclude=list()):
|
||||||
|
"""
|
||||||
|
Return a list of units within the rack available to accommodate a device of a given U height (default 1).
|
||||||
|
Optionally exclude one or more devices when calculating empty units (needed when moving a device from one
|
||||||
|
position to another within a rack).
|
||||||
|
|
||||||
|
:param u_height: Minimum number of contiguous free units required
|
||||||
|
:param rack_face: The face of the rack (front or rear) required; 'None' if device is full depth
|
||||||
|
:param exclude: List of devices IDs to exclude (useful when moving a device within a rack)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Gather all devices which consume U space within the rack
|
||||||
|
devices = self.devices.prefetch_related('device_type').filter(position__gte=1).exclude(pk__in=exclude)
|
||||||
|
|
||||||
|
# Initialize the rack unit skeleton
|
||||||
|
units = list(range(1, self.u_height + 1))
|
||||||
|
|
||||||
|
# Remove units consumed by installed devices
|
||||||
|
for d in devices:
|
||||||
|
if rack_face is None or d.face == rack_face or d.device_type.is_full_depth:
|
||||||
|
for u in range(d.position, d.position + d.device_type.u_height):
|
||||||
|
try:
|
||||||
|
units.remove(u)
|
||||||
|
except ValueError:
|
||||||
|
# Found overlapping devices in the rack!
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Remove units without enough space above them to accommodate a device of the specified height
|
||||||
|
available_units = []
|
||||||
|
for u in units:
|
||||||
|
if set(range(u, u + u_height)).issubset(units):
|
||||||
|
available_units.append(u)
|
||||||
|
|
||||||
|
return list(reversed(available_units))
|
||||||
|
|
||||||
|
def get_reserved_units(self):
|
||||||
|
"""
|
||||||
|
Return a dictionary mapping all reserved units within the rack to their reservation.
|
||||||
|
"""
|
||||||
|
reserved_units = {}
|
||||||
|
for r in self.reservations.all():
|
||||||
|
for u in r.units:
|
||||||
|
reserved_units[u] = r
|
||||||
|
return reserved_units
|
||||||
|
|
||||||
def get_0u_devices(self):
|
def get_0u_devices(self):
|
||||||
return self.devices.filter(position=0)
|
return self.devices.filter(position=0)
|
||||||
|
|
||||||
|
@ -18,9 +18,9 @@
|
|||||||
<p><small class="text-muted">{{ rack.facility_id|truncatechars:"30" }}</small></p>
|
<p><small class="text-muted">{{ rack.facility_id|truncatechars:"30" }}</small></p>
|
||||||
</div>
|
</div>
|
||||||
{% if face_id %}
|
{% if face_id %}
|
||||||
{% include 'dcim/inc/rack_elevation.html' with face=1 %}
|
{% include 'dcim/inc/rack_elevation.html' with face='rear' %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% include 'dcim/inc/rack_elevation.html' with face=0 %}
|
{% include 'dcim/inc/rack_elevation.html' with face='front' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
<div class="rack_header">
|
<div class="rack_header">
|
||||||
|
Reference in New Issue
Block a user