Coverage for src/mesh/views/forms/base_forms.py: 30%

77 statements  

« prev     ^ index     » next       coverage.py v7.8.2, created at 2025-06-03 13:52 +0000

1from collections.abc import Collection 

2from enum import Enum, unique 

3 

4from django import forms 

5from django.utils.translation import gettext_lazy as _ 

6 

7from mesh.models.file_models import BaseFileWrapperModel 

8from mesh.views.components.button import Button 

9 

10from .fields import FileField 

11 

12 

13@unique 

14class FormAction(Enum): 

15 DELETE = "_action_delete" 

16 SAVE = "_action_save" 

17 SUBMIT = "_action_submit" 

18 SUBMIT_CONFIRM = "_action_submit_confirm" 

19 NEXT = "_action_next" 

20 PREVIOUS = "_action_previous" 

21 

22 

23SUBMIT_QUERY_PARAMETER = "_submit_confirm" 

24 

25 

26class FileModelForm(forms.ModelForm): 

27 """ 

28 `ModelForm` template to automatically handle custom `FileField` fields. 

29 It correctly initializes the fields from the model instance and saves them 

30 with the foreign key to the form's instance: `attached_to=self.instance`. 

31 

32 WARNING: A `FileField` declared in the form must have the same name as 

33 the model attribute/relation or have the `related_name` kwargs 

34 set to the model's attribute. 

35 """ 

36 

37 # If auto_handle is False, only process the fields declared in `custom_file_fields` 

38 custom_file_fields = [] 

39 auto_handle = True 

40 

41 def __init__(self, *args, **kwargs): 

42 """ 

43 Init `FileField` from model data. 

44 """ 

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

46 # Nothing to do if the form instance has never been 

47 if self.instance._state.adding or not self.instance.pk: 

48 return 

49 

50 for field_name, field in self.fields.items(): 

51 if not isinstance(field, FileField) or ( 

52 not self.auto_handle and field_name not in self.custom_file_fields 

53 ): 

54 continue 

55 

56 related_name = field.related_name or field_name 

57 instance_field = getattr(self.instance, related_name) 

58 if not field.allow_multiple_selected and not instance_field: 

59 continue 

60 elif not field.allow_multiple_selected and instance_field: 

61 field.initial = instance_field 

62 

63 # Handle multiple files 

64 else: 

65 files = field.filter_initial(instance_field.all()) 

66 if files: 

67 field.initial = files 

68 

69 def save(self, commit=True): 

70 """ 

71 Handle new files creation on form save. This requires the attribute 

72 `model_class` to be set on the form field. 

73 """ 

74 obj = super().save(commit) 

75 

76 if not commit: 

77 return obj 

78 

79 for field_name, field_value in self.cleaned_data.items(): 

80 if not field_value: 

81 continue 

82 

83 form_field = self.fields[field_name] 

84 model_class: type[BaseFileWrapperModel] | None = getattr( 

85 form_field, "model_class", None 

86 ) 

87 if ( 

88 model_class is None 

89 or not isinstance(form_field, FileField) 

90 or (not self.auto_handle and field_name not in self.custom_file_fields) 

91 ): 

92 continue 

93 

94 # Cast single value to list 

95 if not form_field.allow_multiple_selected or not isinstance(field_value, Collection): 

96 field_value = [field_value] 

97 

98 for temp_file in field_value: 

99 file_wrapper: BaseFileWrapperModel = model_class( 

100 attached_to=self.instance, file=temp_file, **form_field.save_kwargs 

101 ) 

102 file_wrapper._user = self.instance._user 

103 file_wrapper.save() 

104 

105 return obj 

106 

107 

108DEFAULT_FORM_BUTTONS = [ 

109 Button( 

110 id="form_save", 

111 title=_("Save"), 

112 icon_class="fa-floppy-disk", 

113 attrs={ 

114 "type": ["submit"], 

115 "class": ["save-button"], 

116 "name": [FormAction.SAVE.value], 

117 }, 

118 ) 

119] 

120 

121 

122class MeshFormMixin: 

123 """ 

124 Base form mixin to be used by every form rendered using `form_full_page.html` 

125 template. 

126 """ 

127 

128 required_css_class = "required" 

129 # The list of form buttons to display at the bottom of the form. 

130 buttons = [*DEFAULT_FORM_BUTTONS] 

131 

132 

133class SubmittableModelForm(MeshFormMixin, forms.ModelForm): 

134 """ 

135 Base form for a submittable model. 

136 

137 There are 2 submit buttons (save as draft and submit with distinct names). 

138 """ 

139 

140 is_submittable = True 

141 

142 def __init__(self, *args, **kwargs): 

143 self.submit_confirm = kwargs.pop(SUBMIT_QUERY_PARAMETER, False) 

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

145 

146 # Disable all fields if this is the submit confirm view 

147 if self.submit_confirm: 

148 for field in self.fields.values(): 

149 field.disabled = True 

150 if isinstance(field, FileField): 

151 field.widget.deletable = False # type:ignore 

152 

153 # Update the form defaut buttons according to the submit status 

154 self.buttons = [ 

155 Button( 

156 id="form_confirm", 

157 title=_("Confirm"), 

158 icon_class="fa-file-export", 

159 attrs={ 

160 "type": ["submit"], 

161 "class": ["button-highlight"], 

162 "name": [FormAction.SUBMIT_CONFIRM.value], 

163 }, 

164 ) 

165 ] 

166 

167 else: 

168 self.buttons = [ 

169 Button( 

170 id="form_save", 

171 title=_("Save as draft"), 

172 icon_class="fa-floppy-disk", 

173 attrs={ 

174 "type": ["submit"], 

175 "class": ["save-button", "light"], 

176 "name": [FormAction.SAVE.value], 

177 }, 

178 ), 

179 Button( 

180 id="form_submit", 

181 title=_("Submit"), 

182 icon_class="fa-file-export", 

183 attrs={ 

184 "type": ["submit"], 

185 "class": ["save-button", "button-highlight"], 

186 "name": [FormAction.SUBMIT.value], 

187 "data-tooltip": [ 

188 _( 

189 "Save and submit the form. You will no longer be " 

190 + "able to edit this form once submitted." 

191 ) 

192 ], 

193 }, 

194 ), 

195 ] 

196 

197 

198class HiddenModelChoiceForm(forms.Form): 

199 """ 

200 Generic form to handle a hidden model choice field. 

201 Mainly used as a Remove/Delete button throughout the app. 

202 """ 

203 

204 FORM_ACTION = FormAction.SAVE.value 

205 instance = forms.ModelChoiceField(queryset=None, required=True, widget=forms.HiddenInput) 

206 

207 def __init__(self, *args, **kwargs): 

208 self.FORM_ACTION = kwargs.pop("form_action", None) or self.FORM_ACTION 

209 queryset = kwargs.pop("_queryset") 

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

211 self.fields["instance"].queryset = queryset