Coverage for extras/ui/panels.py: 92%
227 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 18:35 +0000
1from django.conf import settings
2from django.contrib.contenttypes.models import ContentType
3from django.utils.translation import gettext_lazy as _
5from netbox.ui import actions, attrs, panels
6from utilities.data import resolve_attr_path
8__all__ = (
9 'ConfigContextAssignmentPanel',
10 'ConfigContextPanel',
11 'ConfigContextProfilePanel',
12 'ConfigTemplatePanel',
13 'CustomFieldBehaviorPanel',
14 'CustomFieldChoiceSetChoicesPanel',
15 'CustomFieldChoiceSetPanel',
16 'CustomFieldObjectTypesPanel',
17 'CustomFieldPanel',
18 'CustomFieldRelatedObjectsPanel',
19 'CustomFieldValidationPanel',
20 'CustomFieldsPanel',
21 'CustomLinkPanel',
22 'EventRuleActionPanel',
23 'EventRuleEventTypesPanel',
24 'EventRulePanel',
25 'ExportTemplatePanel',
26 'ImageAttachmentFilePanel',
27 'ImageAttachmentImagePanel',
28 'ImageAttachmentPanel',
29 'ImageAttachmentsPanel',
30 'JournalEntryPanel',
31 'NotificationGroupGroupsPanel',
32 'NotificationGroupPanel',
33 'NotificationGroupUsersPanel',
34 'ObjectTypesPanel',
35 'SavedFilterObjectTypesPanel',
36 'SavedFilterPanel',
37 'TableConfigColumnsPanel',
38 'TableConfigOrderingPanel',
39 'TableConfigPanel',
40 'TagItemTypesPanel',
41 'TagObjectTypesPanel',
42 'TagPanel',
43 'TagsPanel',
44 'WebhookHTTPPanel',
45 'WebhookPanel',
46 'WebhookSSLPanel',
47)
50#
51# Generic panels
52#
54class CustomFieldsPanel(panels.ObjectPanel):
55 """
56 A panel showing the value of all custom fields defined on an object.
57 """
58 template_name = 'extras/panels/custom_fields.html'
59 title = _('Custom Fields')
61 def get_context(self, context):
62 obj = resolve_attr_path(context, self.accessor)
63 return {
64 **super().get_context(context),
65 'custom_fields': obj.get_custom_fields_by_group(),
66 }
68 def should_render(self, context):
69 return bool(context['custom_fields'])
72class ImageAttachmentsPanel(panels.ObjectsTablePanel):
73 """
74 A panel showing all images attached to the object.
75 """
76 actions = [
77 actions.AddObject(
78 'extras.imageattachment',
79 url_params={
80 'object_type': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk,
81 'object_id': lambda ctx: ctx['object'].pk,
82 'return_url': lambda ctx: ctx['object'].get_absolute_url(),
83 },
84 label=_('Attach an image'),
85 ),
86 ]
88 def __init__(self, **kwargs):
89 super().__init__(
90 'extras.imageattachment',
91 filters={
92 'object_type_id': lambda ctx: ContentType.objects.get_for_model(ctx['object']).pk,
93 'object_id': lambda ctx: ctx['object'].pk,
94 },
95 **kwargs,
96 )
99class TagsPanel(panels.ObjectPanel):
100 """
101 A panel showing the tags assigned to the object.
102 """
103 template_name = 'extras/panels/tags.html'
104 title = _('Tags')
106 def get_context(self, context):
107 return {
108 **super().get_context(context),
109 'object': resolve_attr_path(context, self.accessor),
110 }
113class ObjectTypesPanel(panels.ObjectPanel):
114 """
115 A panel listing the object types assigned to the object.
116 """
117 template_name = 'extras/panels/object_types.html'
118 title = _('Object Types')
121#
122# CustomField panels
123#
125class CustomFieldPanel(panels.ObjectAttributesPanel):
126 title = _('Custom Field')
128 name = attrs.TextAttr('name')
129 status = attrs.ChoiceAttr('status')
130 type = attrs.TemplatedAttr('type', label=_('Type'), template_name='extras/customfield/attrs/type.html')
131 label = attrs.TextAttr('label')
132 group_name = attrs.TextAttr('group_name', label=_('Group name'))
133 description = attrs.TextAttr('description')
134 required = attrs.BooleanAttr('required')
135 unique = attrs.BooleanAttr('unique', label=_('Must be unique'))
136 is_cloneable = attrs.BooleanAttr('is_cloneable', label=_('Cloneable'))
137 choice_set = attrs.TemplatedAttr(
138 'choice_set',
139 template_name='extras/customfield/attrs/choice_set.html',
140 )
141 default = attrs.TextAttr('default', label=_('Default value'))
142 related_object_filter = attrs.TemplatedAttr(
143 'related_object_filter',
144 template_name='extras/customfield/attrs/related_object_filter.html',
145 )
148class CustomFieldBehaviorPanel(panels.ObjectAttributesPanel):
149 title = _('Behavior')
151 search_weight = attrs.TemplatedAttr(
152 'search_weight',
153 template_name='extras/customfield/attrs/search_weight.html',
154 )
155 filter_logic = attrs.ChoiceAttr('filter_logic')
156 weight = attrs.NumericAttr('weight', label=_('Display weight'))
157 ui_visible = attrs.ChoiceAttr('ui_visible', label=_('UI visible'))
158 ui_editable = attrs.ChoiceAttr('ui_editable', label=_('UI editable'))
159 nulls_first = attrs.BooleanAttr('nulls_first', label=_('Nulls first'))
162class CustomFieldValidationPanel(panels.ObjectAttributesPanel):
163 title = _('Validation Rules')
165 validation_minimum = attrs.NumericAttr('validation_minimum', label=_('Minimum value'))
166 validation_maximum = attrs.NumericAttr('validation_maximum', label=_('Maximum value'))
167 validation_regex = attrs.TextAttr(
168 'validation_regex',
169 label=_('Regular expression'),
170 style='font-monospace',
171 )
174class CustomFieldObjectTypesPanel(panels.ObjectPanel):
175 template_name = 'extras/panels/object_types.html'
176 title = _('Object Types')
179class CustomFieldRelatedObjectsPanel(panels.ObjectPanel):
180 template_name = 'extras/panels/customfield_related_objects.html'
181 title = _('Related Objects')
183 def get_context(self, context):
184 return {
185 **super().get_context(context),
186 'related_models': context.get('related_models'),
187 }
190#
191# CustomFieldChoiceSet panels
192#
194class CustomFieldChoiceSetPanel(panels.ObjectAttributesPanel):
195 title = _('Custom Field Choice Set')
197 name = attrs.TextAttr('name')
198 description = attrs.TextAttr('description')
199 base_choices = attrs.ChoiceAttr('base_choices')
200 order_alphabetically = attrs.BooleanAttr('order_alphabetically')
201 choices_for = attrs.RelatedObjectListAttr('choices_for', linkify=True, label=_('Used by'))
204class CustomFieldChoiceSetChoicesPanel(panels.ObjectPanel):
205 template_name = 'extras/panels/customfieldchoiceset_choices.html'
207 def get_context(self, context):
208 obj = context.get('object')
209 total = len(obj.choices) if obj else 0
210 return {
211 **super().get_context(context),
212 'title': f'{_("Choices")} ({total})',
213 'choices': context.get('choices'),
214 }
217#
218# CustomLink panels
219#
221class CustomLinkPanel(panels.ObjectAttributesPanel):
222 title = _('Custom Link')
224 name = attrs.TextAttr('name')
225 enabled = attrs.BooleanAttr('enabled')
226 group_name = attrs.TextAttr('group_name')
227 weight = attrs.NumericAttr('weight')
228 button_class = attrs.ChoiceAttr('button_class')
229 new_window = attrs.BooleanAttr('new_window')
232#
233# ExportTemplate panels
234#
236class ExportTemplatePanel(panels.ObjectAttributesPanel):
237 title = _('Export Template')
239 name = attrs.TextAttr('name')
240 description = attrs.TextAttr('description')
241 mime_type = attrs.TextAttr('mime_type', label=_('MIME type'))
242 file_name = attrs.TextAttr('file_name')
243 file_extension = attrs.TextAttr('file_extension')
244 as_attachment = attrs.BooleanAttr('as_attachment', label=_('Attachment'))
247#
248# SavedFilter panels
249#
251class SavedFilterPanel(panels.ObjectAttributesPanel):
252 title = _('Saved Filter')
254 name = attrs.TextAttr('name')
255 description = attrs.TextAttr('description')
256 user = attrs.TextAttr('user')
257 enabled = attrs.BooleanAttr('enabled')
258 shared = attrs.BooleanAttr('shared')
259 weight = attrs.NumericAttr('weight')
262class SavedFilterObjectTypesPanel(panels.ObjectPanel):
263 template_name = 'extras/panels/savedfilter_object_types.html'
264 title = _('Assigned Models')
267#
268# TableConfig panels
269#
271class TableConfigPanel(panels.ObjectAttributesPanel):
272 title = _('Table Config')
274 name = attrs.TextAttr('name')
275 description = attrs.TextAttr('description')
276 object_type = attrs.TextAttr('object_type')
277 table = attrs.TextAttr('table')
278 user = attrs.TextAttr('user')
279 enabled = attrs.BooleanAttr('enabled')
280 shared = attrs.BooleanAttr('shared')
281 weight = attrs.NumericAttr('weight')
284class TableConfigColumnsPanel(panels.ObjectPanel):
285 template_name = 'extras/panels/tableconfig_columns.html'
286 title = _('Columns Displayed')
288 def get_context(self, context):
289 return {
290 **super().get_context(context),
291 'columns': context.get('columns'),
292 }
295class TableConfigOrderingPanel(panels.ObjectPanel):
296 template_name = 'extras/panels/tableconfig_ordering.html'
297 title = _('Ordering')
299 def get_context(self, context):
300 return {
301 **super().get_context(context),
302 'columns': context.get('columns'),
303 }
306#
307# NotificationGroup panels
308#
310class NotificationGroupPanel(panels.ObjectAttributesPanel):
311 title = _('Notification Group')
313 name = attrs.TextAttr('name')
314 description = attrs.TextAttr('description')
317class NotificationGroupGroupsPanel(panels.ObjectPanel):
318 template_name = 'extras/panels/notificationgroup_groups.html'
319 title = _('Groups')
322class NotificationGroupUsersPanel(panels.ObjectPanel):
323 template_name = 'extras/panels/notificationgroup_users.html'
324 title = _('Users')
327#
328# Webhook panels
329#
331class WebhookPanel(panels.ObjectAttributesPanel):
332 title = _('Webhook')
334 name = attrs.TextAttr('name')
335 description = attrs.TextAttr('description')
338class WebhookTimeoutAttr(attrs.TextAttr):
339 """
340 Render a webhook's timeout. Webhooks which do not define their own timeout fall back to the globally
341 configured default, which is displayed (and annotated as such) in place of an empty value.
342 """
343 def get_value(self, obj):
344 if obj.timeout is not None:
345 return _('{timeout} seconds').format(timeout=obj.timeout)
346 return _('{timeout} seconds (default)').format(timeout=settings.WEBHOOK_DEFAULT_TIMEOUT)
349class WebhookHTTPPanel(panels.ObjectAttributesPanel):
350 title = _('HTTP Request')
352 http_method = attrs.ChoiceAttr('http_method', label=_('HTTP method'))
353 payload_url = attrs.TextAttr('payload_url', label=_('Payload URL'), style='font-monospace')
354 http_content_type = attrs.TextAttr('http_content_type', label=_('HTTP content type'))
355 secret = attrs.TextAttr('secret')
356 timeout = WebhookTimeoutAttr('timeout')
359class WebhookSSLPanel(panels.ObjectAttributesPanel):
360 title = _('SSL')
362 ssl_verification = attrs.BooleanAttr('ssl_verification', label=_('SSL verification'))
363 ca_file_path = attrs.TextAttr('ca_file_path', label=_('CA file path'))
366#
367# EventRule panels
368#
370class EventRulePanel(panels.ObjectAttributesPanel):
371 title = _('Event Rule')
373 name = attrs.TextAttr('name')
374 enabled = attrs.BooleanAttr('enabled')
375 description = attrs.TextAttr('description')
378class EventRuleEventTypesPanel(panels.ObjectPanel):
379 template_name = 'extras/panels/eventrule_event_types.html'
380 title = _('Event Types')
382 def get_context(self, context):
383 return {
384 **super().get_context(context),
385 'registry': context.get('registry'),
386 }
389class EventRuleActionPanel(panels.ObjectAttributesPanel):
390 title = _('Action')
392 action_type = attrs.ChoiceAttr('action_type', label=_('Type'))
393 action_object = attrs.RelatedObjectAttr('action_object', linkify=True, label=_('Object'))
394 action_data = attrs.TemplatedAttr(
395 'action_data',
396 label=_('Data'),
397 template_name='extras/eventrule/attrs/action_data.html',
398 )
401#
402# Tag panels
403#
405class TagPanel(panels.ObjectAttributesPanel):
406 title = _('Tag')
408 name = attrs.TextAttr('name')
409 description = attrs.TextAttr('description')
410 color = attrs.ColorAttr('color')
411 weight = attrs.NumericAttr('weight')
412 tagged_items = attrs.TemplatedAttr(
413 'extras_taggeditem_items',
414 template_name='extras/tag/attrs/tagged_item_count.html',
415 )
418class TagObjectTypesPanel(panels.ObjectPanel):
419 template_name = 'extras/panels/tag_object_types.html'
420 title = _('Allowed Object Types')
423class TagItemTypesPanel(panels.ObjectPanel):
424 template_name = 'extras/panels/tag_item_types.html'
425 title = _('Tagged Item Types')
427 def get_context(self, context):
428 return {
429 **super().get_context(context),
430 'object_types': context.get('object_types'),
431 }
434#
435# ConfigContextProfile panels
436#
438class ConfigContextProfilePanel(panels.ObjectAttributesPanel):
439 title = _('Config Context Profile')
441 name = attrs.TextAttr('name')
442 description = attrs.TextAttr('description')
445#
446# ConfigContext panels
447#
449class ConfigContextPanel(panels.ObjectAttributesPanel):
450 title = _('Config Context')
452 name = attrs.TextAttr('name')
453 weight = attrs.NumericAttr('weight')
454 profile = attrs.RelatedObjectAttr('profile', linkify=True)
455 description = attrs.TextAttr('description')
456 is_active = attrs.BooleanAttr('is_active', label=_('Active'))
459class ConfigContextAssignmentPanel(panels.ObjectPanel):
460 template_name = 'extras/panels/configcontext_assignment.html'
461 title = _('Assignment')
463 def get_context(self, context):
464 return {
465 **super().get_context(context),
466 'assigned_objects': context.get('assigned_objects'),
467 }
470#
471# ConfigTemplate panels
472#
474class ConfigTemplatePanel(panels.ObjectAttributesPanel):
475 title = _('Config Template')
477 name = attrs.TextAttr('name')
478 description = attrs.TextAttr('description')
479 mime_type = attrs.TextAttr('mime_type', label=_('MIME type'))
480 file_name = attrs.TextAttr('file_name')
481 file_extension = attrs.TextAttr('file_extension')
482 as_attachment = attrs.BooleanAttr('as_attachment', label=_('Attachment'))
483 debug = attrs.BooleanAttr('debug')
484 data_source = attrs.RelatedObjectAttr('data_source', linkify=True)
485 data_file = attrs.TemplatedAttr(
486 'data_path',
487 template_name='extras/configtemplate/attrs/data_file.html',
488 )
489 data_synced = attrs.DateTimeAttr('data_synced')
490 auto_sync_enabled = attrs.BooleanAttr('auto_sync_enabled')
493#
494# ImageAttachment panels
495#
497class ImageAttachmentPanel(panels.ObjectAttributesPanel):
498 title = _('Image Attachment')
500 parent = attrs.RelatedObjectAttr('parent', linkify=True, label=_('Parent object'))
501 name = attrs.TextAttr('name')
502 description = attrs.TextAttr('description')
505class ImageAttachmentFilePanel(panels.ObjectPanel):
506 template_name = 'extras/panels/imageattachment_file.html'
507 title = _('File')
510class ImageAttachmentImagePanel(panels.ObjectPanel):
511 template_name = 'extras/panels/imageattachment_image.html'
512 title = _('Image')
515#
516# JournalEntry panels
517#
519class JournalEntryPanel(panels.ObjectAttributesPanel):
520 title = _('Journal Entry')
522 assigned_object = attrs.RelatedObjectAttr('assigned_object', linkify=True, label=_('Object'))
523 created = attrs.DateTimeAttr('created', spec='minutes')
524 created_by = attrs.TextAttr('created_by')
525 kind = attrs.ChoiceAttr('kind')