Coverage for utilities/forms/forms.py: 45%

64 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-10-10 18:35 +0000

1import re 

2 

3from django import forms 

4from django.utils.translation import gettext as _ 

5 

6from netbox.models.features import ChangeLoggingMixin 

7from utilities.forms.fields import QueryField 

8from utilities.forms.mixins import BackgroundJobMixin, FilterModifierMixin 

9 

10__all__ = ( 

11 'BulkDeleteForm', 

12 'BulkEditForm', 

13 'BulkRenameForm', 

14 'CSVModelForm', 

15 'ConfirmationForm', 

16 'DeleteForm', 

17 'FilterForm', 

18 'TableConfigForm', 

19) 

20 

21 

22class ConfirmationForm(forms.Form): 

23 """ 

24 A generic confirmation form. The form is not valid unless the `confirm` field is checked. 

25 """ 

26 return_url = forms.CharField( 

27 required=False, 

28 widget=forms.HiddenInput() 

29 ) 

30 confirm = forms.BooleanField( 

31 required=True, 

32 widget=forms.HiddenInput(), 

33 initial=True 

34 ) 

35 

36 

37class DeleteForm(ConfirmationForm): 

38 """ 

39 Confirm the deletion of an object, optionally providing a changelog message. 

40 """ 

41 changelog_message = forms.CharField( 

42 required=False, 

43 max_length=200 

44 ) 

45 

46 def __init__(self, *args, instance=None, **kwargs): 

47 super().__init__(*args, **kwargs) 

48 

49 # Hide the changelog_message filed if the model doesn't support change logging 

50 if instance is None or not issubclass(instance._meta.model, ChangeLoggingMixin): 

51 self.fields.pop('changelog_message') 

52 

53 

54class BulkEditForm(BackgroundJobMixin, forms.Form): 

55 """ 

56 Provides bulk edit support for objects. 

57 

58 Attributes: 

59 nullable_fields: A list of field names indicating which fields support being set to null/empty 

60 """ 

61 nullable_fields = () 

62 

63 

64class BulkRenameForm(forms.Form): 

65 """ 

66 An extendable form to be used for renaming objects in bulk. 

67 """ 

68 find = forms.CharField( 

69 strip=False 

70 ) 

71 replace = forms.CharField( 

72 strip=False, 

73 required=False 

74 ) 

75 use_regex = forms.BooleanField( 

76 required=False, 

77 initial=True, 

78 label=_('Use regular expressions') 

79 ) 

80 

81 def clean(self): 

82 super().clean() 

83 

84 # Validate regular expression in "find" field 

85 if self.cleaned_data['use_regex']: 

86 try: 

87 re.compile(self.cleaned_data['find']) 

88 except re.error: 

89 raise forms.ValidationError({ 

90 'find': "Invalid regular expression" 

91 }) 

92 

93 

94class BulkDeleteForm(BackgroundJobMixin, ConfirmationForm): 

95 pk = forms.ModelMultipleChoiceField( 

96 queryset=None, 

97 widget=forms.MultipleHiddenInput 

98 ) 

99 changelog_message = forms.CharField( 

100 required=False, 

101 max_length=200 

102 ) 

103 

104 def __init__(self, model, *args, **kwargs): 

105 super().__init__(*args, **kwargs) 

106 

107 self.fields['pk'].queryset = model.objects.all() 

108 

109 # Hide the changelog_message filed if the model doesn't support change logging 

110 if model is None or not issubclass(model, ChangeLoggingMixin): 

111 self.fields.pop('changelog_message') 

112 

113 

114class CSVModelForm(forms.ModelForm): 

115 """ 

116 ModelForm used for the import of objects in CSV format. 

117 """ 

118 id = forms.IntegerField( 

119 label=_('ID'), 

120 required=False, 

121 help_text=_('Numeric ID of an existing object to update (if not creating a new object)') 

122 ) 

123 

124 def __init__(self, *args, headers=None, **kwargs): 

125 self.headers = headers or {} 

126 super().__init__(*args, **kwargs) 

127 

128 # Modify the model form to accommodate any customized to_field_name properties 

129 for field, to_field in self.headers.items(): 

130 if to_field is not None: 

131 self.fields[field].to_field_name = to_field 

132 

133 def clean(self): 

134 # Flag any invalid CSV headers 

135 for header in self.headers: 

136 if header not in self.fields: 

137 raise forms.ValidationError( 

138 _("Unrecognized header: {name}").format(name=header) 

139 ) 

140 

141 return super().clean() 

142 

143 

144class FilterForm(FilterModifierMixin, forms.Form): 

145 """ 

146 Base Form class for FilterSet forms. 

147 """ 

148 q = QueryField( 

149 required=False, 

150 label=_('Search') 

151 ) 

152 

153 

154class TableConfigForm(forms.Form): 

155 """ 

156 Form for configuring user's table preferences. 

157 """ 

158 available_columns = forms.MultipleChoiceField( 

159 choices=[], 

160 required=False, 

161 widget=forms.SelectMultiple( 

162 attrs={'size': 10, 'class': 'form-select'} 

163 ), 

164 label=_('Available Columns') 

165 ) 

166 columns = forms.MultipleChoiceField( 

167 choices=[], 

168 required=False, 

169 widget=forms.SelectMultiple( 

170 attrs={'size': 10, 'class': 'form-select select-all'} 

171 ), 

172 label=_('Selected Columns') 

173 ) 

174 

175 def __init__(self, table, *args, **kwargs): 

176 self.table = table 

177 

178 super().__init__(*args, **kwargs) 

179 

180 # Initialize columns field based on table attributes 

181 if table: 

182 self.fields['available_columns'].choices = table.available_columns 

183 self.fields['columns'].choices = table.selected_columns 

184 

185 @property 

186 def table_name(self): 

187 return self.table.__class__.__name__