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

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
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
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
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
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.

Summary
Review Date
Reviewed Item
Django Filter Package - Part-2
Author Rating
51star1star1star1star1star
Software Name
Django Framework
Software Name
Windows Os, Mac Os, Ubuntu Os
Software Category
Web Development