Coverage for src/mesh/views/forms/fields.py: 67%
56 statements
« prev ^ index » next coverage.py v7.7.0, created at 2025-04-28 07:45 +0000
« prev ^ index » next coverage.py v7.7.0, created at 2025-04-28 07:45 +0000
1from __future__ import annotations
3from collections.abc import Callable, Iterable
4from typing import TYPE_CHECKING
6from ckeditor.fields import RichTextFormField
7from django import forms
8from django.db.models import QuerySet
10from .widgets import FileInput
12if TYPE_CHECKING: 12 ↛ 13line 12 didn't jump to line 13 because the condition on line 12 was never true
13 from mesh.models.file_models import BaseFileWrapperModel
14 from mesh.views.components.ckeditor_config import CKEditorConfig
17class FileField(forms.FileField):
18 """
19 Custom `FileField` for forms.
20 Should be used only for `BaseFileWrapperModel` objects.
22 Our logic for file handling is the following:
23 - A file field is always empty when displaying a form. The initial data, if any,
24 is stored in the widget `file_value` field.
25 - The deletion of a file by the user should be handled in the view treating the
26 form POST. The deletion process is triggered by a specific input with syntax
27 `_delete_file_{field_name}[_{pk}]`.
28 """
30 allow_multiple_selected: bool
31 widget = FileInput
32 model_class: type[BaseFileWrapperModel] | None
33 save_kwargs: dict
34 filter_initial: Callable[[QuerySet], QuerySet]
35 related_name: str | None
36 deletable: bool
38 def __init__(
39 self,
40 *args,
41 allow_multiple_selected=False,
42 related_name: str | None = None,
43 model_class: type[BaseFileWrapperModel] | None = None,
44 save_kwargs: dict = {},
45 filter_initial: Callable[[QuerySet], QuerySet] = lambda x: x,
46 deletable: bool = True,
47 **kwargs,
48 ):
49 """
50 Setup various additional field and widget attributes from the attached model
51 class.
52 Params:
53 - `allow_multiple_selected` Whether the user can select multiple files.
54 - `model_class` The BaseFileWrapperModel class the field will use to
55 automatically save the submitted data
56 - `save_kwargs` kwargs to be passed to the model_class constructor
57 - `filter_initial` Callable that takes a queryset as parameter and returns
58 a potentially filtered queryset. Used to filter initial
59 data provided to the field.
60 - `related_name` The field's related name to the model class.
61 - `deletable` Whether the file is deletable.
62 """
63 self.allow_multiple_selected = allow_multiple_selected
64 self.model_class = model_class
65 self.related_name = related_name
66 self.save_kwargs = save_kwargs
67 self.filter_initial = filter_initial
68 self.deletable = deletable
70 kwargs.setdefault(
71 "widget",
72 self.widget(
73 model_class=model_class, # type:ignore
74 related_name=related_name, # type:ignore
75 deletable=self.deletable, # type:ignore
76 allow_multiple_selected=self.allow_multiple_selected, # type:ignore
77 ),
78 )
79 super().__init__(*args, **kwargs)
80 if model_class: 80 ↛ exitline 80 didn't return from function '__init__' because the condition on line 80 was always true
81 # Get the file validators from the attached class
82 model_validators = getattr(model_class, "file_validators", lambda: [])
83 self.validators = [*self.validators, *model_validators()]
84 # Get the help text from the attached class
85 help_text = getattr(model_class, "get_help_text", lambda: None)()
86 if help_text: 86 ↛ exitline 86 didn't return from function '__init__' because the condition on line 86 was always true
87 self.help_text = help_text
89 def bound_data(self, data, initial):
90 """
91 The bound data is always the initial data.
92 The file field is always empty when displaying a form.
93 """
94 return initial
96 def clean(self, data, initial=None):
97 """
98 Returns `None` when no file should be created, ie. when there's no input from
99 the user.
101 If we don't do this, the FileField pipeline will fail because it will get
102 our custom data (BaseFileWrapper) instead of a FieldFile.
103 """
104 if not data or self.disabled:
105 return None
106 if not self.allow_multiple_selected:
107 return super().clean(data, initial)
109 single_file_clean = super().clean
110 if isinstance(data, Iterable):
111 result = [single_file_clean(d, initial) for d in data]
112 else:
113 result = single_file_clean(data, initial)
114 return result
116 def has_changed(self, initial, data):
117 return not self.disabled and data
120class CKEditorFormField(RichTextFormField):
121 """
122 Variant of the default `RichTextFormField` that automatically sanitizes
123 the input value according to its CKEditor config.
124 """
126 def __init__(self, editor_config: CKEditorConfig, *args, **kwargs):
127 self.editor_config = editor_config
128 kwargs["config_name"] = editor_config.id
129 super().__init__(*args, **kwargs)
131 def clean(self, value: str):
132 """
133 Sanitize the input value before the beginning the cleaning process.
134 """
135 if value: 135 ↛ 137line 135 didn't jump to line 137 because the condition on line 135 was always true
136 value = self.editor_config.sanitize_value(value)
137 return super().clean(value)