mirror of
https://github.com/netbox-community/netbox.git
synced 2026-01-10 05:42:16 -06:00
Split ObjectPanel into a base class and ObjectAttrsPanel; use base class for e.g. CommentsPanels, JSONPanel, etc.
This commit is contained in:
@@ -3,7 +3,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
from netbox.ui import attrs, panels
|
from netbox.ui import attrs, panels
|
||||||
|
|
||||||
|
|
||||||
class SitePanel(panels.ObjectPanel):
|
class SitePanel(panels.ObjectAttributesPanel):
|
||||||
region = attrs.NestedObjectAttr('region', linkify=True)
|
region = attrs.NestedObjectAttr('region', linkify=True)
|
||||||
group = attrs.NestedObjectAttr('group', linkify=True)
|
group = attrs.NestedObjectAttr('group', linkify=True)
|
||||||
status = attrs.ChoiceAttr('status')
|
status = attrs.ChoiceAttr('status')
|
||||||
@@ -23,7 +23,7 @@ class LocationPanel(panels.NestedGroupObjectPanel):
|
|||||||
facility = attrs.TextAttr('facility')
|
facility = attrs.TextAttr('facility')
|
||||||
|
|
||||||
|
|
||||||
class RackDimensionsPanel(panels.ObjectPanel):
|
class RackDimensionsPanel(panels.ObjectAttributesPanel):
|
||||||
form_factor = attrs.ChoiceAttr('form_factor')
|
form_factor = attrs.ChoiceAttr('form_factor')
|
||||||
width = attrs.ChoiceAttr('width')
|
width = attrs.ChoiceAttr('width')
|
||||||
u_height = attrs.TextAttr('u_height', format_string='{}U', label=_('Height'))
|
u_height = attrs.TextAttr('u_height', format_string='{}U', label=_('Height'))
|
||||||
@@ -33,12 +33,12 @@ class RackDimensionsPanel(panels.ObjectPanel):
|
|||||||
mounting_depth = attrs.TextAttr('mounting_depth', format_string='{}mm')
|
mounting_depth = attrs.TextAttr('mounting_depth', format_string='{}mm')
|
||||||
|
|
||||||
|
|
||||||
class RackNumberingPanel(panels.ObjectPanel):
|
class RackNumberingPanel(panels.ObjectAttributesPanel):
|
||||||
starting_unit = attrs.TextAttr('starting_unit')
|
starting_unit = attrs.TextAttr('starting_unit')
|
||||||
desc_units = attrs.BooleanAttr('desc_units', label=_('Descending units'))
|
desc_units = attrs.BooleanAttr('desc_units', label=_('Descending units'))
|
||||||
|
|
||||||
|
|
||||||
class RackPanel(panels.ObjectPanel):
|
class RackPanel(panels.ObjectAttributesPanel):
|
||||||
region = attrs.NestedObjectAttr('site.region', linkify=True)
|
region = attrs.NestedObjectAttr('site.region', linkify=True)
|
||||||
site = attrs.ObjectAttr('site', linkify=True, grouped_by='group')
|
site = attrs.ObjectAttr('site', linkify=True, grouped_by='group')
|
||||||
location = attrs.NestedObjectAttr('location', linkify=True)
|
location = attrs.NestedObjectAttr('location', linkify=True)
|
||||||
@@ -55,7 +55,7 @@ class RackPanel(panels.ObjectPanel):
|
|||||||
power_utilization = attrs.UtilizationAttr('get_power_utilization')
|
power_utilization = attrs.UtilizationAttr('get_power_utilization')
|
||||||
|
|
||||||
|
|
||||||
class RackWeightPanel(panels.ObjectPanel):
|
class RackWeightPanel(panels.ObjectAttributesPanel):
|
||||||
weight = attrs.NumericAttr('weight', unit_accessor='get_weight_unit_display')
|
weight = attrs.NumericAttr('weight', unit_accessor='get_weight_unit_display')
|
||||||
max_weight = attrs.NumericAttr('max_weight', unit_accessor='get_weight_unit_display', label=_('Maximum weight'))
|
max_weight = attrs.NumericAttr('max_weight', unit_accessor='get_weight_unit_display', label=_('Maximum weight'))
|
||||||
total_weight = attrs.NumericAttr('total_weight', unit_accessor='get_weight_unit_display')
|
total_weight = attrs.NumericAttr('total_weight', unit_accessor='get_weight_unit_display')
|
||||||
@@ -65,14 +65,14 @@ class RackRolePanel(panels.OrganizationalObjectPanel):
|
|||||||
color = attrs.ColorAttr('color')
|
color = attrs.ColorAttr('color')
|
||||||
|
|
||||||
|
|
||||||
class RackTypePanel(panels.ObjectPanel):
|
class RackTypePanel(panels.ObjectAttributesPanel):
|
||||||
manufacturer = attrs.ObjectAttr('manufacturer', linkify=True)
|
manufacturer = attrs.ObjectAttr('manufacturer', linkify=True)
|
||||||
model = attrs.TextAttr('model')
|
model = attrs.TextAttr('model')
|
||||||
description = attrs.TextAttr('description')
|
description = attrs.TextAttr('description')
|
||||||
airflow = attrs.ChoiceAttr('airflow')
|
airflow = attrs.ChoiceAttr('airflow')
|
||||||
|
|
||||||
|
|
||||||
class DevicePanel(panels.ObjectPanel):
|
class DevicePanel(panels.ObjectAttributesPanel):
|
||||||
region = attrs.NestedObjectAttr('site.region', linkify=True)
|
region = attrs.NestedObjectAttr('site.region', linkify=True)
|
||||||
site = attrs.ObjectAttr('site', linkify=True, grouped_by='group')
|
site = attrs.ObjectAttr('site', linkify=True, grouped_by='group')
|
||||||
location = attrs.NestedObjectAttr('location', linkify=True)
|
location = attrs.NestedObjectAttr('location', linkify=True)
|
||||||
@@ -89,7 +89,7 @@ class DevicePanel(panels.ObjectPanel):
|
|||||||
config_template = attrs.ObjectAttr('config_template', linkify=True)
|
config_template = attrs.ObjectAttr('config_template', linkify=True)
|
||||||
|
|
||||||
|
|
||||||
class DeviceManagementPanel(panels.ObjectPanel):
|
class DeviceManagementPanel(panels.ObjectAttributesPanel):
|
||||||
status = attrs.ChoiceAttr('status')
|
status = attrs.ChoiceAttr('status')
|
||||||
role = attrs.NestedObjectAttr('role', linkify=True, max_depth=3)
|
role = attrs.NestedObjectAttr('role', linkify=True, max_depth=3)
|
||||||
platform = attrs.NestedObjectAttr('platform', linkify=True, max_depth=3)
|
platform = attrs.NestedObjectAttr('platform', linkify=True, max_depth=3)
|
||||||
@@ -110,7 +110,7 @@ class DeviceManagementPanel(panels.ObjectPanel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class DeviceTypePanel(panels.ObjectPanel):
|
class DeviceTypePanel(panels.ObjectAttributesPanel):
|
||||||
manufacturer = attrs.ObjectAttr('manufacturer', linkify=True)
|
manufacturer = attrs.ObjectAttr('manufacturer', linkify=True)
|
||||||
model = attrs.TextAttr('model')
|
model = attrs.TextAttr('model')
|
||||||
part_number = attrs.TextAttr('part_number')
|
part_number = attrs.TextAttr('part_number')
|
||||||
@@ -126,6 +126,6 @@ class DeviceTypePanel(panels.ObjectPanel):
|
|||||||
rear_image = attrs.ImageAttr('rear_image')
|
rear_image = attrs.ImageAttr('rear_image')
|
||||||
|
|
||||||
|
|
||||||
class ModuleTypeProfilePanel(panels.ObjectPanel):
|
class ModuleTypeProfilePanel(panels.ObjectAttributesPanel):
|
||||||
name = attrs.TextAttr('name')
|
name = attrs.TextAttr('name')
|
||||||
description = attrs.TextAttr('description')
|
description = attrs.TextAttr('description')
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from django.utils.safestring import mark_safe
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from netbox.config import get_config
|
from netbox.config import get_config
|
||||||
|
from utilities.data import resolve_attr_path
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
@@ -26,15 +27,6 @@ class Attr(ABC):
|
|||||||
def render(self, obj, context=None):
|
def render(self, obj, context=None):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@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):
|
class TextAttr(Attr):
|
||||||
template_name = 'ui/attrs/text.html'
|
template_name = 'ui/attrs/text.html'
|
||||||
@@ -47,7 +39,7 @@ class TextAttr(Attr):
|
|||||||
|
|
||||||
def render(self, obj, context=None):
|
def render(self, obj, context=None):
|
||||||
context = context or {}
|
context = context or {}
|
||||||
value = self._resolve_attr(obj, self.accessor)
|
value = resolve_attr_path(obj, self.accessor)
|
||||||
if value in (None, ''):
|
if value in (None, ''):
|
||||||
return self.placeholder
|
return self.placeholder
|
||||||
if self.format_string:
|
if self.format_string:
|
||||||
@@ -70,10 +62,10 @@ class NumericAttr(Attr):
|
|||||||
|
|
||||||
def render(self, obj, context=None):
|
def render(self, obj, context=None):
|
||||||
context = context or {}
|
context = context or {}
|
||||||
value = self._resolve_attr(obj, self.accessor)
|
value = resolve_attr_path(obj, self.accessor)
|
||||||
if value in (None, ''):
|
if value in (None, ''):
|
||||||
return self.placeholder
|
return self.placeholder
|
||||||
unit = self._resolve_attr(obj, self.unit_accessor) if self.unit_accessor else None
|
unit = resolve_attr_path(obj, self.unit_accessor) if self.unit_accessor else None
|
||||||
return render_to_string(self.template_name, {
|
return render_to_string(self.template_name, {
|
||||||
**context,
|
**context,
|
||||||
'value': value,
|
'value': value,
|
||||||
@@ -90,7 +82,7 @@ class ChoiceAttr(Attr):
|
|||||||
try:
|
try:
|
||||||
value = getattr(obj, f'get_{self.accessor}_display')()
|
value = getattr(obj, f'get_{self.accessor}_display')()
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
value = self._resolve_attr(obj, self.accessor)
|
value = resolve_attr_path(obj, self.accessor)
|
||||||
if value in (None, ''):
|
if value in (None, ''):
|
||||||
return self.placeholder
|
return self.placeholder
|
||||||
try:
|
try:
|
||||||
@@ -113,7 +105,7 @@ class BooleanAttr(Attr):
|
|||||||
|
|
||||||
def render(self, obj, context=None):
|
def render(self, obj, context=None):
|
||||||
context = context or {}
|
context = context or {}
|
||||||
value = self._resolve_attr(obj, self.accessor)
|
value = resolve_attr_path(obj, self.accessor)
|
||||||
if value in (None, '') and not self.display_false:
|
if value in (None, '') and not self.display_false:
|
||||||
return self.placeholder
|
return self.placeholder
|
||||||
return render_to_string(self.template_name, {
|
return render_to_string(self.template_name, {
|
||||||
@@ -128,7 +120,7 @@ class ColorAttr(Attr):
|
|||||||
|
|
||||||
def render(self, obj, context=None):
|
def render(self, obj, context=None):
|
||||||
context = context or {}
|
context = context or {}
|
||||||
value = self._resolve_attr(obj, self.accessor)
|
value = resolve_attr_path(obj, self.accessor)
|
||||||
return render_to_string(self.template_name, {
|
return render_to_string(self.template_name, {
|
||||||
**context,
|
**context,
|
||||||
'color': value,
|
'color': value,
|
||||||
@@ -140,7 +132,7 @@ class ImageAttr(Attr):
|
|||||||
|
|
||||||
def render(self, obj, context=None):
|
def render(self, obj, context=None):
|
||||||
context = context or {}
|
context = context or {}
|
||||||
value = self._resolve_attr(obj, self.accessor)
|
value = resolve_attr_path(obj, self.accessor)
|
||||||
if value in (None, ''):
|
if value in (None, ''):
|
||||||
return self.placeholder
|
return self.placeholder
|
||||||
return render_to_string(self.template_name, {
|
return render_to_string(self.template_name, {
|
||||||
@@ -159,7 +151,7 @@ class ObjectAttr(Attr):
|
|||||||
|
|
||||||
def render(self, obj, context=None):
|
def render(self, obj, context=None):
|
||||||
context = context or {}
|
context = context or {}
|
||||||
value = self._resolve_attr(obj, self.accessor)
|
value = resolve_attr_path(obj, self.accessor)
|
||||||
if value is None:
|
if value is None:
|
||||||
return self.placeholder
|
return self.placeholder
|
||||||
group = getattr(value, self.grouped_by, None) if self.grouped_by else None
|
group = getattr(value, self.grouped_by, None) if self.grouped_by else None
|
||||||
@@ -182,7 +174,7 @@ class NestedObjectAttr(Attr):
|
|||||||
|
|
||||||
def render(self, obj, context=None):
|
def render(self, obj, context=None):
|
||||||
context = context or {}
|
context = context or {}
|
||||||
value = self._resolve_attr(obj, self.accessor)
|
value = resolve_attr_path(obj, self.accessor)
|
||||||
if value is None:
|
if value is None:
|
||||||
return self.placeholder
|
return self.placeholder
|
||||||
nodes = value.get_ancestors(include_self=True)
|
nodes = value.get_ancestors(include_self=True)
|
||||||
@@ -209,7 +201,7 @@ class AddressAttr(Attr):
|
|||||||
|
|
||||||
def render(self, obj, context=None):
|
def render(self, obj, context=None):
|
||||||
context = context or {}
|
context = context or {}
|
||||||
value = self._resolve_attr(obj, self.accessor)
|
value = resolve_attr_path(obj, self.accessor)
|
||||||
if value in (None, ''):
|
if value in (None, ''):
|
||||||
return self.placeholder
|
return self.placeholder
|
||||||
return render_to_string(self.template_name, {
|
return render_to_string(self.template_name, {
|
||||||
@@ -236,8 +228,8 @@ class GPSCoordinatesAttr(Attr):
|
|||||||
|
|
||||||
def render(self, obj, context=None):
|
def render(self, obj, context=None):
|
||||||
context = context or {}
|
context = context or {}
|
||||||
latitude = self._resolve_attr(obj, self.latitude_attr)
|
latitude = resolve_attr_path(obj, self.latitude_attr)
|
||||||
longitude = self._resolve_attr(obj, self.longitude_attr)
|
longitude = resolve_attr_path(obj, self.longitude_attr)
|
||||||
if latitude is None or longitude is None:
|
if latitude is None or longitude is None:
|
||||||
return self.placeholder
|
return self.placeholder
|
||||||
return render_to_string(self.template_name, {
|
return render_to_string(self.template_name, {
|
||||||
@@ -253,7 +245,7 @@ class TimezoneAttr(Attr):
|
|||||||
|
|
||||||
def render(self, obj, context=None):
|
def render(self, obj, context=None):
|
||||||
context = context or {}
|
context = context or {}
|
||||||
value = self._resolve_attr(obj, self.accessor)
|
value = resolve_attr_path(obj, self.accessor)
|
||||||
if value in (None, ''):
|
if value in (None, ''):
|
||||||
return self.placeholder
|
return self.placeholder
|
||||||
return render_to_string(self.template_name, {
|
return render_to_string(self.template_name, {
|
||||||
@@ -270,7 +262,7 @@ class TemplatedAttr(Attr):
|
|||||||
|
|
||||||
def render(self, obj, context=None):
|
def render(self, obj, context=None):
|
||||||
context = context or {}
|
context = context or {}
|
||||||
value = self._resolve_attr(obj, self.accessor)
|
value = resolve_attr_path(obj, self.accessor)
|
||||||
if value is None:
|
if value is None:
|
||||||
return self.placeholder
|
return self.placeholder
|
||||||
return render_to_string(
|
return render_to_string(
|
||||||
@@ -289,7 +281,7 @@ class UtilizationAttr(Attr):
|
|||||||
|
|
||||||
def render(self, obj, context=None):
|
def render(self, obj, context=None):
|
||||||
context = context or {}
|
context = context or {}
|
||||||
value = self._resolve_attr(obj, self.accessor)
|
value = resolve_attr_path(obj, self.accessor)
|
||||||
return render_to_string(self.template_name, {
|
return render_to_string(self.template_name, {
|
||||||
**context,
|
**context,
|
||||||
'value': value,
|
'value': value,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from netbox.ui import attrs
|
from netbox.ui import attrs
|
||||||
from netbox.ui.actions import CopyContent
|
from netbox.ui.actions import CopyContent
|
||||||
|
from utilities.data import resolve_attr_path
|
||||||
from utilities.querydict import dict_to_querydict
|
from utilities.querydict import dict_to_querydict
|
||||||
from utilities.string import title
|
from utilities.string import title
|
||||||
from utilities.templatetags.plugins import _get_registered_content
|
from utilities.templatetags.plugins import _get_registered_content
|
||||||
@@ -15,6 +16,7 @@ __all__ = (
|
|||||||
'CommentsPanel',
|
'CommentsPanel',
|
||||||
'JSONPanel',
|
'JSONPanel',
|
||||||
'NestedGroupObjectPanel',
|
'NestedGroupObjectPanel',
|
||||||
|
'ObjectAttributesPanel',
|
||||||
'ObjectPanel',
|
'ObjectPanel',
|
||||||
'ObjectsTablePanel',
|
'ObjectsTablePanel',
|
||||||
'OrganizationalObjectPanel',
|
'OrganizationalObjectPanel',
|
||||||
@@ -25,6 +27,10 @@ __all__ = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Base classes
|
||||||
|
#
|
||||||
|
|
||||||
class Panel(ABC):
|
class Panel(ABC):
|
||||||
"""
|
"""
|
||||||
A block of content rendered within an HTML template.
|
A block of content rendered within an HTML template.
|
||||||
@@ -74,7 +80,44 @@ class Panel(ABC):
|
|||||||
return render_to_string(self.template_name, self.get_context(context))
|
return render_to_string(self.template_name, self.get_context(context))
|
||||||
|
|
||||||
|
|
||||||
class ObjectPanelMeta(ABCMeta):
|
#
|
||||||
|
# Object-specific panels
|
||||||
|
#
|
||||||
|
|
||||||
|
class ObjectPanel(Panel):
|
||||||
|
"""
|
||||||
|
Base class for object-specific panels.
|
||||||
|
"""
|
||||||
|
accessor = 'object'
|
||||||
|
|
||||||
|
def __init__(self, accessor=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Instantiate a new ObjectPanel.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
accessor: The name of the attribute on the object (default: "object")
|
||||||
|
"""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
|
if accessor is not None:
|
||||||
|
self.accessor = accessor
|
||||||
|
|
||||||
|
def get_context(self, context):
|
||||||
|
"""
|
||||||
|
Return the context data to be used when rendering the panel.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
context: The template context
|
||||||
|
"""
|
||||||
|
obj = resolve_attr_path(context, self.accessor)
|
||||||
|
return {
|
||||||
|
**super().get_context(context),
|
||||||
|
'title': self.title or title(obj._meta.verbose_name),
|
||||||
|
'object': obj,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ObjectAttributesPanelMeta(ABCMeta):
|
||||||
|
|
||||||
def __new__(mcls, name, bases, namespace, **kwargs):
|
def __new__(mcls, name, bases, namespace, **kwargs):
|
||||||
declared = {}
|
declared = {}
|
||||||
@@ -101,7 +144,7 @@ class ObjectPanelMeta(ABCMeta):
|
|||||||
return cls
|
return cls
|
||||||
|
|
||||||
|
|
||||||
class ObjectPanel(Panel, metaclass=ObjectPanelMeta):
|
class ObjectAttributesPanel(ObjectPanel, metaclass=ObjectAttributesPanelMeta):
|
||||||
"""
|
"""
|
||||||
A panel which displays selected attributes of an object.
|
A panel which displays selected attributes of an object.
|
||||||
|
|
||||||
@@ -109,10 +152,9 @@ class ObjectPanel(Panel, metaclass=ObjectPanelMeta):
|
|||||||
template_name: The name of the template to render
|
template_name: The name of the template to render
|
||||||
accessor: The name of the attribute on the object
|
accessor: The name of the attribute on the object
|
||||||
"""
|
"""
|
||||||
template_name = 'ui/panels/object.html'
|
template_name = 'ui/panels/object_attributes.html'
|
||||||
accessor = None
|
|
||||||
|
|
||||||
def __init__(self, accessor=None, only=None, exclude=None, **kwargs):
|
def __init__(self, only=None, exclude=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Instantiate a new ObjectPanel.
|
Instantiate a new ObjectPanel.
|
||||||
|
|
||||||
@@ -123,9 +165,6 @@ class ObjectPanel(Panel, metaclass=ObjectPanelMeta):
|
|||||||
"""
|
"""
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
if accessor is not None:
|
|
||||||
self.accessor = accessor
|
|
||||||
|
|
||||||
# Set included/excluded attributes
|
# Set included/excluded attributes
|
||||||
if only is not None and exclude is not None:
|
if only is not None and exclude is not None:
|
||||||
raise ValueError("only and exclude cannot both be specified.")
|
raise ValueError("only and exclude cannot both be specified.")
|
||||||
@@ -155,21 +194,20 @@ class ObjectPanel(Panel, metaclass=ObjectPanelMeta):
|
|||||||
elif self.exclude:
|
elif self.exclude:
|
||||||
attr_names -= set(self.exclude)
|
attr_names -= set(self.exclude)
|
||||||
|
|
||||||
obj = getattr(context['object'], self.accessor) if self.accessor else context['object']
|
ctx = super().get_context(context)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
**super().get_context(context),
|
**ctx,
|
||||||
'title': self.title or title(obj._meta.verbose_name),
|
|
||||||
'attrs': [
|
'attrs': [
|
||||||
{
|
{
|
||||||
'label': attr.label or self._name_to_label(name),
|
'label': attr.label or self._name_to_label(name),
|
||||||
'value': attr.render(obj, {'name': name}),
|
'value': attr.render(ctx['object'], {'name': name}),
|
||||||
} for name, attr in self._attrs.items() if name in attr_names
|
} for name, attr in self._attrs.items() if name in attr_names
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class OrganizationalObjectPanel(ObjectPanel, metaclass=ObjectPanelMeta):
|
class OrganizationalObjectPanel(ObjectAttributesPanel, metaclass=ObjectAttributesPanelMeta):
|
||||||
"""
|
"""
|
||||||
An ObjectPanel with attributes common to OrganizationalModels.
|
An ObjectPanel with attributes common to OrganizationalModels.
|
||||||
"""
|
"""
|
||||||
@@ -177,20 +215,82 @@ class OrganizationalObjectPanel(ObjectPanel, metaclass=ObjectPanelMeta):
|
|||||||
description = attrs.TextAttr('description', label=_('Description'))
|
description = attrs.TextAttr('description', label=_('Description'))
|
||||||
|
|
||||||
|
|
||||||
class NestedGroupObjectPanel(OrganizationalObjectPanel, metaclass=ObjectPanelMeta):
|
class NestedGroupObjectPanel(ObjectAttributesPanel, metaclass=ObjectAttributesPanelMeta):
|
||||||
"""
|
"""
|
||||||
An ObjectPanel with attributes common to NestedGroupObjects.
|
An ObjectPanel with attributes common to NestedGroupObjects.
|
||||||
"""
|
"""
|
||||||
parent = attrs.NestedObjectAttr('parent', label=_('Parent'), linkify=True)
|
parent = attrs.NestedObjectAttr('parent', label=_('Parent'), linkify=True)
|
||||||
|
|
||||||
|
|
||||||
class CommentsPanel(Panel):
|
class CommentsPanel(ObjectPanel):
|
||||||
"""
|
"""
|
||||||
A panel which displays comments associated with an object.
|
A panel which displays comments associated with an object.
|
||||||
"""
|
"""
|
||||||
template_name = 'ui/panels/comments.html'
|
template_name = 'ui/panels/comments.html'
|
||||||
title = _('Comments')
|
title = _('Comments')
|
||||||
|
|
||||||
|
def __init__(self, field_name='comments', **kwargs):
|
||||||
|
"""
|
||||||
|
Instantiate a new CommentsPanel.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
field_name: The name of the comment field on the object
|
||||||
|
"""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.field_name = field_name
|
||||||
|
|
||||||
|
def get_context(self, context):
|
||||||
|
"""
|
||||||
|
Return the context data to be used when rendering the panel.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
context: The template context
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
**super().get_context(context),
|
||||||
|
'comments': getattr(context['object'], self.field_name),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class JSONPanel(ObjectPanel):
|
||||||
|
"""
|
||||||
|
A panel which renders formatted JSON data from an object's JSONField.
|
||||||
|
"""
|
||||||
|
template_name = 'ui/panels/json.html'
|
||||||
|
|
||||||
|
def __init__(self, field_name, copy_button=True, **kwargs):
|
||||||
|
"""
|
||||||
|
Instantiate a new JSONPanel.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
field_name: The name of the JSON field on the object
|
||||||
|
copy_button: Set to True (default) to include a copy-to-clipboard button
|
||||||
|
"""
|
||||||
|
super().__init__(**kwargs)
|
||||||
|
self.field_name = field_name
|
||||||
|
|
||||||
|
if copy_button:
|
||||||
|
self.actions.append(
|
||||||
|
CopyContent(f'panel_{field_name}'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_context(self, context):
|
||||||
|
"""
|
||||||
|
Return the context data to be used when rendering the panel.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
context: The template context
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
**super().get_context(context),
|
||||||
|
'data': getattr(context['object'], self.field_name),
|
||||||
|
'field_name': self.field_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Miscellaneous panels
|
||||||
|
#
|
||||||
|
|
||||||
class RelatedObjectsPanel(Panel):
|
class RelatedObjectsPanel(Panel):
|
||||||
"""
|
"""
|
||||||
@@ -261,42 +361,6 @@ class ObjectsTablePanel(Panel):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class JSONPanel(Panel):
|
|
||||||
"""
|
|
||||||
A panel which renders formatted JSON data.
|
|
||||||
"""
|
|
||||||
template_name = 'ui/panels/json.html'
|
|
||||||
|
|
||||||
def __init__(self, field_name, copy_button=True, **kwargs):
|
|
||||||
"""
|
|
||||||
Instantiate a new JSONPanel.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
field_name: The name of the JSON field on the object
|
|
||||||
copy_button: Set to True (default) to include a copy-to-clipboard button
|
|
||||||
"""
|
|
||||||
super().__init__(**kwargs)
|
|
||||||
self.field_name = field_name
|
|
||||||
|
|
||||||
if copy_button:
|
|
||||||
self.actions.append(
|
|
||||||
CopyContent(f'panel_{field_name}'),
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_context(self, context):
|
|
||||||
"""
|
|
||||||
Return the context data to be used when rendering the panel.
|
|
||||||
|
|
||||||
Parameters:
|
|
||||||
context: The template context
|
|
||||||
"""
|
|
||||||
return {
|
|
||||||
**super().get_context(context),
|
|
||||||
'data': getattr(context['object'], self.field_name),
|
|
||||||
'field_name': self.field_name,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class TemplatePanel(Panel):
|
class TemplatePanel(Panel):
|
||||||
"""
|
"""
|
||||||
A panel which renders content using an HTML template.
|
A panel which renders content using an HTML template.
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
|
|
||||||
{% block panel_content %}
|
{% block panel_content %}
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{% if object.comments %}
|
{% if comments %}
|
||||||
{{ object.comments|markdown }}
|
{{ comments|markdown }}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-muted">{% trans "None" %}</span>
|
<span class="text-muted">{% trans "None" %}</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ __all__ = (
|
|||||||
'flatten_dict',
|
'flatten_dict',
|
||||||
'ranges_to_string',
|
'ranges_to_string',
|
||||||
'ranges_to_string_list',
|
'ranges_to_string_list',
|
||||||
|
'resolve_attr_path',
|
||||||
'shallow_compare_dict',
|
'shallow_compare_dict',
|
||||||
'string_to_ranges',
|
'string_to_ranges',
|
||||||
)
|
)
|
||||||
@@ -213,3 +214,23 @@ def string_to_ranges(value):
|
|||||||
return None
|
return None
|
||||||
values.append(NumericRange(int(lower), int(upper) + 1, bounds='[)'))
|
values.append(NumericRange(int(lower), int(upper) + 1, bounds='[)'))
|
||||||
return values
|
return values
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# Attribute resolution
|
||||||
|
#
|
||||||
|
|
||||||
|
def resolve_attr_path(obj, path):
|
||||||
|
"""
|
||||||
|
Follow a dotted path across attributes and/or dictionary keys and return the final value.
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
obj: The starting object
|
||||||
|
path: The dotted path to follow (e.g. "foo.bar.baz")
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
return cur
|
||||||
|
|||||||
Reference in New Issue
Block a user