Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/contrib/admin/filters.py: 28%

243 statements  

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

1""" 

2This encapsulates the logic for displaying filters in the Django admin. 

3Filters are specified in models with the "list_filter" option. 

4 

5Each filter subclass knows how to display a filter for a field that passes a 

6certain test -- e.g. being a DateField or ForeignKey. 

7""" 

8import datetime 

9 

10from django.contrib.admin.options import IncorrectLookupParameters 

11from django.contrib.admin.utils import ( 

12 get_model_from_relation, 

13 prepare_lookup_value, 

14 reverse_field_path, 

15) 

16from django.core.exceptions import ImproperlyConfigured, ValidationError 

17from django.db import models 

18from django.utils import timezone 

19from django.utils.translation import gettext_lazy as _ 

20 

21 

22class ListFilter: 

23 title = None # Human-readable title to appear in the right sidebar. 

24 template = "admin/filter.html" 

25 

26 def __init__(self, request, params, model, model_admin): 

27 # This dictionary will eventually contain the request's query string 

28 # parameters actually used by this filter. 

29 self.used_parameters = {} 

30 if self.title is None: 

31 raise ImproperlyConfigured( 

32 "The list filter '%s' does not specify a 'title'." 

33 % self.__class__.__name__ 

34 ) 

35 

36 def has_output(self): 

37 """ 

38 Return True if some choices would be output for this filter. 

39 """ 

40 raise NotImplementedError( 

41 "subclasses of ListFilter must provide a has_output() method" 

42 ) 

43 

44 def choices(self, changelist): 

45 """ 

46 Return choices ready to be output in the template. 

47 

48 `changelist` is the ChangeList to be displayed. 

49 """ 

50 raise NotImplementedError( 

51 "subclasses of ListFilter must provide a choices() method" 

52 ) 

53 

54 def queryset(self, request, queryset): 

55 """ 

56 Return the filtered queryset. 

57 """ 

58 raise NotImplementedError( 

59 "subclasses of ListFilter must provide a queryset() method" 

60 ) 

61 

62 def expected_parameters(self): 

63 """ 

64 Return the list of parameter names that are expected from the 

65 request's query string and that will be used by this filter. 

66 """ 

67 raise NotImplementedError( 

68 "subclasses of ListFilter must provide an expected_parameters() method" 

69 ) 

70 

71 

72class SimpleListFilter(ListFilter): 

73 # The parameter that should be used in the query string for that filter. 

74 parameter_name = None 

75 

76 def __init__(self, request, params, model, model_admin): 

77 super().__init__(request, params, model, model_admin) 

78 if self.parameter_name is None: 

79 raise ImproperlyConfigured( 

80 "The list filter '%s' does not specify a 'parameter_name'." 

81 % self.__class__.__name__ 

82 ) 

83 if self.parameter_name in params: 

84 value = params.pop(self.parameter_name) 

85 self.used_parameters[self.parameter_name] = value 

86 lookup_choices = self.lookups(request, model_admin) 

87 if lookup_choices is None: 

88 lookup_choices = () 

89 self.lookup_choices = list(lookup_choices) 

90 

91 def has_output(self): 

92 return len(self.lookup_choices) > 0 

93 

94 def value(self): 

95 """ 

96 Return the value (in string format) provided in the request's 

97 query string for this filter, if any, or None if the value wasn't 

98 provided. 

99 """ 

100 return self.used_parameters.get(self.parameter_name) 

101 

102 def lookups(self, request, model_admin): 

103 """ 

104 Must be overridden to return a list of tuples (value, verbose value) 

105 """ 

106 raise NotImplementedError( 

107 "The SimpleListFilter.lookups() method must be overridden to " 

108 "return a list of tuples (value, verbose value)." 

109 ) 

110 

111 def expected_parameters(self): 

112 return [self.parameter_name] 

113 

114 def choices(self, changelist): 

115 yield { 

116 "selected": self.value() is None, 

117 "query_string": changelist.get_query_string(remove=[self.parameter_name]), 

118 "display": _("All"), 

119 } 

120 for lookup, title in self.lookup_choices: 

121 yield { 

122 "selected": self.value() == str(lookup), 

123 "query_string": changelist.get_query_string( 

124 {self.parameter_name: lookup} 

125 ), 

126 "display": title, 

127 } 

128 

129 

130class FieldListFilter(ListFilter): 

131 _field_list_filters = [] 

132 _take_priority_index = 0 

133 

134 def __init__(self, field, request, params, model, model_admin, field_path): 

135 self.field = field 

136 self.field_path = field_path 

137 self.title = getattr(field, "verbose_name", field_path) 

138 super().__init__(request, params, model, model_admin) 

139 for p in self.expected_parameters(): 

140 if p in params: 

141 value = params.pop(p) 

142 self.used_parameters[p] = prepare_lookup_value(p, value) 

143 

144 def has_output(self): 

145 return True 

146 

147 def queryset(self, request, queryset): 

148 try: 

149 return queryset.filter(**self.used_parameters) 

150 except (ValueError, ValidationError) as e: 

151 # Fields may raise a ValueError or ValidationError when converting 

152 # the parameters to the correct type. 

153 raise IncorrectLookupParameters(e) 

154 

155 @classmethod 

156 def register(cls, test, list_filter_class, take_priority=False): 

157 if take_priority: 157 ↛ 161line 157 didn't jump to line 161, because the condition on line 157 was never true

158 # This is to allow overriding the default filters for certain types 

159 # of fields with some custom filters. The first found in the list 

160 # is used in priority. 

161 cls._field_list_filters.insert( 

162 cls._take_priority_index, (test, list_filter_class) 

163 ) 

164 cls._take_priority_index += 1 

165 else: 

166 cls._field_list_filters.append((test, list_filter_class)) 

167 

168 @classmethod 

169 def create(cls, field, request, params, model, model_admin, field_path): 

170 for test, list_filter_class in cls._field_list_filters: 

171 if test(field): 

172 return list_filter_class( 

173 field, request, params, model, model_admin, field_path=field_path 

174 ) 

175 

176 

177class RelatedFieldListFilter(FieldListFilter): 

178 def __init__(self, field, request, params, model, model_admin, field_path): 

179 other_model = get_model_from_relation(field) 

180 self.lookup_kwarg = "%s__%s__exact" % (field_path, field.target_field.name) 

181 self.lookup_kwarg_isnull = "%s__isnull" % field_path 

182 self.lookup_val = params.get(self.lookup_kwarg) 

183 self.lookup_val_isnull = params.get(self.lookup_kwarg_isnull) 

184 super().__init__(field, request, params, model, model_admin, field_path) 

185 self.lookup_choices = self.field_choices(field, request, model_admin) 

186 if hasattr(field, "verbose_name"): 

187 self.lookup_title = field.verbose_name 

188 else: 

189 self.lookup_title = other_model._meta.verbose_name 

190 self.title = self.lookup_title 

191 self.empty_value_display = model_admin.get_empty_value_display() 

192 

193 @property 

194 def include_empty_choice(self): 

195 """ 

196 Return True if a "(None)" choice should be included, which filters 

197 out everything except empty relationships. 

198 """ 

199 return self.field.null or (self.field.is_relation and self.field.many_to_many) 

200 

201 def has_output(self): 

202 if self.include_empty_choice: 

203 extra = 1 

204 else: 

205 extra = 0 

206 return len(self.lookup_choices) + extra > 1 

207 

208 def expected_parameters(self): 

209 return [self.lookup_kwarg, self.lookup_kwarg_isnull] 

210 

211 def field_admin_ordering(self, field, request, model_admin): 

212 """ 

213 Return the model admin's ordering for related field, if provided. 

214 """ 

215 related_admin = model_admin.admin_site._registry.get(field.remote_field.model) 

216 if related_admin is not None: 

217 return related_admin.get_ordering(request) 

218 return () 

219 

220 def field_choices(self, field, request, model_admin): 

221 ordering = self.field_admin_ordering(field, request, model_admin) 

222 return field.get_choices(include_blank=False, ordering=ordering) 

223 

224 def choices(self, changelist): 

225 yield { 

226 "selected": self.lookup_val is None and not self.lookup_val_isnull, 

227 "query_string": changelist.get_query_string( 

228 remove=[self.lookup_kwarg, self.lookup_kwarg_isnull] 

229 ), 

230 "display": _("All"), 

231 } 

232 for pk_val, val in self.lookup_choices: 

233 yield { 

234 "selected": self.lookup_val == str(pk_val), 

235 "query_string": changelist.get_query_string( 

236 {self.lookup_kwarg: pk_val}, [self.lookup_kwarg_isnull] 

237 ), 

238 "display": val, 

239 } 

240 if self.include_empty_choice: 

241 yield { 

242 "selected": bool(self.lookup_val_isnull), 

243 "query_string": changelist.get_query_string( 

244 {self.lookup_kwarg_isnull: "True"}, [self.lookup_kwarg] 

245 ), 

246 "display": self.empty_value_display, 

247 } 

248 

249 

250FieldListFilter.register(lambda f: f.remote_field, RelatedFieldListFilter) 250 ↛ exitline 250 didn't run the lambda on line 250

251 

252 

253class BooleanFieldListFilter(FieldListFilter): 

254 def __init__(self, field, request, params, model, model_admin, field_path): 

255 self.lookup_kwarg = "%s__exact" % field_path 

256 self.lookup_kwarg2 = "%s__isnull" % field_path 

257 self.lookup_val = params.get(self.lookup_kwarg) 

258 self.lookup_val2 = params.get(self.lookup_kwarg2) 

259 super().__init__(field, request, params, model, model_admin, field_path) 

260 if ( 

261 self.used_parameters 

262 and self.lookup_kwarg in self.used_parameters 

263 and self.used_parameters[self.lookup_kwarg] in ("1", "0") 

264 ): 

265 self.used_parameters[self.lookup_kwarg] = bool( 

266 int(self.used_parameters[self.lookup_kwarg]) 

267 ) 

268 

269 def expected_parameters(self): 

270 return [self.lookup_kwarg, self.lookup_kwarg2] 

271 

272 def choices(self, changelist): 

273 field_choices = dict(self.field.flatchoices) 

274 for lookup, title in ( 

275 (None, _("All")), 

276 ("1", field_choices.get(True, _("Yes"))), 

277 ("0", field_choices.get(False, _("No"))), 

278 ): 

279 yield { 

280 "selected": self.lookup_val == lookup and not self.lookup_val2, 

281 "query_string": changelist.get_query_string( 

282 {self.lookup_kwarg: lookup}, [self.lookup_kwarg2] 

283 ), 

284 "display": title, 

285 } 

286 if self.field.null: 

287 yield { 

288 "selected": self.lookup_val2 == "True", 

289 "query_string": changelist.get_query_string( 

290 {self.lookup_kwarg2: "True"}, [self.lookup_kwarg] 

291 ), 

292 "display": field_choices.get(None, _("Unknown")), 

293 } 

294 

295 

296FieldListFilter.register( 296 ↛ exitline 296 didn't jump to the function exit

297 lambda f: isinstance(f, models.BooleanField), BooleanFieldListFilter 

298) 

299 

300 

301class ChoicesFieldListFilter(FieldListFilter): 

302 def __init__(self, field, request, params, model, model_admin, field_path): 

303 self.lookup_kwarg = "%s__exact" % field_path 

304 self.lookup_kwarg_isnull = "%s__isnull" % field_path 

305 self.lookup_val = params.get(self.lookup_kwarg) 

306 self.lookup_val_isnull = params.get(self.lookup_kwarg_isnull) 

307 super().__init__(field, request, params, model, model_admin, field_path) 

308 

309 def expected_parameters(self): 

310 return [self.lookup_kwarg, self.lookup_kwarg_isnull] 

311 

312 def choices(self, changelist): 

313 yield { 

314 "selected": self.lookup_val is None, 

315 "query_string": changelist.get_query_string( 

316 remove=[self.lookup_kwarg, self.lookup_kwarg_isnull] 

317 ), 

318 "display": _("All"), 

319 } 

320 none_title = "" 

321 for lookup, title in self.field.flatchoices: 

322 if lookup is None: 

323 none_title = title 

324 continue 

325 yield { 

326 "selected": str(lookup) == self.lookup_val, 

327 "query_string": changelist.get_query_string( 

328 {self.lookup_kwarg: lookup}, [self.lookup_kwarg_isnull] 

329 ), 

330 "display": title, 

331 } 

332 if none_title: 

333 yield { 

334 "selected": bool(self.lookup_val_isnull), 

335 "query_string": changelist.get_query_string( 

336 {self.lookup_kwarg_isnull: "True"}, [self.lookup_kwarg] 

337 ), 

338 "display": none_title, 

339 } 

340 

341 

342FieldListFilter.register(lambda f: bool(f.choices), ChoicesFieldListFilter) 342 ↛ exitline 342 didn't run the lambda on line 342

343 

344 

345class DateFieldListFilter(FieldListFilter): 

346 def __init__(self, field, request, params, model, model_admin, field_path): 

347 self.field_generic = "%s__" % field_path 

348 self.date_params = { 

349 k: v for k, v in params.items() if k.startswith(self.field_generic) 

350 } 

351 

352 now = timezone.now() 

353 # When time zone support is enabled, convert "now" to the user's time 

354 # zone so Django's definition of "Today" matches what the user expects. 

355 if timezone.is_aware(now): 

356 now = timezone.localtime(now) 

357 

358 if isinstance(field, models.DateTimeField): 

359 today = now.replace(hour=0, minute=0, second=0, microsecond=0) 

360 else: # field is a models.DateField 

361 today = now.date() 

362 tomorrow = today + datetime.timedelta(days=1) 

363 if today.month == 12: 

364 next_month = today.replace(year=today.year + 1, month=1, day=1) 

365 else: 

366 next_month = today.replace(month=today.month + 1, day=1) 

367 next_year = today.replace(year=today.year + 1, month=1, day=1) 

368 

369 self.lookup_kwarg_since = "%s__gte" % field_path 

370 self.lookup_kwarg_until = "%s__lt" % field_path 

371 self.links = ( 

372 (_("Any date"), {}), 

373 ( 

374 _("Today"), 

375 { 

376 self.lookup_kwarg_since: str(today), 

377 self.lookup_kwarg_until: str(tomorrow), 

378 }, 

379 ), 

380 ( 

381 _("Past 7 days"), 

382 { 

383 self.lookup_kwarg_since: str(today - datetime.timedelta(days=7)), 

384 self.lookup_kwarg_until: str(tomorrow), 

385 }, 

386 ), 

387 ( 

388 _("This month"), 

389 { 

390 self.lookup_kwarg_since: str(today.replace(day=1)), 

391 self.lookup_kwarg_until: str(next_month), 

392 }, 

393 ), 

394 ( 

395 _("This year"), 

396 { 

397 self.lookup_kwarg_since: str(today.replace(month=1, day=1)), 

398 self.lookup_kwarg_until: str(next_year), 

399 }, 

400 ), 

401 ) 

402 if field.null: 

403 self.lookup_kwarg_isnull = "%s__isnull" % field_path 

404 self.links += ( 

405 (_("No date"), {self.field_generic + "isnull": "True"}), 

406 (_("Has date"), {self.field_generic + "isnull": "False"}), 

407 ) 

408 super().__init__(field, request, params, model, model_admin, field_path) 

409 

410 def expected_parameters(self): 

411 params = [self.lookup_kwarg_since, self.lookup_kwarg_until] 

412 if self.field.null: 

413 params.append(self.lookup_kwarg_isnull) 

414 return params 

415 

416 def choices(self, changelist): 

417 for title, param_dict in self.links: 

418 yield { 

419 "selected": self.date_params == param_dict, 

420 "query_string": changelist.get_query_string( 

421 param_dict, [self.field_generic] 

422 ), 

423 "display": title, 

424 } 

425 

426 

427FieldListFilter.register(lambda f: isinstance(f, models.DateField), DateFieldListFilter) 427 ↛ exitline 427 didn't run the lambda on line 427

428 

429 

430# This should be registered last, because it's a last resort. For example, 

431# if a field is eligible to use the BooleanFieldListFilter, that'd be much 

432# more appropriate, and the AllValuesFieldListFilter won't get used for it. 

433class AllValuesFieldListFilter(FieldListFilter): 

434 def __init__(self, field, request, params, model, model_admin, field_path): 

435 self.lookup_kwarg = field_path 

436 self.lookup_kwarg_isnull = "%s__isnull" % field_path 

437 self.lookup_val = params.get(self.lookup_kwarg) 

438 self.lookup_val_isnull = params.get(self.lookup_kwarg_isnull) 

439 self.empty_value_display = model_admin.get_empty_value_display() 

440 parent_model, reverse_path = reverse_field_path(model, field_path) 

441 # Obey parent ModelAdmin queryset when deciding which options to show 

442 if model == parent_model: 

443 queryset = model_admin.get_queryset(request) 

444 else: 

445 queryset = parent_model._default_manager.all() 

446 self.lookup_choices = ( 

447 queryset.distinct().order_by(field.name).values_list(field.name, flat=True) 

448 ) 

449 super().__init__(field, request, params, model, model_admin, field_path) 

450 

451 def expected_parameters(self): 

452 return [self.lookup_kwarg, self.lookup_kwarg_isnull] 

453 

454 def choices(self, changelist): 

455 yield { 

456 "selected": self.lookup_val is None and self.lookup_val_isnull is None, 

457 "query_string": changelist.get_query_string( 

458 remove=[self.lookup_kwarg, self.lookup_kwarg_isnull] 

459 ), 

460 "display": _("All"), 

461 } 

462 include_none = False 

463 for val in self.lookup_choices: 

464 if val is None: 

465 include_none = True 

466 continue 

467 val = str(val) 

468 yield { 

469 "selected": self.lookup_val == val, 

470 "query_string": changelist.get_query_string( 

471 {self.lookup_kwarg: val}, [self.lookup_kwarg_isnull] 

472 ), 

473 "display": val, 

474 } 

475 if include_none: 

476 yield { 

477 "selected": bool(self.lookup_val_isnull), 

478 "query_string": changelist.get_query_string( 

479 {self.lookup_kwarg_isnull: "True"}, [self.lookup_kwarg] 

480 ), 

481 "display": self.empty_value_display, 

482 } 

483 

484 

485FieldListFilter.register(lambda f: True, AllValuesFieldListFilter) 485 ↛ exitline 485 didn't run the lambda on line 485

486 

487 

488class RelatedOnlyFieldListFilter(RelatedFieldListFilter): 

489 def field_choices(self, field, request, model_admin): 

490 pk_qs = ( 

491 model_admin.get_queryset(request) 

492 .distinct() 

493 .values_list("%s__pk" % self.field_path, flat=True) 

494 ) 

495 ordering = self.field_admin_ordering(field, request, model_admin) 

496 return field.get_choices( 

497 include_blank=False, limit_choices_to={"pk__in": pk_qs}, ordering=ordering 

498 ) 

499 

500 

501class EmptyFieldListFilter(FieldListFilter): 

502 def __init__(self, field, request, params, model, model_admin, field_path): 

503 if not field.empty_strings_allowed and not field.null: 

504 raise ImproperlyConfigured( 

505 "The list filter '%s' cannot be used with field '%s' which " 

506 "doesn't allow empty strings and nulls." 

507 % ( 

508 self.__class__.__name__, 

509 field.name, 

510 ) 

511 ) 

512 self.lookup_kwarg = "%s__isempty" % field_path 

513 self.lookup_val = params.get(self.lookup_kwarg) 

514 super().__init__(field, request, params, model, model_admin, field_path) 

515 

516 def queryset(self, request, queryset): 

517 if self.lookup_kwarg not in self.used_parameters: 

518 return queryset 

519 if self.lookup_val not in ("0", "1"): 

520 raise IncorrectLookupParameters 

521 

522 lookup_conditions = [] 

523 if self.field.empty_strings_allowed: 

524 lookup_conditions.append((self.field_path, "")) 

525 if self.field.null: 

526 lookup_conditions.append((f"{self.field_path}__isnull", True)) 

527 lookup_condition = models.Q(*lookup_conditions, _connector=models.Q.OR) 

528 if self.lookup_val == "1": 

529 return queryset.filter(lookup_condition) 

530 return queryset.exclude(lookup_condition) 

531 

532 def expected_parameters(self): 

533 return [self.lookup_kwarg] 

534 

535 def choices(self, changelist): 

536 for lookup, title in ( 

537 (None, _("All")), 

538 ("1", _("Empty")), 

539 ("0", _("Not empty")), 

540 ): 

541 yield { 

542 "selected": self.lookup_val == lookup, 

543 "query_string": changelist.get_query_string( 

544 {self.lookup_kwarg: lookup} 

545 ), 

546 "display": title, 

547 }