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:
@ -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()
|
||||||
|
23
netbox/dcim/migrations/0098_devicetype_images.py
Normal file
23
netbox/dcim/migrations/0098_devicetype_images.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
@ -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)
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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">—</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">—</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>
|
||||||
|
@ -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,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>
|
||||||
|
@ -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 %}
|
||||||
|
Reference in New Issue
Block a user