Django公式チュートリアル(1~4)で分からない所、徹底的に調べた。

あとで読む

最初に

本格的なWebアプリケーションを作成したいのでPythonのフレームワークDjango(読み方:ジャンゴらしい。ディーどこ行った。)についてチュートリアルをこなしながら学んで行こうと思う。実際に作成するアプリは質問に対して回答して投票を表示するアプリになる。

この記事は公式チュートリアルの1〜4までに気になった事躓いた事をまとめていく。全部で1〜7まであるが4までにアプリは完成する。

5からはテストコード等を書いていくのでボリュームが多くなるため、前編として今回の記事を投稿する。後編も必ず書こうと思う。

今回のチュートリアルで作成したもの

質問一覧のページがあって、そこから質問に対して投票を行う。その後今まで投票された数を表示するページリダイレクトされる。

django-tutorial

VSCodeのPython用の拡張機能をインストールする。

コードを書くにあたって構文エラーは事前に無くしたいので、拡張機能をインストールする。マイクロソフトがPython用に提供しているものがあるのでそちらをインストールする。

自分はanaconda環境での使用をしているので下記の通知が出てきた。

terminal.integrated.inheritEnvfalse にした方が良いらしい。

We noticed you're using a conda environment. If you are experiencing issues with this environment in the integrated terminal, we recommend that you let the Python extension change "terminal.integrated.inheritEnv" to false in your user settings.

VSCodeの code > preference > settings に terminal.integrated.inheritEnv と入力して出てきたチェックを外す。

【Mac/Python】VSCodeターミナル動作が通常ターミナルと違う時 | ゆうきのせかい

それだけだと Import "django.contrib" could not be resolved from source という警告がでたままなので、赤枠の箇所をクリックしてDjangoをインストールした環境を選択すると警告文が消えます。

インストール

好きなフォルダーを作成して、そこに開発していく。Pythonの環境構築は自分は下記のように行っている。

https://techblog-pink.vercel.app/posts/cc111706c3167

anacondaで仮想環境を作成して、Djangoをインストールしていく。

pip install Django

開発を始めたいフォルダに移動して下記を実行する。

django-admin startproject mysite

mysiteというディレクトリが生成される。

mysite/
    manage.py
    mysite/
        __init__.py
        settings.py
        urls.py
        asgi.py
        wsgi.py

フォルダはこのような構成になっている。 mysite/mysite となっているのが不思議だ。外側の mysite はDjangoのシステム的には何でも良いらしい。

urls.py はプロジェクトのURLを宣言する。目次のような機能を提供する。

サーバを起動する。

実行すると http://127.0.0.1:8000/ でサイトにアクセス出来るようになる。

最初はデータベース等の設定をしていないので、ターミナルに警告が表示されるが他に問題がなければアクセス出来る。 ctr + c でサーバを終了することが出来る。

python manage.py runserver

GitHubで管理する

これから開発していくので、Githubにコードをあげて進捗を管理したい。しかし、このままだと settings.py に書かれた SECRET_KEY も一緒にアップロードしてしまうので別ファイル local_settings.pySECRET_KEY を書く。ちなみに私は気付かずに一度GitHubに、そのままアップしてしまった。そのため新たに SECRET_KEY を作成する手間が掛かる。

local_settings.py

SECRET_KEY = 'settings.pyにあったシークレットキーまたは、新たに作成したもの'

この変数を settings.py で読み込んでいく。下記のコードを追加する。

try:
    # 同じ階層のlocal_settingsファイルからSECRET_KEYをkeyとして読み込む。
    # 参考記事等では .local_settingsとなっているが、local_settingsはファイルなので必要ない
    # 将来的に複雑にファイルを分ける必要が出てフォルダにする場合は .が必要となる。
    # ダメだった .local_settingsが正しかった。Django環境だとパッケージとして見なされるのか...
    from .local_settings import SECRET_KEY as key
except ImportError:
    pass

# シークレットキーの部分を読み込んだ変数に置き換える。
SECRET_KEY = key

実際に test.pytest2.py を同じ階層に作成してどのように読み込まれるか調べてみた。

test.py

hello = "hello"

test2.py

# .testとするとImportError: attempted relative import with no known parent package
# と表示される。
# asを付けない場合は helloで読み込まれる。
from test import hello as A

print(A)

settings.py と同じ環境に出来ていると思ったが、全然違った。 local_settings.py は パッケージとして認識されるが、 test.py はパッケージとして認識されないので . を使用するとエラーになる。そもそも python test2.py と直接実行しているので settings.py とは違う実行状況になる。

詳しくはここで回答を頂いている。

SECRET_KEYの作成

自分は知らずにシークレットキーをGitHubにあげてしまったので新たに作り直す必要があるのでシークレットキーを生成してくれるプログラムを実行する。これを直接ターミナルで実行するとシークレットキーが生成されるのでそれを local_settings.py に貼り付ける。

get_random_secret_key.py

from django.core.management.utils import get_random_secret_key

secret_key = get_random_secret_key()
text = 'SECRET_KEY = \'{0}\''.format(secret_key)
print(text)

これでようやくチュートリアルに専念してコードを書き進める事が出来る。

プロジェクトとアプリ

Djangoではプロジェクトの中にアプリが含まれる。なので特定のDjangoで作成されたWebサイト全体をプロジェクトと呼び、その中に含まれる小規模な投票アプリ、ログシステムをアプリと呼ぶ。

アプリを作成する。

manage.py と同じ階層に移動して

python manage.py startapp polls

を実行すると polls というフォルダが生成される。

polls/
    __init__.py
    admin.py
    apps.py
    migrations/
        __init__.py
    models.py
    tests.py
    views.py

これでプロジェクトにpollsというアプリが作成された事になる。

Viewを作成する。

URLからパスにアクセスがあって、その際に実行する関数がViewになるここでHTMLファイルを返したりと処理を決めることが出来る。

views.py に記述する。

def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)

def results(request, question_id):
    response = "You're looking at the results of question %s."
    return HttpResponse(response % question_id)

def vote(request, question_id):
    return HttpResponse("You're voting on question %s." % question_id)

viewメソッドの第一引数には必ずHttpRequestクラスを受け取る。

引数 request には今ユーザがアクセスしているURLやIPアドレスなどの情報が入ってくる。そして戻り値としては HttpResponseクラスを返す必要がある。

実際にHttpResponseとHttpRequestの中身がどんな感じになっているのか気になる人は下記のページで確認できます。

ざっくりですが、 views.py があって実行されるとクラスの中身はこんな感じに格納されているみたいです。

def sample(request):
    print(request)
    response = HttpResponse('')
    print(response)
    return response

# 実行結果
# <WSGIRequest: GET '/hello/'>
# <HttpResponse status_code=200, "text/html; charset=utf-8">

Djangoはリクエストを受け取ってレスポンスを返しているだけです【詳しく解説】

views.py だけではURLと紐づいていないので URLconfを作成する。

URLconf urls.py を作成する

URLconfを作成するには urls.py というファイルを views.py と同じ階層に作成する。mysiteフォルダ内には urls.py がすでにあるので pollsフォルダ内に作成する。中身はこんな感じになる。

include()を使ってアプリのURLを結び付ける。

polls/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('<int:question_id>/results/', views.results, name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

<int:question_id>views.py 関数の引数として渡される。この <> を使用すると、URLの一部がキャプチャされ渡される。文字列 :quesiton_id> は一致するパターンを定義し、 <int: の部分はURLパスに当てはまる値の型を指定している。なので <str:, <slug: などもある。

そして、これをmysiteフォルダ内の urls.py に結びつけてあげる必要がある。一応こっちが最初に読み込まれるのでここに後から追加したアプリのURLを include() を使って追加していくイメージになる。

mysite/urls.py

from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('polls/', include('polls.urls')),
    path('admin/', admin.site.urls),
]

これでpollsアプリのURLを結び付けることができた。サーバを起動して http://localhost:8000/polls/ にアクセスするとViewが返されるようになる。

path()の引数

4つの引数を受け取ることが出来る。そのうち routeview の2つは必須で残り kwargsname は省略出来る。

  • route:URLパターンを含む文字列が入る。リクエストを処理する際に urlpatterns を順番にみて最初にマッチしたものを取り出す。このパターンはGET、POSTのパラメータに影響は受けないあくまでURLパスだけを見る。
  • view:URLがマッチしたら、そこに付随するView関数を返す仕組みになっている。
  • kwargs:任意のキーワード引数を辞書としてView関数に渡せる。
  • name:URLに名前を付ける事で reverse() を使って呼び出せるようになり、htmlのテンプレートでformに指定するURLを変更する際に動的にURLが変更されるようになる。

polls/urls.py がこんな感じだとして

from django.urls import path

from . import views
import re

urlpatterns = [
    path('', views.index, name='index'),
    path('<int:number>', views.subView, name='suburl'),
]

試しに下記の views.py を実行してみる。

from django.http import HttpResponse
from django.urls import reverse

def index(request):
    urlName = reverse('index')
    print(urlName)
    return HttpResponse("Hello, world. You're at the polls index.{0}".format(urlName))

# 実行結果
# /polls/

こうすると例えば pollllllls/urls.py とURLを変更してもコードを変更する必要がないので、変更に強いコードになります。

※pathのnameについて

python、djangoのurls.pyで設定するnameってなんやねん?? - Qiita

DjangoのURL

ユーザがDjangoで作られたサイトにアクセスした際にどのような処理が走るのか。

  1. ROOT_URLCONFに設定されているURLを確認する。(HttpRequestオブジェクトにurlconfという属性が設定されている場合はその値をROOT_URLCONFとする。)
  2. urlpatternsという名前の変数を探す。この変数値は django.urls.path() または django.urls.re_path() インスタンスのsequenceでなければならない。
  3. urlpatternsから順番に要求されたURLパターンを探す。
  4. マッチしたらViewを返す。
  5. マッチしなかったらエラーハンドリングビューを返す。

URLconfのサンプル

pathの左側がマッチするURL(route), 右側がマッチしたら呼び出される関数(view)

from django.urls import path

from . import views

urlpatterns = [
    # /articles/2003/にアクセスした場合
    # Views.special_case_2003(request)を呼び出す。最後の/もしっかりないとマッチしない。
    # 引数としてrequestが関数に渡る。
    path('articles/2003/', views.special_case_2003),
    path('articles/<int:year>/', views.year_archive),
    path('articles/<int:year>/<int:month>/', views.month_archive),
    path('articles/<int:year>/<int:month>/<slug:slug>/', views.article_detail),
]

/articles/2005/03/ というアクセスがあった場合上記のパターンの中からviews.month_archive(request, year=2005, month=3) という views.py (上記で記したviews.pyとは別で例としてあげてるurls.pyに対応するviews.pyがあったらという話で見てもらいたい。)に書かれたviewメソッドに引数を渡して呼び出す事になる。引数 request にはHttpRequestクラスが入り 今ユーザがアクセスしているURLやIPアドレスなどの情報が入ってる。

/articles/2003/03/building-a-django-site/ なら最後のパターンにマッチして、このように views.article_detail(request, year=2003, month=3, slug="building-a-django-site") 関数を呼び出す。

タイムゾーンの設定

デフォルトではUTCと世界標準時間になっているので、ここで日本時間に変更しておきたいと思う。 settings.pyTIME_ZONE = 'UTC'TIME_ZONE = 'Asia/Tokyo' に変更する。

データベースの作成

ここでデータベース用語についてまとめて置く。

  • カラム:縦列の事を指し、別名では列と呼ばれる。
  • カラム名:列全体に付けられた名前
  • フィールド:データが入っている場所。
  • フィールド名:そのデータが入っているカラム名を指す。
  • 項目名:フィールド名を指す。
  • テーブル:Excelでいうシートのようなもの。
  • レコード:データそのものを指す言葉になる。もう一つは横列の事を指し、行と呼ばれる。そして行をロウと呼ぶこともある。

これからデータベースを設定していく、チュートリアルの段階なのでまずは複雑な設定の必要がないSQLiteを使用していく。

python manage.py migrate

実行すると settings.py に書かれた INSTALLED_APPS の設定を参照して mysite/settings.py ファイルのデータベース設定に従って必要な全てのデータベーステーブルを作成する。

migrate(マイグレート)するとは

データベースを削除してから作り直すと、DBに保存されている情報が全て削除されてしまう。こういった事態を回避する方法として、データベースマイグレーションを行う方法が生まれた。マイグレーションとは、DBに保存されているデータを保持したまま、テーブルの作成やカラムの変更などを行うことが出来る。

モデルの作成 models.py

models.py はデータを保存したり、取り出したりするときの設定を記録するファイルになる。データベースの取扱説明書と書かれることが多い。

models.py からデータベースが作成される流れ

  1. models.pyファイルでモデル(データベースの型)を作成する。
  2. migrationファイルを作成する。
  3. migrate(最終的にはおそらくSQL文に変換されて、データベースに命令を出してデータベースを作成する。)する。ここで上記のファイルを元にデータベースが作成される。

つまりモデルはデータベースのレイアウトとそれに付随するメタデータになる。

簡単な例を示す。(チュートリアルとは関係ないモデル)

# modelsモジュールを読み込むこれはデータベースを作成するのに必要な機能が格納されている。
from django.db import models

# データベース作成機能を継承してモデルを書き込んでいく。
class BookModel(models.Model):
    # booknameという文字を入力することが出来るフィールドを作成する命令をだす。
    # bookname = models.CharField(max_length = 50)とすると50文字までと制限をかけれる。
    # CharFieldには必須の引数がありmax_lengthを設定しないといけない。
    bookname = models.CharField(max_length = 50)
    # CharFieldとほぼ同じで文字列を扱うがTextFieldの方がデータの読み出し等にコストがかかるらしい。
    summary = models.TextField()
    # 整数値を入れるフィールドを作成する。
    rating = models.IntegerField()

ForeignKey(外部制約キー)

これを使ってこの後2つのモデルを双方向に参照できるようにするのだが、その前にデータベースにおいて外部制約キーまたは外部キーとも呼ばれるがどのような役割を果たしているのかみていこうと思う。

外部キーとは関係データベースにおいてデータの整合性を保つための制約(参照整合性制約)

外部キーに設定されている列(子テーブルのカラム)には、参照先となるテーブルの列内(親テーブルのカラム内)に存在している値しか設定できない。

そのため、外部キーに設定されている子テーブルの列内に親テーブルの列内に存在しない値を追加しようとするとエラーになる。

なので新しく値を追加したい場合は一度、親テーブルで追加する必要がある。

このように制約を結ぶ事でデータの整合性を保つ事ができる。

Djangoでは foreign keyが設定されている方が子テーブルになる。

今度は少し複雑なモデルを見ていく。

図のようなQuestionとChoiceという2つのモデルを作成する。

ChoiceにQuestionが ForeignKey を使って紐ずけられている。 on_delete=models.CASCADE は紐づけられたモデルが削除される際にどのような動作をするかを決める事が出来る。削除された後そのモデルだった部分をNullで埋めたり、そもそも削除できないようにしたりと出来る。 CASCADE は紐づけられた側のモデルで関連するオブジェクトも削除するという動きになる。なので Questionが削除されたら、Questionと関連のあるChoice側のオブジェクトも削除するような動作を取る。

詳しくはここの記事が分かりやすい。

Django2.0から必須になったon_deleteの使い方 - DjangoBrothers

from django.db import models

class Question(models.Model):
    # Question textというフィードを作成してそこに入る文字列は200文字までと制限している。
    question_text = models.CharField(max_length=200)
    # 基本的には変数名がフィールド名として使用されるが、引数で文字列を渡す事でフィールド名設定する事が出来る。
    pub_date = models.DateTimeField('date published')

class Choice(models.Model):
    # Question ← → Choiseと双方向のやりとりが可能となる。
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
    choice_text = models.CharField(max_length=200)
    # Votesフィールドは整数値を受け付ける。最初は0が入る。
    votes = models.IntegerField(default=0)

モデル間の双方向やりとりについて

【Django】1対多の関係( related_name, _set.all() )について - Qiita

モデルを作成したのでデータベースにマイグレート(モデルを元にデータベースのレイアウトを作成するデータを追加するわけではない。)していく

migrationファイルを作成する前に、新たに作成したアプリpollsを伝える必要がある。 settings.pyINSTALLED_APPS の配列に 'polls.apps.PollsConfig', を追加する。

次にmigrationファイルを作成する。

新たにファイルを作成する必要はなくターミナルでコマンド実行する。

python manage.py makemigrations polls

実行すると models.py を元にmigrationファイルが生成される。

python manage.py migrate

実行するとmigrationファイルを元にデータベースが作成される。

試しにコマンドラインからDjango shellを通してデータベースを変更したりしてみる。

シェルに入るには下記のコマンドを実行する。

python manage.py shell

するとPythonコード >>> を書ける状態になるのでここからデータベースにアクセスするコードを書く。

# 作成したモデルを読み込む
from polls.models import Choice, Question

# 格納されたデータを確認する。まだ追加していないので、空になっている。
Question.objects.all()

from django.utils import timezone
# モデルにデータを追加する。
q = Question(question_text="What's new?", pub_date=timezone.now())
# データベースに保存する。
q.save()

q.id
q.question_text
q.pub_date

# データの上書き
q.question_text = "What's up?"
q.save()

# データが格納されているのが確認できる。
Question.objects.all()
# 実行結果:<QuerySet [<Question: Question object (1)>]>

このままではadminページでオブジェクト名が Question object (1) と表示され分かりにくいので 特殊メソッド __str__() をモデルに追加する。

あと追加で was_published_recently(self) メソッドを書きました。

データが最近追加されたかどうかを判定するメソッドで True or False で返します。

class Question(models.Model):
  # クラス変数を定義する。データベースフィールドを表現している。
  # Charフィールドは文字のフィールド
  question_text = models.CharField(max_length=200)

  # 日時のフィールド
  pub_date = models.DateTimeField('date published')

  def __str__(self):
    # インスタンスを生成して、printした際にここが実行される。
    # シェルで表示されるオブジェクトに質問名が使われるだけでなく
    # adminでオブジェクトを表現する際にも使用されるので追加する必要がある。
    return self.question_text

  def was_published_recently(self):
    now = timezone.now()
    # now - datetime.timedelta(days=1)は今の時間から一日引いた日付を出す。
    # 2021-05-19 23:29:56.216634こんな感じの値になる。
    # pub_dateが現在時刻より過去で現在時刻から一日以内の場合はTrueを返すメソッド
    return now - datetime.timedelta(days=1) <= self.pub_date <= now
  
class Choice(models.Model):

  # これはChoiceがQuestionに関連付けられている事を伝えている。
  # データベースの多対一、多対多、一対一のようなデータベースリレーションシップに対応する。
  # Question ← → Choiseと双方向のやりとりが可能となる。
  question = models.ForeignKey(Question, on_delete=models.CASCADE)
  choice_text = models.CharField(max_length=200)
  votes = models.IntegerField(default=0)

  def __str__(self):
    return self.choice_text

すると下記のように表示されるので、

何のデータが入っているのか分かり易くなった。

Djangoのshell内でも下記のようにオブジェクトの中身が分かり易くなった。

from polls.models import Choice, Question
Question.objects.all()
# 実行結果:<QuerySet [<Question: What's up?>]>

# 続けて色々な関数を試してデータベースから
# データを取得してみる。
# filterをかけてデータを取得する。
# idはデータベースにデータを追加した際に1から順番に自動で割り振られる。
Question.objects.filter(id=1)
# 実行結果:<QuerySet [<Question: What's up?>]>

# Questionオブジェクトのquestion_textフィールドで"What"から
# 始まるデータを取得する。
Question.objects.filter(question_text__startswith="What")
# 実行結果:<QuerySet [<Question: What's up?>]>

from django.utils import timezone

# 今年作成されたデータを取得する。
current_year = timezone.now().year
Question.objects.get(pub_date__year=current_year)

# id 2のデータを取得する。
# filterで指定しなくてもgetでも取得できる。
Question.objects.get(id=2)
# 実行結果はない場合はエラーになります。

# プライマリーキーと呼ばれるものでいまいちidとの違いが分からない。
# 取得するデータはidの場合と同じになる。
q = Question.objects.get(pk=1)
q.was_published_recently()
# 実行結果:True

# ChoiseはQuestionと関連付けられてるのでQuestionからも
# データにアクセスする事ができる。
# Choice側にはまだデータを入れてないので結果は何も表示されない。
# Choicecは質問に対する回答の選択肢をデータとして持つ。
# qには今What's upという質問が入っているので、それに対しての
# 選択肢を作成した。
q.choice_set.all()
q.choice_set.create(choice_text="Not much", votes=0)
q.choice_set.create(choice_text="The sky", votes=0)
c = q.choice_set.create(choice_text='Just hacking again', votes=0)

# 選択肢が関連づけられている質問を返す。
c.question
# 実行結果:<Question: What's up?>
q.choice_set.all()

# 選択肢が何個あるか数える。
q.choice_set.count()

Choice.objects.filter(question__pub_date__year=current_year)
# 実行結果:<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>

# just hackingの選択肢だけ削除する。
c = q.choice_set.filter(choice_text__startswith="Just hacking")
c.delete()

pkとidの違い

pkは primary key の略で、データベースでは 主キー と呼ばれている。主キーはテーブルで一意の値を取る。

どのレコードを主キーにするかはフィールド名を定義する時に primary_key=True を付ければ設定できる。

codeというフィールドに入る値を主キーとする。

code = models.CharField(max_length=10, primary_key=True)

djangoの場合はModel(=テーブル)には必ず1つの主キー用のフィールドが必要になる。ユーザが定義しない場合はidという名前のAutoFiekd(int型の連番1~n)が作成される。

そのため、pkキーを定義しない場合はpkはidのショートカットになる。

pkキーを上記の code のように定義した場合はidは作成されない。

SQLに直接アクセスする。(おまけ)

sqllite3を使用してデータベースを作成している場合はmysiteフォルダ内にデータベースのファイルが生成されていると思うので、下記のコマンドからSQLで操作するシェルに入る事ができる。

sqlite3 db.sqlite3
# シェル内での操作
# 作成されたテーブル一覧を確認できる。
>>> .table
auth_group                  django_admin_log          
auth_group_permissions      django_content_type       
auth_permission             django_migrations         
auth_user                   django_session  
                            # 先ほどモデルから作成されたテーブル          
auth_user_groups            polls_choice              
auth_user_user_permissions  polls_question

# 構造を確認できるみたいだけど、見てもよく分からなかった。
>>> .schema

# シェルから抜ける。
>>> .quit

モデルからデータベースを操作する事ができたので、次に先ほど登場したadminページにアクセスしたいと思う。

adminページアクセスする。

ログインが必要なのでユーザを下記のコマンドから作成する。

python manage.py createsuperuser
# 実行すると下記の入力画面が登場する。
Username:名前を入力
Email address:@と.comがあれば架空で良い
Password:
Password(again):

ユーザを作成したら開発サーバを起動してhttp://127.0.0.1:8000/admin/ にアクセスするとログイン画面となるのでログインする。

するとそこから作成したモデルを閲覧したりデータを追加したりできる。

ChoiceモデルからはQuestionを選択して使用することしか出来ないのが確認できる。 右側にある + ボタンを押すと Questionのページに飛びそこから新しい質問を追加することはできる。

Views.pyからデータベースの値を取得する

先ほどDjango shellで使用したPythonコードを使って、 views.py にデータを取得していく。

# 最後はHttpResponseを返す必要があるのでimportする。
from django.http import HttpResponse

# データベースを操作するためにモデルを読み込んでおく。
from .models import Question

def index(request):
    # データベースから最新5件を取得する。
    # こんな感じのデータになる。<QuerySet [<Question: test3>, <Question: hello>, <Question: what's up?>]>
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    # "test3, hello, what's up?"区切った文字列にしてHttpResponseに渡す。
    output = ', '.join([q.question_text for q in latest_question_list])
    return HttpResponse(output)

# Leave the rest of the views (detail, results, vote) unchanged

Viewとページデザインを切り離す

Viewではデータの取得や操作を専門的に行ってもらい、そのデータをテンプレート(htmlにPythonの変数を入れられる。)に渡してページをレンダリングしてもらうようにする。

pollsディレクトリの中に、templatesディレクトリを作成する。システムでそのディレクトリを認識する。作成した templatesディレクトリにpollsフォルダを作成する。なので polls/templates/polls みたいなディレクトリが完成する。その中にテンプレート index.html を作成する。なぜ templates/polls とするのかというとDjangoは名前がマッチした最初のテンプレートを使用するので、もし異なるアプリケーションの中に同じ名前のテンプレートがあるとそちらを読み込む。それを回避するために名前空間(所属する領域)を与えている。

views.py をテンプレートにデータを渡せるように書き換える。

from django.http import HttpResponse
from django.template import loader

from .models import Question

def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    # テンプレートを読み込む
    template = loader.get_template('polls/index.html')
    # 辞書型に最新5件のデータを格納する。
    context = {
        'latest_question_list': latest_question_list,
    }
    # 辞書型のデータをテンプレートに渡してページを作成する。その結果をHttpResponseに返す。
    return HttpResponse(template.render(context, request))

view.pyをさらに短くする。

from django.template import loader

from django.http import HttpResponse

を使わない書き方

より簡素にする事が出来る。

その場合、 render() 関数は第一引数に requestオブジェクト , 第二引数に テンプレート名 , 第三引数に 辞書型(テンプレートに渡したいデータ) を記述する。

from django.shortcuts import render
from .models import Question

def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    context = {'latest_question_list': latest_question_list}
    return render(request, 'polls/index.html', context)

404エラーを出力する。

from django.http import Http404
from django.shortcuts import render

from .models import Question

def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    context = {'latest_question_list': latest_question_list}
    return render(request, 'polls/index.html', context)

# 質問の詳細ページのビュー
def detail(request, question_id):
    try:
        # アクセスのあったURLでpkの値が変わる。/polls/1/なら1になる。
        # データベースでエラーになるとHttp404を出力する。
        question = Question.objects.get(pk=question_id)
    except Question.DoesNotExist:
        raise Http404("Question does not exist")
    return render(request, 'polls/detail.html', {'question': question})

上記のdetail()を短くする。

django.shortcuts にはこうしたコードを省略する関数が多くあるので調べると面白いかもしれない。

from django.shortcuts import get_object_or_404, render

from .models import Question

def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    context = {'latest_question_list': latest_question_list}
    return render(request, 'polls/index.html', context)# モデルにアクセスするobjects.get()とHttp404が一緒になっている。
def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})

index.html

index() に対するテンプレートにはこのように記述する。

ビューで latest_question_list オブジェクトが辞書型に格納されて渡されているので、それを受け取ってテンプレート内でオブジェクトに格納された値を属性アクセス . して取得している。

<!-- 受け取った変数にデータがあるか確認する。 -->
{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
        <li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

index.htmlのハードコード(直接記述している)を削除する

このように直接URLを書き込むと変更に弱いコードになってしまうので、 polls.urlsモジュールのpath() 関数でname引数を定義したのでそれをURLに使用する。 {%url%} を使う。

<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>

変更後

<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>

そしてテンプレートを入れるディレクトリを作成した際のように名前空間を追加する。システムが別々のアプリ内で同じname引数を含んでいても区別が付けられるように template/polls/detail.html にアクセスしたい場合は polls:detail と記述する。

<!-- 受け取った変数にデータがあるか確認する。 -->
{% if latest_question_list %}
    <ul>
    {% for question in latest_question_list %}
       <li><a href="{% url 'polls:detail' question.id %}">{{ question.question_text }}</a></li>
    {% endfor %}
    </ul>
{% else %}
    <p>No polls are available.</p>
{% endif %}

そして、URLconf(urls.py)に名前空間を追加 app_name = 'polls'

from django.urls import path

from . import views

# ここを新たに追加した。
app_name = 'polls'
urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('<int:question_id>/results/', views.results, name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

こうするとモジュールに指定されたURLの定義を検索出来る。例えば polls/specifics/12 のようにURLを変更した場合

urls.py に書かれたパスを変更する事でテンプレート側に変更を加える必要がない。

path('specifics/<int:question_id>/', views.detail, name='detail'),

detail.html

detail() に対するテンプレートはこのように記述する。

<h1>{{ question.question_text }}</h1>
<ul>
{% for choice in question.choice_set.all %}
    <li>{{ choice.choice_text }}</li>
{% endfor %}
</ul>

フォームを使って質問に対して回答を送信する。

detail.html<form> を追加してサーバにデータを送信して質問に対して、投票出来るようにする。

下記のように detail.html を変更する。

<!-- 質問の内容 -->
<h1>{{ question.question_text }}</h1>

<!-- もしデータベースから質問が取得出来ない場合エラーが表示される。 -->
{% if error_message %}<p><strong>{{ error_message }}</strong></p>{% endif %}

<form action="{% url 'polls:vote' question.id %}" method="post">

<!-- セキュリティのため -->
{% csrf_token %}

<!-- 質問に対する選択肢を並べる -->
{% for choice in question.choice_set.all %}
    <!-- forloop.counterはforタグのループが何度実行されたかを表す値です。 -->
    <input type="radio" name="choice" id="choice{{ forloop.counter }}" value="{{ choice.id }}">
    <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br>
{% endfor %}
<input type="submit" value="Vote">
</form>

views.pyにvote()関数を追加する

コードの流れとしては

  1. ユーザがdetailページの質問に対する選択肢を選択する。

  2. Voteボタンをクリックする。

  3. データがサーバに送信される。

  4. 選択された選択肢の投票数をインクリメントする。

  5. results.htmlにリダイレクトする。Postデータが成功した後は基本的に HttpResponse ではなく HttpResponseRedirect を返す必要がある。

# 追加した。HttpResponse, HttpResponseRedirect
from django.http import HttpResponse, HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
# 追加した。reverse
from django.urls import reverse
# Choiceを追加した。
from .models import Choice, Question

def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    context = {'latest_question_list': latest_question_list}
    return render(request, 'polls/index.html', context)# モデルにアクセスするobjects.get()とHttp404が一緒になっている。
def detail(request, question_id):
    question = get_object_or_404(Question, pk=question_id)
    return render(request, 'polls/detail.html', {'question': question})

# 質問に対して選択して投票する。
def vote(request, question_id):
    # まず質問があるかどうか確認する。
    question = get_object_or_404(Question, pk=question_id)
    try:
        # ユーザが選択した値からpk値を取得して、それを元にモデルから選択肢のオブジェクトを取得する。
        # なければYou didn't...choiceと表示される。
        selected_choice = question.choice_set.get(pk=request.POST['choice'])
    except (KeyError, Choice.DoesNotExist):
        # Redisplay the question voting form.
        return render(request, 'polls/detail.html', {
            'question': question,
            'error_message': "You didn't select a choice.",
        })
    else:
        # 選択肢オブジェクトから何回投票されたか表示するvotesオブジェクトをインクリメントする。
        selected_choice.votes += 1
        # データベースに保存する。
        selected_choice.save()
        # Always return an HttpResponseRedirect after successfully dealing
        # with POST data. This prevents data from being posted twice if a
        # user hits the Back button.
        # データの保存に成功したら、results.htmlにリダイレクトする。
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))

reverse()関数とは

この関数を使うと、vote関数中でのURLのハードコードを防ぐ事が出来る。

引数としては polls:results リダイレクト先のビュー名とそのビューに与えるURLパターン question.id を渡せる。

reverse('polls:results', args=(question.id,))

# 返り値
'/polls/3/results/'

Post通信が成功した際のresults関数を作成する。

views.pyにresults関数を追加する。

# 先ほどまで書いてきたviews.pyにresults関数を追加する。

def results(request, question_id):
    # 指定したpkキーにデータがあれば返す、なければエラーを返す。
    question = get_object_or_404(Question, pk=question_id)
    # 質問オブジェクトを引数で貰ってページを作成する。
    return render(request, 'polls/results.html', {'question': question})

results.htmlを作成する。

<!-- 質問を表示する。 -->
<h1>{{ question.question_text }}</h1>

<!--質問の選択肢とそれに対する投票数を取得する。-->
<ul>
{% for choice in question.choice_set.all %}
    <!--choice.votes|pluralizeは投票数が2以上の場合vote s とsを追加してくれる。-->
    <li>{{ choice.choice_text }} -- {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>

<a href="{% url 'polls:detail' question.id %}">Vote again?</a>

Built-in template tags and filters | Django documentation | Django

汎用ビューを使って今まで書いたコードをさらに短くする。

views.py に書かれた index(), detail(), results() 関数は3つとも似たような機能でURLを介して渡されたパラメータに従ってデータベースからデータを取り出しページを作成する。これらの一連の動作はよくある事なのでDjangoでは汎用ビューというショートカットを用意してより簡素に機能を実装出来るようにしている。

汎用ビューを適用するにはいくつかこれまでに書いたコードを修正する必要がある。

  1. URLconfを変換する。
  2. 古い不要なビューを削除する。
  3. 新しいビューにDjango汎用ビューを設定する。

URLconfの修正

変更前

from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.index, name='index'),
    path('<int:question_id>/', views.detail, name='detail'),
    path('<int:question_id>/results/', views.results, name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

変更後

from django.urls import path

from . import views

app_name = 'polls'
urlpatterns = [
    path('', views.IndexView.as_view(), name='index'),
    path('<int:pk>/', views.DetailView.as_view(), name='detail'),
    path('<int:pk>/results/', views.ResultsView.as_view(), name='results'),
    path('<int:question_id>/vote/', views.vote, name='vote'),
]

views.IndexView.as_view(), views.DetailView.as_view(), views.ResultsView.as_view()as_view() と書くようになった。

そして、 question_idpk に変更された。ここは同じでもいいような気もする。結局同じ数値が返り値として入るから。

※後述する DetailView には pk キーを渡す必要があるので同じではダメなようだ。

viewsの修正

index(), detail(), results() 関数を削除しクラスベースに書き換える。

indexでは ListView を継承している。

detail, resultsでは DetailView を継承している。

ListView

「オブジェクトのリストを表示する。」

メソッドのフローチャート継承したメソッドが下記の順番で自動で実行される。

1.setup() 2.dispatch() 3.http_method_not_allowed() 4.get_template_names() 5.get_queryset() 6.get_context_object_name() 7.get_context_data() 8.get() 9.render_to_response()

このようにメソッドが実行されるので継承したクラスに get_quesryset() メソッドを追加して内容を上書きする事が出来る。

template_name

ListView ではデフォルトの場合 <app name>/<model name>_list.html を自動で生成して使用する。

その場合、テンプレート名は polls/question_list.html になる。

しかし元々作成してある polls/index.html を使用したい場合は template_name'polls/detail.html' を代入する事でDjangoがそちらを使用するように認識してくれる。

DetailView

「あるタイプのオブジェクト詳細ページを表示する。」

なので ListViewの詳細ページをDetailViewで表示するみたいな使われ方をする。

template_name

そしてデフォルトでは DetailView<app name>/<model name>_detail.html という名前のテンプレートを自動生成して使用する。

その場合、テンプレート名は polls/question_detail.html になるが、今回は自動生成されたものではなく元々作成してある polls/detail.html を使いたいので template_name を指定して元々のテンプレートを使用する。方法はListViewの時と同じで template_namepolls/detail.html を代入する。

model

このクラス変数はビューが使用するモデルを指定している。

model = Question の場合は裏側で Question.objects.all() を行ってくれる。なので queryset = Question.objects.all() としても良い。

from django.http import HttpResponseRedirect
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.views import generic

from .models import Choice, Question

class IndexView(generic.ListView):
    # デフォルトのビューを使用せず、元々作成してあったものを使用する。
    template_name = 'polls/index.html'
    # 自動で渡されるquestion_listというコンテキスト変数の変数名を独自のものに変更している。
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """最新の5件を取得する。"""
        return Question.objects.order_by('-pub_date')[:5]

class DetailView(generic.DetailView):
    # 自分がどのモデルに対して動作するかを伝えている。
    # おそらくget_object_or_404(Question, pk=question_id)のQuestion部分を担っている。
    # pkの部分はurls.pyで先に指定してある。
    model = Question
    template_name = 'polls/detail.html'

class ResultsView(generic.DetailView):
    model = Question
    template_name = 'polls/results.html'

def vote(request, question_id):
    ... # 前回のまま変更しない。

これでサーバを起動して特にエラーもなく質問のリストページ(index.html)、詳細ページ(投票するページdetail.html)、投票後の今ままでの投票数を表示するページ(results.html)が表示されていれば汎用ビューでのアプリ構築ができたと思う。

最後に

過去にRuby on railsのフレームワークの中身がどう動作しているのかイメージ出来ないのが苦痛(フレームワークは面倒な中身を気にしなくてもアプリが作れるように設計してあるので仕方ないかもしれない。)で挫折しているので今回Djangoのチュートリアルまだ途中ですが挫折せずにアプリ作成まで出来てよかったです。普段Jsonしか触らなかったので少しですがデータベースを作成して操作する経験が出来たのでこれを気にSQL構文をもう少し勉強しようと思う。多対多、多対一の関係とかも自分で作成出来るまでになります。自分の作成したアプリのER図をかける書けるようになりたい。

参照

SECRET_KEYを誤ってGitHubにプッシュしたときの対処法(Django編) - Qiita

Pythonの相対インポートで上位ディレクトリ・サブディレクトリを指定 | note.nkmk.me

ワイルドカードインポート(import *)は推奨されない

「カラム名」と「フィールド名」の違い|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

3. データベースマイグレーション | densan-labs.net

モデル(データベース)の作成

プログラミングでよく見かける"コンテキスト(context)って何? - Qiita

Python Django チュートリアル(3) - Qiita

DJangoのお勉強(1) - Qiita

ドキュメント

Djangoの汎用ビュー入門(ListView)

FOREIGN KEY制約(外部キー制約を設定する)

外部キー制約とは|「分かりそう」で「分からない」でも「分かった」気になれるIT用語辞典

Django ForeignKeyで1対多のモデルを構築

記事に関するコメント等は

🕊:Twitter 📺:Youtube 📸:Instagram 👨🏻‍💻:Github 😥:Stackoverflow

でも受け付けています。どこかにはいます。