Python の Webアプリフレームワーク Django の始め方 -その2-

f:id:Naotsugu:20211124230837p:plain


はじめに

前回は Django の導入からモデルの定義まで説明しました。

blog1.mammb.com

今回は、モデルを利用するビューの作成方法について見ていきましょう。


モデルビューテンプレート

Django で作成するアプリケーションは、モデルビューテンプレート(Model View Template, MVT)の構成を取ります。

f:id:Naotsugu:20211125212741p:plain

urls.py による HTTP リクエストのルーティングと、models.py によるモデル定義については前回説明した内容になります。

urls.py でルーティングされたリクエストは、ビュー(views.py) でモデルを介してデータベースを操作し、テンプレートを使ってレスポンスを構築します。

Template の定義からはじめて、views.py でリクエストに応じたレスポンスを返す流れを見ていきましょう。


テンプレートの配備場所

Django のテンプレートは、アプリケーションの template ディレクトリに配備します。今回の例では、/catalog/templates/ にテンプレートを配備することになります。

この設定は、settings.py の以下の項目でカスタマイズできます。

TEMPLATES = [
  {
    'BACKEND': 'django.template.backends.django.DjangoTemplates',
    'DIRS': [],
    'APP_DIRS': True,
    'OPTIONS': { ... },
  },
]

'APP_DIRS': True とすることで template ディレクトリの検索が有効になります。DIRS には検索対象のディレクトリを追加することができます。

ここでは、デフォルトの設定のまま進めるものとします。


ベーステンプレート

Django では、アプリケーション共通のベーステンプレートを定義し、個々の画面ではベーステンプレートを拡張(extends)することでマークアップを共通化することができます。

最初にベーステンプレートとして catalog/templates/base_generic.html を作成しましょう。

<!DOCTYPE html>
<html lang="en">
<head>
  {% block title %}<title>Local Library</title>{% endblock %}
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
  <!-- Add additional CSS in static file -->
  {% load static %}
  <link rel="stylesheet" href="{% static 'catalog/css/styles.css' %}">
</head>
<body>
  <div class="container-fluid">
    <div class="row">
      <div class="col-sm-2">
      {% block sidebar %}
        <ul class="sidebar-nav">
          <li><a href="{% url 'index' %}">Home</a></li>
          <li><a href="">All books</a></li>
          <li><a href="">All authors</a></li>
        </ul>
      {% endblock %}
      </div>
      <div class="col-sm-10 mt-2">
        {% block content %}{% endblock %}
      </div>
    </div>
  </div>
</body>
</html>

テンプレートには、{% block title %} ... {% endblock %} {% block sidebar %} ... {% endblock %} {% block content %}{% endblock %} というテンプレートタグがあります。 このブロックには、extends で派生したページでコンテンツを埋め込むことになります(デフォルトのコンテンツを含めることができます)。

サードバーには {% url 'index' %} としてURLリンクを定義しています。 これは、 urls.py で定義したURL Conf path('', views.index, name='index'), に付けた名前で、URLパスを参照するものになります。

ベーステンプレートでは、{% load static %}{% static 'catalog/css/styles.css' %} により、CSSファイルを読み込んでいます。これについて少しだけ詳細を見ておきましょう。


静的ファイルの取り扱い

Django では通常、静的ファイルを扱いません。静的ファイルは、フロントに配置した Apache や Nginx で処理したり、専用サーバや CDN から配信することになります。

Django では、このような静的ファイルの取り扱いを行うために django.contrib.staticfiles が提供されています。settings.py で以下のようになっていれば django.contrib.staticfiles が有効化されています。

INSTALLED_APPS = [
    ...
    'django.contrib.staticfiles',
]

django.contrib.staticfilesでは、それぞれのアプリケーションから静的ファイルを集め、運用環境で公開しやすくするものです。ここでは詳細には立ち入りませんが、必要に応じて Deploying static files を参照してください。

django.contrib.staticfiles では、DEBUG = True(settings.pyの設定)となっていた場合、runserver を実行すれば、開発サーバで静的ファイルを扱えるように自動的に処理されます。ですので、開発時には意識せずに作業をすすめることができますが、極めて非効率であり、セキュリティ上の問題がある可能性が高いため、運用環境での利用は推奨されていません。


このように、開発時と運用時の静的ファイルの扱いがことなるため、テンプレート上では、{% load static %}{% static 'catalog/css/styles.css' %} により、STATIC_URL というグローバルな設定値を基準にしてCSSを読み込むようになっています。

STATIC_URL は settings.py にて、デフォルトで STATIC_URL = '/static/' として定義されています。そのため、アプリケーション用の CSS は、catalog/static/ 以下に配備することになります。 catalog/static/catalog/css/styles.css として以下のように定義しましょう。

.sidebar-nav {
    margin-top: 20px;
    padding: 0;
    list-style: none;
}

なお、catalog/static/ 以下にアプリケーション名のディレクトリを設けることで、django.contrib.staticfiles で複数アプリケーションの静的ファイルを扱う際の名前の競合を防ぐことができます。


index ページのテンプレート

ベーステンプレートを extends した index ページのテンプレートを作成します。

catalog/templates/index.html を以下の内容で作成してください。

{% extends "base_generic.html" %}

{% block content %}
  <h1>Local Library Home</h1>
  <p>Welcome to LocalLibrary</p>
  <h2>Dynamic content</h2>
  <p>The library has the following record counts:</p>
  <ul>
    <li><strong>Books:</strong> {{ num_books }}</li>
    <li><strong>Copies:</strong> {{ num_instances }}</li>
    <li><strong>Copies available:</strong> {{ num_instances_available }}</li>
    <li><strong>Authors:</strong> {{ num_authors }}</li>
  </ul>
{% endblock %}

1 行目の {% extends "base_generic.html" %} で、先ほど作成したベーステンプレートを指定しています。

{% block content %} ... {% endblock %} が、このページで定義するコンテンツで、ベーステンプレートに埋め込まれます。

{{ num_books }}{{ num_instances }} といった記載は、テンプレート変数で、この後作成する views.py のメソッドから dictionary 形式で受け渡すことで、名前で参照できます。


index ビューの作成

テンプレートの準備が終わったので、ビューを定義していきます。

catalog/views.py を以下のように編集します。

from django.shortcuts import render
from .models import Book, Author, BookInstance, Genre

def index(request):

    num_books = Book.objects.all().count()
    num_instances = BookInstance.objects.all().count()
    num_instances_available = BookInstance.objects.filter(status__exact='a').count()
    num_authors = Author.objects.count()

    context = {
        'num_books': num_books,
        'num_instances': num_instances,
        'num_instances_available': num_instances_available,
        'num_authors': num_authors,
    }

    return render(request, 'index.html', context=context)

モデルの取得は、Entry.objects として取得したモデルマネージャを介してクエリできます。

上記では、Book.objects.all() のように取得した QuerySet から count() で件数を取得しています。

BookInstance.objects.filter(status__exact='a') として抽出条件を指定して QuerySet を取得することもできます。exact は完全一致を意味し、その他 contains だったり in だったり gt といった条件を指定しています。他の条件指定は Field lookups を参照してください。 フィールドは二重アンダースコアで連結し、例えばリレーションを辿る必要がある場合には genre__name__icontains='fiction' のように複数を連結します。

取得した情報は、dictionary として context に設定し、render() によりテンプレートへバインドしてレスポンスを返します。


では、ここまでの内容でサーバを起動しましょう。

$ python manage.py runserver

ブラウザで http://localhost:8000/catalog/ にアクセスすれば、以下のような画面が表示されます。

f:id:Naotsugu:20211126191845p:plain


ブックリストページの作成

続いて書籍の一覧ページに移りましょう。

Djangoでは、一覧ページ用の汎用リストビュー(ListView)が用意されており、これを継承したクラスを定義することで簡単に一覧ビューが作成できます。

catalog/views.py を開いて以下のように編集します。

from django.views import generic

class BookListView(generic.ListView):
    model = Book

これだけで、Book の一覧を取得して catalog/templates/catalog/book_list.html というテンプレートをレンダリングする処理が行われます。テンプレートでは object_list または book_list という名前でデータベースから取得した値を参照できます。

デフォルトの動作は、作成した BookListView を変更することでカスタマイズできます(例えば、テンプレートファイルを指定したり、データベースから取得するレコード数を制限するなど)。

では、テンプレートファイルを catalog/templates/catalog/book_list.html を作成しましょう。

{% extends "base_generic.html" %}

{% block content %}
  <h1>Book List</h1>
  {% if book_list %}
  <ul>
    {% for book in book_list %}
      <li>
        <a href="">{{ book.title }}</a> ({{book.author}})
      </li>
    {% endfor %}
  </ul>
  {% else %}
    <p>There are no books in the library.</p>
  {% endif %}
{% endblock %}

テンプレートは Indexページで作成したものと大差ありませんが、if条件 {% if book_list %} や forループ {% for book in book_list %} を使っていることに注意してください。


ブックリストページへの遷移

ベーステンプレートを更新してブックリストページへのリンクを挿入しましょう。

catalog/templates/base_generic.html のリンクを編集します。

<li><a href="{% url 'index' %}">Home</a></li>
<li><a href="{% url 'books' %}">All books</a></li>
<li><a href="">All authors</a></li>

booksのURLでアクセスした場合のURLマッピング定義の追加も必要です。

catalog/urls.py を以下のように変更しましょう。

urlpatterns = [
    path('', views.index, name='index'),
    path('books/', views.BookListView.as_view(), name='books'),
]

クラスベースのビューを利用するため、views.BookListView.as_view() という形でビューを指定している点に注意してください。 クラスメソッド as_view() を呼び出して、適切なビュー関数にアクセスします。これは、クラスのインスタンスを生成したり、HTTP リクエストの着信時に適切なハンドラメソッドが呼び出されるようにしたりする作業をすべて行います。

変更内容を確認しておきましょう。

$ python manage.py runserver

f:id:Naotsugu:20211126195601p:plain

一覧画面が表示できました。


ブック詳細ページの作成

詳細ページについても Django の提供する DetailView を使うことができます。

catalog/views.py に以下のクラスを追加します。

class BookDetailView(generic.DetailView):
    model = Book

テンプレートは catalog/templates/catalog/book_detail.html として以下のように作成します。

{% extends "base_generic.html" %}

{% block content %}
  <h1>Title: {{ book.title }}</h1>

  <p><strong>Author:</strong> <a href="">{{ book.author }}</a></p>
  <p><strong>Summary:</strong> {{ book.summary }}</p>
  <p><strong>ISBN:</strong> {{ book.isbn }}</p>
  <p><strong>Language:</strong> {{ book.language }}</p>
  <p><strong>Genre:</strong> {{ book.genre.all|join:", " }}</p>

  <div style="margin-left:20px;margin-top:20px">
    <h4>Copies</h4>

    {% for copy in book.bookinstance_set.all %}
      <hr>
      <p class="{% if copy.status == 'a' %}text-success{% elif copy.status == 'm' %}text-danger{% else %}text-warning{% endif %}">
        {{ copy.get_status_display }}
      </p>
      {% if copy.status != 'a' %}
        <p><strong>Due to be returned:</strong> {{ copy.due_back }}</p>
      {% endif %}
      <p><strong>Imprint:</strong> {{ copy.imprint }}</p>
      <p class="text-muted"><strong>Id:</strong> {{ copy.id }}</p>
    {% endfor %}
  </div>
{% endblock %}

少し長いですが、ほとんど以前に説明したとおりです。

注目すべき点は、book.bookinstance_set.all という記載です。これは、 Django によって自動的に作られたメソッドで、Book に関連する BookInstance レコードのセットを返すものです。 Book と BookInstance のリレーションは、Book ← BookInstance の方向で、Book から BookInstance をたどることができません。このような場合、Django はForeignKey が宣言されたモデル名を小文字にして、その後に _set を付けた名前の逆引き関数を提供します。

もう一つ注意すべきものは、{{ copy.get_status_display }} という記載です。 ここで copy は、BookInstance のオブジェクトとなりますが、 BookInstance には get_status_display というメソッドを定義していません。 Django は、モデル内の choices フィールドに対して、get_フィールド名_display() というメソッドを自動的に作成します。

念のため、BookInstance の該当箇所を載せておきます。

    LOAN_STATUS = (
        ('d', 'Maintenance'),
        ('o', 'On loan'),
        ('a', 'Available'),
        ('r', 'Reserved'),
    )

    status = models.CharField(
        max_length=1,
        choices=LOAN_STATUS,
        blank=True,
        default='d',
        help_text='Book availability')


ブック詳細ページへの遷移

ブック詳細ページへのリンクを catalog/templates/catalog/book_list.html に追加しましょう。{{ book.get_absolute_url }} を追加します。

    {% for book in book_list %}
      <li>
        <a href="{{ book.get_absolute_url }}">{{ book.title }}</a> ({{book.author}})
      </li>
    {% endfor %}

このメソッドは、Book で以下のように定義したものです。

class Book(models.Model):
    ...
    def get_absolute_url(self):
        return reverse('book-detail', args=[str(self.id)])

book-detail という名前のパスに自身のIDを付けたURLを生成するメソッドです。

book-detail を追加するために catalog/urls.py を以下のように変更しましょう。

urlpatterns = [
    path('', views.index, name='index'),
    path('books/', views.BookListView.as_view(), name='books'),
    path('book/<int:pk>', views.BookDetailView.as_view(), name='book-detail'),
]

URLパスは、book/<int:pk> となっています。これは、URLパスパラメータを整数型で pk というキーでキャプチャする意味となります。型はその他 str, slug, uuid, path などが指定できます。 DetailView ではこのキーで対応するモデルの詳細画面を処理します。

内容を確認しておきましょう。

$ python manage.py runserver

一覧画面から詳細画面へ遷移できるようになりました。

f:id:Naotsugu:20211127103249p:plain


一覧ページへ Pagination を追加

ここまでで作成した一覧画面は、全てのレコードを一覧するものでしたので、Pagination を追加していきます。

一覧用のビューは ListView を使っているので、Pagination の追加は簡単に行うことができます。

catalog/views.pyBookListViewpaginate_by を追加します。

class BookListView(generic.ListView):
    model = Book
    paginate_by = 5

テンプレートは、ベーステンプレート catalog/templates/base_generic.html を編集します。

{% block content %}{% endblock %} の下に Pagination 用のブロックを追加します。

...
{% block content %}{% endblock %}
{% block pagination %}
  {% if is_paginated %}
    <div class="pagination">
      <span class="page-links">
        {% if page_obj.has_previous %}
            <a href="{{ request.path }}?page={{ page_obj.previous_page_number }}">previous</a>
        {% endif %}
        <span class="page-current">
            Page {{ page_obj.number }} of {{ page_obj.paginator.num_pages }}.
        </span>
        {% if page_obj.has_next %}
            <a href="{{ request.path }}?page={{ page_obj.next_page_number }}">next</a>
        {% endif %}
      </span>
    </div>
  {% endif %}
{% endblock %}

内容を確認しておきましょう。

$ python manage.py runserver

f:id:Naotsugu:20211127101524p:plain

Pagination ができるようになりました。


著者ページの作成

ブックページと同様に、著者ページも作成しましょう。

作業はブックページと同じなので、コードだけ示します。

catalog/urls.py

urlpatterns = [
    ...
    path('authors/', views.AuthorListView.as_view(), name='authors'),
    path('author/<int:pk>', views.AuthorDetailView.as_view(), name='author-detail'),
]

catalog/views.py

class AuthorListView(generic.ListView):
    model = Author
    paginate_by = 5

class AuthorDetailView(generic.DetailView):
    model = Author

catalog/templates/catalog/author_list.html

{% extends "base_generic.html" %}

{% block content %}
<h1>Author List</h1>
{% if author_list %}
  <ul>
  {% for author in author_list %}
    <li>
      <a href="{{ author.get_absolute_url }}">
      {{ author }} ({{author.date_of_birth}} - {% if author.date_of_death %}{{author.date_of_death}}{% endif %})
      </a>
    </li>
  {% endfor %}
 </ul>
{% else %}
  <p>There are no authors available.</p>
{% endif %}
{% endblock %}

catalog/templates/catalog/author_detail.html

{% extends "base_generic.html" %}

{% block content %}
<h1>Author: {{ author }} </h1>
<p>{{author.date_of_birth}} - {% if author.date_of_death %}{{author.date_of_death}}{% endif %}</p>
<div style="margin-left:20px;margin-top:20px">
<h4>Books</h4>
<dl>
{% for book in author.book_set.all %}
  <dt><a href="{% url 'book-detail' book.pk %}">{{book}}</a> ({{book.bookinstance_set.all.count}})</dt>
  <dd>{{book.summary}}</dd>
{% endfor %}
</dl>
</div>
{% endblock %}


サイドバーのリンクを追加します。catalog/templates/base_generic.html の All authors のリンクURLを編集します。

  <ul class="sidebar-nav">
    <li><a href="{% url 'index' %}">Home</a></li>
    <li><a href="{% url 'books' %}">All books</a></li>
    <li><a href="{% url 'authors' %}">All authors</a></li>
  </ul>

ブック詳細ページ catalog/templates/catalog/book_detail.html からのリンクも編集します。

<p><strong>Author:</strong> <a href="{{ book.author.get_absolute_url }}">{{ book.author }}</a></p>

これで、著者ページの完成です。

f:id:Naotsugu:20211127113639p:plain

f:id:Naotsugu:20211127113717p:plain



まとめ

今回は、Django の ビューとテンプレートの簡単な利用方法についてみてきました。

次回は、フォーム操作を扱っていきます。



プロフェッショナルWebプログラミング Django

プロフェッショナルWebプログラミング Django

Amazon