ListViewの使い方をマスターしよう

ListViewを使いこなすクラスベースビューをマスターしよう

↓↓↓はじめて講座を受ける方はこちら↓↓↓
開発環境を作ろう

わいへい
わいへい

やったーーーー!!

Djangoのアプリができたぞ〜〜〜〜!!!!

日記アプリのトップページ
ウサギ
ウサギ

チュートリアルアプリかな?

わいへい
わいへい

どうだ! いいだろ!

この日記アプリさえあれば、いちいちQiitaだのZennだのに書かなくても大丈夫。

わいへい
わいへい

どんどん機能追加していって、

ゆくゆくはQiitaを超えるプラットフォーマーになる!

ウサギ
ウサギ

うん、根拠なき自信って大切だよな。。。

クラスベースビュー使った?

ウサギ
ウサギ

ところでこの画面、クラスベースビューで作ったのか?

わいへい
わいへい

クラスベースビュー? なんだそれ??

ウサギ
ウサギ

クラスベースビューは、少ない記述でCRUDの機能を作れる、汎用クラスだ

ウサギ
ウサギ

今のお前のviews.pyを見てみよう

from django.shortcuts import render

# Create your views here.
from .models import Post

# Create your views here.
def index(request):
  return render(request, 'index.html', {
    'posts': Post.objects.all(),
  })
ウサギ
ウサギ

これは関数ベースビューというやつだ

ウサギ
ウサギ

関数ベースビューは、自由に記述できる代わりに

ウサギ
ウサギ

実装者によって書き方が違ったり、いろんな画面で同じような処理を繰り返し書いたり

ウサギ
ウサギ

どこに何を書いてもいいが故に、冗長になりがちだ

ウサギ
ウサギ

クラスベースビューを使うと、CRUDでよく使う機能を簡単に実装できる

ウサギ
ウサギ

例えばXXの機能であればこう書く、みたいなお作法が決まっているので、

ウサギ
ウサギ

オレオレ実装になりにくい

ウサギ
ウサギ

大人数で開発したときに、実装スタイルが統一されるってわけ

わいへい
わいへい

え〜、めんどくさいな。関数ベースビューにゴリゴリ書いてもよくない??

ウサギ
ウサギ

うむ。確かにめんどくさい場面はあるが

ウサギ
ウサギ

似たような一覧画面

ウサギ
ウサギ

似たような編集画面

ウサギ
ウサギ

似たような新規作成画面

ウサギ
ウサギ

こんなもんをコピペで作りまくるのはナンセンスだし

ウサギ
ウサギ

実装者によっては書き方がしっちゃかめっちゃかになる

ウサギ
ウサギ

そんなとき、画面実装に一定の秩序を与えてくれるのがクラスベースビューだ

ウサギ
ウサギ

簡単ではないが、やってみようぜ

わいへい
わいへい

簡単じゃないんだ。。。

完成形の確認

ウサギ
ウサギ

今回は一覧の実装ということで、ListViewというクラスベースビューを使ってみる

ウサギ
ウサギ

ListViewを使って機能を加え、デザインもいい感じにしたのが下記だ

ListViewを使った日記一覧
わいへい
わいへい

え。。。めっちゃいい感じになってない?

ウサギ
ウサギ

最初がクソだからいい感じに見えるだけだ

わいへい
わいへい

悪口言われた??

ウサギ
ウサギ

下記の機能を実装した

  • カテゴリーと表示件数をFormで絞り込む
  • ログイン済みの場合各日記に編集リンクをつける
  • ページネーション

関数ベースビューでこの機能をまともに作ろうとすると、めちゃくちゃめんどくさい。

ListViewを使うとどれだけ分かりやすく、簡単に書けるかやってみるぞ!

ベースアプリの準備

リポジトリのクローン

ウサギ
ウサギ

まずは日記アプリのリポジトリをクローンする

GitHub - yheihei/base_diary: ウサでも分かる中級Django講座の教材
ウサでも分かる中級Django講座の教材. Contribute to yheihei/base_diary development by creating an account on GitHub.
$ git clone https://github.com/yheihei/base_diary.git
$ cd base_diary

マイグレーションと初期データ投入

ウサギ
ウサギ

次にマイグレーションとloaddataによる初期データの投入だ

$ python manage.py migrate
$ python manage.py loaddata initial.json

この状態でhttp://127.0.0.1:8000/を開くと。。。

わいへい
わいへい

よし、日記一覧が表示されたね

日記の記事を追加したり、編集したりする場合はhttp://127.0.0.1:8000/admin/diary/post/に入って行う。

ログインできるユーザー名とパスワードは下記だ。

UserName: yhei
Password: password
日記編集画面

完成版ソースだけ見る場合

ウサギ
ウサギ

ちなみに完成形を書いたブランチも用意しておいた。

答えだけ先に見たいやつは下記コマンドでブランチをチェックアウトしてくれ

GitHub - yheihei/base_diary at feature/#13
ウサでも分かる中級Django講座の教材. Contribute to yheihei/base_diary development by creating an account on GitHub.
# 完成版ブランチを見たい場合はこちら
$ git fetch && git checkout feature/#13

ベースアプリの解説

ウサギ
ウサギ

もう一度この日記アプリの画面表示を見てみよう。

トップページにアクセスすると、こんな画面が出てくる

クソ日記アプリ

仕様をまとめるとこんな感じだ。

  • トップページにアクセスすると、日記一覧が表示される
  • 各日記の表示項目は
    • タイトル
    • 投稿者
    • 本文
    • 投稿日
    • カテゴリー

モデル定義

ウサギ
ウサギ

モデルはこんな感じだな。

from django.db import models
from django.db.models.deletion import CASCADE

from django.contrib.auth.models import AbstractUser


# Create your models here.
class User(AbstractUser):
  pass

class Post(models.Model):
  id = models.AutoField(primary_key=True)
  user = models.ForeignKey(User, on_delete=CASCADE)
  title = models.CharField(max_length=2048)
  body = models.TextField()
  created_at = models.DateTimeField(auto_now_add=True)
  updated_at = models.DateTimeField(auto_now=True)
  categories = models.ManyToManyField(
    'Category',
    blank=True,
    related_name="posts",
  )

class Category(models.Model):
  name = models.CharField(max_length=1024)
  slug = models.CharField(max_length=1024)
  created_at = models.DateTimeField(auto_now_add=True)
  updated_at = models.DateTimeField(auto_now=True)

  def __str__(self) -> str:
    return f'{self.name}'
ウサギ
ウサギ

Postモデル(投稿)と、Userモデル(投稿者)が1対1で紐つく。

ウサギ
ウサギ

さらに、PostモデルとCategoryモデルが多対多で紐ついてる。

Viewとテンプレートの定義

ウサギ
ウサギ

views.pyはこんな感じ

from django.shortcuts import render

# Create your views here.
from .models import Post

# Create your views here.
def index(request):
  return render(request, 'index.html', {
    'posts': Post.objects.all(),
  })
ウサギ
ウサギ

取得する記事をPost.objects.all()で取得して、

index.htmlのテンプレート側に渡し

<html>
  <head>
    <title>とあるエンジニアの日記帳</title>
  </head>
  <body>
    {% load static %}
    <h1>とあるエンジニアの日記帳</h1>
    {% for post in posts %}
      <div>
        <h2>{{ post.title }}</h2>
        <div>投稿 : {{ post.user }}</div>
        <div>{{ post.body |safe }}</div>
        <div>{{ post.updated_at }}</div>
        <div>カテゴリー : 
        {% for category in post.categories.all %}
          <span>{{ category }},</span>
        {% endfor %}
        </div>
      </div>
    {% empty %}
      <p>No Diaries.</p>
    {% endfor %}
  </body>
</html>
ウサギ
ウサギ

トップページのテンプレートで表示するってだけの単純な構造だ

クラスベースビューで機能を作ってみる

一覧表示を簡単に作る

まず最初に、一覧表示を作ってみる。

diary/views.py に下記を追記する。

from django.views.generic import ListView


class PostListView(ListView):
  model = Post

ListViewクラスを継承したPostListViewクラスを作る。

model変数に、Postモデルを指定するだけだ。

これだけで、Postモデルに紐ついたレコードを全て持ってきてくれる。

次にdiary/urls.pyを下記のようにし、トップページを開いたら、PostListViewを呼び出すようにする。

from django.urls import path
from . import views
from diary.views import PostListView

app_name = "diary"
urlpatterns = [
  # path("", views.index, name="index"),  # コメントアウト
  path("", PostListView.as_view(), name="index"),  # 追加
]

最後に、PostListViewで表示するテンプレートを作る。

ListViewクラスを継承すると、紐つくテンプレートは下記の規則で自動的に決まる。

class PostListView(ListView):
  model = Post
  # テンプレートは templates/アプリ名/クラス名(末尾のViewを除く).html が選ばれる
  # この場合は templates/diary/post_list.html

ということで、templates/diary/post_list.htmlを作って、下記のように記述する。

<html>
  <head>
    <title>とあるエンジニアの日記帳</title>
  </head>
  <body>
    {% load static %}
    <h1>とあるエンジニアの日記帳</h1>
    {% for post in object_list %}
      <div>
        <h2>{{ post.title }}</h2>
        <div>投稿 : {{ post.user }}</div>
        <div>{{ post.body |safe }}</div>
        <div>{{ post.updated_at }}</div>
        <div>カテゴリー : 
        {% for category in post.categories.all %}
          <span>{{ category }},</span>
        {% endfor %}
        </div>
      </div>
    {% empty %}
      <p>No Diaries.</p>
    {% endfor %}
  </body>
</html>

デフォルトのtemplates/index.htmlとほぼ同じだが、下記だけが違う。

    <h1>とあるエンジニアの日記帳</h1>
    {% for post in object_list %}

for文でcontextから記事一覧を取得するところだ。

ListViewで指定したモデルのレコードがobject_listという変数に自動的に入ってくる。

このobject_listというのは、context_object_nameで指定することで任意の変数名に変更可能だ。

class PostListView(ListView):
  model = Post
  context_object_name = 'posts'  # object_listからpostsという変数に変える
    <h1>とあるエンジニアの日記帳</h1>
    {% comment %} PostListViewで指定した変数で取得 {% endcomment %}
    {% for post in posts %}

こちらの方が可読性は高くなる

特に理由がなければ、context_object_nameを指定するほうが良いだろう。

この状態で、アプリを開いてみると。。。

わいへい
わいへい

これだけでちゃんと表示された。。。

投稿の取得方法を変更する

ただこれだけで実装終わるわけないじゃん?

普通の開発ってもっと面倒なこといっぱいあるよな??

こっからは実践的な機能を作ってみて、それも全部ListViewでできるぜ?? 便利だぜ?? っていうのを説明していくぞ。

作成日の新しいもの順で並び替える

まずは、日記を新しいもの順で並べ替えてみよう。

モデルからレコードを取得する際に、下記のようにクエリを指定すればいい。

class PostListView(ListView):
  ...
  ...
  # querysetで取得するものの詳細を決める
  queryset = Post.objects.order_by(
    '-created_at'  # 作成日の新しいものから並べる
  )

投稿の取得方法をさらに改善する(N+1問題の解消)

あとは、このアプリ、潜在的な問題をはらんでいる。

わいへい
わいへい

えっ!!

テンプレートの下記を見てくれ。

    {% for post in posts %}
      <div>
        ...
        {% comment %} ここでuserテーブルの情報を取得するSQL発行 {% endcomment %}
        <div>投稿 : {{ post.user }}</div>
        ...
        <div>カテゴリー : 
        {% for category in post.categories.all %}
          {% comment %} ここでcategoryテーブルの情報を取得するSQL発行 {% endcomment %}
          <span>{{ category }},</span>
        {% endfor %}

コメント部分に注目してくれ。

それぞれ forループの中で userテーブルへのアクセス、categoryのforループの中でcategoryテーブルへのアクセスが発生する。

極端な例を挙げる。

記事が1000件あったとして、その1000件に100件ずつカテゴリーが付いているとしたら。

呼ばれるSQL数は

    {% for post in posts %}
      # ★記事1000件分ループ
      <div>
        ...
        # ★ここで1件SQLが呼ばれる
        <div>投稿 : {{ post.user }}</div>
        ...
        {% for category in post.categories.all %}
          # ★ここで100件SQLが呼ばれる
          {% comment %} ここでcategoryテーブルの情報を取得するSQL発行 {% endcomment %}
          <span>{{ category }},</span>
        {% endfor %}

1000×1(ユーザー) + 1000×100(カテゴリー) = 101000 SQL呼ばれることになる。

するとどうなると思う?

わいへい
わいへい

一覧表示がめっちゃ遅くなる。。。??

その通り。いわゆるN+1問題ってやつだ。

これを防ぐには、Postを取得するクエリであらかじめテーブルをJOINする必要がある。

こんな感じだ。

class PostListView(ListView):
  model = Post
  context_object_name = 'posts'
  # querysetで取得するものの詳細を決める
  queryset = Post.objects.order_by(
    '-created_at'  # 作成日の新しいものから並べる
  ).select_related(
    'user',  # あらかじめuserテーブルをjoinしておく
  ).prefetch_related(
    'categories',  # あらかじめcategoryテーブルをjoinしておく
  )

ListViewを使っていても、クエリ指定は自由にできるため、問題なく対応できる。

contextを追加する

次に、下記の仕様を実現する。

・ログイン済みの場合各日記に編集リンクをつける

XX一覧とか言っても、たいていの場合モデルのレコードだけ出して終わるのは稀だ。

付加情報をつける必要が出てくる。

つーことでモデルの情報に応じた付加情報をつける方法を学ぶ。

やり方は簡単で、ListViewのget_context_dataという関数をオーバーライドして設定する。

from django.urls import reverse


class PostListView(ListView):
  ...
  context_object_name = 'posts'
  # querysetで取得するものの詳細を決める
  queryset = Post.objects.order_by(
    '-created_at'  # 作成日の新しいものから並べる
  ).select_related(
    'user',  # あらかじめuserテーブルをjoinしておく
  ).prefetch_related(
    'categories',  # あらかじめcategoryテーブルをjoinしておく
  )
  ...
  def get_context_data(self, **kwargs):
    # 継承元(ListView)のget_context_dataを呼んで、contextを取得
    context = super().get_context_data(**kwargs)
    # ログインしていたら、postに編集用のURLを付与する
    if self.request.user.is_authenticated:
      posts = context['posts']  # このpostsにはquerysetで指定したPostのデータが既に入っている
      for post in posts:
        # adminの編集リンクをpostのidから作成する
        post.edit_url = reverse(f'admin:{post._meta.app_label}_{post._meta.model_name}_change', args=[post.id] )
      context['posts'] = posts
    return context  # 最後に編集したcontextをreturnしてやる

querysetで指定したものを取得した後で、contextを編集できる。

これで、context[‘posts’]内のpostには、edit_urlという日記の編集リンクが付いた。

これをテンプレート側のpostの部分で表示してやる。

    {% for post in posts %}
      <div>
        ...
        ...
        <div>カテゴリー : 
        {% for category in post.categories.all %}
          <span>{{ category }},</span>
        {% endfor %}
        </div>
        {% comment %} 編集リンクを追加 {% endcomment %}
        {% if post.edit_url %}
          <a href="{{ post.edit_url }}">編集</a>
        {% endif %}
      </div>
    {% empty %}

トップページを表示してみると。。。

編集ページに遷移する
わいへい
わいへい

編集リンクが現れた!!

ページネーションを実装する

次は、ページネーションを実装するぞ。

ListViewにはページネーションの仕組みが最初から入っている。

少ないコード量でページング処理ができるようになる。

まずは、1ページあたり、2件の日記に絞り込んでみる。

diary/views.pyに下記を追加するだけだ。

class PostListView(ListView):
  ...
  ...
  # ページネーション
  paginate_by = 2

これだけで、記事が全件表示ではなく2件表示に変更される。

querysetをいじらなくても勝手にやってくれるのがポイントだ。

paginate_by=2を設定しただけで2件表示になる

さらに、今何ページかの表示、次のページへ遷移するリンクを画面に表示してみるぞ。

テンプレートの末尾に下記を入れるだけだ。

    ...
    {% empty %}
      <p>No Diaries.</p>
    {% endfor %}
    <ul class="pagination">
      {% comment %} 前へ {% endcomment %}
      {% if page_obj.has_previous %}
        <li class="page-item"><a class="page-link" href="?page={{ page_obj.previous_page_number }}">前へ</a></li>
      {% endif %}

      {% comment %} 間のページ {% endcomment %}
      {% for i in page_obj.paginator.page_range %}
        {% if page_obj.number == i %}
          <li class="page-item active"><a class="page-link">{{ i }}</a></li>
        {% else %}
          <li class="page-item"><a class="page-link" href="?page={{ i }}">{{ i }}</a></li>
        {% endif %}
      {% endfor %}

      {% comment %} 次へ {% endcomment %}
      {% if page_obj.has_next %}
        <li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}">次へ</a></li>
      {% endif %}
    </ul>
  </body>
</html>

page_objというオブジェクトに、ページネーション関連の情報が入ってくる。

{% if page_obj.has_previous %}

で、現在のページの前のページがあるか取得できる。

page_obj.previous_page_number

で、前のページの番号を取得。

{% for i in page_obj.paginator.page_range %}

で、間のページ数のリストが取得できる。ここをfor文で回せば、間のページ数のリンクが作れる。

{% if page_obj.has_next %}

で、現在のページの次のページがあるかどうか。

page_obj.next_page_number

で次のページのページ番号が取れる。

実際に遷移できるか確認してみると。。。

ページネーション機能
わいへい
わいへい

ページネーションできてる〜〜〜!!

http://127.0.0.1:8000/?page=1のように、pageというクエリストリングをつけてページ番号を指定しているが、

下記のようにpage_kwargを指定すれば、任意のクエリストリングに変更できる。これも覚えておいてくれ。

class PostListView(ListView):
  ...
  # ページネーション
  paginate_by = 2
  page_kwarg = 'page'  # 未指定でも良い。デフォルトは'page'

さらに。

1ページに表示する件数も変更できるようにしてみる。

↑表示件数の絞り込み部分のベースとなるやつだ。

通常、下記のpaginate_byの値で表示件数は固定されてしまうが、

class PostListView(ListView):
  ...
  # ページネーション
  paginate_by = 2

これを動的に変更する仕組みがある。

例えば、URLにper_page=数値というクエリストリングがついたときに、per_pageで指定された値に、paginate_byを変えてみる。

get_paginate_byメソッドをオーバーライドすることで可能だ。

  def get_paginate_by(self, queryset):
    '''
    per_pageをクエリによって動的に変える
    Notes
    -----
    https://github.com/django/django/blob/stable/3.2.x/django/views/generic/list.py
    '''
    # 継承元のget_paginate_byを読んで今のpaginate_byを取得
    # なくても良いが、継承元の関数は呼ぶのが定石。継承元で必要な処理をおこなっている場合がある
    paginate_by = super().get_paginate_by(queryset)
    # per_pageをリクエストのURLから取得
    if self.request.GET.get('per_page'):
      # paginate_byを指定された値に変更
      paginate_by = int(self.request.GET.get('per_page'))
    # 変更したpaginate_byを返却する
    return paginate_by

get_paginate_by中でpaginate_byを変更してやればOKだ。

http://127.0.0.1:8000/?per_page=1 にアクセスしてみると。。。

わいへい
わいへい

1ページ1件になった!!

動的な検索を行う

次はカテゴリーで日記を絞り込めるようにしてみるぞ。

category=スラグ名の形式でクエリストリングがついたときに、そのスラグのカテゴリーが付与された日記だけを持ってくる実装だ。

# スラグがdiaryのカテゴリーの日記を持ってくる場合
http://127.0.0.1:8000/?category=diary

リクエストに応じてquerysetを変えるには、get_querysetというメソッドをオーバーライドして使う。

  def get_queryset(self, **kwargs):
    # 継承元のget_querysetを呼び出して、querysetを取得
    queryset = super().get_queryset(**kwargs)
    # カテゴリーのクエリストリングがある場合
    if self.request.GET.get('category'):
      # querysetにカテゴリーの検索条件を入れて
      queryset = queryset.filter(
        categories__slug=self.request.GET.get('category')
      )
    # 変更後のquerysetを返す
    return queryset

実際にカテゴリーで絞り込んでみると。。。

わいへい
わいへい

日記カテゴリーのカテゴリーだけが表示されてるね

検索フォームを追加する

後もう少しだ。検索フォームを追加していくぞ。

↑のカテゴリのselectボックスと、表示件数のselectボックスを作っていく。

まずはdiary/views.pyに↑のformクラスを作成する。

from django import forms


class PostSearchForm(forms.Form):
  category = forms.fields.ChoiceField(
    choices = (
      # (formのoptionのvalue, 表示名)
      ('-', '未設定'),
      ('diary', '日記'),
    ),
    required=False,
    widget=forms.widgets.Select
  )
  per_page = forms.fields.ChoiceField(
    label='表示件数',
    choices = (
      (None, "-"),
      (1, "1"),
      (2, "2"),
      (10, "10"),
      (50, "50"),
      (100, "100"),
    ),
    required=False,
    widget=forms.widgets.Select
  )

カテゴリーも表示件数も、forms.widgets.Selectを使い、choicesに条件を入れてやればいい。

あとは、作ったPostSearchFormクラスをListViewに組み込む。

get_context_dataの末尾に入れるのがいいだろう。

def get_context_data(self, **kwargs):
    ...

    # 検索フォーム
    context['search_form'] = PostSearchForm(self.request.GET)
    return context

最後にget_context_dataで入れたsearch_formをテンプレート側で表示する。

  <body>
    {% load static %}
    <h1>とあるエンジニアの日記帳</h1>
    {% comment %} search_formを表示する {% endcomment %}
    <form method="GET" class="post-form">
      {% csrf_token %}
      {{ search_form.as_p }}
      <input type="submit" value="検索">
    </form>

これで、トップページから検索ができるようになった。

formで検索する

selectボックスにカテゴリーモデルの値を入力する

これで検索ができるようになったと思いきや、まだまだ重大な問題がある。

下記を見てくれ。

class PostSearchForm(forms.Form):
  category = forms.fields.ChoiceField(
    choices = (
      # (formのoptionのvalue, 表示名)
      ('-', '未設定'),
      ('diary', '日記'),
    ),
    required=False,
    widget=forms.widgets.Select
  )

categoryのフォームはchoicesに固定でカテゴリーの情報が入っている。

つまり、カテゴリーを増やすたびにハードコーディングで追加する必要がある。

理想は、Categoryのレコードから自動的にひっぱってくることだが。。。

下記だと失敗する。

from .models import Post, Category
...
class PostSearchForm(forms.Form):
  category = forms.fields.ChoiceField(
    # ★ORMを使って持ってこようとしてもエラーになる
    choices = Category.objects.all().values_list('slug', 'name'),
    required=False,
    widget=forms.widgets.Select
  )

なぜなら、メンバ変数の定義時点ではORMが使えないからだ。

そこでPostSearchFormクラスのコンストラクタでchoicesを後入れしてやる。

こんな感じだ。

class PostSearchForm(forms.Form):
  category = forms.fields.ChoiceField(
    choices = (),
    required=False,
    widget=forms.widgets.Select
  )
  ...

  def __init__(self, *args, **kwargs):
    super(PostSearchForm, self).__init__(*args, **kwargs)
    # カテゴリーの選択肢を設定モデルから取得する
    self.fields['category'].choices = Category.objects.all().values_list('slug', 'name')
    # 0番目に未設定の選択肢を挿入
    self.fields['category'].choices.insert(0, ('', '未設定'))

self.fieldsのリストの中に、categoryのChoiceFieldオブジェクトが入ってくる。

↑のようにORMを使って編集が可能になる。

よく使うので覚えておいてくれ。

検索フォームの見た目を整える

わいへい
わいへい

これでListViewはだいたい使えるようになった?

ウサギ
ウサギ

うむ。使えるようにはなったが。。。

ウサギ
ウサギ

検索フォームの見た目の整え方もついでにやっておくぞ

ウサギ
ウサギ

これはListViewには関係しないが、解説させてくれ

まずは検索フォームの最終形を確認する。

こんな感じに、bootstrapを使って、いい感じの検索フォームの見た目にする。

しかし、formにCSSをあてる方法があまり出回ってない。

公式は

    <form method="GET" class="post-form">
      {% csrf_token %}
      {{ search_form.as_p }}
      <input type="submit" value="検索">
    </form>

form.as_pを使えばいい感じにformが出るとか書いてあるが。

このままの見た目でリリースするやつを俺は知らない。

にも関わらず、formにどうやってCSSをあてるかの説明が全然Web上にない。

てことで、bootstrapを使いつつ、最終的なformのHTMLを下記のようにする方法を説明する。

<form method="GET" class="mt-5">
  <input type="hidden" name="csrfmiddlewaretoken" value="ZsOeJWyax9kSaYHWxhYwxENncgpqH8HApn5n15fFMLO6sFdUXwzyeTyUEWr0Vbvq">
    <div class="row">
      <div class="form-group col-3">
        Category
        <select name="category" class="form-control" id="id_category">
          <option value="" selected="">未設定</option>
          <option value="diary">日記</option>
        </select>
      </div>

      <div class="form-group col-3">
        表示件数
        <select name="per_page" class="form-control" id="id_per_page">
          <option value="" selected="">-</option>
          <option value="1">1</option>
          <option value="2">2</option>
          <option value="10">10</option>
          <option value="50">50</option>
          <option value="100">100</option>
        </select>
      </div>
    </div>
  <button type="submit" class="btn btn-primary">検索</button>
</form>

ではテンプレート側を改修していく。

まずはbootstrapのCSSとJSをhead部分で読み込んでおく。

<html>
  <head>
    ...
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
    <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
    <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
  </head>

bootstrapではformをいい感じにするコンポーネントがある。

Form Controls

これを真似して、テンプレートを作っていく。

formをfor文で回して各フィールドの構造を作る。下記のような感じだ。

...
    <h1>とあるエンジニアの日記帳</h1>
    <form method="GET" class="mt-5">
      {% csrf_token %}
      {% for field in search_form.visible_fields %}
        <div class="form-group col-3">
          {{ field.errors }}
          {{ field.label }}
          {{ field }}   {% comment %} ここにselectボックスがくる {% endcomment %}
          {{ field.help_text }}
        </div>
      {% endfor %}
      <button type="submit" class="btn btn-primary">検索</button>
    </form>
...

search_form.visible_fieldsにはhidden input以外のフィールドが入ってくる。

それをfor文で回すことで、

<div class="form-group col-3">
  各selectボックス
</div>

という構造を作りながらformのフィールドを作ることができる。

が、ここで問題がある。

        <div class="form-group col-3">
          {{ field.errors }}
          {{ field.label }}
          {{ field }}   {% comment %} ここにselectボックスがくる {% endcomment %}
          {{ field.help_text }}
        </div>

bootstrapのForm Controlsを見ると、{{ field }}の部分にform-controlというクラスをつけてやる必要がある。

ただ、この構造だとテンプレート側からつけるのは不可能だ。

ではどうするか。

Formクラスのコンストラクタ内で指定する。

class PostSearchForm(forms.Form):
  ...
  def __init__(self, *args, **kwargs):
    ...
    ...
    for field in self.fields.values():
      field.widget.attrs["class"] = "form-control"

self.fieldsの中には各フィールドの情報が入っているので。

それをfor文で回すことで、ここのfieldのclass属性に、form-controlをつけてやる。

すると。。。

わいへい
わいへい

いい感じになってる!!!

ウサギ
ウサギ

まあこんな感じで

ウサギ
ウサギ

formを使ったhtmlの組み立てはクセがある

ウサギ
ウサギ

formのデザインをするときは、Djangoのルールに従いながらどう実装するかを常に考えるようにしてくれ。

Bootstrapを使って見た目を整えてみよう

ウサギ
ウサギ

ということで、最後はやってみようのコーナー。

ウサギ
ウサギ

Bootstrapを使って、下記のようなデザインを実現してみてくれ

Bootstrapを使って綺麗なテンプレートにした図

解答は下記にあるので、どうしてもできなかったら見てくれ。

base_diary/diary/templates/diary/post_list.html at feature/#13 · yheihei/base_diary
ウサでも分かる中級Django講座の教材. Contribute to yheihei/base_diary development by creating an account on GitHub.

まとめ

ウサギ
ウサギ

ListViewの使い方をまとめるぞ

  • 一覧表示をするにはListViewを継承する
  • model変数にモデルを定義すると、勝手にレコードを取得する
  • querysetでクエリを指定。get_queryset関数で動的に検索クエリを設定可能
  • get_context_dataでcontextを編集できる
  • get_paginate_by関数で動的に表示件数を変更
  • N+1に気をつけろ

\ 講座で学んだことを即アウトプットしよう /

次の講座

Djangoの基礎を学びたい方は

Djangoの基礎を固めたい方はこちら(セール時に買うのがおすすめ)

『Djangoパーフェクトマスター』〜インスタ映えを支えるPython超高速開発Webフレームワークを徹底解説!

動画講座で手を動かしながら、ほとんどのことが学べます。

ウサギもここから始めました。

コメント

  1. […] ListViewの使い方をマスターしようLIstViewを使って一覧表示を簡単に作る。並… […]

タイトルとURLをコピーしました