Skip to content

Commit 37bea1e

Browse files
committed
Introduce panel actions
1 parent da68503 commit 37bea1e

File tree

4 files changed

+121
-55
lines changed

4 files changed

+121
-55
lines changed

netbox/netbox/ui/actions.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from urllib.parse import urlencode
2+
3+
from django.apps import apps
4+
from django.urls import reverse
5+
from django.utils.translation import gettext_lazy as _
6+
7+
from utilities.permissions import get_permission_for_model
8+
from utilities.views import get_viewname
9+
10+
__all__ = (
11+
'AddObject',
12+
'PanelAction',
13+
)
14+
15+
16+
class PanelAction:
17+
label = None
18+
button_class = 'primary'
19+
button_icon = None
20+
21+
def __init__(self, view_name, view_kwargs=None, url_params=None, permissions=None, label=None):
22+
self.view_name = view_name
23+
self.view_kwargs = view_kwargs
24+
self.url_params = url_params or {}
25+
self.permissions = permissions
26+
if label is not None:
27+
self.label = label
28+
29+
def get_url(self, obj):
30+
url = reverse(self.view_name, kwargs=self.view_kwargs or {})
31+
if self.url_params:
32+
url_params = {
33+
k: v(obj) if callable(v) else v for k, v in self.url_params.items()
34+
}
35+
url = f'{url}?{urlencode(url_params)}'
36+
return url
37+
38+
def get_context(self, obj):
39+
return {
40+
'url': self.get_url(obj),
41+
'label': self.label,
42+
'button_class': self.button_class,
43+
'button_icon': self.button_icon,
44+
}
45+
46+
47+
class AddObject(PanelAction):
48+
label = _('Add')
49+
button_icon = 'plus-thick'
50+
51+
def __init__(self, model, label=None, url_params=None):
52+
app_label, model_name = model.split('.')
53+
model = apps.get_model(app_label, model_name)
54+
view_name = get_viewname(model, 'add')
55+
super().__init__(view_name=view_name, label=label, url_params=url_params)
56+
self.permissions = [get_permission_for_model(model, 'add')]

netbox/netbox/ui/panels.py

Lines changed: 48 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
from abc import ABC, ABCMeta, abstractmethod
1+
from abc import ABC, ABCMeta
22

3+
from django.contrib.contenttypes.models import ContentType
34
from django.template.loader import render_to_string
45
from django.utils.translation import gettext_lazy as _
56

6-
from netbox.ui import attrs
7+
from netbox.ui import actions, attrs
78
from netbox.ui.attrs import Attr
89
from utilities.querydict import dict_to_querydict
910
from utilities.string import title
@@ -24,14 +25,28 @@
2425

2526

2627
class Panel(ABC):
28+
template_name = None
29+
title = None
30+
actions = []
2731

28-
def __init__(self, title=None):
32+
def __init__(self, title=None, actions=None):
2933
if title is not None:
3034
self.title = title
35+
if actions is not None:
36+
self.actions = actions
37+
38+
def get_context(self, obj):
39+
return {}
3140

32-
@abstractmethod
33-
def render(self, obj):
34-
pass
41+
def render(self, context):
42+
obj = context.get('object')
43+
return render_to_string(self.template_name, {
44+
'request': context.get('request'),
45+
'object': obj,
46+
'title': self.title,
47+
'actions': [action.get_context(obj) for action in self.actions],
48+
**self.get_context(obj),
49+
})
3550

3651

3752
class ObjectPanelMeta(ABCMeta):
@@ -64,20 +79,16 @@ def __new__(mcls, name, bases, namespace, **kwargs):
6479
class ObjectPanel(Panel, metaclass=ObjectPanelMeta):
6580
template_name = 'ui/panels/object.html'
6681

67-
def get_attributes(self, obj):
68-
return [
82+
def get_context(self, obj):
83+
attrs = [
6984
{
7085
'label': attr.label or title(name),
7186
'value': attr.render(obj, {'name': name}),
7287
} for name, attr in self._attrs.items()
7388
]
74-
75-
def render(self, context):
76-
obj = context.get('object')
77-
return render_to_string(self.template_name, {
78-
'title': self.title,
79-
'attrs': self.get_attributes(obj),
80-
})
89+
return {
90+
'attrs': attrs,
91+
}
8192

8293

8394
class NestedGroupObjectPanel(ObjectPanel, metaclass=ObjectPanelMeta):
@@ -90,44 +101,27 @@ class CustomFieldsPanel(Panel):
90101
template_name = 'ui/panels/custom_fields.html'
91102
title = _('Custom Fields')
92103

93-
def render(self, context):
94-
obj = context.get('object')
95-
custom_fields = obj.get_custom_fields_by_group()
96-
if not custom_fields:
97-
return ''
98-
return render_to_string(self.template_name, {
99-
'title': self.title,
100-
'custom_fields': custom_fields,
101-
})
104+
def get_context(self, obj):
105+
return {
106+
'custom_fields': obj.get_custom_fields_by_group(),
107+
}
102108

103109

104110
class TagsPanel(Panel):
105111
template_name = 'ui/panels/tags.html'
106112
title = _('Tags')
107113

108-
def render(self, context):
109-
return render_to_string(self.template_name, {
110-
'title': self.title,
111-
'object': context.get('object'),
112-
})
113-
114114

115115
class CommentsPanel(Panel):
116116
template_name = 'ui/panels/comments.html'
117117
title = _('Comments')
118118

119-
def render(self, context):
120-
obj = context.get('object')
121-
return render_to_string(self.template_name, {
122-
'title': self.title,
123-
'comments': obj.comments,
124-
})
125-
126119

127120
class RelatedObjectsPanel(Panel):
128121
template_name = 'ui/panels/related_objects.html'
129122
title = _('Related Objects')
130123

124+
# TODO: Handle related_models from context
131125
def render(self, context):
132126
return render_to_string(self.template_name, {
133127
'title': self.title,
@@ -139,35 +133,37 @@ def render(self, context):
139133
class ImageAttachmentsPanel(Panel):
140134
template_name = 'ui/panels/image_attachments.html'
141135
title = _('Image Attachments')
142-
143-
def render(self, context):
144-
return render_to_string(self.template_name, {
145-
'title': self.title,
146-
'request': context.get('request'),
147-
'object': context.get('object'),
148-
})
136+
actions = [
137+
actions.AddObject(
138+
'extras.imageattachment',
139+
url_params={
140+
'object_type': lambda obj: ContentType.objects.get_for_model(obj).pk,
141+
'object_id': lambda obj: obj.pk,
142+
'return_url': lambda obj: obj.get_absolute_url(),
143+
},
144+
label=_('Attach an image'),
145+
),
146+
]
149147

150148

151149
class EmbeddedTablePanel(Panel):
152150
template_name = 'ui/panels/embedded_table.html'
153151
title = None
154152

155-
def __init__(self, viewname, url_params=None, **kwargs):
153+
def __init__(self, view_name, url_params=None, **kwargs):
156154
super().__init__(**kwargs)
157-
self.viewname = viewname
155+
self.view_name = view_name
158156
self.url_params = url_params or {}
159157

160-
def render(self, context):
161-
obj = context.get('object')
158+
def get_context(self, obj):
162159
url_params = {
163160
k: v(obj) if callable(v) else v for k, v in self.url_params.items()
164161
}
165162
# url_params['return_url'] = return_url or context['request'].path
166-
return render_to_string(self.template_name, {
167-
'title': self.title,
168-
'viewname': self.viewname,
163+
return {
164+
'viewname': self.view_name,
169165
'url_params': dict_to_querydict(url_params),
170-
})
166+
}
171167

172168

173169
class PluginContentPanel(Panel):
Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,18 @@
11
<div class="card">
2-
<h2 class="card-header">{{ title }}</h2>
2+
<h2 class="card-header">
3+
{{ title }}
4+
{% if actions %}
5+
<div class="card-actions">
6+
{% for action in actions %}
7+
<a href="{{ action.url }}" class="btn btn-ghost-{{ action.button_class|default:"primary" }} btn-sm">
8+
{% if action.button_icon %}
9+
<i class="mdi mdi-{{ action.button_icon }}" aria-hidden="true"></i>
10+
{% endif %}
11+
{{ action.label }}
12+
</a>
13+
{% endfor %}
14+
</div>
15+
{% endif %}
16+
</h2>
317
{% block panel_content %}{% endblock %}
418
</div>

netbox/templates/ui/panels/comments.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33

44
{% block panel_content %}
55
<div class="card-body">
6-
{% if comments %}
7-
{{ comments|markdown }}
6+
{% if object.comments %}
7+
{{ object.comments|markdown }}
88
{% else %}
99
<span class="text-muted">{% trans "None" %}</span>
1010
{% endif %}

0 commit comments

Comments
 (0)