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

dcim: refactor reservations and make them resizable

hellerve 6 лет назад
Родитель
Сommit
5d5b7e0dd9

+ 143 - 34
netbox/dcim/api/views.py

@@ -31,6 +31,8 @@ from utilities.api import (
     get_serializer_for_model, IsAuthenticatedOrLoginNotRequired, FieldChoicesViewSet, ModelViewSet, ServiceUnavailable,
 )
 from utilities.utils import get_subquery
+# XXX: should this be moved to a util function so that we don’t have to import templatetags?
+from utilities.templatetags.helpers import fgcolor
 from virtualization.models import VirtualMachine
 from . import serializers
 from .exceptions import MissingFilterException
@@ -211,26 +213,50 @@ RACK_ELEVATION_STYLE = """
 rect {
     box-sizing: border-box;
 }
-
+text {
+    text-anchor: middle;
+    dominant-baseline: middle;
+}
 .rack {
     background-color: #f0f0f0;
     fill: none;
     stroke: black;
     stroke-width: 3px;
 }
-.empty {
-    fill: #f9f9f9;
-    stroke: grey;  
+.slot {
+    fill: #f7f7f7;
+    stroke: #a0a0a0;
 }
-.empty:hover {
+.slot:hover {
     fill: #fff;
 }
-.empty+.add-device {   
+.slot+.add-device {
     fill: none;
 }
-.empty:hover+.add-device {
+.slot:hover+.add-device {
     fill: blue;
 }
+.reserved {
+    fill: url(#reserved);
+}
+.reserved:hover {
+    fill: url(#reserved);
+}
+.occupied {
+    fill: url(#occupied);
+}
+.occupied:hover {
+    fill: url(#occupied);
+}
+.blocked {
+    fill: url(#blocked);
+}
+.blocked:hover {
+    fill: url(#blocked);
+}
+.blocked:hover+.add-device {
+    fill: none;
+}
 """
 
 
@@ -242,6 +268,96 @@ class RackElevationViewSet(ViewSet):
     def get_view_name(self):
         return "Rack Elevations"
 
+    def _add_gradient(self, drawing, id_, color):
+        gradient = drawing.linearGradient(start=('0', '20%'), end=('0', '40%'), spreadMethod='repeat', id_=id_, gradientTransform='rotate(80)')
+        gradient.add_stop_color(offset='0%', color='#f7f7f7')
+        gradient.add_stop_color(offset='50%', color='#f7f7f7')
+        gradient.add_stop_color(offset='50%', color=color)
+        gradient.add_stop_color(offset='100%', color=color)
+        drawing.defs.add(gradient)
+
+    def _setup_drawing(self, width, height):
+        drawing = svgwrite.Drawing(size=(width, height))
+
+        # add the stylesheet
+        drawing.defs.add(drawing.style(RACK_ELEVATION_STYLE))
+
+        # add gradients
+        self._add_gradient(drawing, 'reserved', '#c7c7ff')
+        self._add_gradient(drawing, 'occupied', '#f0f0f0')
+        self._add_gradient(drawing, 'blocked', '#ffc7c7')
+
+        return drawing
+
+    def _draw_device_front(self, drawing, device, start, end, text):
+        color = device.device_role.color
+        link = drawing.add(
+            drawing.a(
+                reverse('dcim:device', kwargs={'pk': device.pk}), fill='black'
+            )
+        )
+        link.add(
+            drawing.rect(start, end, fill='#{}'.format(color))
+        )
+        link.add(
+            drawing.text(device.name, insert=text, fill=fgcolor(color))
+        )
+
+    def _draw_device_rear(self, drawing, device, start, end, text):
+        drawing.add(drawing.rect(start, end, class_="blocked"))
+        drawing.add(drawing.text(device.name, insert=text))
+
+    def _draw_empty(self, rack, drawing, start, end, text, id_, face_id, class_):
+        link = drawing.add(
+            drawing.a('{}?{}'.format(
+                reverse('dcim:device_add'),
+                urlencode({'rack': rack.pk, 'site': rack.site.pk, 'face': face_id, 'position': id_})
+            ))
+        )
+        link.add(drawing.rect(start, end, class_=class_))
+        link.add(drawing.text("add device", insert=text, class_='add-device'))
+
+    def _draw_elevations(self, rack, elevation, reserved, face_id, width, slot_height):
+        drawing = self._setup_drawing(width, slot_height*rack.u_height)
+        i = 0
+        for u in elevation:
+            device = u['device']
+            height = u['height']
+            start_y = i * slot_height
+            end_y = slot_height * height
+            start = (0, start_y)
+            end = (width, end_y)
+            text = (width/2, start_y + end_y/2)
+            if device and device.face == face_id:
+                self._draw_device_front(drawing, device, start, end, text)
+            elif device and device.device_type.is_full_depth:
+                self._draw_device_rear(drawing, device, start, end, text)
+            else:
+                class_ = 'slot'
+                if device:
+                    class_ += ' occupied'
+                if u["id"] in reserved:
+                    class_ += ' reserved'
+                self._draw_empty(
+                    rack, drawing, start, end, text, u["id"], face_id, class_
+                )
+            i += height
+        drawing.add(drawing.rect((0, 0), (width, rack.u_height*slot_height), class_='rack'))
+        return drawing
+
+    def _get_elevation(self, rack):
+        elevation = OrderedDict()
+        for u in rack.units:
+            elevation[u] = {'id': u, 'device': None, 'height': 1}
+
+        for device in Device.objects.prefetch_related('device_role')\
+                .filter(rack=rack, position__gt=0):
+            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 elevation.values()
 
     def retrieve(self, request, pk=None):
         """
@@ -249,36 +365,29 @@ class RackElevationViewSet(ViewSet):
         """
         rack = get_object_or_404(Rack, pk=pk)
 
-        side = request.GET.get('face', 'front')
-        if side == 'front':
-            elevation = rack.get_front_elevation()
-        elif side == 'rear':
-            elevation = rack.get_rear_elevation()
-        else:
-            return HttpResponseBadRequest('side should either be "front" or "back".')
+        face_id = request.GET.get('face', '0')
+        if face_id not in ['0', '1']:
+            return HttpResponseBadRequest('side should either be "0" or "1".')
+        # this is safe because of the validation above
+        face_id = int(face_id)
 
-        drawing = svgwrite.Drawing(size=(230, len(elevation)*20))
-        drawing.defs.add(drawing.style(RACK_ELEVATION_STYLE))
+        width = request.GET.get('width', '230')
+        try:
+            width = int(width)
+        except ValueError:
+            return HttpResponseBadRequest('width must be numeric.')
 
-        for i, u in enumerate(elevation):
-            device = u['device']
-            start = i * 20
-            end = 20
-            if device:
-                link = drawing.add(drawing.a(reverse('dcim:device', kwargs={'pk': device.pk}), fill='black'))
-                link.add(drawing.rect((0, start), (230, end), fill='#{}'.format(device.device_role.color), stroke='grey'))
-                link.add(drawing.text(device.name, insert=(115, start+10), text_anchor="middle", dominant_baseline="middle"))
-            else:
-                link = drawing.add(
-                    drawing.a('{}?{}'.format(
-                        reverse('dcim:device_add'),
-                        urlencode({'rack': rack.pk, 'site': rack.site.pk, 'face': 0, 'position': u['id']})
-                    ))
-                )
-                link.add(drawing.rect((0, start), (230, end), class_='empty'))
-                link.add(drawing.text("add device", insert=(115, start+10), text_anchor="middle", dominant_baseline="middle", class_="add-device"))
+        slot_height = request.GET.get('slot_height', '20')
+        try:
+            slot_height = int(slot_height)
+        except ValueError:
+            return HttpResponseBadRequest('slot_height must be numeric.')
+
+        elevation = self._get_elevation(rack)
+
+        reserved = rack.get_reserved_units().keys()
 
-        drawing.add(drawing.rect((0, 0), (230, len(elevation*20)), class_='rack'))
+        drawing = self._draw_elevations(rack, elevation, reserved, face_id, width, slot_height)
 
         return HttpResponse(drawing.tostring(), content_type='image/svg+xml')
 

+ 2 - 2
netbox/templates/dcim/rack.html

@@ -317,13 +317,13 @@
                 <div class="rack_header">
                     <h4>Front</h4>
                 </div>
-                {% include 'dcim/inc/rack_elevation.html' with face="front" %}
+                {% include 'dcim/inc/rack_elevation.html' with face=0 %}
             </div>
             <div class="col-md-6 col-sm-6 col-xs-12">
                 <div class="rack_header">
                     <h4>Rear</h4>
                 </div>
-                {% include 'dcim/inc/rack_elevation.html' with face="rear" %}
+                {% include 'dcim/inc/rack_elevation.html' with face=1 %}
             </div>
         </div>
         <div class="panel panel-default">

+ 2 - 2
netbox/templates/dcim/rack_elevation_list.html

@@ -18,9 +18,9 @@
                             <p><small class="text-muted">{{ rack.facility_id|truncatechars:"30" }}</small></p>
                         </div>
                         {% if face_id %}
-                            {% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_rear_elevation secondary_face=rack.get_front_elevation face_id=1 reserved_units=rack.get_reserved_units %}
+                            {% include 'dcim/inc/rack_elevation.html' with face=1 %}
                         {% else %}
-                            {% include 'dcim/inc/rack_elevation.html' with primary_face=rack.get_front_elevation secondary_face=rack.get_rear_elevation face_id=0 reserved_units=rack.get_reserved_units %}
+                            {% include 'dcim/inc/rack_elevation.html' with face=0 %}
                         {% endif %}
                         <div class="clearfix"></div>
                         <div class="rack_header">