DjangoでListViewを用いて検索画面を実装する方法

DjangoでListViewを用いて検索画面を実装する方法をご紹介します。

条件

  • Django 2.1.7
  • Python 3.7.0

検索画面の実装

構成

ソースの構成は以下の通りです。

以下、ポイントとなるソースをピックアップします。

ソースの詳細はGitHubをご参照ください。
https://github.com/kzmrt/list

models.py

以下のようなモデルを想定します。

from django.db import models
from django.urls import reverse
from django.contrib.auth.models import AbstractUser


class CustomUser(AbstractUser):

    def __str__(self):
        return self.username + ":" + self.email


class Post(models.Model):
    """投稿モデル"""
    class Meta:
        db_table = 'post'

    title = models.CharField(verbose_name='タイトル', max_length=255)
    text = models.CharField(verbose_name='内容', max_length=255, default='', blank=True)
    author = models.ForeignKey(
        'search.CustomUser',
        on_delete=models.CASCADE,
    )
    created_at = models.DateTimeField(verbose_name='登録日時', auto_now_add=True)
    updated_at = models.DateTimeField(verbose_name='更新日時', auto_now=True)

    def __str__(self):
        return self.title + ',' + self.text

    @staticmethod
    def get_absolute_url(self):
        return reverse('search:index')

urls.py

今回はトップ画面(検索画面)のみ作成します。

from django.urls import path
from . import views

app_name = 'search'

urlpatterns = [
    # トップ画面
    path('', views.IndexView.as_view(), name='index'),
]

forms.py

カスタムユーザモデルに加えて、検索用のフォームも定義します。

from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from .models import CustomUser
from django import forms


class CustomUserCreationForm(UserCreationForm):
    class Meta(UserCreationForm):
        model = CustomUser
        fields = ('username', 'email')


class CustomUserChangeForm(UserChangeForm):
    class Meta:
        model = CustomUser
        fields = ('username', 'email')


class SearchForm(forms.Form):

    title = forms.CharField(
        initial='',
        label='タイトル',
        required = False, # 必須ではない
    )
    text = forms.CharField(
        initial='',
        label='内容',
        required=False,  # 必須ではない
    )

views.py

post受け取り時の処理、フォームの配置、クエリ発行について処理を記述します。

処理のポイントは以下の通りです。

  • def post()でセッションに検索フォームの値を渡す。
  • def get_context_data()でセッションから検索フォームの値を取得して、検索フォームの初期値としてセットする。
  • def get_queryset()でセッションから取得した検索フォームの値に応じてクエリ発行を行う。
  • def post()で検索時にページネーションに関連したエラーを防ぐ処理を記述。
import logging
from django.contrib.auth.mixins import LoginRequiredMixin
from django.views import generic
from .models import Post
from .forms import SearchForm
from django.db.models import Q

logger = logging.getLogger('development')


class IndexView(LoginRequiredMixin, generic.ListView):

    paginate_by = 5
    template_name = 'search/index.html'
    model = Post

    def post(self, request, *args, **kwargs):

        form_value = [
            self.request.POST.get('title', None),
            self.request.POST.get('text', None),
        ]
        request.session['form_value'] = form_value

        # 検索時にページネーションに関連したエラーを防ぐ
        self.request.GET = self.request.GET.copy()
        self.request.GET.clear()

        return self.get(request, *args, **kwargs)

    def get_context_data(self, **kwargs):
        context = super().get_context_data(**kwargs)

        # sessionに値がある場合、その値をセットする。(ページングしてもform値が変わらないように)
        title = ''
        text = ''
        if 'form_value' in self.request.session:
            form_value = self.request.session['form_value']
            title = form_value[0]
            text = form_value[1]

        default_data = {'title': title,  # タイトル
                        'text': text,  # 内容
                        }

        test_form = SearchForm(initial=default_data) # 検索フォーム
        context['test_form'] = test_form

        return context

    def get_queryset(self):

        # sessionに値がある場合、その値でクエリ発行する。
        if 'form_value' in self.request.session:
            form_value = self.request.session['form_value']
            title = form_value[0]
            text = form_value[1]

            # 検索条件
            condition_title = Q()
            condition_text = Q()

            if len(title) != 0 and title[0]:
                condition_title = Q(title__icontains=title)
            if len(text) != 0 and text[0]:
                condition_text = Q(text__contains=text)

            return Post.objects.select_related().filter(condition_title & condition_text)
        else:
            # 何も返さない
            return Post.objects.none()

base.html

bootstrap4を用いてデザインの設定を行います。
(別途、style.cssを追加してデザイン調整を行っています。)

{% load staticfiles %}
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">

        <!-- bootstrap4 -->
        {% load bootstrap4 %}
        {% bootstrap_css %}
        {% bootstrap_javascript jquery='full' %}
        <!-- bootstrap4 -->

        <link href="https://fonts.googleapis.com/css?family=Open+Sans" rel="stylesheet">
        <link href="{% static 'css/style.css' %}" rel="stylesheet">

        <script type="text/javascript">
            $(function() {
                var topBtn = $('#page-top');
                topBtn.hide();
                //スクロールが500に達したらボタン表示
                $(window).scroll(function () {
                    if ($(this).scrollTop() > 500) {
                        topBtn.fadeIn();
                    } else {
                        topBtn.fadeOut();
                    }
                });
                //スクロールしてトップ
                topBtn.click(function () {
                    $('body,html').animate({
                        scrollTop: 0
                    }, 500);
                    return false;
                });
            });
        </script>

        <title>{% block title %}Search{% endblock %}</title>
    </head>
    <body>

        <!-- ナビゲーションバーの設定 -->
        <nav class="navbar navbar-expand-md navbar-dark bg-dark sticky-top">
            <div class="container">
                <a class="navbar-brand" href="{% url 'search:index' %}">Search</a>
                <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false" aria-label="Toggle navigation">
                  <span class="navbar-toggler-icon"></span>
                </button>
                <div class="collapse navbar-collapse" id="navbarResponsive">
                    <ul class="navbar-nav ml-auto">

                    {% if user.is_authenticated %}
                        <li class="nav-item">
                            <a class="nav-link" href="{% url 'search:index' %}">TOP</a>
                        </li>
                        <li class="nav-item">
                            <a class="nav-link" href="{% url 'logout' %}">ログアウト</a>
                        </li>
                    {% else %}
                        {% url 'login' as login %}
                        {% ifnotequal request.path login %}
                            <li class="nav-item">
                                <a class="nav-link" href="{% url 'login' %}" class="login">ログイン</a>
                            </li>
                        {% endifnotequal %}
                    {% endif %}
                    </ul>
                </div>
            </div>
        </nav>
         <main>
            {% block content %}
            {% endblock %}
        </main>
        <p id="page-top"><a href="#">PAGE TOP</a></p>
            <footer class="py-4 bg-dark">
                <div class="container text-center">
                    <p class="text-light"><small>Copyright &copy;2019 Search, All Rights Reserved.</small></p>
                </div>
            </footer>
    </body>
</html>

index.html

検索フォームおよび、検索結果の表示について記述します。

{% extends 'base.html' %}

{% block content %}
<div class="col-lg-6 offset-lg-3">

     <h1>検索条件</h1>
    <form method="POST">
        {% csrf_token %}
        {% for field in test_form %}
            <div class="form-group form-inline">
                <label class="col-md-offset-2 col-md-3 control-label">{{ field.label }}:</label>
                <div class="col-md-8">
                    {{ field }}
                </div>
            </div>
        {% endfor %}
        <input class="btn btn-success offset-md-8 col-md-3" type="submit" id="button" name="button" value="検索">
    </form>

    <h1>検索結果</h1>

    <section class="post-list">
        {% if object_list|length == 0 %}
            <p>検索結果が存在しません。</p>
        {% else %}
            <table class="table table-hover table-bordered">
                <tr>
                    <th>タイトル</th>
                    <th>内容</th>
                </tr>
                <tbody>
                {% for post in object_list %}
                    <tr>
                        <td width="35%">{{ post.title }}</td>
                        <td width="65%">{{ post.text }}</td>
                    </tr>
                {% endfor %}
                </tbody>
            </table>
        {% endif %}

    </section>
    <div class="col-6 offset-3 justify-content-center">
        {% if is_paginated %}
            {% include 'pagination.html' %}
        {% endif %}
    </div>
</div>

{% endblock %}

pagination.html

ページネーションはindex.htmlで読み込みます。

<div class="container">
    <div class="row">
        <div class="col-md-6 offset-md-3 py-2">
            <nav aria-label="ページ送り">
                <div class="text-center">
                    <ul class="pagination justify-content-center">
                        {% if page_obj.has_previous %}
                            <li class="page-item"><a class="page-link" href="?page={{ page_obj.previous_page_number }}">&laquo;</a></li>
                        {% else %}
                            <li class="page-item disabled"><span class="page-link">&laquo;</span></li>
                        {% endif %}
                        {% for i in page_obj.paginator.page_range %}
                            {% if page_obj.number == i %}
                                <li class="page-item active" aria-current="page">
                                    <a class="page-link" href="#">{{ i }} <span class="sr-only">(現位置)</span></a>
                                </li>
                            {% else %}
                                <li class="page-item"><a class="page-link" href="?page={{ i }}">{{ i }}</a></li>
                            {% endif %}
                        {% endfor %}
                        {% if page_obj.has_next %}
                            <li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}">&raquo;</a></li>
                        {% else %}
                            <li class="page-item disabled"><span class="page-link">&raquo;</span></li>
                        {% endif %}
                    </ul>
                </div>
            </nav>
        </div>
    </div>
</div>

実行結果

DBレコード

POSTテーブルには、以下のように12レコード存在するものとします。

検索の例

初期画面

検索フォーム入力なしで検索

タイトルのみ指定

タイトルと内容の指定

サンプルソース

GitHubに当該記事のサンプルソースを公開しています。

https://github.com/kzmrt/list

参考

stackoverflow

https://stackoverflow.com/questions/18664182/is-it-possible-to-have-a-form-in-a-listview-template

DjangoでListViewを用いて検索画面を実装する方法” に対して5件のコメントがあります。

  1. 匿名 より:

    初めまして。
    「DjangoでListViewを用いて検索画面を実装する方法」
    上記記事を読みました。
    大変勉強になりました。
    一つ質問があるのですが、例えば、フォームに何も入力しないで検索ボタンを押すと、挙動としてはID順になるかと思いますが、これを任意の順序に変えるにはどうしたら良いでしょうか?
    例)更新順、作成順等。
    お忙しい中お手数掛けて大変申し訳ないのですが、お返事頂けると嬉しいです。
    可能でしたら記載のメールアドレスに連絡頂けると助かります。

    1. 確認飛行物体 より:

      メールでも回答いたしましたが、情報共有のため、こちらでもコメントを残したいと思います。

      ”検索結果を任意の順にする方法”は以下の通りです。

      ■変更箇所

      ・views.pyにおける、以下のメソッドのreturnにおいて、order_byを付ければ検索結果の順番を変更することが出来ます。

      def get_queryset(self):

          ・・・
          return Post.objects.select_related().filter(condition_title & condition_text)

      ■変更例

      ・IDの昇順
          return Post.objects.select_related().filter(condition_title & condition_text).order_by(‘id’)

      ・IDの降順
          return Post.objects.select_related().filter(condition_title & condition_text).order_by(‘-id’)

      以下、models.pyで定義したカラム名を指定した場合

      ・登録日時の昇順
          return Post.objects.select_related().filter(condition_title & condition_text).order_by(‘created_at’)

      ・登録日時の降順
          return Post.objects.select_related().filter(condition_title & condition_text).order_by(‘-created_at’)

      ・更新日時の昇順
          return Post.objects.select_related().filter(condition_title & condition_text).order_by(‘updated_at’)

      ・更新日時の降順
          return Post.objects.select_related().filter(condition_title & condition_text).order_by(‘-updated_at’)

      ■参考:Djangoドキュメント

      ・order_byの詳細については以下のサイトをご参照ください。

          https://docs.djangoproject.com/ja/3.0/ref/models/querysets/#order-by

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です