Ver Fonte

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

Jeremy Stretch há 6 anos atrás
pai
commit
d2157a3423

+ 2 - 2
netbox/dcim/forms.py

@@ -930,8 +930,8 @@ class DeviceTypeForm(BootstrapMixin, CustomFieldModelForm):
     class Meta:
     class Meta:
         model = DeviceType
         model = DeviceType
         fields = [
         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 = {
         widgets = {
             'subdevice_role': StaticSelect2()
             '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.contenttypes.models import ContentType
 from django.contrib.postgres.fields import ArrayField, JSONField
 from django.contrib.postgres.fields import ArrayField, JSONField
 from django.core.exceptions import ObjectDoesNotExist, ValidationError
 from django.core.exceptions import ObjectDoesNotExist, ValidationError
+from django.core.files.storage import default_storage
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.db import models
 from django.db.models import Count, F, ProtectedError, Sum
 from django.db.models import Count, F, ProtectedError, Sum
@@ -409,6 +410,13 @@ class RackElevationHelperMixin:
         hex_color = '#{}'.format(foreground_color(color))
         hex_color = '#{}'.format(foreground_color(color))
         link.add(drawing.text(str(name), insert=text, fill=hex_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
     @staticmethod
     def _draw_device_rear(drawing, device, start, end, text):
     def _draw_device_rear(drawing, device, start, end, text):
         rect = drawing.rect(start, end, class_="slot blocked")
         rect = drawing.rect(start, end, class_="slot blocked")
@@ -419,6 +427,13 @@ class RackElevationHelperMixin:
         drawing.add(rect)
         drawing.add(rect)
         drawing.add(drawing.text(str(device), insert=text))
         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
     @staticmethod
     def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
     def _draw_empty(drawing, rack, start, end, text, id_, face_id, class_, reservation):
         link = drawing.add(
         link = drawing.add(
@@ -1025,6 +1040,14 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
         help_text='Parent devices house child devices in device bays. Leave blank '
         help_text='Parent devices house child devices in device bays. Leave blank '
                   'if this device type is neither a parent nor a child.'
                   '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(
     comments = models.TextField(
         blank=True
         blank=True
     )
     )
@@ -1056,6 +1079,10 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
         # Save a copy of u_height for validation in clean()
         # Save a copy of u_height for validation in clean()
         self._original_u_height = self.u_height
         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):
     def get_absolute_url(self):
         return reverse('dcim:devicetype', args=[self.pk])
         return reverse('dcim:devicetype', args=[self.pk])
 
 
@@ -1175,6 +1202,26 @@ class DeviceType(ChangeLoggedModel, CustomFieldModel):
                 'u_height': "Child device types must be 0U."
                 '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
     @property
     def display_name(self):
     def display_name(self):
         return '{} {}'.format(self.manufacturer.name, self.model)
         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 {
 .blocked:hover+.add-device {
     fill: none;
     fill: none;
 }
 }
-
 .unit {
 .unit {
     margin: 0;
     margin: 0;
     padding: 5px 0px;
     padding: 5px 0px;
@@ -65,3 +64,6 @@ text {
     font-size: 10px;
     font-size: 10px;
     font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
     font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
 }
 }
+.hidden {
+    visibility: hidden;
+}

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

@@ -109,6 +109,30 @@
                         {% endif %}
                         {% endif %}
                     </td>
                     </td>
                 </tr>
                 </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>
                 <tr>
                     <td>Instances</td>
                     <td>Instances</td>
                     <td><a href="{% url 'dcim:device_list' %}?device_type_id={{ devicetype.pk }}">{{ devicetype.instances.count }}</a></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 %}
             {% render_field form.subdevice_role %}
         </div>
         </div>
     </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 %}
     {% if form.custom_fields %}
         <div class="panel panel-default">
         <div class="panel panel-default">
             <div class="panel-heading"><strong>Custom Fields</strong></div>
             <div class="panel-heading"><strong>Custom Fields</strong></div>

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

@@ -1,7 +1,4 @@
 {% load helpers %}
 {% load helpers %}
-
 <div class="rack_frame">
 <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>
 </div>

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

@@ -47,6 +47,11 @@
     <div class="pull-right noprint">
     <div class="pull-right noprint">
         {% custom_links rack %}
         {% custom_links rack %}
     </div>
     </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">
     <ul class="nav nav-tabs">
         <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
         <li role="presentation"{% if not active_tab %} class="active"{% endif %}>
             <a href="{{ rack.get_absolute_url }}">Rack</a>
             <a href="{{ rack.get_absolute_url }}">Rack</a>
@@ -371,6 +376,22 @@
 <script type="text/javascript">
 <script type="text/javascript">
 $(function() {
 $(function() {
   $('[data-toggle="popover"]').popover()
   $('[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>
 </script>
 {% endblock %}
 {% endblock %}