[Django] 記事の一覧ページと詳細ページの追加

前回、記事を投稿できるようになったので、今回は投稿した記事を見られるようにしたいと思います。

シリーズ一覧

  1. プロジェクトの作成とアプリケーションの作成
  2. view関数の書き方
  3. URLの指定の仕方(URLディスパッチャ)
  4. データベース(モデル)の設定
  5. 記事の投稿ページの作成
  6. 記事の一覧ページと詳細ページの追加 <- 今回
  7. 記事の編集ページと記事の削除機能の追加
  8. いいね機能の追加
  9. コメント機能の追加
  10. Bootstrapを利用したwebデザイン

はじめに

前回までに記事の投稿ができるようになっていることを想定しているので、まだできていない人は出来るようにしてからご覧ください。

Djangoテンプレート言語

変数

Djangoでは変数という形で、pythonプログラムの中のデータをテンプレート(HTMLファイル)に渡すことができます。

テンプレートに変数を渡すにはviews.pyの中でrender関数の第三引数に辞書を渡すことで実現できます。
具体的には以下のように書きます。

def sample_view(request):
  context = {"text":"hello!"}
  return render(request, template_name, context)

変数を受け取ったテンプレート(HTMLファイル)側は

{{ text }}

とすることで受け取った変数の中身を表示できます。

ちょうど辞書の中のキーがテンプレート側での変数名になります。

上の例だと{{ text }}hello!に置き換えられます。

テンプレートタグ

テンプレートタグと呼ばれる特別な機能を持ったタグの紹介です。
テンプレートタグは両端を{%%}で囲みます。

繰り返し

テンプレート(HTMLファイル)内に

{% for v in values %}
  <p>{{ v }}</p>
{% endfor %}

というように書くとpythonのfor文と同じように繰り返しをさせることができます。
valuesの中身が次々とvに代入されて繰り返します。

上の例だとvaluesの中身を一つずつ pタグ でくくって表示させています。

valuesは変数名でviewから渡した変数なら何でも構いません。
ただし、変数の中身はpythonのforにも使えるイテレータブルなオブジェクトに限ります。

注意点は繰り返しの終了地点に{% endfor %}を書いてあげることです。

もう少し詳しく知りたい場合は

これだけは知っておきたいDjangoテンプレートの基本文法

とか

[Django]知ってると便利なforで使える小技

に詳しくまとめてます。

リンクの自動生成

{% url 'index' %}

のようにすると自動的にURLの文字列を生成してくれます。

シングルクォートで囲ってある部分にはurls.pyで指定したpathのnameを入れます。

urlpatterns = [
    path("", views.index, name="index"), 
    path("new/", views.new, name="new"),
    # path関数で指定したnameの部分
    # この例だと "index"と"new"
]

実際に利用するときは以下のように使うことが多いと思います。

<a href="{% url 'index' %}">トップページへ</a>

リンクが深くなると自分でURL書くのが面倒になるので便利な機能です。

一覧ページの追加

先ほどのテンプレート言語を踏まえて、投稿した記事の一覧が見れるページを作成します。

URLの設定

http://localhost:8000/article/all/が一覧ページになるようにURLを設定します。

myblog/blog/urls.py

from django.urls import path
from . import views

urlpatterns = [
    path("", views.index, name="index"),
    path("new/", views.new, name="new"),
    path("article/all/", views.article_all, name="article_all"), # 追記
]

viewの設定

views.py

def article_all(request):
    template_name = "blog/article_all.html"
    context = {"articles": models.Article.objects.all()}
    return render(request, template_name, context)

models.Article.objects.all()でデータベースのArticleのテーブルにあるすべてのデータを取得しています。
取得したデータにarticlesという変数名を付けてテンプレート側に渡しています。

models.Article.objects.all()で取得したデータはリスト同様、for文で回すことが可能です。

テンプレートの記述

新しくテンプレートファイルtemplates/blog/article_all.html(blogはアプリケーション名)を作りましょう。
中身は以下のようにします。

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>article all</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <h3>投稿された記事一覧</h3>
    {% for article in articles %}
        <p>{{ article.title }} - {{ article.posted_at }}</p>
    {% endfor %}
</body>
</html>
{% for article in articles %}
    <p>{{ article.title }} - {{ article.posted_at }}</p>
{% endfor %}

articlesが先ほどテンプレートに渡した変数です。
中身をfor文で回しています。
{{ article.title }}で記事のタイトルを、{{ article.posted_at }}で記事の投稿時間をそれぞれ展開しています。

では、表示させてみましょう。

python manage.py runserver

でサーバーを起動した後にhttp://localhost:8000/article/all/にアクセス

記事の一覧ページ

たぶんこんな感じになるはずです。

詳細ページの追加

それぞれの記事の詳細ページを追加します。

URL

http://localhost:8000/article/数字/がそれぞれのページのURLになるように設定します。

と、ここで疑問です。
今までは、それぞれ個別のページに固有のURLを設定しました。

では、http://localhost:8000/article/数字のように特定の規則を持ったURLを指定するにはどうすればよいのでしょうか?

答えは、正規表現を用いることです。

数字にマッチする正規表現はpythonではr"\d+"又は、r"[0-9]+"と表すことができます。

それを適用すると

re_path(r"article/(\d+)/", views.article, name="article")

とすることができます。

が、

ちょっと見づらいですよね。

そこで、Django2系から使えるようになった機能、パスコンバータを利用します。
簡単に紹介すると正規表現を簡単に表現できるようにしたものです。

パスコンバータを利用して書くと以下のように書くことができます。

path("article/<int>/", views.article, name="article")

<int>で数字を受け取ることを表現できます。
受け取る値が数字であることがものすごくわかり易くなりました。

パスコンバータでは数字意外にも任意の文字列を受け取れる<str>や数字とアルファベットの組み合わせを受け取れる<slug>等があります。

詳しくは

[Django] 2系から導入されたパスコンバータとは

urls.pyにはパスコンバータを利用したほうを書いておきましょう。

from django.urls import path
from . import views

urlpatterns = [
    path("", views.index, name="index"),
    path("new/", views.new, name="new"),
    path("article/all/", views.article_all, name="article_all"),
    path("article/<int:pk>/", views.view_article, name="view_article"), # 追記
]

view

views.pyview_articleを書き足します。

from django.shortcuts import render
from django.http import Http404 # 追記
from . import models

# 略

def view_article(request, pk):
    template_name = "blog/view_article.html"
    try:
        article = models.Article.objects.get(pk=pk)
    except models.Article.DoesNotExist:
        raise Http404
    context = {"article": article}
    return render(request, template_name, context)

今までと違い二つ引数を受け取るようになっていますね。
第一引数は今までと変わりありませんが、第二引数で新たにpkを受け取るようになっています。
このpkには先ほど設定したURLの<int:pk>にマッチした数字が入ります。
例えばhttp://localhost:8000/articles/1/にアクセスするとpkには1が入ります。

ちなみに<int:pk>のpkと引数で受け取るpkは対応しているので名前を揃えておきましょう。
違う名前にしているとエラーを吐かれる可能性があります。

関数の中では、ここで受け取った数字を利用してデーターベースから該当するデータを取り出しています。
models.Article.objects.get(pk=pk) の部分がそれにあたります。
get(pk=pk)でデータベースの主ーキーを利用して当該の記事のデータを取得しています。

except models.Article.DoesNotExist:で記事が見つからなかったときに、ページがない404エラーを出すようにしています。

テンプレート

新しくテンプレートファイルtemplates/blog/view_article.html(blogはアプリケーション名)を作成します。

中身はこんな感じで

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>{{ article.title }}</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <h3>{{ article.title }}</h3>
    <small>投稿日時 : {{ article.posted_at }}</small>
    <small>最終更新 : {{ article.last_modify }}</small>
    <p>{{ article.text }}</p>
</body>
</html>

リンクの設定

詳細ページを追加したので一覧ページから詳細ページへ遷移できるリンクを設置しましょう。
利便性向上のためにトップページに戻れるリンクも設置します。

article_all.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>article all</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <a href="{% url 'index' %}">トップページへ</a>
    <h3>投稿された記事一覧</h3>
    {% for article in articles %}
    <p><a href="{% url 'view_article' article.pk %}">{{ article.title }} - {{ article.posted_at }}</a></p>
    {% endfor %}
</body>
</html>
<a href="{% url 'view_article' article.pk %}">

またしても、見慣れない書き方が出てきました。

{% url 'view_article' %}ならまだわかります。
ですがそれに加えてarticle.pkなるものがくっついています。

規則を持ったURLを設定した(正規表現、又はパスコンバータを利用した)場合、urlテンプレートタグに追加で規則にマッチする引数を与えてあげる必要があります。

詳細ページには

path("article/<int:pk>/", views.view_article, name="view_article")

のようにパスコンバータを利用して規則を持ったURLを設定しているので、引数を与えてやる必要があります。

intを使っているので数字の引数です。

続いて、トップページに「新規記事の投稿ページ」へのリンクと「一覧ページ」へのリンクを設定しましょう。
こちらにもトップページに戻れるリンクを設置しておきます。
index.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>my-blog-index</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
    <p>まい ぶろぐ とっぷぺーじ</p>
    <a href="{% url 'new' %}">新規記事の投稿</a>
    <a href="{% url 'article_all' %}">投稿された記事一覧</a>
</body>

</html>

残りのページにも適宜リンクを設置していきます。

new.html

<!DOCTYPE html>
<html>

<head>
    <meta charset="utf-8" />
    <title>my-blog-new</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>

<body>
    <a href="{% url 'index' %}">トップページへ</a>
    
    <h3>まい ぶろぐ 新規記事の投稿ページ</h3>
    <form action="" method="post">
        {% csrf_token %}
        <p><label for="title_input">記事のタイトル</label></p>
        <p><input type="text" name="title" id="title_input"></p>
        <p><label for="text_area">記事の本文</label></p>
        <p><textarea name="text" id="text_area" cols="30" rows="10"></textarea></p>
        <p><button type="submit">投稿</button></p>
    </form>
</body>

</html>

view_article.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>{{ article.title }}</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
  <p><a href="{% url 'index' %}">トップページへ</a></p>
  <p><a href="{% url 'article_all' %}">一覧へ戻る</a></p>
  <h3>{{ article.title }}</h3>
  <small>投稿日時 : {{ article.posted_at }}</small>
  <small>最終更新 : {{ article.last_modify }}</small>
  <p>{{ article.text }}</p>
</body>
</html>

リンクを設置したのでちゃんと移動できるかどうか確認してみましょう。

Djangoサーバを起動して

python manage.py runserver

適当なページへアクセス
http://localhost:8000/

ページ内にあるリンクを踏みまくって遊んでみましょう。

新しく記事を投稿して一覧ページに要素が増えるかもチェックしておきましょう。

飽きたらやめてもらって大丈夫です。

まとめ

データーベースからのデータの全件取得は

models.Model.objects.all()

データベースからの条件に一致するデータ一件の取り出しは

models.Model.objects.get(フィールド名=値)

テンプレートに変数を渡すにはrender関数の第三引数に辞書の形にして渡す(キーがテンプレート内での変数名)。

{% for v in values %}でテンプレート内で繰り返しができる(終了点には{% endfor %}をお忘れなく)。

テンプレート内での変数の展開は{{ 変数名 }}

{% url 'path関数で指定したname' %}でURLを自動生成できる。

最後に

今回はちょっと長めでしたが、基本的にはこれでブログとして機能しうるものは出来ました。
ただ、このままだと寂しいので次回は、投稿した記事を編集できる機能と削除機能を追加しようと思います。

デザインはやらへんのかいっ!

…最終回あたりにBootstrap使ってみるに堪えるようなものにはします。
お楽しみに

次回

[Django] 記事の編集ページと記事の削除機能の追加