Enable panel inheritance; add location panel

This commit is contained in:
Jeremy Stretch 2025-10-30 16:25:42 -04:00
parent 83de784196
commit 2a629d6f74
4 changed files with 37 additions and 51 deletions

View File

@ -1,10 +1,16 @@
from django.utils.translation import gettext_lazy as _
from netbox.ui import attrs
from netbox.ui.components import ObjectPanel
from netbox.ui import attrs, components
class DevicePanel(ObjectPanel):
class LocationPanel(components.NestedGroupObjectPanel):
site = attrs.ObjectAttr('site', label=_('Site'), linkify=True, grouped_by='group')
status = attrs.ChoiceAttr('status', label=_('Status'))
tenant = attrs.ObjectAttr('tenant', label=_('Tenant'), linkify=True, grouped_by='group')
facility = attrs.TextAttr('facility', label=_('Facility'))
class DevicePanel(components.ObjectPanel):
region = attrs.NestedObjectAttr('site.region', label=_('Region'), linkify=True)
site = attrs.ObjectAttr('site', label=_('Site'), linkify=True, grouped_by='group')
location = attrs.NestedObjectAttr('location', label=_('Location'), linkify=True)
@ -25,7 +31,7 @@ class DevicePanel(ObjectPanel):
config_template = attrs.ObjectAttr('config_template', label=_('Config template'), linkify=True)
class DeviceManagementPanel(ObjectPanel):
class DeviceManagementPanel(components.ObjectPanel):
status = attrs.ChoiceAttr('status', label=_('Status'))
role = attrs.NestedObjectAttr('role', label=_('Role'), linkify=True, max_depth=3)
platform = attrs.NestedObjectAttr('platform', label=_('Platform'), linkify=True, max_depth=3)
@ -46,7 +52,7 @@ class DeviceManagementPanel(ObjectPanel):
)
class SitePanel(ObjectPanel):
class SitePanel(components.ObjectPanel):
region = attrs.NestedObjectAttr('region', label=_('Region'), linkify=True)
group = attrs.NestedObjectAttr('group', label=_('Group'), linkify=True)
status = attrs.ChoiceAttr('status', label=_('Status'))

View File

@ -571,6 +571,7 @@ class LocationView(GetRelatedModelsMixin, generic.ObjectView):
locations = instance.get_descendants(include_self=True)
location_content_type = ContentType.objects.get_for_model(instance)
return {
'location_panel': panels.LocationPanel(instance, _('Location')),
'related_models': self.get_related_models(
request,
locations,

View File

@ -21,13 +21,29 @@ class Component(ABC):
class ObjectDetailsPanelMeta(ABCMeta):
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)
def __new__(mcls, name, bases, namespace, **kwargs):
declared = {}
# Walk MRO parents (excluding `object`) for declared attributes
for base in reversed([b for b in bases if hasattr(b, "_attrs")]):
for key, attr in getattr(base, '_attrs', {}).items():
if key not in declared:
declared[key] = attr
# Add local declarations in the order they appear in the class body
for key, attr in namespace.items():
if isinstance(attr, Attr):
declared[key] = attr
namespace['_attrs'] = declared
# Remove Attrs from the class namespace to keep things tidy
local_items = [key for key, attr in namespace.items() if isinstance(attr, Attr)]
for key in local_items:
namespace.pop(key)
cls = super().__new__(mcls, name, bases, namespace, **kwargs)
return cls
class ObjectPanel(Component, metaclass=ObjectDetailsPanelMeta):
@ -56,7 +72,7 @@ class ObjectPanel(Component, metaclass=ObjectDetailsPanelMeta):
return self.render()
class NestedGroupObjectPanel(ObjectPanel):
class NestedGroupObjectPanel(ObjectPanel, metaclass=ObjectDetailsPanelMeta):
name = attrs.TextAttr('name', label=_('Name'))
description = attrs.TextAttr('description', label=_('Description'))
parent = attrs.NestedObjectAttr('parent', label=_('Parent'), linkify=True)

View File

@ -22,44 +22,7 @@
{% block content %}
<div class="row mb-3">
<div class="col col-12 col-md-6">
<div class="card">
<h2 class="card-header">{% trans "Location" %}</h2>
<table class="table table-hover attr-table">
<tr>
<th scope="row">{% trans "Name" %}</th>
<td>{{ object.name }}</td>
</tr>
<tr>
<th scope="row">{% trans "Description" %}</th>
<td>{{ object.description|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Site" %}</th>
<td>{{ object.site|linkify }}</td>
</tr>
<tr>
<th scope="row">{% trans "Parent" %}</th>
<td>{{ object.parent|linkify|placeholder }}</td>
</tr>
<tr>
<th scope="row">{% trans "Status" %}</th>
<td>{% badge object.get_status_display bg_color=object.get_status_color %}</td>
</tr>
<tr>
<th scope="row">{% trans "Tenant" %}</th>
<td>
{% if object.tenant.group %}
{{ object.tenant.group|linkify }} /
{% endif %}
{{ object.tenant|linkify|placeholder }}
</td>
</tr>
<tr>
<th scope="row">{% trans "Facility" %}</th>
<td>{{ object.facility|placeholder }}</td>
</tr>
</table>
</div>
{{ location_panel }}
{% include 'inc/panels/tags.html' %}
{% include 'inc/panels/custom_fields.html' %}
{% include 'inc/panels/comments.html' %}