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
« 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
4from django import forms
5from django.utils.translation import gettext_lazy as _
7from mesh.models.file_models import BaseFileWrapperModel
8from mesh.views.components.button import Button
10from .fields import FileField
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"
23SUBMIT_QUERY_PARAMETER = "_submit_confirm"
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`.
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 """
37 # If auto_handle is False, only process the fields declared in `custom_file_fields`
38 custom_file_fields = []
39 auto_handle = True
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
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
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
63 # Handle multiple files
64 else:
65 files = field.filter_initial(instance_field.all())
66 if files:
67 field.initial = files
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)
76 if not commit:
77 return obj
79 for field_name, field_value in self.cleaned_data.items():
80 if not field_value:
81 continue
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
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]
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()
105 return obj
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]
122class MeshFormMixin:
123 """
124 Base form mixin to be used by every form rendered using `form_full_page.html`
125 template.
126 """
128 required_css_class = "required"
129 # The list of form buttons to display at the bottom of the form.
130 buttons = [*DEFAULT_FORM_BUTTONS]
133class SubmittableModelForm(MeshFormMixin, forms.ModelForm):
134 """
135 Base form for a submittable model.
137 There are 2 submit buttons (save as draft and submit with distinct names).
138 """
140 is_submittable = True
142 def __init__(self, *args, **kwargs):
143 self.submit_confirm = kwargs.pop(SUBMIT_QUERY_PARAMETER, False)
144 super().__init__(*args, **kwargs)
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
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 ]
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 ]
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 """
204 FORM_ACTION = FormAction.SAVE.value
205 instance = forms.ModelChoiceField(queryset=None, required=True, widget=forms.HiddenInput)
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