Hướng dẫn tạo tìm kiếm dùng django-filter

Like bài viết:2 likes

Việc tạo chức năng bộ lọc tìm kiếm là vấn đề rất hay gặp trong các ứng dụng. Trong hướng dẫn này mình sẽ trình bày cách sử dụng django-filter là một package rất phù hợp cho việc thực hiện này. Trong ví dụ này, mình sẽ tạo chức năng tìm kiếm các quyển sách dựa theo tên, giá, loại sách...

 

Ví dụ

 Khởi tạo dự án 

Đầu tiên khởi tạo project:

django-admin startproject myproject .

Lưu ý có dấu chấm ở cuối câu lệnh.

Tạo một app tên là books:

python manage.py startapp books

Thêm app vào INSTALLED_APPS:

myproject/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'books',
]

Model

Tạo Model của app books như sau:

books/models.py

from django.db import models


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

    def __str__(self):
        return self.name


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

    def __str__(self):
        return self.name


class Book(models.Model):
    owner = models.ForeignKey(Author, on_delete=models.CASCADE)
    title = models.CharField(max_length=100)
    description = models.TextField()
    price = models.IntegerField()
    category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True)
    created = models.DateField(auto_now_add=True)

    def __str__(self):
        return self.title

    class Meta:
        default_related_name = 'books'

Chạy migrations đồng bộ database:

python manage.py makemigrations books
python manage.py migrate

URL

books/urls.py

from django.urls import path

from . import views


urlpatterns = [
    path('books/', views.books_list, name='books_list'),
]

Thêm vào URL project:

myproject/urls.py

from django.contrib import admin
from django.urls import path, include


urlpatterns = [
    path('admin/', admin.site.urls),
    path('', include('books.urls'))
]

View

Mình chỉ mới trả về các books, chút nữa mình sẽ sửa lại để thêm django-filter vào.

books/views.py

from django.shortcuts import render

from .models import Book


def books_list(request):
    books = Book.objects.all()
    
    return render(request, 'books/list.html', {'books': books})

Template

Mình sẽ xác định thư mục templates sẽ sử dụng bằng cách sửa file settings.py như sau:

myproject/settings.py

TEMPLATES = [
    {
        ....
        'DIRS': [
            os.path.join(BASE_DIR, 'templates')
        ],
        ....
    },
]

Tạo một thư mục templates để chứa các file html, sau đó tạo file base.html như sau:

templates/base.html

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>{% block title %}{% endblock title %}</title>
    <!-- Bootstrap core CSS -->
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css"\
      integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
</head>
<body>
    <header>
        <div class="d-flex flex-column flex-md-row align-items-center p-3 px-md-4 mb-3 bg-white border-bottom box-shadow">
            <a href="" class="navbar-brand my-0 mr-md-auto font-weight-normal">djangobat</a>
            <nav class="my-2 my-md-0 mr-md-3">
                <a class="p-2 text-dark" href="{% url 'books_list' %}">Books</a>
            </nav>
            <a class="btn btn-outline-success" href="https://github.com/djangobat/django-filter">Github</a>
        </div>
    </header>
    <div class="container">
        {% block main %}
        {% endblock main %}
    </div>
</body>
</html>

Ở đây mình dùng CSS là Bootstrap.

  • block title để chèn title mỗi trang
  • block main để chèn nội dung chính các trang

templates/books/list.html

{% extends "base.html" %}
{% block title %}
Books
{% endblock title %}
{% block main %}
<div class="row">
    <div class="col-md-3">
        <h4 class="d-flex justify-content-between align-items-center mb-3">
            <span class="text-muted">Lọc tìm kiếm</span>
        </h4>
        <!-- Hiện thị bộ lọc tìm kiếm -->
    </div>
    <div class="col-md-8">
        <h2>Books</h2>
        <h4 class="d-flex justify-content-between align-items-center mb-3">
            <span class="text-muted">Danh sách books</span>
            <span class="badge badge-secondary badge-pill">{{ books.count }}</span>
        </h4>
        <table class="table">
            <thead>
                <tr>
                    <th>STT</th>
                    <th>Tên</th>
                    <th>Tác giả</th>
                    <th>Giá (VNĐ)</th>
                    <th>Miêu tả</th>
                    <th>Xuất bản</th>
                    <th>Thể loại</th>
                </tr>
            </thead>
            <tbody>
                {% for book in books %}
                <tr>
                    <td>{{ forloop.counter }}</td>
                    <td>{{ book.title }}</td>
                    <td>{{ book.owner.name }}</td>
                    <td>{{ book.price }}</td>
                    <td>{{ book.description|truncatewords:15 }}</td>
                    <td>{{ book.created|date:"Y" }}</td>
                    <td>{{ book.category }}</td>
                </tr>
                {% endfor %}
            </tbody>
        </table>
    </div>
</div>
{% endblock main %}

Chúng ta sẽ thêm bộ lọc tìm kiếm sau, hiện tại chỉ hiện thị ra thông tin book. Mở terminal lên để chạy server:

python manage.py runserver

Mở 127.0.0.1:8000/books/ ta có kết quả như sau:

Thêm dữ liệu

Bây giờ chúng ta sẽ tạo thêm các dữ liệu để sử dụng. Có 2 cách để tạo:

Cách 1: Sử dụng Admin

Thêm các model đã tạo vào admin:

books/admin.py

from django.contrib import admin

from .models import Author, Category, Book


admin.site.register(Author)
admin.site.register(Category)
admin.site.register(Book)

Tạo tài khoản superuser để truy cập admin:

$ python manage.py createsuperuser --username test_user --email test_user@gmail.com
$ password: ******

Nhập password của bạn vào. Sau đó dùng tài khoản này để truy cập 127.0.0.1:8000/admin 

Tại đây bạn có thể tạo các dữ liệu của: Author, Book, Category

Cách 2: Sử dụng manage.py

Mình đã tạo sẵn được dữ liệu, để trong fixtures/books.json. Để load dữ liệu này vào trong project thì sử dụng câu lệnh sau:

python manage.py loaddata fixtures/books.json

Bây giờ cập nhật lại trang 127.0.0.1:8000/books/ ta có kết quả như sau:

Cài đặt django-filter

 Đầu tiên thực hiện cài đặt django-filter bằng pip:

pip install django-filter

Tiếp theo thêm vào INSTALLED_APPS:

myproject/settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',

    'django_filters',

    'books',
]

Như vậy giờ đã có thể sử dụng django-filter trong dự án.

Sử dụng django-filter

Bắt đầu với ví dụ đơn giản là tìm kiếm tên sách. Đầu tiên tạo một file tên filters.py như sau:

books/filters.py

from django import forms

import django_filters
from .models import Book, Category

class BookFilter(django_filters.FilterSet):
    class Meta:
        model = Book
        fields = ('title',)

Về cơ bản nó khá giống với tạo form trong django. 

  • model: Xác định Model bạn muốn dùng
  • fields: Xác định các trường để tạo tìm kiếm

Tiếp theo, sửa lại views như sau: 

books/views.py

from django.shortcuts import render

from .models import Book
from .filters import BookFilter


def books_list(request):
    query = Book.objects.all()
    book_filter = BookFilter(request.GET, queryset=query)
    
    context = {
        'form': book_filter.form,
        'books': book_filter.qs,
    }

    return render(request, 'books/list.html', context)
  • queryset: Sẽ xác định các books mà sẽ được tìm kiếm
  • request.GET: Sẽ lấy giá trị của form mà người dùng nhập để lọc tìm kiếm
  • book_filter.qs: Sẽ trả về các books được tìm thấy 
  • book_filter.form: Sẽ trả về form để sử dụng trong html mà chúng ta sẽ sử dụng

Template

Thêm dòng sau ở file list.html đã tạo trước đó như thế này:

templates/books/list.html

<!-- Hiện thị bộ lọc tìm kiếm -->
{% load widget_tweaks %}
<form method="get" class="border p-3">
    <div class="mb-3">
        <label for="">Tên sách</label>
        {% render_field form.title class="form-control" %}
    </div>
    <hr class="mb-4">
    <button class="btn btn-primary btn-lg btn-block" type="submit">Tìm kiếm</button>
</form>

Ở đây mình có sử dụng django-widget-tweaks, để tùy chỉnh form khi render ra html, ở đây mục đích là thêm các class của bootstrap. Các bạn xem thêm tại django-widget-tweaks

Mở 127.0.0.1:8000/books/ ta có kết quả như sau:

Thử gõ tên là "Learning Python":

Nếu bạn thử tìm kiếm với tên là "Python" sẽ không cho ra kết quả:

??? Vì theo mặc định django-filter sẽ tìm kiếm các book có title giống hệt như từ bạn gõ. Vì không có sách nào tên là "Python" cả nên sẽ không cho ra kết quả. Chúng ta sẽ tùy chỉnh bằng các biến sau:

 lookup_expr

Sửa lại file filters.py như sau:

books/filters.py

from django import forms

import django_filters
from .models import Book, Category


class BookFilter(django_filters.FilterSet):
    title = django_filters.CharFilter(lookup_expr='icontains')

    class Meta:
        model = Book
        fields = ('title',)

Theo mặc định nếu không đặt tham số gì thì djang-filter sẽ thấy giá trịlookup_expr="exact" tức là lấy chính xác bằng giá trị đó.

Giá trị icontains xác định tên sách chỉ cần chứa giá trị mà người dùng nhập.

Bạn có thể tham khảo giá trị lookup_expr tại: field-lookups

Bây giờ thử tìm kiếm lại "Python" ta có kết quả :

Tùy chỉnh khác

Bây giờ thêm một số trường tìm kiếm khác:

View

books/filters.py

from django import forms

import django_filters
from .models import Book, Category


class BookFilter(django_filters.FilterSet):
    title = django_filters.CharFilter(lookup_expr='icontains')
    price = django_filters.NumberFilter(lookup_expr='lte')
    category = django_filters.ModelMultipleChoiceFilter(queryset=Category.objects.all(),
                                                    widget=forms.CheckboxSelectMultiple)
    created = django_filters.NumberFilter(lookup_expr='year')

    class Meta:
        model = Book
        fields = ('owner', 'title', 'price', 'category', 'created',)

Ở đây mình có thêm nhiều fields của django-filter. Bạn có thể đọc thêm tại filters

Template

templates/books/list.html

<!-- Hiện thị bộ lọc tìm kiếm -->
<form method="get" class="border p-3">
    <div class="mb-3">
        <label for="">Tên sách</label>
        {% render_field form.title class="form-control" %}
    </div>
    <div class="mb-3">
        <label for="">Tác giả</label>
        {% render_field form.owner class="form-control" %}
    </div>
    <div class="row">
        <div class="col-md-6 mb-3">
            <label for="">Giá</label>
            {% render_field form.price class="form-control" placeholder="Giá tối đa" %}
        </div>
        <div class="col-md-6 mb-3">
            <label for="">Năm sản xuất</label>
            {% render_field form.created class="custom-select d-block w-100" %}
        </div>
    </div>
    <hr class="mb-4">
    <h4 class="mb-3">Thể loại</h4>
    {% for item in form.category  %}
    <div class="custom-control custom-checkbox">
        {{ item.tag }}
        <label>{{ item.choice_label }}</label>
    </div> 
    {% endfor %}
    <hr class="mb-4">
    <button class="btn btn-primary btn-lg btn-block" type="submit">Tìm kiếm</button>
</form>

Kết quả :

Kết luận

Còn rất nhiều tùy chỉnh khác bạn có thể tham khảo tại: django-filter

Xem các fields của django-filter: filters

Bạn có thể tham khảo giá trị lookup_expr tại: field-lookups

Các bạn xem thêm  django-widget-tweaks

Tất cả code xem tại Github: 

https://github.com/djangobat/django-filter