|
|
@@ -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">—</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()
|