diff --git a/netbox/dcim/views.py b/netbox/dcim/views.py index a41078a11..2439a3c06 100644 --- a/netbox/dcim/views.py +++ b/netbox/dcim/views.py @@ -16,6 +16,9 @@ from extras.views import ObjectConfigContextView, ObjectRenderConfigView from ipam.models import ASN, IPAddress, Prefix, VLANGroup, VLAN from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable from netbox.object_actions import * +from netbox.templates.components import ( + AttributesPanel, EmbeddedTemplate, GPSCoordinatesAttr, NestedObjectAttr, ObjectAttr, TextAttr, +) from netbox.views import generic from utilities.forms import ConfirmationForm from utilities.paginator import EnhancedPaginator, get_paginate_count @@ -2223,9 +2226,28 @@ class DeviceView(generic.ObjectView): else: vc_members = [] + device_attrs = AttributesPanel(_('Device'), { + _('Region'): NestedObjectAttr(instance.site.region, linkify=True), + _('Site'): ObjectAttr(instance.site, linkify=True, grouped_by='group'), + _('Location'): ObjectAttr(instance.location, linkify=True), + # TODO: Include position & face of parent device (if applicable) + _('Rack'): EmbeddedTemplate('dcim/device/attrs/rack.html', {'device': instance}), + _('Virtual Chassis'): ObjectAttr(instance.virtual_chassis, linkify=True), + _('Parent Device'): EmbeddedTemplate('dcim/device/attrs/parent_device.html', {'device': instance}), + _('GPS Coordinates'): GPSCoordinatesAttr(instance.latitude, instance.longitude), + _('Tenant'): ObjectAttr(instance.tenant, linkify=True, grouped_by='group'), + _('Device Type'): ObjectAttr(instance.device_type, linkify=True, grouped_by='manufacturer'), + _('Description'): TextAttr(instance.description), + _('Airflow'): TextAttr(instance.get_airflow_display()), + _('Serial Number'): TextAttr(instance.serial, style='font-monospace'), + _('Asset Tag'): TextAttr(instance.asset_tag, style='font-monospace'), + _('Config Template'): ObjectAttr(instance.config_template, linkify=True), + }) + return { 'vc_members': vc_members, - 'svg_extra': f'highlight=id:{instance.pk}' + 'svg_extra': f'highlight=id:{instance.pk}', + 'device_attrs': device_attrs, } diff --git a/netbox/netbox/templates/__init__.py b/netbox/netbox/templates/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/netbox/netbox/templates/components.py b/netbox/netbox/templates/components.py new file mode 100644 index 000000000..cb99b0e5a --- /dev/null +++ b/netbox/netbox/templates/components.py @@ -0,0 +1,139 @@ +from abc import ABC, abstractmethod + +from django.template.loader import render_to_string +from django.utils.html import escape +from django.utils.safestring import mark_safe + +from netbox.config import get_config + + +class Component(ABC): + + @abstractmethod + def render(self): + pass + + def __str__(self): + return self.render() + + +# +# Attributes +# + +class Attr(Component): + template_name = None + placeholder = mark_safe('') + + +class TextAttr(Attr): + + def __init__(self, value, style=None): + self.value = value + self.style = style + + def render(self): + if self.value in (None, ''): + return self.placeholder + if self.style: + return mark_safe(f'{escape(self.value)}') + return self.value + + +class ObjectAttr(Attr): + template_name = 'components/object.html' + + def __init__(self, obj, linkify=None, grouped_by=None, template_name=None): + self.object = obj + self.linkify = linkify + self.group = getattr(obj, grouped_by, None) if grouped_by else None + self.template_name = template_name or self.template_name + + def render(self): + if self.object is None: + return self.placeholder + + # Determine object & group URLs + # TODO: Add support for reverse() lookups + if self.linkify and hasattr(self.object, 'get_absolute_url'): + object_url = self.object.get_absolute_url() + else: + object_url = None + if self.linkify and hasattr(self.group, 'get_absolute_url'): + group_url = self.group.get_absolute_url() + else: + group_url = None + + return render_to_string(self.template_name, { + 'object': self.object, + 'object_url': object_url, + 'group': self.group, + 'group_url': group_url, + }) + + +class NestedObjectAttr(Attr): + template_name = 'components/nested_object.html' + + def __init__(self, obj, linkify=None): + self.object = obj + self.linkify = linkify + + def render(self): + if not self.object: + return self.placeholder + return render_to_string(self.template_name, { + 'nodes': self.object.get_ancestors(include_self=True), + 'linkify': self.linkify, + }) + + +class GPSCoordinatesAttr(Attr): + template_name = 'components/gps_coordinates.html' + + def __init__(self, latitude, longitude, map_url=True): + self.latitude = latitude + self.longitude = longitude + if map_url is True: + self.map_url = get_config().MAPS_URL + elif map_url: + self.map_url = map_url + else: + self.map_url = None + + def render(self): + if not (self.latitude and self.longitude): + return self.placeholder + return render_to_string(self.template_name, { + 'latitude': self.latitude, + 'longitude': self.longitude, + 'map_url': self.map_url, + }) + + +# +# Components +# + +class AttributesPanel(Component): + template_name = 'components/attributes_panel.html' + + def __init__(self, title, attrs): + self.title = title + self.attrs = attrs + + def render(self): + return render_to_string(self.template_name, { + 'title': self.title, + 'attrs': self.attrs, + }) + + +class EmbeddedTemplate(Component): + + def __init__(self, template_name, context=None): + self.template_name = template_name + self.context = context or {} + + def render(self): + return render_to_string(self.template_name, self.context) diff --git a/netbox/templates/components/attributes_panel.html b/netbox/templates/components/attributes_panel.html new file mode 100644 index 000000000..90c470b0d --- /dev/null +++ b/netbox/templates/components/attributes_panel.html @@ -0,0 +1,13 @@ +
+

{{ title }}

+ + {% for label, attr in attrs.items %} + + + + + {% endfor %} +
{{ label }} +
{{ attr }}
+
+
diff --git a/netbox/templates/components/gps_coordinates.html b/netbox/templates/components/gps_coordinates.html new file mode 100644 index 000000000..8e72f08bb --- /dev/null +++ b/netbox/templates/components/gps_coordinates.html @@ -0,0 +1,8 @@ +{% load i18n %} +{% load l10n %} +{{ latitude }}, {{ longitude }} +{% if map_url %} + + {% trans "Map" %} + +{% endif %} diff --git a/netbox/templates/components/nested_object.html b/netbox/templates/components/nested_object.html new file mode 100644 index 000000000..8cae08189 --- /dev/null +++ b/netbox/templates/components/nested_object.html @@ -0,0 +1,11 @@ + diff --git a/netbox/templates/components/object.html b/netbox/templates/components/object.html new file mode 100644 index 000000000..53702adc6 --- /dev/null +++ b/netbox/templates/components/object.html @@ -0,0 +1,26 @@ +{% if group %} + {# Display an object with its parent group #} + +{% else %} + {# Display only the object #} + {% if object_url %} + {{ object }} + {% else %} + {{ object }} + {% endif %} +{% endif %} diff --git a/netbox/templates/dcim/device.html b/netbox/templates/dcim/device.html index f8b8e95c2..03666dee9 100644 --- a/netbox/templates/dcim/device.html +++ b/netbox/templates/dcim/device.html @@ -177,6 +177,7 @@ {% plugin_left_page object %}
+ {{ device_attrs }}

{% trans "Management" %}

diff --git a/netbox/templates/dcim/device/attrs/parent_device.html b/netbox/templates/dcim/device/attrs/parent_device.html new file mode 100644 index 000000000..e8674e23b --- /dev/null +++ b/netbox/templates/dcim/device/attrs/parent_device.html @@ -0,0 +1,8 @@ +{% if device.parent_bay %} + +{% else %} + {{ ''|placeholder }} +{% endif %} diff --git a/netbox/templates/dcim/device/attrs/rack.html b/netbox/templates/dcim/device/attrs/rack.html new file mode 100644 index 000000000..41a031bad --- /dev/null +++ b/netbox/templates/dcim/device/attrs/rack.html @@ -0,0 +1,18 @@ +{% load i18n %} +{% if device.rack %} + + {{ device.rack|linkify }} + {% if device.rack and device.position %} + (U{{ device.position|floatformat }} / {{ device.get_face_display }}) + {% elif device.rack and device.device_type.u_height %} + {% trans "Not racked" %} + {% endif %} + + {% if device.rack and device.position %} + + + + {% endif %} +{% else %} + {{ ''|placeholder }} +{% endif %}