filters.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715
  1. """
  2. This encapsulates the logic for displaying filters in the Django admin.
  3. Filters are specified in models with the "list_filter" option.
  4. Each filter subclass knows how to display a filter for a field that passes a
  5. certain test -- e.g. being a DateField or ForeignKey.
  6. """
  7. import datetime
  8. from django.contrib.admin.exceptions import NotRegistered
  9. from django.contrib.admin.options import IncorrectLookupParameters
  10. from django.contrib.admin.utils import (
  11. build_q_object_from_lookup_parameters,
  12. get_last_value_from_parameters,
  13. get_model_from_relation,
  14. prepare_lookup_value,
  15. reverse_field_path,
  16. )
  17. from django.core.exceptions import ImproperlyConfigured, ValidationError
  18. from django.db import models
  19. from django.utils import timezone
  20. from django.utils.translation import gettext_lazy as _
  21. class ListFilter:
  22. title = None # Human-readable title to appear in the right sidebar.
  23. template = "admin/filter.html"
  24. def __init__(self, request, params, model, model_admin):
  25. self.request = request
  26. # This dictionary will eventually contain the request's query string
  27. # parameters actually used by this filter.
  28. self.used_parameters = {}
  29. if self.title is None:
  30. raise ImproperlyConfigured(
  31. "The list filter '%s' does not specify a 'title'."
  32. % self.__class__.__name__
  33. )
  34. def has_output(self):
  35. """
  36. Return True if some choices would be output for this filter.
  37. """
  38. raise NotImplementedError(
  39. "subclasses of ListFilter must provide a has_output() method"
  40. )
  41. def choices(self, changelist):
  42. """
  43. Return choices ready to be output in the template.
  44. `changelist` is the ChangeList to be displayed.
  45. """
  46. raise NotImplementedError(
  47. "subclasses of ListFilter must provide a choices() method"
  48. )
  49. def queryset(self, request, queryset):
  50. """
  51. Return the filtered queryset.
  52. """
  53. raise NotImplementedError(
  54. "subclasses of ListFilter must provide a queryset() method"
  55. )
  56. def expected_parameters(self):
  57. """
  58. Return the list of parameter names that are expected from the
  59. request's query string and that will be used by this filter.
  60. """
  61. raise NotImplementedError(
  62. "subclasses of ListFilter must provide an expected_parameters() method"
  63. )
  64. class FacetsMixin:
  65. def get_facet_counts(self, pk_attname, filtered_qs):
  66. raise NotImplementedError(
  67. "subclasses of FacetsMixin must provide a get_facet_counts() method."
  68. )
  69. def get_facet_queryset(self, changelist):
  70. filtered_qs = changelist.get_queryset(
  71. self.request, exclude_parameters=self.expected_parameters()
  72. )
  73. return filtered_qs.aggregate(
  74. **self.get_facet_counts(changelist.pk_attname, filtered_qs)
  75. )
  76. class SimpleListFilter(FacetsMixin, ListFilter):
  77. # The parameter that should be used in the query string for that filter.
  78. parameter_name = None
  79. def __init__(self, request, params, model, model_admin):
  80. super().__init__(request, params, model, model_admin)
  81. if self.parameter_name is None:
  82. raise ImproperlyConfigured(
  83. "The list filter '%s' does not specify a 'parameter_name'."
  84. % self.__class__.__name__
  85. )
  86. if self.parameter_name in params:
  87. value = params.pop(self.parameter_name)
  88. self.used_parameters[self.parameter_name] = value[-1]
  89. lookup_choices = self.lookups(request, model_admin)
  90. if lookup_choices is None:
  91. lookup_choices = ()
  92. self.lookup_choices = list(lookup_choices)
  93. def has_output(self):
  94. return len(self.lookup_choices) > 0
  95. def value(self):
  96. """
  97. Return the value (in string format) provided in the request's
  98. query string for this filter, if any, or None if the value wasn't
  99. provided.
  100. """
  101. return self.used_parameters.get(self.parameter_name)
  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. def expected_parameters(self):
  111. return [self.parameter_name]
  112. def get_facet_counts(self, pk_attname, filtered_qs):
  113. original_value = self.used_parameters.get(self.parameter_name)
  114. counts = {}
  115. for i, choice in enumerate(self.lookup_choices):
  116. self.used_parameters[self.parameter_name] = choice[0]
  117. lookup_qs = self.queryset(self.request, filtered_qs)
  118. if lookup_qs is not None:
  119. counts[f"{i}__c"] = models.Count(
  120. pk_attname,
  121. filter=lookup_qs.query.where,
  122. )
  123. self.used_parameters[self.parameter_name] = original_value
  124. return counts
  125. def choices(self, changelist):
  126. add_facets = changelist.add_facets
  127. facet_counts = self.get_facet_queryset(changelist) if add_facets else None
  128. yield {
  129. "selected": self.value() is None,
  130. "query_string": changelist.get_query_string(remove=[self.parameter_name]),
  131. "display": _("All"),
  132. }
  133. for i, (lookup, title) in enumerate(self.lookup_choices):
  134. if add_facets:
  135. if (count := facet_counts.get(f"{i}__c", -1)) != -1:
  136. title = f"{title} ({count})"
  137. else:
  138. title = f"{title} (-)"
  139. yield {
  140. "selected": self.value() == str(lookup),
  141. "query_string": changelist.get_query_string(
  142. {self.parameter_name: lookup}
  143. ),
  144. "display": title,
  145. }
  146. class FieldListFilter(FacetsMixin, ListFilter):
  147. _field_list_filters = []
  148. _take_priority_index = 0
  149. list_separator = ","
  150. def __init__(self, field, request, params, model, model_admin, field_path):
  151. self.field = field
  152. self.field_path = field_path
  153. self.title = getattr(field, "verbose_name", field_path)
  154. super().__init__(request, params, model, model_admin)
  155. for p in self.expected_parameters():
  156. if p in params:
  157. value = params.pop(p)
  158. self.used_parameters[p] = prepare_lookup_value(
  159. p, value, self.list_separator
  160. )
  161. def has_output(self):
  162. return True
  163. def queryset(self, request, queryset):
  164. try:
  165. q_object = build_q_object_from_lookup_parameters(self.used_parameters)
  166. return queryset.filter(q_object)
  167. except (ValueError, ValidationError) as e:
  168. # Fields may raise a ValueError or ValidationError when converting
  169. # the parameters to the correct type.
  170. raise IncorrectLookupParameters(e)
  171. @classmethod
  172. def register(cls, test, list_filter_class, take_priority=False):
  173. if take_priority:
  174. # This is to allow overriding the default filters for certain types
  175. # of fields with some custom filters. The first found in the list
  176. # is used in priority.
  177. cls._field_list_filters.insert(
  178. cls._take_priority_index, (test, list_filter_class)
  179. )
  180. cls._take_priority_index += 1
  181. else:
  182. cls._field_list_filters.append((test, list_filter_class))
  183. @classmethod
  184. def create(cls, field, request, params, model, model_admin, field_path):
  185. for test, list_filter_class in cls._field_list_filters:
  186. if test(field):
  187. return list_filter_class(
  188. field, request, params, model, model_admin, field_path=field_path
  189. )
  190. class RelatedFieldListFilter(FieldListFilter):
  191. def __init__(self, field, request, params, model, model_admin, field_path):
  192. other_model = get_model_from_relation(field)
  193. self.lookup_kwarg = "%s__%s__exact" % (field_path, field.target_field.name)
  194. self.lookup_kwarg_isnull = "%s__isnull" % field_path
  195. self.lookup_val = params.get(self.lookup_kwarg)
  196. self.lookup_val_isnull = get_last_value_from_parameters(
  197. params, self.lookup_kwarg_isnull
  198. )
  199. super().__init__(field, request, params, model, model_admin, field_path)
  200. self.lookup_choices = self.field_choices(field, request, model_admin)
  201. if hasattr(field, "verbose_name"):
  202. self.lookup_title = field.verbose_name
  203. else:
  204. self.lookup_title = other_model._meta.verbose_name
  205. self.title = self.lookup_title
  206. self.empty_value_display = model_admin.get_empty_value_display()
  207. @property
  208. def include_empty_choice(self):
  209. """
  210. Return True if a "(None)" choice should be included, which filters
  211. out everything except empty relationships.
  212. """
  213. return self.field.null or (self.field.is_relation and self.field.many_to_many)
  214. def has_output(self):
  215. if self.include_empty_choice:
  216. extra = 1
  217. else:
  218. extra = 0
  219. return len(self.lookup_choices) + extra > 1
  220. def expected_parameters(self):
  221. return [self.lookup_kwarg, self.lookup_kwarg_isnull]
  222. def field_admin_ordering(self, field, request, model_admin):
  223. """
  224. Return the model admin's ordering for related field, if provided.
  225. """
  226. try:
  227. related_admin = model_admin.admin_site.get_model_admin(
  228. field.remote_field.model
  229. )
  230. except NotRegistered:
  231. return ()
  232. else:
  233. return related_admin.get_ordering(request)
  234. def field_choices(self, field, request, model_admin):
  235. ordering = self.field_admin_ordering(field, request, model_admin)
  236. return field.get_choices(include_blank=False, ordering=ordering)
  237. def get_facet_counts(self, pk_attname, filtered_qs):
  238. counts = {
  239. f"{pk_val}__c": models.Count(
  240. pk_attname, filter=models.Q(**{self.lookup_kwarg: pk_val})
  241. )
  242. for pk_val, _ in self.lookup_choices
  243. }
  244. if self.include_empty_choice:
  245. counts["__c"] = models.Count(
  246. pk_attname, filter=models.Q(**{self.lookup_kwarg_isnull: True})
  247. )
  248. return counts
  249. def choices(self, changelist):
  250. add_facets = changelist.add_facets
  251. facet_counts = self.get_facet_queryset(changelist) if add_facets else None
  252. yield {
  253. "selected": self.lookup_val is None and not self.lookup_val_isnull,
  254. "query_string": changelist.get_query_string(
  255. remove=[self.lookup_kwarg, self.lookup_kwarg_isnull]
  256. ),
  257. "display": _("All"),
  258. }
  259. count = None
  260. for pk_val, val in self.lookup_choices:
  261. if add_facets:
  262. count = facet_counts[f"{pk_val}__c"]
  263. val = f"{val} ({count})"
  264. yield {
  265. "selected": self.lookup_val is not None
  266. and str(pk_val) in self.lookup_val,
  267. "query_string": changelist.get_query_string(
  268. {self.lookup_kwarg: pk_val}, [self.lookup_kwarg_isnull]
  269. ),
  270. "display": val,
  271. }
  272. empty_title = self.empty_value_display
  273. if self.include_empty_choice:
  274. if add_facets:
  275. count = facet_counts["__c"]
  276. empty_title = f"{empty_title} ({count})"
  277. yield {
  278. "selected": bool(self.lookup_val_isnull),
  279. "query_string": changelist.get_query_string(
  280. {self.lookup_kwarg_isnull: "True"}, [self.lookup_kwarg]
  281. ),
  282. "display": empty_title,
  283. }
  284. FieldListFilter.register(lambda f: f.remote_field, RelatedFieldListFilter)
  285. class BooleanFieldListFilter(FieldListFilter):
  286. def __init__(self, field, request, params, model, model_admin, field_path):
  287. self.lookup_kwarg = "%s__exact" % field_path
  288. self.lookup_kwarg2 = "%s__isnull" % field_path
  289. self.lookup_val = get_last_value_from_parameters(params, self.lookup_kwarg)
  290. self.lookup_val2 = get_last_value_from_parameters(params, self.lookup_kwarg2)
  291. super().__init__(field, request, params, model, model_admin, field_path)
  292. if (
  293. self.used_parameters
  294. and self.lookup_kwarg in self.used_parameters
  295. and self.used_parameters[self.lookup_kwarg] in ("1", "0")
  296. ):
  297. self.used_parameters[self.lookup_kwarg] = bool(
  298. int(self.used_parameters[self.lookup_kwarg])
  299. )
  300. def expected_parameters(self):
  301. return [self.lookup_kwarg, self.lookup_kwarg2]
  302. def get_facet_counts(self, pk_attname, filtered_qs):
  303. return {
  304. "true__c": models.Count(
  305. pk_attname, filter=models.Q(**{self.field_path: True})
  306. ),
  307. "false__c": models.Count(
  308. pk_attname, filter=models.Q(**{self.field_path: False})
  309. ),
  310. "null__c": models.Count(
  311. pk_attname, filter=models.Q(**{self.lookup_kwarg2: True})
  312. ),
  313. }
  314. def choices(self, changelist):
  315. field_choices = dict(self.field.flatchoices)
  316. add_facets = changelist.add_facets
  317. facet_counts = self.get_facet_queryset(changelist) if add_facets else None
  318. for lookup, title, count_field in (
  319. (None, _("All"), None),
  320. ("1", field_choices.get(True, _("Yes")), "true__c"),
  321. ("0", field_choices.get(False, _("No")), "false__c"),
  322. ):
  323. if add_facets:
  324. if count_field is not None:
  325. count = facet_counts[count_field]
  326. title = f"{title} ({count})"
  327. yield {
  328. "selected": self.lookup_val == lookup and not self.lookup_val2,
  329. "query_string": changelist.get_query_string(
  330. {self.lookup_kwarg: lookup}, [self.lookup_kwarg2]
  331. ),
  332. "display": title,
  333. }
  334. if self.field.null:
  335. display = field_choices.get(None, _("Unknown"))
  336. if add_facets:
  337. count = facet_counts["null__c"]
  338. display = f"{display} ({count})"
  339. yield {
  340. "selected": self.lookup_val2 == "True",
  341. "query_string": changelist.get_query_string(
  342. {self.lookup_kwarg2: "True"}, [self.lookup_kwarg]
  343. ),
  344. "display": display,
  345. }
  346. FieldListFilter.register(
  347. lambda f: isinstance(f, models.BooleanField), BooleanFieldListFilter
  348. )
  349. class ChoicesFieldListFilter(FieldListFilter):
  350. def __init__(self, field, request, params, model, model_admin, field_path):
  351. self.lookup_kwarg = "%s__exact" % field_path
  352. self.lookup_kwarg_isnull = "%s__isnull" % field_path
  353. self.lookup_val = params.get(self.lookup_kwarg)
  354. self.lookup_val_isnull = get_last_value_from_parameters(
  355. params, self.lookup_kwarg_isnull
  356. )
  357. super().__init__(field, request, params, model, model_admin, field_path)
  358. def expected_parameters(self):
  359. return [self.lookup_kwarg, self.lookup_kwarg_isnull]
  360. def get_facet_counts(self, pk_attname, filtered_qs):
  361. return {
  362. f"{i}__c": models.Count(
  363. pk_attname,
  364. filter=models.Q(
  365. (self.lookup_kwarg, value)
  366. if value is not None
  367. else (self.lookup_kwarg_isnull, True)
  368. ),
  369. )
  370. for i, (value, _) in enumerate(self.field.flatchoices)
  371. }
  372. def choices(self, changelist):
  373. add_facets = changelist.add_facets
  374. facet_counts = self.get_facet_queryset(changelist) if add_facets else None
  375. yield {
  376. "selected": self.lookup_val is None,
  377. "query_string": changelist.get_query_string(
  378. remove=[self.lookup_kwarg, self.lookup_kwarg_isnull]
  379. ),
  380. "display": _("All"),
  381. }
  382. none_title = ""
  383. for i, (lookup, title) in enumerate(self.field.flatchoices):
  384. if add_facets:
  385. count = facet_counts[f"{i}__c"]
  386. title = f"{title} ({count})"
  387. if lookup is None:
  388. none_title = title
  389. continue
  390. yield {
  391. "selected": self.lookup_val is not None
  392. and str(lookup) in self.lookup_val,
  393. "query_string": changelist.get_query_string(
  394. {self.lookup_kwarg: lookup}, [self.lookup_kwarg_isnull]
  395. ),
  396. "display": title,
  397. }
  398. if none_title:
  399. yield {
  400. "selected": bool(self.lookup_val_isnull),
  401. "query_string": changelist.get_query_string(
  402. {self.lookup_kwarg_isnull: "True"}, [self.lookup_kwarg]
  403. ),
  404. "display": none_title,
  405. }
  406. FieldListFilter.register(lambda f: bool(f.choices), ChoicesFieldListFilter)
  407. class DateFieldListFilter(FieldListFilter):
  408. def __init__(self, field, request, params, model, model_admin, field_path):
  409. self.field_generic = "%s__" % field_path
  410. self.date_params = {
  411. k: v[-1] for k, v in params.items() if k.startswith(self.field_generic)
  412. }
  413. now = timezone.now()
  414. # When time zone support is enabled, convert "now" to the user's time
  415. # zone so Django's definition of "Today" matches what the user expects.
  416. if timezone.is_aware(now):
  417. now = timezone.localtime(now)
  418. if isinstance(field, models.DateTimeField):
  419. today = now.replace(hour=0, minute=0, second=0, microsecond=0)
  420. else: # field is a models.DateField
  421. today = now.date()
  422. tomorrow = today + datetime.timedelta(days=1)
  423. if today.month == 12:
  424. next_month = today.replace(year=today.year + 1, month=1, day=1)
  425. else:
  426. next_month = today.replace(month=today.month + 1, day=1)
  427. next_year = today.replace(year=today.year + 1, month=1, day=1)
  428. self.lookup_kwarg_since = "%s__gte" % field_path
  429. self.lookup_kwarg_until = "%s__lt" % field_path
  430. self.links = (
  431. (_("Any date"), {}),
  432. (
  433. _("Today"),
  434. {
  435. self.lookup_kwarg_since: today,
  436. self.lookup_kwarg_until: tomorrow,
  437. },
  438. ),
  439. (
  440. _("Past 7 days"),
  441. {
  442. self.lookup_kwarg_since: today - datetime.timedelta(days=7),
  443. self.lookup_kwarg_until: tomorrow,
  444. },
  445. ),
  446. (
  447. _("This month"),
  448. {
  449. self.lookup_kwarg_since: today.replace(day=1),
  450. self.lookup_kwarg_until: next_month,
  451. },
  452. ),
  453. (
  454. _("This year"),
  455. {
  456. self.lookup_kwarg_since: today.replace(month=1, day=1),
  457. self.lookup_kwarg_until: next_year,
  458. },
  459. ),
  460. )
  461. if field.null:
  462. self.lookup_kwarg_isnull = "%s__isnull" % field_path
  463. self.links += (
  464. (_("No date"), {self.field_generic + "isnull": True}),
  465. (_("Has date"), {self.field_generic + "isnull": False}),
  466. )
  467. super().__init__(field, request, params, model, model_admin, field_path)
  468. def expected_parameters(self):
  469. params = [self.lookup_kwarg_since, self.lookup_kwarg_until]
  470. if self.field.null:
  471. params.append(self.lookup_kwarg_isnull)
  472. return params
  473. def get_facet_counts(self, pk_attname, filtered_qs):
  474. return {
  475. f"{i}__c": models.Count(pk_attname, filter=models.Q(**param_dict))
  476. for i, (_, param_dict) in enumerate(self.links)
  477. }
  478. def choices(self, changelist):
  479. add_facets = changelist.add_facets
  480. facet_counts = self.get_facet_queryset(changelist) if add_facets else None
  481. for i, (title, param_dict) in enumerate(self.links):
  482. param_dict_str = {key: str(value) for key, value in param_dict.items()}
  483. if add_facets:
  484. count = facet_counts[f"{i}__c"]
  485. title = f"{title} ({count})"
  486. yield {
  487. "selected": self.date_params == param_dict_str,
  488. "query_string": changelist.get_query_string(
  489. param_dict_str, [self.field_generic]
  490. ),
  491. "display": title,
  492. }
  493. FieldListFilter.register(lambda f: isinstance(f, models.DateField), DateFieldListFilter)
  494. # This should be registered last, because it's a last resort. For example,
  495. # if a field is eligible to use the BooleanFieldListFilter, that'd be much
  496. # more appropriate, and the AllValuesFieldListFilter won't get used for it.
  497. class AllValuesFieldListFilter(FieldListFilter):
  498. def __init__(self, field, request, params, model, model_admin, field_path):
  499. self.lookup_kwarg = field_path
  500. self.lookup_kwarg_isnull = "%s__isnull" % field_path
  501. self.lookup_val = params.get(self.lookup_kwarg)
  502. self.lookup_val_isnull = get_last_value_from_parameters(
  503. params, self.lookup_kwarg_isnull
  504. )
  505. self.empty_value_display = model_admin.get_empty_value_display()
  506. parent_model, reverse_path = reverse_field_path(model, field_path)
  507. # Obey parent ModelAdmin queryset when deciding which options to show
  508. if model == parent_model:
  509. queryset = model_admin.get_queryset(request)
  510. else:
  511. queryset = parent_model._default_manager.all()
  512. self.lookup_choices = (
  513. queryset.distinct().order_by(field.name).values_list(field.name, flat=True)
  514. )
  515. super().__init__(field, request, params, model, model_admin, field_path)
  516. def expected_parameters(self):
  517. return [self.lookup_kwarg, self.lookup_kwarg_isnull]
  518. def get_facet_counts(self, pk_attname, filtered_qs):
  519. return {
  520. f"{i}__c": models.Count(
  521. pk_attname,
  522. filter=models.Q(
  523. (self.lookup_kwarg, value)
  524. if value is not None
  525. else (self.lookup_kwarg_isnull, True)
  526. ),
  527. )
  528. for i, value in enumerate(self.lookup_choices)
  529. }
  530. def choices(self, changelist):
  531. add_facets = changelist.add_facets
  532. facet_counts = self.get_facet_queryset(changelist) if add_facets else None
  533. yield {
  534. "selected": self.lookup_val is None and self.lookup_val_isnull is None,
  535. "query_string": changelist.get_query_string(
  536. remove=[self.lookup_kwarg, self.lookup_kwarg_isnull]
  537. ),
  538. "display": _("All"),
  539. }
  540. include_none = False
  541. count = None
  542. empty_title = self.empty_value_display
  543. for i, val in enumerate(self.lookup_choices):
  544. if add_facets:
  545. count = facet_counts[f"{i}__c"]
  546. if val is None:
  547. include_none = True
  548. empty_title = f"{empty_title} ({count})" if add_facets else empty_title
  549. continue
  550. val = str(val)
  551. yield {
  552. "selected": self.lookup_val is not None and val in self.lookup_val,
  553. "query_string": changelist.get_query_string(
  554. {self.lookup_kwarg: val}, [self.lookup_kwarg_isnull]
  555. ),
  556. "display": f"{val} ({count})" if add_facets else val,
  557. }
  558. if include_none:
  559. yield {
  560. "selected": bool(self.lookup_val_isnull),
  561. "query_string": changelist.get_query_string(
  562. {self.lookup_kwarg_isnull: "True"}, [self.lookup_kwarg]
  563. ),
  564. "display": empty_title,
  565. }
  566. FieldListFilter.register(lambda f: True, AllValuesFieldListFilter)
  567. class RelatedOnlyFieldListFilter(RelatedFieldListFilter):
  568. def field_choices(self, field, request, model_admin):
  569. pk_qs = (
  570. model_admin.get_queryset(request)
  571. .distinct()
  572. .values_list("%s__pk" % self.field_path, flat=True)
  573. )
  574. ordering = self.field_admin_ordering(field, request, model_admin)
  575. return field.get_choices(
  576. include_blank=False, limit_choices_to={"pk__in": pk_qs}, ordering=ordering
  577. )
  578. class EmptyFieldListFilter(FieldListFilter):
  579. def __init__(self, field, request, params, model, model_admin, field_path):
  580. if not field.empty_strings_allowed and not field.null:
  581. raise ImproperlyConfigured(
  582. "The list filter '%s' cannot be used with field '%s' which "
  583. "doesn't allow empty strings and nulls."
  584. % (
  585. self.__class__.__name__,
  586. field.name,
  587. )
  588. )
  589. self.lookup_kwarg = "%s__isempty" % field_path
  590. self.lookup_val = get_last_value_from_parameters(params, self.lookup_kwarg)
  591. super().__init__(field, request, params, model, model_admin, field_path)
  592. def get_lookup_condition(self):
  593. lookup_conditions = []
  594. if self.field.empty_strings_allowed:
  595. lookup_conditions.append((self.field_path, ""))
  596. if self.field.null:
  597. lookup_conditions.append((f"{self.field_path}__isnull", True))
  598. return models.Q.create(lookup_conditions, connector=models.Q.OR)
  599. def queryset(self, request, queryset):
  600. if self.lookup_kwarg not in self.used_parameters:
  601. return queryset
  602. if self.lookup_val not in ("0", "1"):
  603. raise IncorrectLookupParameters
  604. lookup_condition = self.get_lookup_condition()
  605. if self.lookup_val == "1":
  606. return queryset.filter(lookup_condition)
  607. return queryset.exclude(lookup_condition)
  608. def expected_parameters(self):
  609. return [self.lookup_kwarg]
  610. def get_facet_counts(self, pk_attname, filtered_qs):
  611. lookup_condition = self.get_lookup_condition()
  612. return {
  613. "empty__c": models.Count(pk_attname, filter=lookup_condition),
  614. "not_empty__c": models.Count(pk_attname, filter=~lookup_condition),
  615. }
  616. def choices(self, changelist):
  617. add_facets = changelist.add_facets
  618. facet_counts = self.get_facet_queryset(changelist) if add_facets else None
  619. for lookup, title, count_field in (
  620. (None, _("All"), None),
  621. ("1", _("Empty"), "empty__c"),
  622. ("0", _("Not empty"), "not_empty__c"),
  623. ):
  624. if add_facets:
  625. if count_field is not None:
  626. count = facet_counts[count_field]
  627. title = f"{title} ({count})"
  628. yield {
  629. "selected": self.lookup_val == lookup,
  630. "query_string": changelist.get_query_string(
  631. {self.lookup_kwarg: lookup}
  632. ),
  633. "display": title,
  634. }