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

Merge pull request #4215 from netbox-community/1529-rack-elevation-images

Closes #1529: Rack elevation images
Jeremy Stretch 6 лет назад
Родитель
Сommit
b81622222d

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

@@ -186,6 +186,9 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
     unit_height = serializers.IntegerField(
         default=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT
     )
+    legend_width = serializers.IntegerField(
+        default=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT
+    )
     exclude = serializers.IntegerField(
         required=False,
         default=None
@@ -194,6 +197,10 @@ class RackElevationDetailFilterSerializer(serializers.Serializer):
         required=False,
         default=True
     )
+    include_images = serializers.BooleanField(
+        required=False,
+        default=True
+    )
 
 
 #

+ 7 - 1
netbox/dcim/api/views.py

@@ -220,7 +220,13 @@ class RackViewSet(CustomFieldModelViewSet):
 
         if data['render'] == 'svg':
             # 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'])
+            drawing = rack.get_elevation_svg(
+                face=data['face'],
+                unit_width=data['unit_width'],
+                unit_height=data['unit_height'],
+                legend_width=data['legend_width'],
+                include_images=data['include_images']
+            )
             return HttpResponse(drawing.tostring(), content_type='image/svg+xml')
 
         else:

+ 192 - 0
netbox/dcim/elevations.py

@@ -0,0 +1,192 @@
+import svgwrite
+
+from django.conf import settings
+from django.urls import reverse
+from django.utils.http import urlencode
+
+from utilities.utils import foreground_color
+from .choices import DeviceFaceChoices
+
+
+class RackElevationSVG:
+    """
+    Use this class to render a rack elevation as an SVG image.
+
+    :param rack: A NetBox Rack instance
+    :param include_images: If true, the SVG document will embed front/rear device face images, where available
+    """
+    def __init__(self, rack, include_images=True):
+        self.rack = rack
+        self.include_images = include_images
+
+    @staticmethod
+    def _add_gradient(drawing, id_, color):
+        gradient = drawing.linearGradient(
+            start=('0', '0%'),
+            end=('0', '5%'),
+            spreadMethod='repeat',
+            id_=id_,
+            gradientTransform='rotate(45, 0, 0)',
+            gradientUnits='userSpaceOnUse'
+        )
+        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)
+
+    @staticmethod
+    def _setup_drawing(width, height):
+        drawing = svgwrite.Drawing(size=(width, height))
+
+        # add the stylesheet
+        with open('{}/css/rack_elevation.css'.format(settings.STATICFILES_DIRS[0])) as css_file:
+            drawing.defs.add(drawing.style(css_file.read()))
+
+        # add gradients
+        RackElevationSVG._add_gradient(drawing, 'reserved', '#c7c7ff')
+        RackElevationSVG._add_gradient(drawing, 'occupied', '#d7d7d7')
+        RackElevationSVG._add_gradient(drawing, 'blocked', '#ffc0c0')
+
+        return drawing
+
+    def _draw_device_front(self, drawing, device, start, end, text):
+        name = str(device)
+        if device.devicebay_count:
+            name += ' ({}/{})'.format(device.get_children().count(), device.devicebay_count)
+
+        color = device.device_role.color
+        link = drawing.add(
+            drawing.a(
+                href=reverse('dcim:device', kwargs={'pk': device.pk}),
+                target='_top',
+                fill='black'
+            )
+        )
+        link.set_desc('{} — {} ({}U) {} {}'.format(
+            device.device_role, device.device_type.display_name,
+            device.device_type.u_height, device.asset_tag or '', device.serial or ''
+        ))
+        link.add(drawing.rect(start, end, style='fill: #{}'.format(color), class_='slot'))
+        hex_color = '#{}'.format(foreground_color(color))
+        link.add(drawing.text(str(name), insert=text, fill=hex_color))
+
+        # Embed front device type image if one exists
+        if self.include_images and device.device_type.front_image:
+            url = device.device_type.front_image.url
+            image = drawing.image(href=url, insert=start, size=end, class_='device-image')
+            image.stretch()
+            link.add(image)
+
+    def _draw_device_rear(self, drawing, device, start, end, text):
+        rect = drawing.rect(start, end, class_="slot blocked")
+        rect.set_desc('{} — {} ({}U) {} {}'.format(
+            device.device_role, device.device_type.display_name,
+            device.device_type.u_height, device.asset_tag or '', device.serial or ''
+        ))
+        drawing.add(rect)
+        drawing.add(drawing.text(str(device), insert=text))
+
+        # Embed rear device type image if one exists
+        if self.include_images and device.device_type.front_image:
+            url = device.device_type.rear_image.url
+            image = drawing.image(href=url, insert=start, size=end, class_='device-image')
+            image.stretch()
+            drawing.add(image)
+
+    @staticmethod
+    def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
+        link = drawing.add(
+            drawing.a(
+                href='{}?{}'.format(
+                    reverse('dcim:device_add'),
+                    urlencode({'rack': rack.pk, 'site': rack.site.pk, 'face': face_id, 'position': id_})
+                ),
+                target='_top'
+            )
+        )
+        if reservation:
+            link.set_desc('{} — {} · {}'.format(
+                reservation.description, reservation.user, reservation.created
+            ))
+        link.add(drawing.rect(start, end, class_=class_))
+        link.add(drawing.text("add device", insert=text, class_='add-device'))
+
+    def merge_elevations(self, face):
+        elevation = self.rack.get_rack_units(face=face, expand_devices=False)
+        if face == DeviceFaceChoices.FACE_REAR:
+            other_face = DeviceFaceChoices.FACE_FRONT
+        else:
+            other_face = DeviceFaceChoices.FACE_REAR
+        other = self.rack.get_rack_units(face=other_face)
+
+        unit_cursor = 0
+        for u in elevation:
+            o = other[unit_cursor]
+            if not u['device'] and o['device']:
+                u['device'] = o['device']
+                u['height'] = 1
+            unit_cursor += u.get('height', 1)
+
+        return elevation
+
+    def render(self, face, unit_width, unit_height, legend_width):
+        """
+        Return an SVG document representing a rack elevation.
+        """
+        drawing = self._setup_drawing(unit_width + legend_width, unit_height * self.rack.u_height)
+        reserved_units = self.rack.get_reserved_units()
+
+        unit_cursor = 0
+        for ru in range(0, self.rack.u_height):
+            start_y = ru * unit_height
+            position_coordinates = (legend_width / 2, start_y + unit_height / 2 + 2)
+            unit = ru + 1 if self.rack.desc_units else self.rack.u_height - ru
+            drawing.add(
+                drawing.text(str(unit), position_coordinates, class_="unit")
+            )
+
+        for unit in self.merge_elevations(face):
+
+            # Loop through all units in the elevation
+            device = unit['device']
+            height = unit.get('height', 1)
+
+            # Setup drawing coordinates
+            start_y = unit_cursor * unit_height
+            end_y = unit_height * height
+            start_cordinates = (legend_width, start_y)
+            end_cordinates = (legend_width + unit_width, end_y)
+            text_cordinates = (legend_width + (unit_width / 2), start_y + end_y / 2)
+
+            # Draw the device
+            if device and device.face == face:
+                self._draw_device_front(drawing, device, start_cordinates, end_cordinates, text_cordinates)
+            elif device and device.device_type.is_full_depth:
+                self._draw_device_rear(drawing, device, start_cordinates, end_cordinates, text_cordinates)
+            else:
+                # Draw shallow devices, reservations, or empty units
+                class_ = 'slot'
+                reservation = reserved_units.get(unit["id"])
+                if device:
+                    class_ += ' occupied'
+                if reservation:
+                    class_ += ' reserved'
+                self._draw_empty(
+                    drawing,
+                    self.rack,
+                    start_cordinates,
+                    end_cordinates,
+                    text_cordinates,
+                    unit["id"],
+                    face,
+                    class_,
+                    reservation
+                )
+
+            unit_cursor += height
+
+        # Wrap the drawing with a border
+        drawing.add(drawing.rect((legend_width, 0), (unit_width, self.rack.u_height * unit_height), class_='rack'))
+
+        return drawing

+ 2 - 2
netbox/dcim/forms.py

@@ -930,8 +930,8 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm):
     class Meta:
         model = DeviceType
         fields = [
-            'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role', 'comments',
-            'tags',
+            'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
+            'front_image', 'rear_image', 'comments', 'tags',
         ]
         widgets = {
             'subdevice_role': StaticSelect2()

+ 23 - 0
netbox/dcim/migrations/0098_devicetype_images.py

@@ -0,0 +1,23 @@
+# Generated by Django 2.2.9 on 2020-02-20 15:11
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('dcim', '0097_interfacetemplate_type_other'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='devicetype',
+            name='front_image',
+            field=models.ImageField(blank=True, upload_to='devicetype-images'),
+        ),
+        migrations.AddField(
+            model_name='devicetype',
+            name='rear_image',
+            field=models.ImageField(blank=True, upload_to='devicetype-images'),
+        ),
+    ]

+ 57 - 177
netbox/dcim/models/__init__.py

@@ -1,7 +1,6 @@
 from collections import OrderedDict
 from itertools import count, groupby
 
-import svgwrite
 import yaml
 from django.conf import settings
 from django.contrib.auth.models import User
@@ -13,7 +12,6 @@ from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.db.models import Count, F, ProtectedError, Sum
 from django.urls import reverse
-from django.utils.http import urlencode
 from mptt.models import MPTTModel, TreeForeignKey
 from taggit.managers import TaggableManager
 from timezone_field import TimeZoneField
@@ -21,10 +19,11 @@ from timezone_field import TimeZoneField
 from dcim.choices import *
 from dcim.constants import *
 from dcim.fields import ASNField
+from dcim.elevations import RackElevationSVG
 from extras.models import ConfigContextModel, CustomFieldModel, TaggedItem
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.models import ChangeLoggedModel
-from utilities.utils import foreground_color, to_meters
+from utilities.utils import to_meters
 from .device_component_templates import (
     ConsolePortTemplate, ConsoleServerPortTemplate, DeviceBayTemplate, FrontPortTemplate, InterfaceTemplate,
     PowerOutletTemplate, PowerPortTemplate, RearPortTemplate,
@@ -350,180 +349,7 @@ class RackRole(ChangeLoggedModel):
         )
 
 
-class RackElevationHelperMixin:
-    """
-    Utility class that renders rack elevations. Contains helper methods for rendering elevations as a list of
-    rack units represented as dictionaries, or an SVG of the elevation.
-    """
-
-    @staticmethod
-    def _add_gradient(drawing, id_, color):
-        gradient = drawing.linearGradient(
-            start=('0', '0%'),
-            end=('0', '5%'),
-            spreadMethod='repeat',
-            id_=id_,
-            gradientTransform='rotate(45, 0, 0)',
-            gradientUnits='userSpaceOnUse'
-        )
-        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)
-
-    @staticmethod
-    def _setup_drawing(width, height):
-        drawing = svgwrite.Drawing(size=(width, height))
-
-        # add the stylesheet
-        with open('{}/css/rack_elevation.css'.format(settings.STATICFILES_DIRS[0])) as css_file:
-            drawing.defs.add(drawing.style(css_file.read()))
-
-        # add gradients
-        RackElevationHelperMixin._add_gradient(drawing, 'reserved', '#c7c7ff')
-        RackElevationHelperMixin._add_gradient(drawing, 'occupied', '#d7d7d7')
-        RackElevationHelperMixin._add_gradient(drawing, 'blocked', '#ffc0c0')
-
-        return drawing
-
-    @staticmethod
-    def _draw_device_front(drawing, device, start, end, text):
-        name = str(device)
-        if device.devicebay_count:
-            name += ' ({}/{})'.format(device.get_children().count(), device.devicebay_count)
-
-        color = device.device_role.color
-        link = drawing.add(
-            drawing.a(
-                href=reverse('dcim:device', kwargs={'pk': device.pk}),
-                target='_top',
-                fill='black'
-            )
-        )
-        link.set_desc('{} — {} ({}U) {} {}'.format(
-            device.device_role, device.device_type.display_name,
-            device.device_type.u_height, device.asset_tag or '', device.serial or ''
-        ))
-        link.add(drawing.rect(start, end, style='fill: #{}'.format(color), class_='slot'))
-        hex_color = '#{}'.format(foreground_color(color))
-        link.add(drawing.text(str(name), insert=text, fill=hex_color))
-
-    @staticmethod
-    def _draw_device_rear(drawing, device, start, end, text):
-        rect = drawing.rect(start, end, class_="slot blocked")
-        rect.set_desc('{} — {} ({}U) {} {}'.format(
-            device.device_role, device.device_type.display_name,
-            device.device_type.u_height, device.asset_tag or '', device.serial or ''
-        ))
-        drawing.add(rect)
-        drawing.add(drawing.text(str(device), insert=text))
-
-    @staticmethod
-    def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
-        link = drawing.add(
-            drawing.a(
-                href='{}?{}'.format(
-                    reverse('dcim:device_add'),
-                    urlencode({'rack': rack.pk, 'site': rack.site.pk, 'face': face_id, 'position': id_})
-                ),
-                target='_top'
-            )
-        )
-        if reservation:
-            link.set_desc('{} — {} · {}'.format(
-                reservation.description, reservation.user, reservation.created
-            ))
-        link.add(drawing.rect(start, end, class_=class_))
-        link.add(drawing.text("add device", insert=text, class_='add-device'))
-
-    def _draw_elevations(self, elevation, reserved_units, face, unit_width, unit_height, legend_width):
-
-        drawing = self._setup_drawing(unit_width + legend_width, unit_height * self.u_height)
-
-        unit_cursor = 0
-        for ru in range(0, self.u_height):
-            start_y = ru * unit_height
-            position_coordinates = (legend_width / 2, start_y + unit_height / 2 + 2)
-            unit = ru + 1 if self.desc_units else self.u_height - ru
-            drawing.add(
-                drawing.text(str(unit), position_coordinates, class_="unit")
-            )
-
-        for unit in elevation:
-
-            # Loop through all units in the elevation
-            device = unit['device']
-            height = unit.get('height', 1)
-
-            # Setup drawing coordinates
-            start_y = unit_cursor * unit_height
-            end_y = unit_height * height
-            start_cordinates = (legend_width, start_y)
-            end_cordinates = (legend_width + unit_width, end_y)
-            text_cordinates = (legend_width + (unit_width / 2), start_y + end_y / 2)
-
-            # Draw the device
-            if device and device.face == face:
-                self._draw_device_front(drawing, device, start_cordinates, end_cordinates, text_cordinates)
-            elif device and device.device_type.is_full_depth:
-                self._draw_device_rear(drawing, device, start_cordinates, end_cordinates, text_cordinates)
-            else:
-                # Draw shallow devices, reservations, or empty units
-                class_ = 'slot'
-                reservation = reserved_units.get(unit["id"])
-                if device:
-                    class_ += ' occupied'
-                if reservation:
-                    class_ += ' reserved'
-                self._draw_empty(
-                    drawing, self, start_cordinates, end_cordinates, text_cordinates, unit["id"], face, class_, reservation
-                )
-
-            unit_cursor += height
-
-        # Wrap the drawing with a border
-        drawing.add(drawing.rect((legend_width, 0), (unit_width, self.u_height * unit_height), class_='rack'))
-
-        return drawing
-
-    def merge_elevations(self, face):
-        elevation = self.get_rack_units(face=face, expand_devices=False)
-        other_face = DeviceFaceChoices.FACE_FRONT if face == DeviceFaceChoices.FACE_REAR else DeviceFaceChoices.FACE_REAR
-        other = self.get_rack_units(face=other_face)
-
-        unit_cursor = 0
-        for u in elevation:
-            o = other[unit_cursor]
-            if not u['device'] and o['device']:
-                u['device'] = o['device']
-                u['height'] = 1
-            unit_cursor += u.get('height', 1)
-
-        return elevation
-
-    def get_elevation_svg(
-            self,
-            face=DeviceFaceChoices.FACE_FRONT,
-            unit_width=RACK_ELEVATION_UNIT_WIDTH_DEFAULT,
-            unit_height=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT,
-            legend_width=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT
-    ):
-        """
-        Return an SVG of the rack elevation
-
-        :param face: Enum of [front, rear] representing the desired side of the rack elevation to render
-        :param width: Width in pixles for the rendered drawing
-        :param unit_height: Height of each rack unit for the rendered drawing. Note this is not the total
-            height of the elevation
-        """
-        elevation = self.merge_elevations(face)
-        reserved_units = self.get_reserved_units()
-
-        return self._draw_elevations(elevation, reserved_units, face, unit_width, unit_height, legend_width)
-
-
-class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin):
+class Rack(ChangeLoggedModel, CustomFieldModel):
     """
     Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
     Each Rack is assigned to a Site and (optionally) a RackGroup.
@@ -835,6 +661,28 @@ class Rack(ChangeLoggedModel, CustomFieldModel, RackElevationHelperMixin):
                 reserved_units[u] = r
         return reserved_units
 
+    def get_elevation_svg(
+            self,
+            face=DeviceFaceChoices.FACE_FRONT,
+            unit_width=RACK_ELEVATION_UNIT_WIDTH_DEFAULT,
+            unit_height=RACK_ELEVATION_UNIT_HEIGHT_DEFAULT,
+            legend_width=RACK_ELEVATION_LEGEND_WIDTH_DEFAULT,
+            include_images=True
+    ):
+        """
+        Return an SVG of the rack elevation
+
+        :param face: Enum of [front, rear] representing the desired side of the rack elevation to render
+        :param unit_width: Width in pixels for the rendered drawing
+        :param unit_height: Height of each rack unit for the rendered drawing. Note this is not the total
+            height of the elevation
+        :param legend_width: Width of the unit legend, in pixels
+        :param include_images: Embed front/rear device images where available
+        """
+        elevation = RackElevationSVG(self, include_images=include_images)
+
+        return elevation.render(face, unit_width, unit_height, legend_width)
+
     def get_0u_devices(self):
         return self.devices.filter(position=0)
 
@@ -1025,6 +873,14 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
         help_text='Parent devices house child devices in device bays. Leave blank '
                   'if this device type is neither a parent nor a child.'
     )
+    front_image = models.ImageField(
+        upload_to='devicetype-images',
+        blank=True
+    )
+    rear_image = models.ImageField(
+        upload_to='devicetype-images',
+        blank=True
+    )
     comments = models.TextField(
         blank=True
     )
@@ -1056,6 +912,10 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
         # Save a copy of u_height for validation in clean()
         self._original_u_height = self.u_height
 
+        # Save references to the original front/rear images
+        self._original_front_image = self.front_image
+        self._original_rear_image = self.rear_image
+
     def get_absolute_url(self):
         return reverse('dcim:devicetype', args=[self.pk])
 
@@ -1175,6 +1035,26 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
                 'u_height': "Child device types must be 0U."
             })
 
+    def save(self, *args, **kwargs):
+        ret = super().save(*args, **kwargs)
+
+        # Delete any previously uploaded image files that are no longer in use
+        if self.front_image != self._original_front_image:
+            self._original_front_image.delete(save=False)
+        if self.rear_image != self._original_rear_image:
+            self._original_rear_image.delete(save=False)
+
+        return ret
+
+    def delete(self, *args, **kwargs):
+        super().delete(*args, **kwargs)
+
+        # Delete any uploaded image files
+        if self.front_image:
+            self.front_image.delete(save=False)
+        if self.rear_image:
+            self.rear_image.delete(save=False)
+
     @property
     def display_name(self):
         return '{} {}'.format(self.manufacturer.name, self.model)

+ 2 - 0
netbox/media/devicetype-images/.gitignore

@@ -0,0 +1,2 @@
+*
+!.gitignore

+ 3 - 1
netbox/project-static/css/rack_elevation.css

@@ -56,7 +56,6 @@ text {
 .blocked:hover+.add-device {
     fill: none;
 }
-
 .unit {
     margin: 0;
     padding: 5px 0px;
@@ -65,3 +64,6 @@ text {
     font-size: 10px;
     font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
 }
+.hidden {
+    visibility: hidden;
+}

+ 16 - 0
netbox/project-static/js/rack_elevations.js

@@ -0,0 +1,16 @@
+// Toggle the display of device images within an SVG rack elevation
+$('button.toggle-images').click(function() {
+    var selected = $(this).attr('selected');
+    var rack_front = $("#rack_front");
+    var rack_rear = $("#rack_rear");
+    if (selected) {
+        $('.device-image', rack_front.contents()).addClass('hidden');
+        $('.device-image', rack_rear.contents()).addClass('hidden');
+    } else {
+        $('.device-image', rack_front.contents()).removeClass('hidden');
+        $('.device-image', rack_rear.contents()).removeClass('hidden');
+    }
+    $(this).attr('selected', !selected);
+    $(this).children('span').toggleClass('glyphicon-check glyphicon-unchecked');
+    return false;
+});

+ 24 - 0
netbox/templates/dcim/devicetype.html

@@ -109,6 +109,30 @@
                         {% endif %}
                     </td>
                 </tr>
+                <tr>
+                    <td>Front Image</td>
+                    <td>
+                        {% if devicetype.front_image %}
+                            <a href="{{ devicetype.front_image.url }}">
+                                <img src="{{ devicetype.front_image.url }}" alt="{{ devicetype.front_image.name }}" class="img-responsive" />
+                            </a>
+                        {% else %}
+                            <span class="text-muted">&mdash;</span>
+                        {% endif %}
+                    </td>
+                </tr>
+                <tr>
+                    <td>Rear Image</td>
+                    <td>
+                        {% if devicetype.rear_image %}
+                            <a href="{{ devicetype.rear_image.url }}">
+                                <img src="{{ devicetype.rear_image.url }}" alt="{{ devicetype.rear_image.name }}" class="img-responsive" />
+                            </a>
+                        {% else %}
+                            <span class="text-muted">&mdash;</span>
+                        {% endif %}
+                    </td>
+                </tr>
                 <tr>
                     <td>Instances</td>
                     <td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ devicetype.instances.count }}</a></td>

+ 7 - 0
netbox/templates/dcim/devicetype_edit.html

@@ -14,6 +14,13 @@
             {% render_field form.subdevice_role %}
         </div>
     </div>
+    <div class="panel panel-default">
+        <div class="panel-heading"><strong>Rack Images</strong></div>
+        <div class="panel-body">
+            {% render_field form.front_image %}
+            {% render_field form.rear_image %}
+        </div>
+    </div>
     {% if form.custom_fields %}
         <div class="panel panel-default">
             <div class="panel-heading"><strong>Custom Fields</strong></div>

+ 1 - 4
netbox/templates/dcim/inc/rack_elevation.html

@@ -1,7 +1,4 @@
 {% load helpers %}
-
 <div class="rack_frame">
-
-  <object data="{% url 'dcim-api:rack-elevation' pk=rack.pk %}?face={{face}}&render=svg"></object>
-
+  <object data="{% url 'dcim-api:rack-elevation' pk=rack.pk %}?face={{face}}&render=svg" id="rack_{{ face }}"></object>
 </div>

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

@@ -2,6 +2,7 @@
 {% load buttons %}
 {% load custom_links %}
 {% load helpers %}
+{% load static %}
 
 {% block header %}
     <div class="row noprint">
@@ -45,6 +46,9 @@
     <h1>{% block title %}Rack {{ rack }}{% endblock %}</h1>
     {% include 'inc/created_updated.html' with obj=rack %}
     <div class="pull-right noprint">
+        <button class="btn btn-sm btn-default toggle-images" selected="selected">
+            <span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show Images
+        </button>
         {% custom_links rack %}
     </div>
     <ul class="nav nav-tabs">
@@ -368,9 +372,5 @@
 {% endblock %}
 
 {% block javascript %}
-<script type="text/javascript">
-$(function() {
-  $('[data-toggle="popover"]').popover()
-})
-</script>
+<script src="{% static 'js/rack_elevations.js' %}?v{{ settings.VERSION }}"></script>
 {% endblock %}

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

@@ -1,8 +1,12 @@
 {% extends '_base.html' %}
 {% load helpers %}
+{% load static %}
 
 {% block content %}
 <div class="btn-group pull-right noprint" role="group">
+    <button class="btn btn-default toggle-images" selected="selected">
+        <span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show Images
+    </button>
     <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='front' %}" class="btn btn-default{% if rack_face == 'front' %} active{% endif %}">Front</a>
     <a href="{% url 'dcim:rack_elevation_list' %}{% querystring request face='rear' %}" class="btn btn-default{% if rack_face == 'rear' %} active{% endif %}">Rear</a>
 </div>
@@ -41,9 +45,5 @@
 {% endblock %}
 
 {% block javascript %}
-    <script type="text/javascript">
-    $(function() {
-        $('[data-toggle="popover"]').popover()
-    })
-    </script>
+<script src="{% static 'js/rack_elevations.js' %}?v{{ settings.VERSION }}"></script>
 {% endblock %}