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

186 statements  

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

1import copy 

2from collections import OrderedDict 

3 

4from django import forms 

5from django.db import models 

6from django.db.models.constants import LOOKUP_SEP 

7from django.db.models.fields.related import ManyToManyRel, ManyToOneRel, OneToOneRel 

8 

9from .conf import settings 

10from .constants import ALL_FIELDS 

11from .filters import ( 

12 BaseInFilter, 

13 BaseRangeFilter, 

14 BooleanFilter, 

15 CharFilter, 

16 ChoiceFilter, 

17 DateFilter, 

18 DateTimeFilter, 

19 DurationFilter, 

20 Filter, 

21 ModelChoiceFilter, 

22 ModelMultipleChoiceFilter, 

23 NumberFilter, 

24 TimeFilter, 

25 UUIDFilter, 

26) 

27from .utils import get_all_model_fields, get_model_field, resolve_field, try_dbfield 

28 

29 

30def remote_queryset(field): 

31 """ 

32 Get the queryset for the other side of a relationship. This works 

33 for both `RelatedField`s and `ForeignObjectRel`s. 

34 """ 

35 model = field.related_model 

36 

37 # Reverse relationships do not have choice limits 

38 if not hasattr(field, "get_limit_choices_to"): 

39 return model._default_manager.all() 

40 

41 limit_choices_to = field.get_limit_choices_to() 

42 return model._default_manager.complex_filter(limit_choices_to) 

43 

44 

45class FilterSetOptions: 

46 def __init__(self, options=None): 

47 self.model = getattr(options, "model", None) 

48 self.fields = getattr(options, "fields", None) 

49 self.exclude = getattr(options, "exclude", None) 

50 

51 self.filter_overrides = getattr(options, "filter_overrides", {}) 

52 

53 self.form = getattr(options, "form", forms.Form) 

54 

55 

56class FilterSetMetaclass(type): 

57 def __new__(cls, name, bases, attrs): 

58 attrs["declared_filters"] = cls.get_declared_filters(bases, attrs) 

59 

60 new_class = super().__new__(cls, name, bases, attrs) 

61 new_class._meta = FilterSetOptions(getattr(new_class, "Meta", None)) 

62 new_class.base_filters = new_class.get_filters() 

63 

64 return new_class 

65 

66 @classmethod 

67 def get_declared_filters(cls, bases, attrs): 

68 filters = [ 

69 (filter_name, attrs.pop(filter_name)) 

70 for filter_name, obj in list(attrs.items()) 

71 if isinstance(obj, Filter) 

72 ] 

73 

74 # Default the `filter.field_name` to the attribute name on the filterset 

75 for filter_name, f in filters: 

76 if getattr(f, "field_name", None) is None: 

77 f.field_name = filter_name 

78 

79 filters.sort(key=lambda x: x[1].creation_counter) 

80 

81 # Ensures a base class field doesn't override cls attrs, and maintains 

82 # field precedence when inheriting multiple parents. e.g. if there is a 

83 # class C(A, B), and A and B both define 'field', use 'field' from A. 

84 known = set(attrs) 

85 

86 def visit(name): 

87 known.add(name) 

88 return name 

89 

90 base_filters = [ 

91 (visit(name), f) 

92 for base in bases 

93 if hasattr(base, "declared_filters") 

94 for name, f in base.declared_filters.items() 

95 if name not in known 

96 ] 

97 

98 return OrderedDict(base_filters + filters) 

99 

100 

101FILTER_FOR_DBFIELD_DEFAULTS = { 

102 models.AutoField: {"filter_class": NumberFilter}, 

103 models.CharField: {"filter_class": CharFilter}, 

104 models.TextField: {"filter_class": CharFilter}, 

105 models.BooleanField: {"filter_class": BooleanFilter}, 

106 models.DateField: {"filter_class": DateFilter}, 

107 models.DateTimeField: {"filter_class": DateTimeFilter}, 

108 models.TimeField: {"filter_class": TimeFilter}, 

109 models.DurationField: {"filter_class": DurationFilter}, 

110 models.DecimalField: {"filter_class": NumberFilter}, 

111 models.SmallIntegerField: {"filter_class": NumberFilter}, 

112 models.IntegerField: {"filter_class": NumberFilter}, 

113 models.PositiveIntegerField: {"filter_class": NumberFilter}, 

114 models.PositiveSmallIntegerField: {"filter_class": NumberFilter}, 

115 models.FloatField: {"filter_class": NumberFilter}, 

116 models.NullBooleanField: {"filter_class": BooleanFilter}, 

117 models.SlugField: {"filter_class": CharFilter}, 

118 models.EmailField: {"filter_class": CharFilter}, 

119 models.FilePathField: {"filter_class": CharFilter}, 

120 models.URLField: {"filter_class": CharFilter}, 

121 models.GenericIPAddressField: {"filter_class": CharFilter}, 

122 models.CommaSeparatedIntegerField: {"filter_class": CharFilter}, 

123 models.UUIDField: {"filter_class": UUIDFilter}, 

124 # Forward relationships 

125 models.OneToOneField: { 

126 "filter_class": ModelChoiceFilter, 

127 "extra": lambda f: { 

128 "queryset": remote_queryset(f), 

129 "to_field_name": f.remote_field.field_name, 

130 "null_label": settings.NULL_CHOICE_LABEL if f.null else None, 

131 }, 

132 }, 

133 models.ForeignKey: { 

134 "filter_class": ModelChoiceFilter, 

135 "extra": lambda f: { 

136 "queryset": remote_queryset(f), 

137 "to_field_name": f.remote_field.field_name, 

138 "null_label": settings.NULL_CHOICE_LABEL if f.null else None, 

139 }, 

140 }, 

141 models.ManyToManyField: { 

142 "filter_class": ModelMultipleChoiceFilter, 

143 "extra": lambda f: { 

144 "queryset": remote_queryset(f), 

145 }, 

146 }, 

147 # Reverse relationships 

148 OneToOneRel: { 

149 "filter_class": ModelChoiceFilter, 

150 "extra": lambda f: { 

151 "queryset": remote_queryset(f), 

152 "null_label": settings.NULL_CHOICE_LABEL if f.null else None, 

153 }, 

154 }, 

155 ManyToOneRel: { 

156 "filter_class": ModelMultipleChoiceFilter, 

157 "extra": lambda f: { 

158 "queryset": remote_queryset(f), 

159 }, 

160 }, 

161 ManyToManyRel: { 

162 "filter_class": ModelMultipleChoiceFilter, 

163 "extra": lambda f: { 

164 "queryset": remote_queryset(f), 

165 }, 

166 }, 

167} 

168 

169 

170class BaseFilterSet: 

171 FILTER_DEFAULTS = FILTER_FOR_DBFIELD_DEFAULTS 

172 

173 def __init__(self, data=None, queryset=None, *, request=None, prefix=None): 

174 if queryset is None: 174 ↛ 175line 174 didn't jump to line 175, because the condition on line 174 was never true

175 queryset = self._meta.model._default_manager.all() 

176 model = queryset.model 

177 

178 self.is_bound = data is not None 

179 self.data = data or {} 

180 self.queryset = queryset 

181 self.request = request 

182 self.form_prefix = prefix 

183 

184 self.filters = copy.deepcopy(self.base_filters) 

185 

186 # propagate the model and filterset to the filters 

187 for filter_ in self.filters.values(): 

188 filter_.model = model 

189 filter_.parent = self 

190 

191 def is_valid(self): 

192 """ 

193 Return True if the underlying form has no errors, or False otherwise. 

194 """ 

195 return self.is_bound and self.form.is_valid() 

196 

197 @property 

198 def errors(self): 

199 """ 

200 Return an ErrorDict for the data provided for the underlying form. 

201 """ 

202 return self.form.errors 

203 

204 def filter_queryset(self, queryset): 

205 """ 

206 Filter the queryset with the underlying form's `cleaned_data`. You must 

207 call `is_valid()` or `errors` before calling this method. 

208 

209 This method should be overridden if additional filtering needs to be 

210 applied to the queryset before it is cached. 

211 """ 

212 for name, value in self.form.cleaned_data.items(): 

213 queryset = self.filters[name].filter(queryset, value) 

214 assert isinstance( 

215 queryset, models.QuerySet 

216 ), "Expected '%s.%s' to return a QuerySet, but got a %s instead." % ( 

217 type(self).__name__, 

218 name, 

219 type(queryset).__name__, 

220 ) 

221 return queryset 

222 

223 @property 

224 def qs(self): 

225 if not hasattr(self, "_qs"): 225 ↛ 232line 225 didn't jump to line 232, because the condition on line 225 was never false

226 qs = self.queryset.all() 

227 if self.is_bound: 227 ↛ 231line 227 didn't jump to line 231, because the condition on line 227 was never false

228 # ensure form validation before filtering 

229 self.errors 

230 qs = self.filter_queryset(qs) 

231 self._qs = qs 

232 return self._qs 

233 

234 def get_form_class(self): 

235 """ 

236 Returns a django Form suitable of validating the filterset data. 

237 

238 This method should be overridden if the form class needs to be 

239 customized relative to the filterset instance. 

240 """ 

241 fields = OrderedDict( 

242 [(name, filter_.field) for name, filter_ in self.filters.items()] 

243 ) 

244 

245 return type(str("%sForm" % self.__class__.__name__), (self._meta.form,), fields) 

246 

247 @property 

248 def form(self): 

249 if not hasattr(self, "_form"): 

250 Form = self.get_form_class() 

251 if self.is_bound: 251 ↛ 254line 251 didn't jump to line 254, because the condition on line 251 was never false

252 self._form = Form(self.data, prefix=self.form_prefix) 

253 else: 

254 self._form = Form(prefix=self.form_prefix) 

255 return self._form 

256 

257 @classmethod 

258 def get_fields(cls): 

259 """ 

260 Resolve the 'fields' argument that should be used for generating filters on the 

261 filterset. This is 'Meta.fields' sans the fields in 'Meta.exclude'. 

262 """ 

263 model = cls._meta.model 

264 fields = cls._meta.fields 

265 exclude = cls._meta.exclude 

266 

267 assert not (fields is None and exclude is None), ( 

268 "Setting 'Meta.model' without either 'Meta.fields' or 'Meta.exclude' " 

269 "has been deprecated since 0.15.0 and is now disallowed. Add an explicit " 

270 "'Meta.fields' or 'Meta.exclude' to the %s class." % cls.__name__ 

271 ) 

272 

273 # Setting exclude with no fields implies all other fields. 

274 if exclude is not None and fields is None: 274 ↛ 275line 274 didn't jump to line 275, because the condition on line 274 was never true

275 fields = ALL_FIELDS 

276 

277 # Resolve ALL_FIELDS into all fields for the filterset's model. 

278 if fields == ALL_FIELDS: 278 ↛ 279line 278 didn't jump to line 279, because the condition on line 278 was never true

279 fields = get_all_model_fields(model) 

280 

281 # Remove excluded fields 

282 exclude = exclude or [] 

283 if not isinstance(fields, dict): 

284 fields = [ 

285 (f, [settings.DEFAULT_LOOKUP_EXPR]) for f in fields if f not in exclude 

286 ] 

287 else: 

288 fields = [(f, lookups) for f, lookups in fields.items() if f not in exclude] 

289 

290 return OrderedDict(fields) 

291 

292 @classmethod 

293 def get_filter_name(cls, field_name, lookup_expr): 

294 """ 

295 Combine a field name and lookup expression into a usable filter name. 

296 Exact lookups are the implicit default, so "exact" is stripped from the 

297 end of the filter name. 

298 """ 

299 filter_name = LOOKUP_SEP.join([field_name, lookup_expr]) 

300 

301 # This also works with transformed exact lookups, such as 'date__exact' 

302 _default_expr = LOOKUP_SEP + settings.DEFAULT_LOOKUP_EXPR 

303 if filter_name.endswith(_default_expr): 

304 filter_name = filter_name[: -len(_default_expr)] 

305 

306 return filter_name 

307 

308 @classmethod 

309 def get_filters(cls): 

310 """ 

311 Get all filters for the filterset. This is the combination of declared and 

312 generated filters. 

313 """ 

314 

315 # No model specified - skip filter generation 

316 if not cls._meta.model: 

317 return cls.declared_filters.copy() 

318 

319 # Determine the filters that should be included on the filterset. 

320 filters = OrderedDict() 

321 fields = cls.get_fields() 

322 undefined = [] 

323 

324 for field_name, lookups in fields.items(): 

325 field = get_model_field(cls._meta.model, field_name) 

326 

327 # warn if the field doesn't exist. 

328 if field is None: 328 ↛ 329line 328 didn't jump to line 329, because the condition on line 328 was never true

329 undefined.append(field_name) 

330 

331 for lookup_expr in lookups: 

332 filter_name = cls.get_filter_name(field_name, lookup_expr) 

333 

334 # If the filter is explicitly declared on the class, skip generation 

335 if filter_name in cls.declared_filters: 

336 filters[filter_name] = cls.declared_filters[filter_name] 

337 continue 

338 

339 if field is not None: 339 ↛ 331line 339 didn't jump to line 331, because the condition on line 339 was never false

340 filters[filter_name] = cls.filter_for_field( 

341 field, field_name, lookup_expr 

342 ) 

343 

344 # Allow Meta.fields to contain declared filters *only* when a list/tuple 

345 if isinstance(cls._meta.fields, (list, tuple)): 

346 undefined = [f for f in undefined if f not in cls.declared_filters] 

347 

348 if undefined: 348 ↛ 349line 348 didn't jump to line 349, because the condition on line 348 was never true

349 raise TypeError( 

350 "'Meta.fields' must not contain non-model field names: %s" 

351 % ", ".join(undefined) 

352 ) 

353 

354 # Add in declared filters. This is necessary since we don't enforce adding 

355 # declared filters to the 'Meta.fields' option 

356 filters.update(cls.declared_filters) 

357 return filters 

358 

359 @classmethod 

360 def filter_for_field(cls, field, field_name, lookup_expr=None): 

361 if lookup_expr is None: 361 ↛ 362line 361 didn't jump to line 362, because the condition on line 361 was never true

362 lookup_expr = settings.DEFAULT_LOOKUP_EXPR 

363 field, lookup_type = resolve_field(field, lookup_expr) 

364 

365 default = { 

366 "field_name": field_name, 

367 "lookup_expr": lookup_expr, 

368 } 

369 

370 filter_class, params = cls.filter_for_lookup(field, lookup_type) 

371 default.update(params) 

372 

373 assert filter_class is not None, ( 

374 "%s resolved field '%s' with '%s' lookup to an unrecognized field " 

375 "type %s. Try adding an override to 'Meta.filter_overrides'. See: " 

376 "https://django-filter.readthedocs.io/en/main/ref/filterset.html" 

377 "#customise-filter-generation-with-filter-overrides" 

378 ) % (cls.__name__, field_name, lookup_expr, field.__class__.__name__) 

379 

380 return filter_class(**default) 

381 

382 @classmethod 

383 def filter_for_lookup(cls, field, lookup_type): 

384 DEFAULTS = dict(cls.FILTER_DEFAULTS) 

385 if hasattr(cls, "_meta"): 385 ↛ 388line 385 didn't jump to line 388, because the condition on line 385 was never false

386 DEFAULTS.update(cls._meta.filter_overrides) 

387 

388 data = try_dbfield(DEFAULTS.get, field.__class__) or {} 

389 filter_class = data.get("filter_class") 

390 params = data.get("extra", lambda field: {})(field) 

391 

392 # if there is no filter class, exit early 

393 if not filter_class: 393 ↛ 394line 393 didn't jump to line 394, because the condition on line 393 was never true

394 return None, {} 

395 

396 # perform lookup specific checks 

397 if lookup_type == "exact" and getattr(field, "choices", None): 

398 return ChoiceFilter, {"choices": field.choices} 

399 

400 if lookup_type == "isnull": 

401 data = try_dbfield(DEFAULTS.get, models.BooleanField) 

402 

403 filter_class = data.get("filter_class") 

404 params = data.get("extra", lambda field: {})(field) 

405 return filter_class, params 

406 

407 if lookup_type == "in": 407 ↛ 409line 407 didn't jump to line 409, because the condition on line 407 was never true

408 

409 class ConcreteInFilter(BaseInFilter, filter_class): 

410 pass 

411 

412 ConcreteInFilter.__name__ = cls._csv_filter_class_name( 

413 filter_class, lookup_type 

414 ) 

415 

416 return ConcreteInFilter, params 

417 

418 if lookup_type == "range": 418 ↛ 420line 418 didn't jump to line 420, because the condition on line 418 was never true

419 

420 class ConcreteRangeFilter(BaseRangeFilter, filter_class): 

421 pass 

422 

423 ConcreteRangeFilter.__name__ = cls._csv_filter_class_name( 

424 filter_class, lookup_type 

425 ) 

426 

427 return ConcreteRangeFilter, params 

428 

429 return filter_class, params 

430 

431 @classmethod 

432 def _csv_filter_class_name(cls, filter_class, lookup_type): 

433 """ 

434 Generate a suitable class name for a concrete filter class. This is not 

435 completely reliable, as not all filter class names are of the format 

436 <Type>Filter. 

437 

438 ex:: 

439 

440 FilterSet._csv_filter_class_name(DateTimeFilter, 'in') 

441 

442 returns 'DateTimeInFilter' 

443 

444 """ 

445 # DateTimeFilter => DateTime 

446 type_name = filter_class.__name__ 

447 if type_name.endswith("Filter"): 

448 type_name = type_name[:-6] 

449 

450 # in => In 

451 lookup_name = lookup_type.capitalize() 

452 

453 # DateTimeInFilter 

454 return str("%s%sFilter" % (type_name, lookup_name)) 

455 

456 

457class FilterSet(BaseFilterSet, metaclass=FilterSetMetaclass): 

458 pass 

459 

460 

461def filterset_factory(model, fields=ALL_FIELDS): 

462 meta = type(str("Meta"), (object,), {"model": model, "fields": fields}) 

463 filterset = type( 

464 str("%sFilterSet" % model._meta.object_name), (FilterSet,), {"Meta": meta} 

465 ) 

466 return filterset