Coverage for netbox/object_actions.py: 69%
108 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.db.models import ForeignKey
2from django.template import loader
3from django.urls.exceptions import NoReverseMatch
4from django.utils.translation import gettext_lazy as _
6from core.models import ObjectType
7from extras.models import ExportTemplate
8from utilities.querydict import prepare_cloned_fields
9from utilities.views import get_action_url
11__all__ = (
12 'AddObject',
13 'BulkDelete',
14 'BulkEdit',
15 'BulkExport',
16 'BulkImport',
17 'BulkRename',
18 'CloneObject',
19 'DeleteObject',
20 'EditObject',
21 'ObjectAction',
22)
25class ObjectAction:
26 """
27 Base class for single- and multi-object operations.
29 Params:
30 name: The action name appended to the module for view resolution
31 label: Human-friendly label for the rendered button
32 template_name: Name of the HTML template which renders the button
33 multi: Set to True if this action is performed by selecting multiple objects (i.e. using a table)
34 permissions_required: The set of permissions a user must have to perform the action
35 url_kwargs: The set of URL keyword arguments to pass when resolving the view's URL
36 """
37 name = ''
38 label = None
39 template_name = None
40 multi = False
41 permissions_required = set()
42 url_kwargs = []
44 @classmethod
45 def get_url(cls, obj):
46 kwargs = {
47 kwarg: getattr(obj, kwarg) for kwarg in cls.url_kwargs
48 }
49 try:
50 return get_action_url(obj, action=cls.name, kwargs=kwargs)
51 except NoReverseMatch:
52 return None
54 @classmethod
55 def get_url_params(cls, context):
56 request = context['request']
57 params = request.GET.copy()
58 if 'return_url' in context:
59 params['return_url'] = context['return_url']
60 return params
62 @classmethod
63 def get_context(cls, context, obj):
64 """
65 Return any additional context data needed to render the button.
66 """
67 return {}
69 @classmethod
70 def render(cls, context, obj, **kwargs):
71 ctx = {
72 'perms': context['perms'],
73 'request': context['request'],
74 'url': cls.get_url(obj),
75 'url_params': cls.get_url_params(context),
76 'label': cls.label,
77 **cls.get_context(context, obj),
78 **kwargs,
79 }
80 return loader.render_to_string(cls.template_name, ctx)
83class AddObject(ObjectAction):
84 """
85 Create a new object.
86 """
87 name = 'add'
88 label = _('Add')
89 permissions_required = {'add'}
90 template_name = 'buttons/add.html'
93class CloneObject(ObjectAction):
94 """
95 Populate the new object form with select details from an existing object.
96 """
97 name = 'add'
98 label = _('Clone')
99 permissions_required = {'add'}
100 template_name = 'buttons/clone.html'
102 @classmethod
103 def get_url(cls, obj):
104 url = super().get_url(obj)
105 param_string = prepare_cloned_fields(obj).urlencode()
106 return f'{url}?{param_string}' if param_string else None
109class EditObject(ObjectAction):
110 """
111 Edit a single object.
112 """
113 name = 'edit'
114 label = _('Edit')
115 permissions_required = {'change'}
116 url_kwargs = ['pk']
117 template_name = 'buttons/edit.html'
120class DeleteObject(ObjectAction):
121 """
122 Delete a single object.
123 """
124 name = 'delete'
125 label = _('Delete')
126 permissions_required = {'delete'}
127 url_kwargs = ['pk']
128 template_name = 'buttons/delete.html'
131class BulkImport(ObjectAction):
132 """
133 Import multiple objects at once.
134 """
135 name = 'bulk_import'
136 label = _('Import')
137 permissions_required = {'add'}
138 template_name = 'buttons/import.html'
141class BulkExport(ObjectAction):
142 """
143 Export multiple objects at once.
144 """
145 name = 'export'
146 label = _('Export')
147 permissions_required = {'view'}
148 template_name = 'buttons/export.html'
150 @classmethod
151 def get_context(cls, context, model):
152 object_type = ObjectType.objects.get_for_model(model)
153 user = context['request'].user
155 # Determine if the "all data" export returns CSV or YAML
156 data_format = 'YAML' if hasattr(object_type.model_class(), 'to_yaml') else 'CSV'
158 # Retrieve all export templates for this model
159 export_templates = ExportTemplate.objects.restrict(user, 'view').filter(object_types=object_type)
161 return {
162 'object_type': object_type,
163 'url_params': context['request'].GET.urlencode() if context['request'].GET else '',
164 'export_templates': export_templates,
165 'data_format': data_format,
166 }
169class BulkEdit(ObjectAction):
170 """
171 Change the value of one or more fields on a set of objects.
172 """
173 name = 'bulk_edit'
174 label = _('Edit Selected')
175 multi = True
176 permissions_required = {'change'}
177 template_name = 'buttons/bulk_edit.html'
179 @classmethod
180 def get_context(cls, context, model):
181 url_params = super().get_url_params(context)
183 # If this is a child object, pass the parent's PK as a URL parameter
184 if parent := context.get('object'):
185 for field in model._meta.get_fields():
186 if isinstance(field, ForeignKey) and field.remote_field.model == parent.__class__:
187 url_params[field.name] = parent.pk
188 break
190 return {
191 'url_params': url_params,
192 }
195class BulkRename(ObjectAction):
196 """
197 Rename multiple objects at once.
198 """
199 name = 'bulk_rename'
200 label = _('Rename Selected')
201 multi = True
202 permissions_required = {'change'}
203 template_name = 'buttons/bulk_rename.html'
206class BulkDelete(ObjectAction):
207 """
208 Delete each of a set of objects.
209 """
210 name = 'bulk_delete'
211 label = _('Delete Selected')
212 multi = True
213 permissions_required = {'delete'}
214 template_name = 'buttons/bulk_delete.html'