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

1from urllib.parse import urlencode 

2 

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 _ 

7 

8from utilities.permissions import get_permission_for_model 

9from utilities.views import get_viewname 

10 

11__all__ = ( 

12 'AddObject', 

13 'CopyContent', 

14 'LinkAction', 

15 'PanelAction', 

16) 

17 

18 

19class PanelAction: 

20 """ 

21 A link (typically a button) within a panel to perform some associated action, such as adding an object. 

22 

23 Attributes: 

24 template_name (str): The name of the template to render 

25 

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 

33 

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 

39 

40 def get_context(self, context): 

41 """ 

42 Return the template context used to render the action element. 

43 

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 } 

52 

53 def render(self, context): 

54 """ 

55 Render the action as HTML. 

56 

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 '' 

64 

65 return render_to_string(self.template_name, self.get_context(context)) 

66 

67 

68class LinkAction(PanelAction): 

69 """ 

70 A hyperlink (typically a button) within a panel to perform some associated action, such as adding an object. 

71 

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' 

79 

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 {} 

85 

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. 

89 

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 

108 

109 def get_context(self, context): 

110 return { 

111 **super().get_context(context), 

112 'url': self.get_url(context), 

113 } 

114 

115 

116class AddObject(LinkAction): 

117 """ 

118 An action to add a new object. 

119 

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}") 

132 

133 kwargs.setdefault('label', _('Add')) 

134 kwargs.setdefault('button_icon', 'plus-thick') 

135 kwargs.setdefault('permissions', [get_permission_for_model(self.model, 'add')]) 

136 

137 super().__init__(view_name=get_viewname(self.model, 'add'), url_params=url_params, **kwargs) 

138 

139 

140class CopyContent(PanelAction): 

141 """ 

142 An action to copy the contents of a panel to the clipboard. 

143 

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' 

148 

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 

154 

155 def get_context(self, context): 

156 return { 

157 **super().get_context(context), 

158 'target_id': self.target_id, 

159 }