Add panels for common inclusion templates

This commit is contained in:
Jeremy Stretch
2025-10-31 14:38:33 -04:00
parent 3fd4664a76
commit 77613b37b2
9 changed files with 178 additions and 8 deletions

View File

@@ -18,6 +18,7 @@ from ipam.models import ASN, IPAddress, Prefix, VLANGroup, VLAN
from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable from ipam.tables import InterfaceVLANTable, VLANTranslationRuleTable
from netbox.object_actions import * from netbox.object_actions import *
from netbox.ui import layout from netbox.ui import layout
from netbox.ui.panels import CommentsPanel, CustomFieldsPanel, ImageAttachmentsPanel, RelatedObjectsPanel, TagsPanel
from netbox.views import generic from netbox.views import generic
from utilities.forms import ConfirmationForm from utilities.forms import ConfirmationForm
from utilities.paginator import EnhancedPaginator, get_paginate_count from utilities.paginator import EnhancedPaginator, get_paginate_count
@@ -468,14 +469,20 @@ class SiteView(GetRelatedModelsMixin, generic.ObjectView):
layout = layout.Layout( layout = layout.Layout(
layout.Row( layout.Row(
layout.Column( layout.Column(
panels.SitePanel(_('Site')) panels.SitePanel(_('Site')),
CustomFieldsPanel(),
TagsPanel(),
CommentsPanel(),
),
layout.Column(
RelatedObjectsPanel(),
ImageAttachmentsPanel(),
), ),
) )
) )
def get_extra_context(self, request, instance): def get_extra_context(self, request, instance):
return { return {
# 'site_panel': panels.SitePanel(instance, _('Site')),
'related_models': self.get_related_models( 'related_models': self.get_related_models(
request, request,
instance, instance,

View File

@@ -8,14 +8,23 @@ from netbox.ui.attrs import Attr
from utilities.string import title from utilities.string import title
__all__ = ( __all__ = (
'CommentsPanel',
'CustomFieldsPanel',
'ImageAttachmentsPanel',
'NestedGroupObjectPanel', 'NestedGroupObjectPanel',
'ObjectPanel', 'ObjectPanel',
'RelatedObjectsPanel',
'Panel', 'Panel',
'TagsPanel',
) )
class Panel(ABC): class Panel(ABC):
def __init__(self, title=None):
if title is not None:
self.title = title
@abstractmethod @abstractmethod
def render(self, obj): def render(self, obj):
pass pass
@@ -51,9 +60,6 @@ class ObjectPanelMeta(ABCMeta):
class ObjectPanel(Panel, metaclass=ObjectPanelMeta): class ObjectPanel(Panel, metaclass=ObjectPanelMeta):
template_name = 'ui/panels/object.html' template_name = 'ui/panels/object.html'
def __init__(self, title=None):
self.title = title
def get_attributes(self, obj): def get_attributes(self, obj):
return [ return [
{ {
@@ -74,3 +80,65 @@ class NestedGroupObjectPanel(ObjectPanel, metaclass=ObjectPanelMeta):
name = attrs.TextAttr('name', label=_('Name')) name = attrs.TextAttr('name', label=_('Name'))
description = attrs.TextAttr('description', label=_('Description')) description = attrs.TextAttr('description', label=_('Description'))
parent = attrs.NestedObjectAttr('parent', label=_('Parent'), linkify=True) parent = attrs.NestedObjectAttr('parent', label=_('Parent'), linkify=True)
class CustomFieldsPanel(Panel):
template_name = 'ui/panels/custom_fields.html'
title = _('Custom Fields')
def render(self, context):
obj = context.get('object')
custom_fields = obj.get_custom_fields_by_group()
if not custom_fields:
return ''
return render_to_string(self.template_name, {
'title': self.title,
'custom_fields': custom_fields,
})
class TagsPanel(Panel):
template_name = 'ui/panels/tags.html'
title = _('Tags')
def render(self, context):
return render_to_string(self.template_name, {
'title': self.title,
'object': context.get('object'),
})
class CommentsPanel(Panel):
template_name = 'ui/panels/comments.html'
title = _('Comments')
def render(self, context):
obj = context.get('object')
return render_to_string(self.template_name, {
'title': self.title,
'comments': obj.comments,
})
class RelatedObjectsPanel(Panel):
template_name = 'ui/panels/related_objects.html'
title = _('Related Objects')
def render(self, context):
return render_to_string(self.template_name, {
'title': self.title,
'object': context.get('object'),
'related_models': context.get('related_models'),
})
class ImageAttachmentsPanel(Panel):
template_name = 'ui/panels/image_attachments.html'
title = _('Image Attachments')
def render(self, context):
return render_to_string(self.template_name, {
'title': self.title,
'request': context.get('request'),
'object': context.get('object'),
})

View File

@@ -0,0 +1,4 @@
<div class="card">
<h2 class="card-header">{{ title }}</h2>
{% block panel_content %}{% endblock %}
</div>

View File

@@ -0,0 +1,12 @@
{% extends "ui/panels/_base.html" %}
{% load i18n %}
{% block panel_content %}
<div class="card-body">
{% if comments %}
{{ comments|markdown }}
{% else %}
<span class="text-muted">{% trans "None" %}</span>
{% endif %}
</div>
{% endblock panel_content %}

View File

@@ -0,0 +1,31 @@
{% extends "ui/panels/_base.html" %}
{% load i18n %}
{% block panel_content %}
<table class="table table-hover attr-table">
{% for group_name, fields in custom_fields.items %}
{% if group_name %}
<tr>
<th scope="row" colspan="2" class="fw-bold">{{ group_name }}</th>
</tr>
{% endif %}
{% for field, value in fields.items %}
<tr>
<th scope="row"{% if group_name %} class="ps-3"{% endif %}>{{ field }}
{% if field.description %}
<i
class="mdi mdi-information text-primary"
data-bs-toggle="tooltip"
data-bs-placement="right"
title="{{ field.description|escape }}"
></i>
{% endif %}
</th>
<td>
{% customfield_value field value %}
</td>
</tr>
{% endfor %}
{% endfor %}
</table>
{% endblock panel_content %}

View File

@@ -0,0 +1,7 @@
{% extends "ui/panels/_base.html" %}
{% load i18n %}
{# TODO: Add "attach an image" button in panel header #}
{% block panel_content %}
{% htmx_table 'extras:imageattachment_list' object_type_id=object|content_type_id object_id=object.pk %}
{% endblock panel_content %}

View File

@@ -1,5 +1,6 @@
<div class="card"> {% extends "ui/panels/_base.html" %}
<h2 class="card-header">{{ title }}</h2>
{% block panel_content %}
<table class="table table-hover attr-table"> <table class="table table-hover attr-table">
{% for attr in attrs %} {% for attr in attrs %}
<tr> <tr>
@@ -10,4 +11,4 @@
</tr> </tr>
{% endfor %} {% endfor %}
</table> </table>
</div> {% endblock panel_content %}

View File

@@ -0,0 +1,25 @@
{% extends "ui/panels/_base.html" %}
{% load helpers %}
{% load i18n %}
{% block panel_content %}
<ul class="list-group list-group-flush" role="presentation">
{% for related_object_count in related_models %}
{% action_url related_object_count.queryset.model 'list' as list_url %}
{% if list_url %}
<a href="{{ list_url }}?{{ related_object_count.filter_param }}={{ object.pk }}" class="list-group-item list-group-item-action d-flex justify-content-between">
{{ related_object_count.name }}
{% with count=related_object_count.queryset.count %}
{% if count %}
<span class="badge text-bg-primary rounded-pill">{{ count }}</span>
{% else %}
<span class="badge text-bg-light rounded-pill">&mdash;</span>
{% endif %}
{% endwith %}
</a>
{% endif %}
{% empty %}
<span class="list-group-item text-muted">{% trans "None" %}</span>
{% endfor %}
</ul>
{% endblock panel_content %}

View File

@@ -0,0 +1,15 @@
{% extends "ui/panels/_base.html" %}
{% load helpers %}
{% load i18n %}
{% block panel_content %}
<div class="card-body">
{% with url=object|validated_viewname:"list" %}
{% for tag in object.tags.all %}
{% tag tag url %}
{% empty %}
<span class="text-muted">{% trans "No tags assigned" %}</span>
{% endfor %}
{% endwith %}
</div>
{% endblock panel_content %}