Django Filter Package – Part-2
In part 2 of our post on Django Filter, we’ll dive deep and learn how we can create a custom filter method and add attributes to form.
To be persistent in our approach we’ll be taking the same example of part 1 of Django Filters.
Table of Contents
- Filter queryset using a custom method
- Filter queryset by Choices and Order By
- Implementation of Widgets and Filter Form Attributes
- Adding Pagination to Queryset
- Conclusion
Filter queryset using a custom method
Suppose we want to display authors who have written more than a certain number of books and also display the count of books written by each author.
So for this, we have to specify a custom method that takes queryset and parameters and returns the filtered queryset of books whose author has written a certain given number of books.
We can do it the same way as shown previous part by books count is not a part of any model so we must pass method
the argument to filter.
In below example, we have specified method method="author_books_count"
to filter field author_books
.
The method author_books_count
takes 4 arguments self
, queryset
, name
, value
. All fields are mandatory.
- self: refers to that filter class itself.
- queryset : this argument has the queryset object which is currently being used for filtering. We just need to modify this queryset and return.
- name : this has the name of the filter field in this case
author_books
. - value`: this has filtered value for that particular field.
In app(filter_and_pagination)/filter.py file.
import django_filters from filter_and_pagination.models import Book,Author from django import forms class BookFilterExp3(django_filters.FilterSet): book_name = django_filters.CharFilter(lookup_expr='iexact') author_name = django_filters.CharFilter(label="Author Name",field_name="author__author_name", lookup_expr='icontains') author_books = django_filters.NumberFilter(label="Number of Books Published By Author",method="author_books_count") class Meta: model = Book fields = ['status'] def author_books_count(self, queryset, name, value): authors_qs = Author.objects.all() authors_id = [] for author in authors_qs: count = Book.objects.filter(author=author).count() if count >= value: authors_id.append(author.id) qs = Book.objects.filter(author__in=authors_id) return qs
Now, let’s make a view and add it to the urls.py file.
def filter_example_3(request): books = BookFilterExp3(request.GET) ctx={ "books" : books } path='filter_and_pagination/filter_listing.html' return render(request,path,ctx)
urlpatterns = [ ... path('filter_example_3', views.filter_example_3, name='filter_example_3'), ]
Output

Filtering List
When a user has inputted value to author_books than Django filter will trigger author_books_count
method.
We’ll retrieve all authors and loop them. If the count
which is a number of books written by the author including Published
and On Hold
are considered and if it greater then equal to the inputted value we’ll append it to list and that list is passed to qs = Book.objects.filter(author__in=authors_id)
queryset.
Filter queryset by Choices and Order By
Consider a use case where we need to provide Choices and let use Order them by book_name
.The new fields are author_sort_books_by
and books_by_order
they both are to be displayed in the select tag.
So we specified ChoiceFilter
and passed label
, choices
and method
as arguments.
Let us see through below following code.
class BookFilterExp4(django_filters.FilterSet): BOOKS_SORT_CHOICES = ( ('LESS_THAN', 'Less Than'), ('GREATER_THAN', 'Greater Than'), ) BOOKS_ORDER_BY_CHOICES = ( ('ASC', 'Ascending Order'), ('DESC', 'Descending Order'), ) book_name = django_filters.CharFilter(lookup_expr='iexact') author_name = django_filters.CharFilter(label="Author Name",field_name="author__author_name", lookup_expr='icontains') author_books = django_filters.NumberFilter(label="Number of Books Published By Author",method="author_books_count") author_sort_books_by = django_filters.ChoiceFilter(label="Sort Number of Books",choices=BOOKS_SORT_CHOICES,method="author_sort_books") books_by_order = django_filters.ChoiceFilter(label="Order By",choices=BOOKS_ORDER_BY_CHOICES,method="books_by_order_method") class Meta: model = Book fields = ['status'] def __init__(self,*args,**kwargs): super().__init__(*args,**kwargs) self.request_data = args[0].dict() def author_books_count(self, queryset, name, value): sort_by = self.request_data.get('author_sort_books_by',None) authors_qs = Author.objects.all() authors_id = [] for author in authors_qs: count = Book.objects.filter(author=author).count() if sort_by==self.BOOKS_SORT_CHOICES[1][0]: #greater than if count >= value: authors_id.append(author.id) else: if count <= value: authors_id.append(author.id) qs = Book.objects.filter(author__in=authors_id) return qs def author_sort_books(self, queryset, name, value): return queryset def books_by_order_method(self, queryset, name, value): order_by = "-book_name" if value == self.BOOKS_ORDER_BY_CHOICES[0][0]: order_by = "book_name" return queryset.order_by(order_by)
now make respective views and routes.
from django.http import QueryDict def filter_example_4(request): initial_data= { 'books_by_order' : 'ASC', 'author_books' : 1, 'author_sort_books_by' : 'GREATER_THAN', } query_dict = QueryDict('', mutable=True) query_dict.update(initial_data) if len(request.GET) != 0: query_dict = request.GET books = BookFilterExp4(query_dict) ctx={ "books" : books } path='filter_and_pagination/filter_listing.html' return render(request,path,ctx)
app_name = 'filter_and_pagination' urlpatterns = [ path('filter_example_4', views.filter_example_4, name='filter_example_4'), ]
We can also pass initial_data
to filter queryset before the user can provide. For that, we have to use QueryDict
by importing from django.http import QueryDict
in views.py
Output

List with Form Elements
Implementation of Widgets and Filter Form Attributes
Here we’ll be adding attributes to form elements such as placeholder, class, etc.
class BookFilterExp5(django_filters.FilterSet): book_name = django_filters.CharFilter(lookup_expr='iexact') author_name = django_filters.CharFilter( label="Author Name", field_name="author__author_name", lookup_expr='icontains', widget=forms.TextInput(attrs={'class':'form-control', 'placeholder' : "Search by Author..."})) class Meta: model = Book fields = ['status'] def __init__(self,*args,**kwargs): super().__init__(*args,**kwargs) self.form.fields['book_name'].widget.attrs = {'placeholder':'Type Book to search...'} #dynamically adding attributes to field
make views and add to routes.
def filter_example_5(request): path='filter_and_pagination/filter_listing.html' books = BookFilterExp5(request.GET) ctx={ "books" : books } return render(request,path,ctx)
app_name = 'filter_and_pagination' urlpatterns = [ ... path('filter_example_5', views.filter_example_5, name='filter_example_5'), ]
In __init__
we first call the parent class and then we can also dynamically modify the form and other attributes. We can change placeholder
of book_name
filter in fields but for demonstration sake, we are changing that in __init__
method.
Output

updated Filter
Adding Pagination to Queryset
It is very easy and simple to paginate queryset. We just have to import from django.core.paginator import Paginator
in filters.py
file and overrides qs
property.
The qs
is a filter property which is a queryset instance currently being processed so we have to pass qs
to Paginator(qs,num_rows)
a method with the number of rows in this case it is dynamic.
Take a look at this code.
@property def qs(self): qs = super().qs num_rows = self.request_data.get('num_rows',5) paginator = Paginator(qs,num_rows) page = self.request_data.get('page') paginated_qs = paginator.get_page(page) return paginated_qs
There is nothing special self.request_data
is the request object which has query_strings
. The page
is also a parameter query string.
It will be like www.example.com/books?page=2.
Take a look at the complete code.
class BookFilterExp6(django_filters.FilterSet): BOOKS_SORT_CHOICES = ( ('LESS_THAN', 'Less Than'), ('GREATER_THAN', 'Greater Than'), ) BOOKS_ORDER_BY_CHOICES = ( ('ASC', 'Ascending Order'), ('DESC', 'Descending Order'), ) book_name = django_filters.CharFilter(lookup_expr='iexact') author_name = django_filters.CharFilter( label="Author Name", field_name="author__author_name", lookup_expr='icontains', widget=forms.TextInput(attrs={'class':'form-control', 'placeholder' : "Search by Author..."})) author_books = django_filters.NumberFilter(label="Number of Books Published By Author",method="author_books_count") author_sort_books_by = django_filters.ChoiceFilter(label="Sort Number of Books",choices=BOOKS_SORT_CHOICES,method="author_sort_books") books_by_order = django_filters.ChoiceFilter(label="Order By",choices=BOOKS_ORDER_BY_CHOICES,method="books_by_order_method") num_rows = django_filters.NumberFilter(label="Number of Rows to show",method="num_rows_method") class Meta: model = Book fields = ['status'] def __init__(self,*args,**kwargs): super().__init__(*args,**kwargs) self.form.fields['book_name'].widget.attrs = {'placeholder':'Type Book to search...'} self.request_data = args[0].dict() def author_books_count(self, queryset, name, value): sort_by = self.request_data.get('author_sort_books_by',None) authors_qs = Author.objects.all() authors_id = [] for author in authors_qs: count = Book.objects.filter(author=author).count() if sort_by==self.BOOKS_SORT_CHOICES[1][0]: #greater than if count >= value: authors_id.append(author.id) else: if count <= value: authors_id.append(author.id) qs = Book.objects.filter(author__in=authors_id) return qs def author_sort_books(self, queryset, name, value): return queryset def books_by_order_method(self, queryset, name, value): order_by = "-book_name" if value == self.BOOKS_ORDER_BY_CHOICES[0][0]: order_by = "book_name" return queryset.order_by(order_by) @property def qs(self): qs = super().qs num_rows = self.request_data.get('num_rows',5) paginator = Paginator(qs,num_rows) page = self.request_data.get('page') paginated_qs = paginator.get_page(page) return paginated_qs def num_rows_method(self, queryset, name, value): return queryset
In views and urls.py
from urllib.parse import urlencode def filter_example_6(request): initial_data= { 'books_by_order' : 'ASC', 'author_books' : 1, 'num_rows' : 10, 'author_sort_books_by' : 'GREATER_THAN', } query_dict = QueryDict('', mutable=True) query_dict.update(initial_data) if len(request.GET) != 0: query_dict = request.GET books = BookFilterExp6(query_dict) ctx={ "books" : books, "filter_url" : urlencode(request.GET.dict()), } path='filter_and_pagination/filter_listing.html' return render(request,path,ctx)
The "filter_url" : urlencode(request.GET.dict()),
is used in a template were we pass it to page number links.
app_name = 'filter_and_pagination' urlpatterns = [ ... path('filter_example_6', views.filter_example_6, name='filter_example_6'), ]
In templates/filter_and_pagination/filter_listing.html.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Book Listing</title> <!-- Latest compiled and minified CSS --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.0/css/bootstrap.min.css"> </head> <body> <div> <div style="border: 1px dashed #333;" > <h5 style="text-align: center;" >Book Filtering</h5> </div> <div> <div class="" > <ul class="pagination"> {% if books.qs.has_previous %} <li><a href="{% url 'filter_and_pagination:filter_example_6' %}?page={{books.qs.previous_page_number}}"><span class="glyphicon glyphicon-chevron-left"></span></a></li> {% endif %} {% for num in books.qs.paginator.page_range %} {% if books.qs.number == num %} <li class="active"><a href="{% url 'filter_and_pagination:filter_example_6' %}?page={{num}}">{{num}}</a></li> {% elif num > books.qs.number|add:'-3' and num < books.qs.number|add:'3' %} <li><a href="{% url 'filter_and_pagination:filter_example_6' %}?{{filter_url}}&page={{num}}">{{num}}</a></li> {% endif %} {% endfor %} {% if books.qs.has_next %} <li><a href="{% url 'filter_and_pagination:filter_example_6' %}?page={{books.qs.next_page_number}}"><span class="glyphicon glyphicon-chevron-right"></span></a></li> {% endif %} </ul> </div> </div> <div style="" > <div> <h5>Filter Form</h5> <div> <form action="" method="get"> {{books.form}} <input type="submit" value="Filter"> </form> </div> </div> <table border="1" style="border-collapse: collapse"> <thead> <tr> <th>Sl no</th> <th>Book Name</th> <th>Author</th> <th>Book Status</th> <th>Total Books Count</th> </tr> </thead> <tbody> {% for book in books.qs %} <tr> <td>{{forloop.counter}}</td> <td>{{book.book_name}}</td> <td>({{book.author.id}}) <strong>{{book.author.author_name}}</strong></td> <td>{{book.get_status_verbose_name}}</td> <td>{{book.author.get_books_count}}</td> </tr> {% endfor %} </tbody> </table> </div> </div> </body> </html>
Output

Filter with multiple fields
Conclusion
This is the end of Part 2 of Django-Filters in the next part we’ll learn about filter using the MultipleChoice Field and formating our template to look nice and professional.




