1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

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

This commit is contained in:
Jeremy Stretch
2020-02-20 12:11:59 -05:00
parent e07ed3de93
commit d2157a3423
8 changed files with 129 additions and 8 deletions

View File

@ -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', 'manufacturer', 'model', 'slug', 'part_number', 'u_height', 'is_full_depth', 'subdevice_role',
'tags', 'front_image', 'rear_image', 'comments', 'tags',
] ]
widgets = { widgets = {
'subdevice_role': StaticSelect2() 'subdevice_role': StaticSelect2()

View File

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

View File

@ -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)

View File

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

View File

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

View File

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

View File

@ -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" id="rack_{{ face }}"></object>
<object data="{% url 'dcim-api:rack-elevation' pk=rack.pk %}?face={{face}}&render=svg"></object>
</div> </div>

View File

@ -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 %}