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

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 _ 

5 

6from core.models import ObjectType 

7from extras.models import ExportTemplate 

8from utilities.querydict import prepare_cloned_fields 

9from utilities.views import get_action_url 

10 

11__all__ = ( 

12 'AddObject', 

13 'BulkDelete', 

14 'BulkEdit', 

15 'BulkExport', 

16 'BulkImport', 

17 'BulkRename', 

18 'CloneObject', 

19 'DeleteObject', 

20 'EditObject', 

21 'ObjectAction', 

22) 

23 

24 

25class ObjectAction: 

26 """ 

27 Base class for single- and multi-object operations. 

28 

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 = [] 

43 

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 

53 

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 

61 

62 @classmethod 

63 def get_context(cls, context, obj): 

64 """ 

65 Return any additional context data needed to render the button. 

66 """ 

67 return {} 

68 

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) 

81 

82 

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' 

91 

92 

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' 

101 

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 

107 

108 

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' 

118 

119 

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' 

129 

130 

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' 

139 

140 

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' 

149 

150 @classmethod 

151 def get_context(cls, context, model): 

152 object_type = ObjectType.objects.get_for_model(model) 

153 user = context['request'].user 

154 

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' 

157 

158 # Retrieve all export templates for this model 

159 export_templates = ExportTemplate.objects.restrict(user, 'view').filter(object_types=object_type) 

160 

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 } 

167 

168 

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' 

178 

179 @classmethod 

180 def get_context(cls, context, model): 

181 url_params = super().get_url_params(context) 

182 

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 

189 

190 return { 

191 'url_params': url_params, 

192 } 

193 

194 

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' 

204 

205 

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'