Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django_filters/fields.py: 47%

171 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1from collections import namedtuple 

2from datetime import datetime, time 

3 

4from django import forms 

5from django.utils.dateparse import parse_datetime 

6from django.utils.encoding import force_str 

7from django.utils.translation import gettext_lazy as _ 

8 

9from .conf import settings 

10from .constants import EMPTY_VALUES 

11from .utils import handle_timezone 

12from .widgets import ( 

13 BaseCSVWidget, 

14 CSVWidget, 

15 DateRangeWidget, 

16 LookupChoiceWidget, 

17 RangeWidget, 

18) 

19 

20 

21class RangeField(forms.MultiValueField): 

22 widget = RangeWidget 

23 

24 def __init__(self, fields=None, *args, **kwargs): 

25 if fields is None: 

26 fields = (forms.DecimalField(), forms.DecimalField()) 

27 super().__init__(fields, *args, **kwargs) 

28 

29 def compress(self, data_list): 

30 if data_list: 

31 return slice(*data_list) 

32 return None 

33 

34 

35class DateRangeField(RangeField): 

36 widget = DateRangeWidget 

37 

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

39 fields = (forms.DateField(), forms.DateField()) 

40 super().__init__(fields, *args, **kwargs) 

41 

42 def compress(self, data_list): 

43 if data_list: 

44 start_date, stop_date = data_list 

45 if start_date: 

46 start_date = handle_timezone( 

47 datetime.combine(start_date, time.min), False 

48 ) 

49 if stop_date: 

50 stop_date = handle_timezone( 

51 datetime.combine(stop_date, time.max), False 

52 ) 

53 return slice(start_date, stop_date) 

54 return None 

55 

56 

57class DateTimeRangeField(RangeField): 

58 widget = DateRangeWidget 

59 

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

61 fields = (forms.DateTimeField(), forms.DateTimeField()) 

62 super().__init__(fields, *args, **kwargs) 

63 

64 

65class IsoDateTimeRangeField(RangeField): 

66 widget = DateRangeWidget 

67 

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

69 fields = (IsoDateTimeField(), IsoDateTimeField()) 

70 super().__init__(fields, *args, **kwargs) 

71 

72 

73class TimeRangeField(RangeField): 

74 widget = DateRangeWidget 

75 

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

77 fields = (forms.TimeField(), forms.TimeField()) 

78 super().__init__(fields, *args, **kwargs) 

79 

80 

81class Lookup(namedtuple("Lookup", ("value", "lookup_expr"))): 

82 def __new__(cls, value, lookup_expr): 

83 if value in EMPTY_VALUES or lookup_expr in EMPTY_VALUES: 

84 raise ValueError( 

85 "Empty values ([], (), {}, '', None) are not " 

86 "valid Lookup arguments. Return None instead." 

87 ) 

88 

89 return super().__new__(cls, value, lookup_expr) 

90 

91 

92class LookupChoiceField(forms.MultiValueField): 

93 default_error_messages = { 

94 "lookup_required": _("Select a lookup."), 

95 } 

96 

97 def __init__(self, field, lookup_choices, *args, **kwargs): 

98 empty_label = kwargs.pop("empty_label", settings.EMPTY_CHOICE_LABEL) 

99 fields = (field, ChoiceField(choices=lookup_choices, empty_label=empty_label)) 

100 widget = LookupChoiceWidget(widgets=[f.widget for f in fields]) 

101 kwargs["widget"] = widget 

102 kwargs["help_text"] = field.help_text 

103 super().__init__(fields, *args, **kwargs) 

104 

105 def compress(self, data_list): 

106 if len(data_list) == 2: 

107 value, lookup_expr = data_list 

108 if value not in EMPTY_VALUES: 

109 if lookup_expr not in EMPTY_VALUES: 

110 return Lookup(value=value, lookup_expr=lookup_expr) 

111 else: 

112 raise forms.ValidationError( 

113 self.error_messages["lookup_required"], code="lookup_required" 

114 ) 

115 return None 

116 

117 

118class IsoDateTimeField(forms.DateTimeField): 

119 """ 

120 Supports 'iso-8601' date format too which is out the scope of 

121 the ``datetime.strptime`` standard library 

122 

123 # ISO 8601: ``http://www.w3.org/TR/NOTE-datetime`` 

124 

125 Based on Gist example by David Medina https://gist.github.com/copitux/5773821 

126 """ 

127 

128 ISO_8601 = "iso-8601" 

129 input_formats = [ISO_8601] 

130 

131 def strptime(self, value, format): 

132 value = force_str(value) 

133 

134 if format == self.ISO_8601: 

135 parsed = parse_datetime(value) 

136 if parsed is None: # Continue with other formats if doesn't match 

137 raise ValueError 

138 return handle_timezone(parsed) 

139 return super().strptime(value, format) 

140 

141 

142class BaseCSVField(forms.Field): 

143 """ 

144 Base field for validating CSV types. Value validation is performed by 

145 secondary base classes. 

146 

147 ex:: 

148 class IntegerCSVField(BaseCSVField, filters.IntegerField): 

149 pass 

150 

151 """ 

152 

153 base_widget_class = BaseCSVWidget 

154 

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

156 widget = kwargs.get("widget") or self.widget 

157 kwargs["widget"] = self._get_widget_class(widget) 

158 

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

160 

161 def _get_widget_class(self, widget): 

162 # passthrough, allows for override 

163 if isinstance(widget, BaseCSVWidget) or ( 

164 isinstance(widget, type) and issubclass(widget, BaseCSVWidget) 

165 ): 

166 return widget 

167 

168 # complain since we are unable to reconstruct widget instances 

169 assert isinstance( 

170 widget, type 

171 ), "'%s.widget' must be a widget class, not %s." % ( 

172 self.__class__.__name__, 

173 repr(widget), 

174 ) 

175 

176 bases = ( 

177 self.base_widget_class, 

178 widget, 

179 ) 

180 return type(str("CSV%s" % widget.__name__), bases, {}) 

181 

182 def clean(self, value): 

183 if value in self.empty_values and self.required: 

184 raise forms.ValidationError( 

185 self.error_messages["required"], code="required" 

186 ) 

187 

188 if value is None: 

189 return None 

190 return [super(BaseCSVField, self).clean(v) for v in value] 

191 

192 

193class BaseRangeField(BaseCSVField): 

194 # Force use of text input, as range must always have two inputs. A date 

195 # input would only allow a user to input one value and would always fail. 

196 widget = CSVWidget 

197 

198 default_error_messages = {"invalid_values": _("Range query expects two values.")} 

199 

200 def clean(self, value): 

201 value = super().clean(value) 

202 

203 assert value is None or isinstance(value, list) 

204 

205 if value and len(value) != 2: 

206 raise forms.ValidationError( 

207 self.error_messages["invalid_values"], code="invalid_values" 

208 ) 

209 

210 return value 

211 

212 

213class ChoiceIterator: 

214 # Emulates the behavior of ModelChoiceIterator, but instead wraps 

215 # the field's _choices iterable. 

216 

217 def __init__(self, field, choices): 

218 self.field = field 

219 self.choices = choices 

220 

221 def __iter__(self): 

222 if self.field.empty_label is not None: 

223 yield ("", self.field.empty_label) 

224 if self.field.null_label is not None: 

225 yield (self.field.null_value, self.field.null_label) 

226 yield from self.choices 

227 

228 def __len__(self): 

229 add = 1 if self.field.empty_label is not None else 0 

230 add += 1 if self.field.null_label is not None else 0 

231 return len(self.choices) + add 

232 

233 

234class ModelChoiceIterator(forms.models.ModelChoiceIterator): 

235 # Extends the base ModelChoiceIterator to add in 'null' choice handling. 

236 # This is a bit verbose since we have to insert the null choice after the 

237 # empty choice, but before the remainder of the choices. 

238 

239 def __iter__(self): 

240 iterable = super().__iter__() 

241 

242 if self.field.empty_label is not None: 

243 yield next(iterable) 

244 if self.field.null_label is not None: 

245 yield (self.field.null_value, self.field.null_label) 

246 yield from iterable 

247 

248 def __len__(self): 

249 add = 1 if self.field.null_label is not None else 0 

250 return super().__len__() + add 

251 

252 

253class ChoiceIteratorMixin: 

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

255 self.null_label = kwargs.pop("null_label", settings.NULL_CHOICE_LABEL) 

256 self.null_value = kwargs.pop("null_value", settings.NULL_CHOICE_VALUE) 

257 

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

259 

260 def _get_choices(self): 

261 return super()._get_choices() 

262 

263 def _set_choices(self, value): 

264 super()._set_choices(value) 

265 value = self.iterator(self, self._choices) 

266 

267 self._choices = self.widget.choices = value 

268 

269 choices = property(_get_choices, _set_choices) 

270 

271 

272# Unlike their Model* counterparts, forms.ChoiceField and forms.MultipleChoiceField do not set empty_label 

273class ChoiceField(ChoiceIteratorMixin, forms.ChoiceField): 

274 iterator = ChoiceIterator 

275 

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

277 self.empty_label = kwargs.pop("empty_label", settings.EMPTY_CHOICE_LABEL) 

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

279 

280 

281class MultipleChoiceField(ChoiceIteratorMixin, forms.MultipleChoiceField): 

282 iterator = ChoiceIterator 

283 

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

285 self.empty_label = None 

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

287 

288 

289class ModelChoiceField(ChoiceIteratorMixin, forms.ModelChoiceField): 

290 iterator = ModelChoiceIterator 

291 

292 def to_python(self, value): 

293 # bypass the queryset value check 

294 if self.null_label is not None and value == self.null_value: 294 ↛ 295line 294 didn't jump to line 295, because the condition on line 294 was never true

295 return value 

296 return super().to_python(value) 

297 

298 

299class ModelMultipleChoiceField(ChoiceIteratorMixin, forms.ModelMultipleChoiceField): 

300 iterator = ModelChoiceIterator 

301 

302 def _check_values(self, value): 

303 null = self.null_label is not None and value and self.null_value in value 

304 if null: # remove the null value and any potential duplicates 

305 value = [v for v in value if v != self.null_value] 

306 

307 result = list(super()._check_values(value)) 

308 result += [self.null_value] if null else [] 

309 return result