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

1from __future__ import annotations 

2 

3from collections.abc import Callable, Iterable 

4from typing import TYPE_CHECKING 

5 

6from ckeditor.fields import RichTextFormField 

7from django import forms 

8from django.db.models import QuerySet 

9 

10from .widgets import FileInput 

11 

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 

15 

16 

17class FileField(forms.FileField): 

18 """ 

19 Custom `FileField` for forms. 

20 Should be used only for `BaseFileWrapperModel` objects. 

21 

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

29 

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 

37 

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 

69 

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 

88 

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 

95 

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. 

100 

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) 

108 

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 

115 

116 def has_changed(self, initial, data): 

117 return not self.disabled and data 

118 

119 

120class CKEditorFormField(RichTextFormField): 

121 """ 

122 Variant of the default `RichTextFormField` that automatically sanitizes 

123 the input value according to its CKEditor config. 

124 """ 

125 

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) 

130 

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)