Coverage for netbox/ui/actions.py: 56%
65 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 urllib.parse import urlencode
3from django.apps import apps
4from django.template.loader import render_to_string
5from django.urls import reverse
6from django.utils.translation import gettext_lazy as _
8from utilities.permissions import get_permission_for_model
9from utilities.views import get_viewname
11__all__ = (
12 'AddObject',
13 'CopyContent',
14 'LinkAction',
15 'PanelAction',
16)
19class PanelAction:
20 """
21 A link (typically a button) within a panel to perform some associated action, such as adding an object.
23 Attributes:
24 template_name (str): The name of the template to render
26 Parameters:
27 label (str): The human-friendly button text
28 permissions (list): An iterable of permissions required to display the action
29 button_class (str): Bootstrap CSS class for the button
30 button_icon (str): Name of the button's MDI icon
31 """
32 template_name = None
34 def __init__(self, label, permissions=None, button_class='primary', button_icon=None):
35 self.label = label
36 self.permissions = permissions
37 self.button_class = button_class
38 self.button_icon = button_icon
40 def get_context(self, context):
41 """
42 Return the template context used to render the action element.
44 Parameters:
45 context (dict): The template context
46 """
47 return {
48 'label': self.label,
49 'button_class': self.button_class,
50 'button_icon': self.button_icon,
51 }
53 def render(self, context):
54 """
55 Render the action as HTML.
57 Parameters:
58 context (dict): The template context
59 """
60 # Enforce permissions
61 user = context['request'].user
62 if self.permissions and not user.has_perms(self.permissions):
63 return ''
65 return render_to_string(self.template_name, self.get_context(context))
68class LinkAction(PanelAction):
69 """
70 A hyperlink (typically a button) within a panel to perform some associated action, such as adding an object.
72 Parameters:
73 view_name (str): Name of the view to which the action will link
74 view_kwargs (dict): Additional keyword arguments to pass to `reverse()` when resolving the URL
75 url_params (dict): A dictionary of arbitrary URL parameters to append to the action's URL. If the value of a key
76 is a callable, it will be passed the current template context.
77 """
78 template_name = 'ui/actions/link.html'
80 def __init__(self, view_name, view_kwargs=None, url_params=None, **kwargs):
81 super().__init__(**kwargs)
82 self.view_name = view_name
83 self.view_kwargs = view_kwargs or {}
84 self.url_params = url_params or {}
86 def get_url(self, context):
87 """
88 Resolve the URL for the action from its view name and kwargs. Append any additional URL parameters.
90 Parameters:
91 context (dict): The template context
92 """
93 url = reverse(self.view_name, kwargs=self.view_kwargs)
94 if self.url_params:
95 url_params = {}
96 for key, value in self.url_params.items():
97 # If the param value is callable, call it with the context and save the result.
98 value = value(context) if callable(value) else value
99 # Omit parameters whose value resolved to None
100 if value is not None:
101 url_params[key] = value
102 # Set the return URL if not already set and an object is available.
103 if 'return_url' not in url_params and 'object' in context:
104 url_params['return_url'] = context['object'].get_absolute_url()
105 if url_params:
106 url = f'{url}?{urlencode(url_params)}'
107 return url
109 def get_context(self, context):
110 return {
111 **super().get_context(context),
112 'url': self.get_url(context),
113 }
116class AddObject(LinkAction):
117 """
118 An action to add a new object.
120 Parameters:
121 model (str): The dotted label of the model to be added (e.g. "dcim.site")
122 url_params (dict): A dictionary of arbitrary URL parameters to append to the resolved URL
123 """
124 def __init__(self, model, url_params=None, **kwargs):
125 # Resolve the model from its label
126 if '.' not in model: 126 ↛ 127line 126 didn't jump to line 127 because the condition on line 126 was never true
127 raise ValueError(f"Invalid model label: {model}")
128 try:
129 self.model = apps.get_model(model)
130 except LookupError:
131 raise ValueError(f"Invalid model label: {model}")
133 kwargs.setdefault('label', _('Add'))
134 kwargs.setdefault('button_icon', 'plus-thick')
135 kwargs.setdefault('permissions', [get_permission_for_model(self.model, 'add')])
137 super().__init__(view_name=get_viewname(self.model, 'add'), url_params=url_params, **kwargs)
140class CopyContent(PanelAction):
141 """
142 An action to copy the contents of a panel to the clipboard.
144 Parameters:
145 target_id (str): The ID of the target element containing the content to be copied
146 """
147 template_name = 'ui/actions/copy_content.html'
149 def __init__(self, target_id, **kwargs):
150 kwargs.setdefault('label', _('Copy'))
151 kwargs.setdefault('button_icon', 'content-copy')
152 super().__init__(**kwargs)
153 self.target_id = target_id
155 def get_context(self, context):
156 return {
157 **super().get_context(context),
158 'target_id': self.target_id,
159 }