Filter on the website

I want to create a filter on the website based on book data so that after selecting these categories on the same page, it is sorted. For example, how it is implemented on e-commerce websites (you can select a range of date, genre, publisher etc.). After the selection, the number of books is reduced.
Could you tell me how to do this?

**views.py**

from django.db.models import Q
from django.contrib import messages
from django.contrib.messages.views import SuccessMessageMixin
from django.shortcuts import render
from django.urls import reverse_lazy
from django.views.generic import (CreateView, DeleteView, DetailView, ListView,
                                  TemplateView, UpdateView)


from .forms import AuthorForm, BookForm
from .models import Author, Book, Genre

# Create your views here.

# Home page


class HomeView(ListView):
    model = Book
    template_name = "home.html"


# Book pages


class BooksView(ListView):
    model = Book
    template_name = "books.html"
    context_object_name = "books_list"
    ordering = ["-created_date"]

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["genre"] = Genre.objects.all()
        context["authors"] = Author.objects.all()
        return context


class BookDetailView(DetailView):
    model = Book
    template_name = "book-detail.html"
    context_object_name = "book"

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["genre"] = Genre.objects.filter(id=1)
        context["author"] = Author.objects.all()
        return context


# CRUD pages


class BookCreateView(SuccessMessageMixin, CreateView):
    model = Book
    form_class = BookForm
    template_name = "create_book.html"
    success_message = "%(title)s was created successfully"


class BookUpdateView(UpdateView):
    model = Book
    form_class = BookForm
    template_name = "update_book.html"
    success_message = "%(title)s was updated successfully"


class BookDeleteView(SuccessMessageMixin, DeleteView):
    model = Book
    form_class = BookForm
    template_name = "delete_book.html"
    success_url = reverse_lazy("books")
    success_message = "%(title)s was deleted successfully"

    def delete(self, request, *args, **kwargs):
        obj = self.get_object()
        data_to_return = super(BookDeleteView, self).delete(
            request, *args, **kwargs)
        messages.success(self.request, self.success_message % obj.__dict__)
        return data_to_return


# Author pages


class AuthorDetailView(DetailView):
    model = Author
    template_name = "author-detail.html"
    context_object_name = "author"


class AuthorCreateView(SuccessMessageMixin, CreateView):
    model = Author
    form_class = AuthorForm
    template_name = "author_create.html"
    success_message = "%(last_name)s was created successfully"


class AboutView(TemplateView):
    template_name = "about.html"


# Search page
class SearchResultsListView(ListView):
    model = Book
    context_object_name = "book_list"
    template_name = "search_results.html"

    def get_queryset(self):
        query = self.request.GET.get('q')
        return Book.objects.filter(
            Q(title__icontains=query) | Q(
                author__first_name__icontains=query) | Q(author__last_name__icontains=query)
        )

**models.py**

import uuid

from django.db import models
from django.urls import reverse
from django.utils import timezone


class Genre(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name


class Book(models.Model):
    title = models.CharField(max_length=200)
    image = models.ImageField(
        upload_to="photos", default=None, blank=True, null=True)
    author = models.ForeignKey("Author", on_delete=models.SET_NULL, null=True)
    summary = models.TextField()
    publisher = models.ForeignKey("Publisher", on_delete=models.SET_NULL, null=True)
    isbn = models.CharField(max_length=10, unique=True, blank=True, null=True)
    genre = models.ManyToManyField("Genre")
    created_date = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return self.title

    def get_absolute_url(self):
        return reverse("book_detail", kwargs={"pk": self.pk})

    class Meta:
        ordering = ["-created_date"]
        constraints = [
            models.UniqueConstraint(
                fields=["title"], name="%(app_label)s_unique_booking"
            )
        ]


class Author(models.Model):
    first_name = models.CharField(max_length=50)
    last_name = models.CharField(max_length=50)

    class Meta:
        ordering = ["first_name", "last_name"]
        constraints = [
            models.UniqueConstraint(
                fields=["first_name", "last_name"], name="uniquename"
            )
        ]

    def get_absolute_url(self):
        return reverse("author_detail", kwargs={"pk": self.pk})

    def __str__(self):
        return f"{self.first_name} {self.last_name}"

class Publisher(model.Model):
      name = models.CharField(max_length=200)

**list_books.html**

{% extends 'base.html' %} {% block content %}

<div class="container">
  <div class="row">
    <div class="col-2">
      <ul class="list-unstyled text-primary">
        {% for genre_list in genre %}
        <li>{{genre_list.name}}</li>
        {% endfor %}
      </ul>
    </div>
    <div class="col-10">
      <div class="card-group gap-3">
        {% for books_list in books_list %}
        <div class="d-flex flex-wrap">
          <div class="card p-2" style="width: 10rem; height: 15rem">
            <div class="card-img-top mb-2 text-center">
              {% if books_list.image %}
              <img src="{{ books_list.image.url }}" class="card-img-top mb-2" alt="{{books_list.title }}"
                style="max-width: 127px; max-height: 127px" />
              {% else %}
              <p>No Image</p>
              {% endif %}
            </div>
            <div class="class-body text-center">
              <h5 class="card-tittle fs-6">
                <a href="{% url 'book_detail' books_list.id %}">{{books_list.title}}</a>
              </h5>
              <p class="card-text fs-6">
                <small class="text-muted">
                  <a href="{% url 'author_detail' books_list.author.id %}">{{books_list.author}}</a>
                </small>
              </p>

            </div>

          </div>
        </div>
        {% endfor %}
      </div>

    </div>

  </div>
</div>


{% endblock content %}

Hej!

you can use django_filters to get a filter on your site.

(just an example)

# filters.py

import django_filters
from django_filters import CharFilter

from models import Book

class BookFilter(django_filters.FilterSet):
    title = CharFilter(field_name="title", lookup_expr="icontains")

    class Meta:
        model = Book
        fields = "__all__"

‘use’ it in your view and add it in the template before you generate the list.

views.py

def search_books(request):

    books = Books.objects.all()
    myFilter = BookFilter(request.GET, queryset=books)
    books = myFilter.qs
    
    context = {"books": books, "myFilter": myFilter}
    return render(request, "search_results.html", context)

# template.html

<br>
	<div class="row collapse {% if show_form %} show  {% endif %}" id="form-holder">
		<div class="row">
			<div class="col">
				<div class="card card-body">

					<form method="get">
						{{myFilter.form.as_p}}

						<button class="btn-primary" type="submit">Search</button>
					</form>
				</div>
			</div>
		</div>
	</div>
</br>


Best have a look in the documentation to get an overview :slight_smile:
https://django-filter.readthedocs.io/en/stable/

1 Like

Thanks! This works, but was not expected. Because books are not removed from the list.
I attach a photo

books.html

{% extends 'base.html' %} {% block content %}

<div class="container">
  <div class="row">
    <div class="col-2">
      <ul class="list-unstyled text-primary">

        <form method="get">
          {{filter.form}}
          <button class="btn-primary" type="submit">Search</button>
        </form>

        {% for obj in filter.qs %}
          <li> {{ obj.title }} </li>
        {% endfor %}


      </ul>
    </div>
    <div class="col-10">
      <div class="card-group gap-3">
        {% for books_list in books_list %}
        {% for obj in filter.qs %}
        <div class="d-flex flex-wrap">
          <div class="card p-2" style="width: 10rem; height: 15rem">
            <div class="card-img-top mb-2 text-center">
              {% if books_list.image %}
              <img src="{{ books_list.image.url }}" class="card-img-top mb-2" alt="{{books_list.title }}"
                style="max-width: 127px; max-height: 127px" />
              {% else %}
              <p>No Image</p>
              {% endif %}
            </div>
            <div class="class-body text-center">
              <h5 class="card-tittle fs-6">
                <a href="{% url 'book_detail' books_list.id %}">{{obj.title}}</a>
              </h5>
              <p class="card-text fs-6">
                <small class="text-muted">
                  <a href="{% url 'author_detail' books_list.author.id %}">{{books_list.author}}</a>
                </small>
              </p>
            </div>

          </div>
        </div>
        {% endfor %}
        {% endfor %}
      </div>

    </div>

  </div>
</div>


{% endblock content %}

how did you add the filter to your view?

and do you need {% for obj in filter.qs %}?

I used CBV

views.py

class BooksView(ListView):
    model = Book
    template_name = "books.html"
    context_object_name = "books_list"
    ordering = ["-created_date"]

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["genre"] = Genre.objects.all()
        context["authors"] = Author.objects.all()
        context["filter"] = BookFilter(
            self.request.GET, queryset=self.get_queryset())
        return context

filters.py

import django_filters
from django_filters import CharFilter

from .models import Book


class BookFilter(django_filters.FilterSet):

    class Meta:
        model = Book
        fields = ['genre']

I understood where my mistake was, as you wrote, you do not need to use “for” again. Thank you so much!

I am attaching my solution, maybe they will be useful to someone later.

book-list.html

    <div class="col-10">
      <div class="card-group gap-3">
        {% for obj in filter.qs %}
        <div class="d-flex flex-wrap">
          <div class="card p-2" style="width: 10rem; height: 15rem">
            <div class="card-img-top mb-2 text-center">
              {% if obj.image %}
              <img src="{{ obj.image.url }}" class="card-img-top mb-2" alt="{{books_list.title }}"
                style="max-width: 127px; max-height: 127px" />
              {% else %}
              <p>No Image</p>
              {% endif %}
            </div>
            <div class="class-body text-center">
              <h5 class="card-tittle fs-6">
                <a href="{% url 'book_detail' obj.id %}">{{obj.title}}</a>
              </h5>
              <p class="card-text fs-6">
                <small class="text-muted">
                  <a href="{% url 'author_detail' obj.author.id %}">{{obj.author}}</a>
                </small>
              </p>
            </div>

          </div>
        </div>
        {% endfor %}
      </div>

filters.py

import django_filters
from django_filters import CharFilter

from .models import Book


class BookFilter(django_filters.FilterSet):

    class Meta:
        model = Book
        fields = ['genre']

views.py

class BooksView(ListView):
    model = Book
    template_name = "books.html"
    context_object_name = "books_list"
    ordering = ["-created_date"]

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)
        context["genre"] = Genre.objects.all()
        context["authors"] = Author.objects.all()
        context["filter"] = BookFilter(
            self.request.GET, queryset=self.get_queryset())
        return context