Introduce panel actions

This commit is contained in:
Jeremy Stretch 2025-11-03 09:55:56 -05:00
parent da68503a19
commit 37bea1e98e
4 changed files with 121 additions and 55 deletions

View File

@ -0,0 +1,56 @@
from urllib.parse import urlencode
from django.apps import apps
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from utilities.permissions import get_permission_for_model
from utilities.views import get_viewname
__all__ = (
'AddObject',
'PanelAction',
)
class PanelAction:
label = None
button_class = 'primary'
button_icon = None
def __init__(self, view_name, view_kwargs=None, url_params=None, permissions=None, label=None):
self.view_name = view_name
self.view_kwargs = view_kwargs
self.url_params = url_params or {}
self.permissions = permissions
if label is not None:
self.label = label
def get_url(self, obj):
url = reverse(self.view_name, kwargs=self.view_kwargs or {})
if self.url_params:
url_params = {
k: v(obj) if callable(v) else v for k, v in self.url_params.items()
}
url = f'{url}?{urlencode(url_params)}'
return url
def get_context(self, obj):
return {
'url': self.get_url(obj),
'label': self.label,
'button_class': self.button_class,
'button_icon': self.button_icon,
}
class AddObject(PanelAction):
label = _('Add')
button_icon = 'plus-thick'
def __init__(self, model, label=None, url_params=None):
app_label, model_name = model.split('.')
model = apps.get_model(app_label, model_name)
view_name = get_viewname(model, 'add')
super().__init__(view_name=view_name, label=label, url_params=url_params)
self.permissions = [get_permission_for_model(model, 'add')]

View File

@ -1,9 +1,10 @@
from abc import ABC, ABCMeta, abstractmethod from abc import ABC, ABCMeta
from django.contrib.contenttypes.models import ContentType
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from netbox.ui import attrs from netbox.ui import actions, attrs
from netbox.ui.attrs import Attr from netbox.ui.attrs import Attr
from utilities.querydict import dict_to_querydict from utilities.querydict import dict_to_querydict
from utilities.string import title from utilities.string import title
@ -24,14 +25,28 @@ __all__ = (
class Panel(ABC): class Panel(ABC):
template_name = None
title = None
actions = []
def __init__(self, title=None): def __init__(self, title=None, actions=None):
if title is not None: if title is not None:
self.title = title self.title = title
if actions is not None:
self.actions = actions
@abstractmethod def get_context(self, obj):
def render(self, obj): return {}
pass
def render(self, context):
obj = context.get('object')
return render_to_string(self.template_name, {
'request': context.get('request'),
'object': obj,
'title': self.title,
'actions': [action.get_context(obj) for action in self.actions],
**self.get_context(obj),
})
class ObjectPanelMeta(ABCMeta): class ObjectPanelMeta(ABCMeta):
@ -64,20 +79,16 @@ 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 get_attributes(self, obj): def get_context(self, obj):
return [ attrs = [
{ {
'label': attr.label or title(name), 'label': attr.label or title(name),
'value': attr.render(obj, {'name': name}), 'value': attr.render(obj, {'name': name}),
} for name, attr in self._attrs.items() } for name, attr in self._attrs.items()
] ]
return {
def render(self, context): 'attrs': attrs,
obj = context.get('object') }
return render_to_string(self.template_name, {
'title': self.title,
'attrs': self.get_attributes(obj),
})
class NestedGroupObjectPanel(ObjectPanel, metaclass=ObjectPanelMeta): class NestedGroupObjectPanel(ObjectPanel, metaclass=ObjectPanelMeta):
@ -90,44 +101,27 @@ class CustomFieldsPanel(Panel):
template_name = 'ui/panels/custom_fields.html' template_name = 'ui/panels/custom_fields.html'
title = _('Custom Fields') title = _('Custom Fields')
def render(self, context): def get_context(self, obj):
obj = context.get('object') return {
custom_fields = obj.get_custom_fields_by_group() '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): class TagsPanel(Panel):
template_name = 'ui/panels/tags.html' template_name = 'ui/panels/tags.html'
title = _('Tags') title = _('Tags')
def render(self, context):
return render_to_string(self.template_name, {
'title': self.title,
'object': context.get('object'),
})
class CommentsPanel(Panel): class CommentsPanel(Panel):
template_name = 'ui/panels/comments.html' template_name = 'ui/panels/comments.html'
title = _('Comments') 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): class RelatedObjectsPanel(Panel):
template_name = 'ui/panels/related_objects.html' template_name = 'ui/panels/related_objects.html'
title = _('Related Objects') title = _('Related Objects')
# TODO: Handle related_models from context
def render(self, context): def render(self, context):
return render_to_string(self.template_name, { return render_to_string(self.template_name, {
'title': self.title, 'title': self.title,
@ -139,35 +133,37 @@ class RelatedObjectsPanel(Panel):
class ImageAttachmentsPanel(Panel): class ImageAttachmentsPanel(Panel):
template_name = 'ui/panels/image_attachments.html' template_name = 'ui/panels/image_attachments.html'
title = _('Image Attachments') title = _('Image Attachments')
actions = [
def render(self, context): actions.AddObject(
return render_to_string(self.template_name, { 'extras.imageattachment',
'title': self.title, url_params={
'request': context.get('request'), 'object_type': lambda obj: ContentType.objects.get_for_model(obj).pk,
'object': context.get('object'), 'object_id': lambda obj: obj.pk,
}) 'return_url': lambda obj: obj.get_absolute_url(),
},
label=_('Attach an image'),
),
]
class EmbeddedTablePanel(Panel): class EmbeddedTablePanel(Panel):
template_name = 'ui/panels/embedded_table.html' template_name = 'ui/panels/embedded_table.html'
title = None title = None
def __init__(self, viewname, url_params=None, **kwargs): def __init__(self, view_name, url_params=None, **kwargs):
super().__init__(**kwargs) super().__init__(**kwargs)
self.viewname = viewname self.view_name = view_name
self.url_params = url_params or {} self.url_params = url_params or {}
def render(self, context): def get_context(self, obj):
obj = context.get('object')
url_params = { url_params = {
k: v(obj) if callable(v) else v for k, v in self.url_params.items() k: v(obj) if callable(v) else v for k, v in self.url_params.items()
} }
# url_params['return_url'] = return_url or context['request'].path # url_params['return_url'] = return_url or context['request'].path
return render_to_string(self.template_name, { return {
'title': self.title, 'viewname': self.view_name,
'viewname': self.viewname,
'url_params': dict_to_querydict(url_params), 'url_params': dict_to_querydict(url_params),
}) }
class PluginContentPanel(Panel): class PluginContentPanel(Panel):

View File

@ -1,4 +1,18 @@
<div class="card"> <div class="card">
<h2 class="card-header">{{ title }}</h2> <h2 class="card-header">
{{ title }}
{% if actions %}
<div class="card-actions">
{% for action in actions %}
<a href="{{ action.url }}" class="btn btn-ghost-{{ action.button_class|default:"primary" }} btn-sm">
{% if action.button_icon %}
<i class="mdi mdi-{{ action.button_icon }}" aria-hidden="true"></i>
{% endif %}
{{ action.label }}
</a>
{% endfor %}
</div>
{% endif %}
</h2>
{% block panel_content %}{% endblock %} {% block panel_content %}{% endblock %}
</div> </div>

View File

@ -3,8 +3,8 @@
{% block panel_content %} {% block panel_content %}
<div class="card-body"> <div class="card-body">
{% if comments %} {% if object.comments %}
{{ comments|markdown }} {{ object.comments|markdown }}
{% else %} {% else %}
<span class="text-muted">{% trans "None" %}</span> <span class="text-muted">{% trans "None" %}</span>
{% endif %} {% endif %}