Przeglądaj źródła

Add front/rear images for device types; include in rack elevations

Jeremy Stretch 6 lat temu
rodzic
commit
d2157a3423

+ 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'),
+        ),
+    ]

+ 47 - 0
netbox/dcim/models/__init__.py

@@ -9,6 +9,7 @@ from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelatio
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.postgres.fields import ArrayField, JSONField
 from django.core.exceptions import ObjectDoesNotExist, ValidationError
+from django.core.files.storage import default_storage
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.db.models import Count, F, ProtectedError, Sum
@@ -409,6 +410,13 @@ class RackElevationHelperMixin:
         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 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)
+
     @staticmethod
     def _draw_device_rear(drawing, device, start, end, text):
         rect = drawing.rect(start, end, class_="slot blocked")
@@ -419,6 +427,13 @@ class RackElevationHelperMixin:
         drawing.add(rect)
         drawing.add(drawing.text(str(device), insert=text))
 
+        # Embed rear device type image if one exists
+        if 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(
@@ -1025,6 +1040,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 +1079,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 +1202,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)

+ 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;
+}

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

+ 22 - 1
netbox/templates/dcim/rack.html

@@ -47,6 +47,11 @@
     <div class="pull-right noprint">
         {% custom_links rack %}
     </div>
+    <div class="pull-right noprint">
+        <button class="btn btn-default btn-xs toggle-images" selected="selected">
+            <span class="glyphicon glyphicon-check" aria-hidden="true"></span> Show Images
+        </button>
+    </div>
     <ul class="nav nav-tabs">
         <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
             <a href="{{ rack.get_absolute_url }}">Rack</a>
@@ -371,6 +376,22 @@
 <script type="text/javascript">
 $(function() {
   $('[data-toggle="popover"]').popover()
-})
+});
+// Toggle the display of device images
+$('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;
+});
 </script>
 {% endblock %}