見出し画像

Djangoで作る初めてのウェブアプリケーション(番外編)検索とページネーション

こ本記事は、マガジン(『超入門』Djangoで作る初めてのウェブアプリケーション)で作成した、Djangoアプリを使用します。

- 検索関連の記事 -
Djangoで作る初めてのウェブアプリケーション(番外編)検索機能
Djangoで作る初めてのウェブアプリケーション(番外編)お問い合わせ機能

本日はデータベース内の検索(絞り込み)にページネーションを加えていきます。

完成図

画面収録 2021-01-16 22.03.08.mov

Djangoでページ ネーションを実現する方法は関数ベースやクラスベースで行う方法など複数あります。公式の方に記載されています。
Django pagenation

検索用とページネーション用のHTMLを作ろう

まずはtemplates直下に検索用のフォームsearch.htmlを作ります。

/templates/search.html

<form class="form-inline my-2 my-lg-0" action="{% url 'blog_app:index' %}" method="GET">
 <input class="form-control mr-sm-2" type="text" name="q" value="{{ request.GET.q }}" placeholder="検索" aria-label="Search">
 <button class="btn btn-outline-success my-2 my-sm-0" type="submit"><i class="fas fa-search saerch"></i></button>
</form>

formクラスのaction属性にはblog_appのindex関数に検索内容を送信するようにします。
methodはデータベースを更新したり削除したりしないのでGETメソッドにします。

inputタグのname属性のqはqueryを表していてこの検索フォームで入力されたqをviews.pyで絞り込んでいきます。value属性は{{ request.GET.q }}とする事によって検索バーに検索内容(検索した値)が残るようにしています。

最後のbuttonタグの虫眼鏡アイコンはFont Awesomeを使用しています。
searchクラスを追加してCSSで少しだけサイズを変えています。

スクリーンショット 2021-01-17 7.28.15

これで検索フォームが完成しました。

header.html内にincludeしましょう。

/templates/header.html

<nav class="navbar fixed-top navbar-expand-lg navbar-light bg-light">
 <button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarTogglerDemo02" aria-controls="navbarTogglerDemo02" aria-expanded="false" aria-label="Toggle navigation">
   <span class="navbar-toggler-icon"></span>
 </button>
 <a class="navbar-brand" href="{% url 'blog_app:index' %}">ブログサイト</a>

 <div class="collapse navbar-collapse" id="navbarTogglerDemo02">
   <ul class="navbar-nav mr-auto mt-2 mt-md-0">
     {% if request.user.is_authenticated %}
     <li class="nav-item">
       <a class="nav-link" href="{% url 'accounts:detail' request.user.pk %}">{{request.user.username}}</a>
     </li>
     <li class="nav-item">
       <a class="nav-link" href="{% url 'blog_app:add' %}">投稿</a>
     </li>
     <li class="nav-item">
       <a class="nav-link" href="{% url 'account_logout' %}">ログアウト</a>
     </li>
     {% else %}
     <li class="nav-item">
       <a class="nav-link" href="{% url 'account_signup' %}">新規登録</a>
     </li>

     <li class="nav-item">
       <a class="nav-link" href="{% url 'account_login' %}">ログイン</a>
     </li>
     {% endif %}
     <li class="nav-item">
       <a class="nav-link js-scroll-trigger" href="{% url 'blog_app:contact' %}">お問い合わせ</a>
     </li>
   </ul>
   <!-- 追加 -->
  {% include 'search.html' %}
  <!--  -->
 </div>
</nav>

続いてページ ネーション用のHTMLもtemplates直下に作ります。
ページ ネーションはBootstrapで見た目を整えています。

/templates/page.html


{% if posts.has_other_pages %}
<nav class="d-flex justify-content-center">
 <ul class="pagination">
   {% if posts.has_previous %}
   <li class="page-item">
     <a class="page-link" href="?page={{ posts.previous_page_number }}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}" tabindex="-1">Previous</a>
   </li>
   {% else %}
   <li class="page-item disabled"><a class="page-link" href="#!">Previous</a></li>
   {% endif %}
   {% for num in posts.paginator.page_range %}
   {% if posts.number == num %}
   <li class="page-item active">
     <a class="page-link" href="#!">{{ num }}<span class="sr-only">(current)</span></a>
   </li>
   {% else %}

   <li class="page-item"><a class="page-link" href="?page={{ num }}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}">{{ num }}</a></li>
   {% endif %}
   {% endfor %}
   {% if posts.has_next %}
   <li class="page-item">
     <a class="page-link" href="?page={{ posts.next_page_number }}{% if request.GET.q %}&q={{ request.GET.q }}{% endif %}">Next</a>
   </li>
   {% else %}
   <li class="page-item disabled"><a class="page-link" href="#!">Next</a></li>
   {% endif %}
 </ul>
</nav>
{% endif %}

page.htmlに見慣れないif文などが存在します。
{% if posts.has_other_pages %}
{% if posts.has_previous %}
{{ posts.previous_page_number }}

その他、、、
これらはviews.pyのindex関数内で後に使用する、Paginator, EmptyPage, PageNotAnIntegerによって生成されたものです。

page.htmlをindex.html内にincludeします。

/templates/blog_app/index.html

<!-- 省略 -->

<div class="d-flex justify-content-center flex-wrap">
 {% for post in posts%}
 <a href="{% url 'blog_app:detail' post_id=post.id %}">
  <div class="card m-4" style="min-width: 300; max-height: 490;">
    {% if post.image %}
    <img style="max-width: 300; max-height: 300;" class="card-img-top" src="{{ post.image.url }}">
    {% else %}
    <div class="p-1 border border-secondary text-center pt-5" style="min-width: 300; min-height: 225;">No Image</div>
    {% endif %}
    <div class="card-body">
      <h4 class="card-title">{{post.title|truncatechars:10 | linebreaksbr}}</h4>
      <p class="card-text">
        {{ post.text|truncatechars:17 }}
      </p>
    </a>
      <p></p>
      <p>#{{post.tag}}</p>
      <p>{{post.created_at}} |
      <a href="{% url 'accounts:detail' post.user.pk %}">{{post.user.username}}</a></p>
  </div>
 </div>
 {% endfor %}
 </div>
 <!-- 追加 -->
 {% include 'page.html' %}
 <!--  -->
{% endblock %}


index関数の編集

検索バーで入力された値を絞り込み、それにページネーションを追加していいましょう。

検索バーのformでの送信先は、action="{% url 'blog_app:index' %}"なのでindex関数を編集します。

/blog_app/views.py

from django.db.models import Q
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger


def index(request):
  posts = Post.objects.all().order_by('-created_at')
  query = request.GET.get('q')
  if query:
       posts = Post.objects.filter(
           Q(title__icontains=query) |
           Q(text__icontains=query) |
           Q(user__username__icontains=query)
       ).distinct()

  paginator = Paginator(posts, 2)
  page = request.GET.get('page')

  try:
       posts = paginator.page(page)
  except PageNotAnInteger:
       posts = paginator.page(1)
  except EmptyPage:
       posts = paginator.page(paginator.num_pages)

  return render(request, 'blog_app/index.html', {'posts': posts})

index関数内の重要な部分の説明です。
検索ワードを絞り込むのに必要なfrom django.db.models import Qを使用し、ページ ネーションに必要なfrom django.core.paginator import Paginator, EmptyPage, PageNotAnIntegerを使用します。

まずはquery = request.GET.get('q')のGETメソッドで検索バーの値を取得し、それをquery関数に代入します。

if文を使用して、もしquery変数に値があれば(Trueならば)
if query:

filter関数を使い絞り込んでいきます。
posts = Post.objects.filter


ここでQ関数の引数にtitle__icontains=queryを渡す事で、query変数の値のtitle、text、username|(パイプ)を使用する事で複数条件で絞り込みます。
title、text、usernameはmodels.pyで作ったデーターベース上のカラムを検索しています。
Q(title__icontains=query) |
Q(text__icontains=query) |
Q(user__username__icontains=query)
)

distinct関数は重複を防ぎます
distinct()

続いてページ ネーション関連です。

Paginator関数の引数に絞り込まれたpostsもしくはデータ検索を行わずに単にデータを取得した場合のpostsを渡し、第二引数に何個データがあればページ ネーションを表示させるかの数字を渡します。
今回はブログ記事が少ないので分かり易いように2にしています。
paginator = Paginator(posts, 2)
page = request.GET.get('page')


そしてtry文とexceptを使用して、pageが存在するときしないときの処理をします。
try:
posts = paginator.page(page)
except PageNotAnInteger:
posts = paginator.page(1)
except EmptyPage:
posts = paginator.page(paginator.num_pages)

最後にrender関数でretuenします。
return render(request, 'blog_app/index.html', {'posts': posts})

完成

以上で以下のように絞り込みが可能になり、ページネーションも使用できるようになりました。

画面収録 2021-01-16 22.03.08.mov

この記事が気に入ったらサポートをしてみませんか?