Просмотр исходного кода

Change approach for declaring object panels

Jeremy Stretch 3 месяцев назад
Родитель
Сommit
3890043b06

+ 0 - 0
netbox/dcim/template_components/__init__.py


+ 26 - 0
netbox/dcim/template_components/object_panels.py

@@ -0,0 +1,26 @@
+from django.utils.translation import gettext_lazy as _
+
+from netbox.templates.components import (
+    GPSCoordinatesAttr, NestedObjectAttr, ObjectAttr, ObjectDetailsPanel, TemplatedAttr, TextAttr,
+)
+
+
+class DevicePanel(ObjectDetailsPanel):
+    region = NestedObjectAttr('site.region', linkify=True)
+    site = ObjectAttr('site', linkify=True, grouped_by='group')
+    location = NestedObjectAttr('location', linkify=True)
+    rack = TemplatedAttr('rack', template_name='dcim/device/attrs/rack.html')
+    virtual_chassis = NestedObjectAttr('virtual_chassis', linkify=True)
+    parent_device = TemplatedAttr(
+        'parent_bay',
+        template_name='dcim/device/attrs/parent_device.html',
+        label=_('Parent Device'),
+    )
+    gps_coordinates = GPSCoordinatesAttr()
+    tenant = ObjectAttr('tenant', linkify=True, grouped_by='group')
+    device_type = ObjectAttr('device_type', linkify=True, grouped_by='manufacturer')
+    description = TextAttr('description')
+    airflow = TextAttr('get_airflow_display')
+    serial = TextAttr('serial', style='font-monospace')
+    asset_tag = TextAttr('asset_tag', style='font-monospace')
+    config_template = ObjectAttr('config_template', linkify=True)

+ 2 - 22
netbox/dcim/views.py

@@ -12,13 +12,11 @@ from django.utils.translation import gettext_lazy as _
 from django.views.generic import View
 
 from circuits.models import Circuit, CircuitTermination
+from dcim.template_components.object_panels import DevicePanel
 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
@@ -2226,28 +2224,10 @@ 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}',
-            'device_attrs': device_attrs,
+            'device_panel': DevicePanel(instance, _('Device')),
         }
 
 

+ 112 - 66
netbox/netbox/templates/components.py

@@ -1,89 +1,91 @@
-from abc import ABC, abstractmethod
+from abc import ABC, ABCMeta, abstractmethod
+from functools import cached_property
 
 from django.template.loader import render_to_string
 from django.utils.html import escape
 from django.utils.safestring import mark_safe
+from django.utils.translation import gettext_lazy as _
 
 from netbox.config import get_config
-
-
-class Component(ABC):
-
-    @abstractmethod
-    def render(self):
-        pass
-
-    def __str__(self):
-        return self.render()
+from utilities.string import title
 
 
 #
 # Attributes
 #
 
-class Attr(Component):
+class Attr:
     template_name = None
     placeholder = mark_safe('<span class="text-muted">&mdash;</span>')
 
+    def __init__(self, accessor, label=None, template_name=None):
+        self.accessor = accessor
+        self.label = label
+        self.template_name = template_name or self.template_name
+
+    @staticmethod
+    def _resolve_attr(obj, path):
+        cur = obj
+        for part in path.split('.'):
+            if cur is None:
+                return None
+            cur = getattr(cur, part) if hasattr(cur, part) else cur.get(part) if isinstance(cur, dict) else None
+        return cur
+
 
 class TextAttr(Attr):
 
-    def __init__(self, value, style=None):
-        self.value = value
+    def __init__(self, *args, style=None, **kwargs):
+        super().__init__(*args, **kwargs)
         self.style = style
 
-    def render(self):
-        if self.value in (None, ''):
+    def render(self, obj):
+        value = self._resolve_attr(obj, self.accessor)
+        if value in (None, ''):
             return self.placeholder
         if self.style:
-            return mark_safe(f'<span class="{self.style}">{escape(self.value)}</span>')
-        return self.value
+            return mark_safe(f'<span class="{self.style}">{escape(value)}</span>')
+        return value
 
 
 class ObjectAttr(Attr):
     template_name = 'components/object.html'
 
-    def __init__(self, obj, linkify=None, grouped_by=None, template_name=None):
-        self.object = obj
+    def __init__(self, *args, linkify=None, grouped_by=None, **kwargs):
+        super().__init__(*args, **kwargs)
         self.linkify = linkify
-        self.group = getattr(obj, grouped_by, None) if grouped_by else None
-        self.template_name = template_name or self.template_name
+        self.grouped_by = grouped_by
 
-    def render(self):
-        if self.object is None:
-            return self.placeholder
+        # Derive label from related object if not explicitly set
+        if self.label is None:
+            self.label = title(self.accessor)
 
-        # 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
+    def render(self, obj):
+        value = self._resolve_attr(obj, self.accessor)
+        if value is None:
+            return self.placeholder
+        group = getattr(value, self.grouped_by, None) if self.grouped_by else None
 
         return render_to_string(self.template_name, {
-            'object': self.object,
-            'object_url': object_url,
-            'group': self.group,
-            'group_url': group_url,
+            'object': value,
+            'group': group,
+            'linkify': self.linkify,
         })
 
 
 class NestedObjectAttr(Attr):
     template_name = 'components/nested_object.html'
 
-    def __init__(self, obj, linkify=None):
-        self.object = obj
+    def __init__(self, *args, linkify=None, **kwargs):
+        super().__init__(*args, **kwargs)
         self.linkify = linkify
 
-    def render(self):
-        if not self.object:
+    def render(self, obj):
+        value = self._resolve_attr(obj, self.accessor)
+        if value is None:
             return self.placeholder
         return render_to_string(self.template_name, {
-            'nodes': self.object.get_ancestors(include_self=True),
+            'nodes': value.get_ancestors(include_self=True),
             'linkify': self.linkify,
         })
 
@@ -91,9 +93,11 @@ class NestedObjectAttr(Attr):
 class GPSCoordinatesAttr(Attr):
     template_name = 'components/gps_coordinates.html'
 
-    def __init__(self, latitude, longitude, map_url=True):
-        self.latitude = latitude
-        self.longitude = longitude
+    def __init__(self, latitude_attr='latitude', longitude_attr='longitude', map_url=True, **kwargs):
+        kwargs.setdefault('label', _('GPS Coordinates'))
+        super().__init__(accessor=None, **kwargs)
+        self.latitude_attr = latitude_attr
+        self.longitude_attr = longitude_attr
         if map_url is True:
             self.map_url = get_config().MAPS_URL
         elif map_url:
@@ -101,39 +105,81 @@ class GPSCoordinatesAttr(Attr):
         else:
             self.map_url = None
 
-    def render(self):
-        if not (self.latitude and self.longitude):
+    def render(self, obj):
+        latitude = self._resolve_attr(obj, self.latitude_attr)
+        longitude = self._resolve_attr(obj, self.longitude_attr)
+        if latitude is None or longitude is None:
             return self.placeholder
         return render_to_string(self.template_name, {
-            'latitude': self.latitude,
-            'longitude': self.longitude,
+            'latitude': latitude,
+            'longitude': longitude,
             'map_url': self.map_url,
         })
 
 
+class TemplatedAttr(Attr):
+
+    def __init__(self, *args, context=None, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.context = context or {}
+
+    def render(self, obj):
+        return render_to_string(
+            self.template_name,
+            {
+                **self.context,
+                'object': obj,
+                'value': self._resolve_attr(obj, self.accessor),
+            }
+        )
+
+
 #
 # Components
 #
 
-class AttributesPanel(Component):
-    template_name = 'components/attributes_panel.html'
-
-    def __init__(self, title, attrs):
-        self.title = title
-        self.attrs = attrs
+class Component(ABC):
 
+    @abstractmethod
     def render(self):
-        return render_to_string(self.template_name, {
-            'title': self.title,
-            'attrs': self.attrs,
-        })
+        pass
+
+    def __str__(self):
+        return self.render()
 
 
-class EmbeddedTemplate(Component):
+class ObjectDetailsPanelMeta(ABCMeta):
 
-    def __init__(self, template_name, context=None):
-        self.template_name = template_name
-        self.context = context or {}
+    def __new__(mcls, name, bases, attrs):
+        # Collect all declared attributes
+        attrs['_attrs'] = {}
+        for key, val in list(attrs.items()):
+            if isinstance(val, Attr):
+                attrs['_attrs'][key] = val
+        return super().__new__(mcls, name, bases, attrs)
+
+
+class ObjectDetailsPanel(Component, metaclass=ObjectDetailsPanelMeta):
+    template_name = 'components/object_details_panel.html'
+
+    def __init__(self, obj, title=None):
+        self.object = obj
+        self.title = title or obj._meta.verbose_name
+
+    @cached_property
+    def attributes(self):
+        return [
+            {
+                'label': attr.label or title(name),
+                'value': attr.render(self.object),
+            } for name, attr in self._attrs.items()
+        ]
 
     def render(self):
-        return render_to_string(self.template_name, self.context)
+        return render_to_string(self.template_name, {
+            'title': self.title,
+            'attrs': self.attributes,
+        })
+
+    def __str__(self):
+        return self.render()

+ 3 - 15
netbox/templates/components/object.html

@@ -2,25 +2,13 @@
   {# Display an object with its parent group #}
   <ol class="breadcrumb" aria-label="breadcrumbs">
     <li class="breadcrumb-item">
-      {% if group_url %}
-        <a href="{{ group_url }}">{{ group }}</a>
-      {% else %}
-        {{ object.group }}
-      {% endif %}
+      {% if linkify %}{{ group|linkify }}{% else %}{{ group }}{% endif %}
     </li>
     <li class="breadcrumb-item">
-      {% if object_url %}
-        <a href="{{ object_url }}">{{ object }}</a>
-      {% else %}
-        {{ object }}
-      {% endif %}
+      {% if linkify %}{{ object|linkify }}{% else %}{{ object }}{% endif %}
     </li>
   </ol>
 {% else %}
   {# Display only the object #}
-  {% if object_url %}
-    <a href="{{ object_url }}">{{ object }}</a>
-  {% else %}
-    {{ object }}
-  {% endif %}
+  {% if linkify %}{{ object|linkify }}{% else %}{{ object }}{% endif %}
 {% endif %}

+ 3 - 3
netbox/templates/components/attributes_panel.html → netbox/templates/components/object_details_panel.html

@@ -1,11 +1,11 @@
 <div class="card">
   <h2 class="card-header">{{ title }}</h2>
   <table class="table table-hover attr-table">
-    {% for label, attr in attrs.items %}
+    {% for attr in attrs %}
       <tr>
-        <th scope="row">{{ label }}</th>
+        <th scope="row">{{ attr.label }}</th>
         <td>
-          <div class="d-flex justify-content-between align-items-start">{{ attr }}</div>
+          <div class="d-flex justify-content-between align-items-start">{{ attr.value }}</div>
         </td>
       </tr>
     {% endfor %}

+ 1 - 1
netbox/templates/dcim/device.html

@@ -177,7 +177,7 @@
             {% plugin_left_page object %}
         </div>
         <div class="col col-12 col-xl-6">
-            {{ device_attrs }}
+            {{ device_panel }}
             <div class="card">
                 <h2 class="card-header">{% trans "Management" %}</h2>
                 <table class="table table-hover attr-table">

+ 1 - 1
netbox/templates/dcim/device/attrs/parent_device.html

@@ -1,4 +1,4 @@
-{% if device.parent_bay %}
+{% if value %}
   <ol class="breadcrumb" aria-label="breadcrumbs">
     <li class="breadcrumb-item">{{ device.parent_bay.device|linkify }}</li>
     <li class="breadcrumb-item">{{ device.parent_bay }}</li>

+ 7 - 7
netbox/templates/dcim/device/attrs/rack.html

@@ -1,15 +1,15 @@
 {% load i18n %}
-{% if device.rack %}
+{% if value %}
   <span>
-    {{ 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 %}
+    {{ value|linkify }}
+    {% if value and object.position %}
+      (U{{ object.position|floatformat }} / {{ object.get_face_display }})
+    {% elif value and object.device_type.u_height %}
       <span class="badge text-bg-warning">{% trans "Not racked" %}</span>
     {% endif %}
   </span>
-  {% if device.rack and device.position %}
-    <a href="{{ device.rack.get_absolute_url }}?device={{ device.pk }}" class="btn btn-primary btn-sm d-print-none" title="{% trans "Highlight device in rack" %}">
+  {% if value and object.position %}
+    <a href="{{ value.get_absolute_url }}?device={{ object.pk }}" class="btn btn-primary btn-sm d-print-none" title="{% trans "Highlight device in rack" %}">
       <i class="mdi mdi-view-day-outline"></i>
     </a>
   {% endif %}