Django admin list_filter "or" condition

Sorry if this question has already been answered, but I did a lot without searching.

I know how to create a custom list_filter in list_filter views (e.g. subclassing SimpleFilter ).

What I really need is a way (in the view of the administrator list) to "check" different filters that combine them in the OR formula.

As an example, suppose you have:

 # models.py class Foo(models.Model): foobar = ... foofie = ... ... # admin.py class FooAdmin(admin.ModelAdmin): list_filter = ( "foobar", "foofie" ) ... 

In the admin list view generated by FooAdmin , I can choose to filter records using either foobar or foofie . Is there a way to filter them by the formula: foobar = X OR foofie = Y , where X and Y are two values ​​that foobar and foofie can take?

Is it possible?

I know that not everything is possible in django admin views, but it seems to be a very common request, I wonder if I missed something to understand or read.

Also, third-party applications that allow this are welcome. Thanks:)

+5
source share
3 answers

I found a third-party application just now, django-advanced-filters , which may meet your requirements.

He has:

Field OR

OR is an optional field that is added to each available field rule.

It allows you to build queries using OR operators. You can use it by creating an "empty" rule with this field "between" a set of 1 or more rules.

I checked the test, add OR field . This is a screenshot: enter image description here

+3
source

Firstly, I am trying to explain the operation of admin django filters. If you want to filter your request on the admin page, django looks for all registered filters. If you set the value for the filter filter django filterset with this value. If you set more than one django filter filter, your query will be double, it will be equal to queryset = queryset.filter (param1 = 1) .filter (param2 = 2) or in SQL: SELECT ... WHERE param1 = 1 AND param2 = 2 This is because you cannot do this with standard django filters. But you can write your own filter as follows:

 from django.contrib.admin import SimpleListFilter from django.db.models import Q from functools import reduce import operator from django.core.exceptions import FieldError class ORListFilter(SimpleListFilter): title = '' parameter_name = '' search_field = ('',) def queryset(self, request, queryset): filters = request.GET.copy() try: #for search search_field_value = filters.pop('q')[0] query_params = [Q((key, search_field_value)) for key in self.search_field] try: queryset = queryset.filter(reduce(operator.or_, query_params)) except FieldError: pass except KeyError: pass try: query_params = [Q((key, value)) for key, value in filters.dict().items()] queryset = queryset.filter(reduce(operator.or_, query_params)) except TypeError: pass return queryset def lookups(self, request, model_admin): qs = model_admin.get_queryset(request) parameters = qs.all().values(self.parameter_name).distinct() for parameter in parameters: value = dict(parameter).pop(self.parameter_name, None) if value: yield (value, value) else: yield (None, 'NULL') class Field1Filter(ORListFilter): title = 'title' parameter_name = 'field1' search_field = ('search1', 'search2') class Field2Filter(ORListFilter): title = 'title' parameter_name = 'field2' search_field = ('search1', 'search2') 

And register it in admin:

 search_fields = ('search1', 'search2') list_filter = (Field1Filter, Field2Filter) 

It does not work with standard django filters, and all values ​​in list_filter must inherit from the ORListFilter class. It also does not work with datetime filters, but you can add this feature.

+2
source

It turned out the solution:

 import operator from functools import reduce from django.contrib.admin import ListFilter, FieldListFilter from django.db.models import Q from django.contrib.admin.utils import ( get_fields_from_path, lookup_needs_distinct, prepare_lookup_value, ) from django.http import QueryDict class OrListFilter(ListFilter): parameter_prefix = None fields = None def __init__(self, request, params, model, model_admin): super(OrListFilter, self).__init__( request, params, model, model_admin) if self.parameter_prefix is None: raise ImproperlyConfigured( "The list filter '%s' does not specify " "a 'parameter_prefix'." % self.__class__.__name__) self.model_admin = model_admin self.model = model self.request = request self.filter_specs = self.get_filters(request, {}, prefix=self.parameter_prefix+'-') for p in self.expected_parameters(): if p in params: value = params.pop(p) field = p.split('-')[1] self.used_parameters[field] = prepare_lookup_value(field, value) def has_output(self): return True # see https://github.com/django/django/blob/1.8.5/django/contrib/admin/views/main.py#L104 def get_filters(self, request, params, prefix=''): filter_specs = [] for field_path in self.fields: field = get_fields_from_path(self.model, field_path)[-1] field_list_filter_class = FieldListFilter.create spec = field_list_filter_class(field, request, params, self.model, self.model_admin, field_path=prefix + field_path) # Check if we need to use distinct() # use_distinct = (use_distinct or # lookup_needs_distinct(self.lookup_opts, # field_path)) filter_specs.append(spec) return filter_specs def expected_parameters(self): parameters = [] for spec in self.filter_specs: parameters += spec.expected_parameters() return parameters def choices(self, cl): return [] def queryset(self, request, queryset): origin_GET = request.GET.copy() fake_GET = QueryDict(mutable=True) fake_GET.update(self.used_parameters) request.GET = fake_GET all_params = {} for spec in self.get_filters(request, self.used_parameters): if spec and spec.has_output(): all_params.update(spec.used_parameters) try: query_params = [Q((key, value)) for key, value in all_params.items()] queryset = queryset.filter(reduce(operator.or_, query_params)) except TypeError as e: pass # restore request.GET = origin_GET return queryset class OrFilter(OrListFilter): title = 'Or filter' parameter_prefix = 'or1' fields = ("foobar", "foofie") class FooAdmin(admin.ModelAdmin): list_filter = (OrFilter, ) 

APP_NAME / templates / admin / app_name / change_list.html:

 {% extends "admin/change_list.html" %} {% load i18n admin_list %} {% block filters %} {% if cl.has_filters %} <div id="changelist-filter"> <h2>{% trans 'Filter' %}</h2> {% for spec in cl.filter_specs %} {% if spec.filter_specs %} {% admin_list_filter cl spec %} <ul> {% for sub_spec in spec.filter_specs %} <li>{% admin_list_filter cl sub_spec %}</li> {% endfor %} </ul> {% else %} {% admin_list_filter cl spec %} {% endif %} {% endfor %} </div> {% endif %} {% endblock %} 

I borrowed some code from @ dima-kudosh.

Description

ChangeList.get_filters() creates ListFilter (filter_specs) from ModelAdmin.list_filter , then uses ListFilter.queryset() - get_queryset() .

FieldListFilter.queryset() uses used_parameters to filter the request: queryset.filter(**self.used_parameters) .

So, we can create a FieldListFilter from OrListFilter.fields and use their used_parameters to create OR queries:

 all_params = {} for spec in self.get_filters(request, self.used_parameters): if spec and spec.has_output(): all_params.update(spec.used_parameters) try: query_params = [Q((key, value)) for key, value in all_params.items()] queryset = queryset.filter(reduce(operator.or_, query_params)) except TypeError as e: pass 
+2
source

Source: https://habr.com/ru/post/1206656/


All Articles