Django公式チュートリアル-簡単な投票 (poll) アプリケーションの完成品をデプロイしました

※ 当サイトではアフィリエイト広告を利用しています。リンクは広告リンクも含みます。

この記事は約29分で読めます。
広告

Django公式チュートリアル、その2~4の内容を補足説明します。
開発環境はPython、Djangoのインストールが全く不要な
PaizaCloudを使ってます。ローカルで開発環境がある方も対応しています。
チュートリアルの完成品がどんな感じか分かった方がいいと思いますので
完成品Railwayデプロイ(毎月21日以降は停止してます)
完成品お名前.comVPS(有料)にデプロイ
完成品Renderにデプロイ(表示するまで数分かかることがあります)
今回ははじめての Django アプリ作成のその2からやっていきます。
こちらの記事の続きになります。

広告

クラス

ここから先はPythonのクラスについて基本を確認してから進んだほうがいいです。
簡単にクラスを学べるところを紹介しておきます。

侍エンジニア塾【Python入門】クラスの使い方を簡単解説で楽々マスター

15. クラス | 中学生でもわかるPython入門シリーズ(Youtube)

Python全般の書籍はこれがおすすめです。

みんなのPython 第4版 Kindle版

はじめての Django アプリ作成、その2

続きをやっていきます。
はじめての Django アプリ作成、その2
ここはデータベースの接続と作成の章になります。

データベースの設定

mysite/settings.pyをデータベースに合わせて変更するのですが
SQLiteに関してはDjangoの場合何もしなくても使えるようになっています。

mysite/settings.py を編集する際、 TIME_ZONE に自分のタイムゾーンも設定します。とあります。
日本はAsia/Tokyoです。日本用にしなくてもデフォルトのままでも動きます。
後で出てくる管理画面で、この設定の違いで時間が変わってきます。いつでも変更可能なので管理画面を作ったら、この値を変えて時間がどう変わるか実験してみてください。
(かつてHeroku(海外サーバー)へデプロイしたとき、ここの設定がUTCでないとダメで、はまったことがあったので一応、気に留めておいてください。)
LANGUAGE_CODEは日本語のjaにすると後で出てくる管理画面が日本語になります。

# LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = 'ja'
# TIME_ZONE = 'UTC'
TIME_ZONE = 'Asia/Tokyo'

順番が逆ですが、先に以下をやっておきます。
構成クラスへの参照(アプリ)を settings.pyのINSTALLED_APPS 設定に追加する必要があります。 
私がDjangoの勉強した動画では’polls’だけでしたが
今回のチュートリアルでは’polls.apps.PollsConfig’になっています。
‘polls.apps.PollsConfig’の中身を見るとname=’polls’となっているだけです。
どっちでもよさそうですがチュートリアルに従ったほうが無難ですね。
‘polls’だけでも動きます。

mysite/settings.py
INSTALLED_APPS = [
    'polls.apps.PollsConfig',
python manage.py migrateは、ここでやってもいいですし何回実行しても大丈夫です。

データベース作成

Djangoデータモデル概念図

基本的流れは

  • モデルを作成、変更する (models.py の中にモデルを定義)
  • モデルの作成、変更をデータベースに反映させるマイグレーションファイルを作成するために python manage.py makemigrations を実行。
  • データベースにマイグレーションファイルを適用するために python manage.py migrate を実行します。

具体的には polls/models.pyがあるので以下のように全部書き換え
(あとから青色部分と緑色部分を追加していますが最初に追加しても大丈夫です。)

こちらを見てmodels.pyの書き方を決まったパターンとして考えるといいと思います。
【django】モデルのフィールドについて:フィールドの型・オプション一覧が分かりやすいです。
下のclassで定義したQuestionとChoiceはモデルと呼んでいます。
下のmodels.pyのイメージを図示すると(データは「API で遊んでみる」で入力されたデータです。)

Questionモデル

id自動割り振りquestion_textpub_date
1‘What’s up?’日付

Choiceモデル

id自動割り振りquestion(親)choice_textvotes
1‘What’s up?’‘Not much’0
2‘What’s up?’‘The sky’0
3‘What’s up?’‘Just hacking again’0

polls/models.py ファイル

import datetime

from django.db import models
from django.utils import timezone

class Question(models.Model):
    question_text = models.CharField(max_length=200)
    pub_date = models.DateTimeField('date published')

    def __str__(self):#後述
        return self.question_text
    def was_published_recently(self):#pub_dateの値が新しかったらTrueをreturn
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)

class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)
#親となるモデルQuestionを指定してる。on_delete=は親のデータが削除されたときの動作を設定。models.CASCADEを設定すると、親側Questionのデータを削除すると、子側Choiceのデータも削除される。
    choice_text = models.CharField(max_length=200)
    votes = models.IntegerField(default=0)

    def __str__(self):
        return self.choice_text
def __str__(self):  return self.○○の役割は下で解説してます。

モデルが定義で来たのでデータベースへ反映させます。

サーバーが動いている場合はCTRL+Cでとめます。
ターミナルで
python manage.py makemigrationsを実行ターミナルで
python manage.py migrateを実行
python manage.py sqlmigrate polls 0001はデータベースに書き込むときSQLが使われるのですが、そのSQLを具体的に見るための命令で実行する必要はないし、SQLの内容を理解する必要もありません。
実際につかうのは
python manage.py makemigrations
python manage.py migrateだけです。

目次へ

 Python シェル( データベース API )でデータを入れる

 Python シェル(対話的にプログラムを実行できる)で
データベースの中身を操作することができます。

python manage.py shell でPythonシェル起動

色々操作していますが意味が分からなくても
>>>の部分を入力してEnterしていってください。
データベースにデータが入ります。
エラーが出たらもう1回 python manage.py makemigrations からやり直してください。

各操作の内容、例えば
Question.objects.all()
の説明はこちらが分かりやすいです。

【Django】データベース操作(取得・作成・更新・削除):ORMの利用

途中コードを追加していますが、これについて補足すると。
__str__は特殊メソッドでオブジェクトを文字列型に変換するときに呼び出される関数
print()関数でオブジェクトを表示するときも暗黙に呼び出される

Questionの中身を見るメソッドを実行すると

>>> Question.objects.all() Questionの中身を見るメソッド
<QuerySet [<Question: Question object (1)>]>
データがあることはわかるが何なのかわからない

polls/models.pyのclass Question(models.Model):以下を追加したことで中身がわかるようになる 
   def __str__(self):
        return self.question_text

>>> Question.objects.all()
<QuerySet [<Question: What's up?>]>
データの中身が What's up?であることがわかる。
choice_setについて

models.pyでchoice_setなんて定義してないのに出てきている。

例えばq.choice_set.create(choice_text='Not much', votes=0)
これは以下2点から推測できる。

・Choiceクラスは以下でQuestionクラスと紐づけされている
class Choice(models.Model):
    question = models.ForeignKey(Question, on_delete=models.CASCADE)

・1対多の参照がオブジェクト.モデル名(小文字)_set.all()で可能らしい
参照元【Django】1対多の関係( related_name, _set.all() )について

使われてる例で説明すると
q = Question(question_text="What's new?", pub_date=timezone.now())
qはQuestionのオブジェクト
q.choice_set.create(choice_text='Not much', votes=0)
はオブジェクト.モデル名(小文字)_set.all()の形になっている。
.all()ではなく.create()ではあるが。

最小限以下だけやってデータを入れておけば先に進んでも問題はありません。

from polls.models import Choice, Question
from django.utils import timezone
q = Question(question_text="What's new?", pub_date=timezone.now())
q.save()
q.question_text = "What's up?"
q.save()
q.choice_set.create(choice_text='Not much', votes=0)
q.choice_set.create(choice_text='The sky', votes=0)
exit()

Pythonシェルから出るにはexit()です。

SQlite3限定ですが、データベースを初期化する簡単な方法
Pythonシェルから出た状態で
mysite/db.sqlite3
mysite/polls/migrations/0001_initial.py
の2つを削除
python manage.py makemigrations
python manage.py migrate
だけで初期化できます。

目次へ

Django Admin

 Python シェルはターミナル操作でデータを操作しました。
それに対してAdminはアプリを起動した状態でAdminに入り
データベースを操作できます。
指示通りにやればできます。
途中メールアドレスは入力しないで、そのままEnterで大丈夫です。
パスワードは入力中、何も表示されません。また半角で入力してください。失敗したら多分CTRL+Cで抜けられると思います。だめだったらターミナルを閉じて、もう一度ターミナルを立ち上げcd mysiteしてpython manage.py createsuperuser
passwordなど簡単なパスワードを入力した場合Bypass password validation and create user anyway? [y/N]:と出ますがyでEnter。

python manage.py runserver

でサーバーを立ち上げると404エラーになるので
アドレスの最後にadminを追加すれば管理画面に行けます。
例https://localhost-○○.paiza-user-free.cloud:8000/admin
polls/admin.pyを以下のように変更するとChoiceも見れるようになります。

from django.contrib import admin

from .models import Question,Choice

admin.site.register(Question)
admin.site.register(Choice)

はじめての Django アプリ作成、その 3

続いてはじめての Django アプリ作成、その 3
この章の目的はテンプレートを使ってHTML形式で出力する方法です。

もっとビューを書いてみる

/polls/番号/にアクセスしたら番号をviewsに渡し
“You’re looking at question 番号.と出力するプログラム
指示通りプログラムするとYou’re looking at question 34.と出てきます。

補足説明
polls/urls.py 
urlpatterns = [
    # ex: /polls/
    path('', views.index, name='index'),
    # ex: /polls/5/
    path('<int:question_id>/', views.detail, name='detail'),

urlが/polls/番号/の時polls/views.pyが呼ばれquestion_idが渡される

polls/views.py 
def detail(request, question_id):
    return HttpResponse("You're looking at question %s." % question_id)
こちらで実験したのと同じことをしてrequestに何が入るのか確認してみます。
polls/views.py のdetailを以下のように修正します。
def detail(request, question_id):
    a=request.GET['test']#requestの中の変数testの中身をget
    return HttpResponse("You're looking at question %s." % question_id +a)
ローカル環境の場合http://127.0.0.1:8000/polls/34/?test=送れたよ!にアクセスすると
You're looking at question 34.送れたよ!
と表示されurlパラメータの部分?test=送れたよ!を受け取ってreturnしていることがわかります。

実際に動作するビューを書く

データベースにある最新の 5 件の質問項目をカンマで区切り、日付順に表示するビューです。
実行結果は、その2でWhat’s up?しかデータベースに入れてないので
それしか表示しません。
データを増やすために、その2でやったadminに入り。
Questionsをクリックすると以下の画面が出ます。
この画面はsettings.pyのLANGUAGE_CODE = ‘en-us’なので英語です。 ‘ja’にすると日本語になります。

ADD Questionで下の画面が出るので
それぞれ入力とクリックしてsaveするとデータベースにデータが登録されます。
5個入れたらプログラムを動かしてみましょう。

補足説明
polls/views.py

def index(request):
    latest_question_list = Question.objects.order_by('-pub_date')[:5]
    output = ', '.join([q.question_text for q in latest_question_list])
    return HttpResponse(output)

Questionモデルの一覧をpub_dateの降順で5個取得
'-pub_date'の-は降順を表す。昇順の場合はなし。

question_textの中身が,区切りで降順に5個表示される。

次はDjangoで一般的なテンプレートを使う方式に変更
htmlを使って出力する。
といってもリスト表示になるだけです。
views.pyを指示通りに変更。
Pizacloudの左に表示してるpollsディレクトリを右クリックして新規ディレクトリ作成
でtemplatesを作り、その下にpollsディレクトリを作る。
そのpollsを右クリックして新規ファイル作成でindex.htmlを作成。
完成したディレクトリ構成は polls/templates/polls/index.html 

テンプレート作成
チュートリアル掲載コードはHTMLファイルの基本部分が抜けているので
完全な HTML ドキュメント を参照して
<p>This is my page</p>の部分を変更すると以下になる。

polls/templates/polls/index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>My test page</title>
  </head>
  <body>
<!-- views.indexからわたされたコンテキストlatest_question_listに値が入っているか -->
    {% if latest_question_list %}
<!-- Questionモデルのリンクリスト作成(リンク先は3章の最初にpolls/urls.py で作られてる) -->
    <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 %}
  </body>
</html>

{% ・・・ %}や{{・・・}} で囲まれた部分は「テンプレートタグ」というもので、この部分が動的な部分です。
テンプレートタグについてはこちらが分かりやすいです。
【django】テンプレートとは:使用方法(templatesフォルダ・変数・タグ )

polls/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')
    context = {
        'latest_question_list': latest_question_list,
    }
    return HttpResponse(template.render(context, request))
略

return HttpResponse(template.render(context, request))は
polls/templates/polls/index.htmltemplateとしてベースにします。
そこにデータベースから取り出したlatest_question_listcontextに入れて渡されます。
ベースとしてるindex.html内にあるテンプレートタグ{%・・・%}の処理でcontextのデータを取り出してindex.htmlを書き換えています。

またrender関数ではrequestという変数(オブジェクト)が入っています。これはdef index(request):で受け取ったrequestの情報をそのままくっつけることが必要なのだと思われます。

viewの役割はデータベースから引き出したデータ(context)とtemplateを使ってユーザーへ
返信する画面を作ることです。

目次へ

ショートカット: render()

ここは上のpolls/views.pyを
別の書き方で書いているだけでやっていることは同じです。

template = loader.get_template("polls/index.html")と
return HttpResponse(template.render(context, request))が合体して

return render(request, "polls/index.html", context)になっています。

ただtemplate.renderはtemplateの関数ですが下のrenderはmoduleですので下のimportが必要になっています。
from django.shortcuts import render

404 エラーの送出、ショートカット: get_object_or_404()、テンプレートシステムを使う

この3つはdetailのページを作ってます。404 エラーの送出の処理があるかないかでエラーがでたとき(この場合polls/10/とかにアクセス)の画面が違います。

この確認を行うにはsettings.pyの
DEBUG = True→False
ALLOWED_HOSTS = []→['*']にしないとDebugモードのときの下とは違う画面が出ます。
確認はやらなくていいですが、入れたほうがエラーが原因が明確にユーザーに示されます。

処理なしだと

Server Error (500)

処理ありだと

Not Found

The requested resource was not found on this server.

テンプレート内のハードコードされたURLを削除

ここはpolls.urlsで定義したnameの役割です。

polls.urls
path('<int:question_id>/', views.detail, name='detail'),が
path('specifics/<int:question_id>/', views.detail, name='detail'),に変わっても

polls/index.htmlの
<li><a href="/polls/{{ question.id }}/">{{ question.question_text }}</a></li>を

<li><a href="{% url 'detail' question.id %}">{{ question.question_text }}</a></li>
に変えてしまえばpolls.urlsの変更だけで済むという話です。
要するにpolls.urlsのname='***'を使って
テンプレートでは{% url '***' ○○○%}の形でURLを表現したほうがいいということです。
またname='detail'としてるurls.pyではpolls/が含まれていないように見えますが
mysite/urls.pyでpolls/urls.pyが呼ばれているのでpolls/が含まれています。

URL 名の名前空間

同名のビューを違うアプリと区別するための方法が書かれています。
urls.pyでapp_name = ‘polls‘というようにアプリ名を明記し
テンプレでは{% url ‘polls:detail’に変更するということです。
違うアプリの意味ですが

最初にpython manage.py startapp pollsを実行したディレクトリで
python manage.py startapp pollsA
python manage.py startapp pollsBと実行することでアプリがいくつも追加できます。
polls、pollsA、pollsBが、ここで言う違うアプリです。
目次へ

はじめての Django アプリ作成、その 4

はじめての Django アプリ作成、その 4

リスト表示はよく使われる処理なのでpolls/views.py の最終的な形は
Djangoの汎用ビュークラス(ListViewとDetailView)を継承して
modelとtemplate_nameを書いておけばいいというはなしです。

latest_question_listとdef get_queryset(self):について補足すると
継承元のクラスListViewの機能が使われています。
def get_queryset(self):でリストにするqueryset(データ)を抽出してます。
context_object_nameはIndexViewをよんでるhtmlで扱うリストの名前を決めてます。
参考 https://yu-nix.com/archives/django-list-view/
Djangoのobject_listの名前を変更する方法

#汎用ビューを使う

class IndexView(generic.ListView):
    template_name = 'polls/index.html'
    context_object_name = 'latest_question_list'

    def get_queryset(self):
        """Return the last five published questions."""
        return Question.objects.order_by('-pub_date')[:5]

#modelとtemplateだけ指定すればいい。
class DetailView(generic.DetailView):
    model = Question
    template_name = 'polls/detail.html'

簡単な投票 (poll) アプリケーションの最終的な形になります。
指示通りやれば完成します。

質問がWhat’s upだけだとよくわからないので質問を追加してみます。
ターミナルでPythonシェルを立ち上げ
既に登録してある質問を調べて、ちゃんとした質問に変更します。
選択肢も追加します。

python manage.py shell
from polls.models import Choice, Question

Question.objects.all()
 <QuerySet [<Question: What's up?>, <Question: test2>, <Question: last>]>
q=Question.objects.get(question_text="test2")
q.question_text='Pythonは好きですか?'
q.save()
q.choice_set.create(choice_text='好き', votes=0)
q.choice_set.create(choice_text='嫌い', votes=0)
q.choice_set.all()で追加されていることを確認
exit()
目次へ

はじめての Django アプリ作成、その 5

はじめての Django アプリ作成、その 5

初めてのテスト作成
models.pyのQuestionモデルで使われている関数のテストです。
def was_published_recently(self):
        return self.pub_date >= timezone.now() - datetime.timedelta(days=1)
この関数は、投稿の公開日が現在から1日以内である場合にTrueを返します。

テストするためにpolls/tests.pyで
time = timezone.now() + datetime.timedelta(days=30)
        future_question = Question(pub_date=time)
としてわざと投稿時間を30日後にしています。
その結果をself.assertIs(future_question.was_published_recently(), False)で判定しています。
self.assertIs(第1引数、第2引数)は第1引数と第2引数を比較し結果をターミナルに表示しています。本来30日後の日付で投稿しているので、future_question.was_published_recently()のreturn値は1日以前の投稿日ではないのでfalseのはずです。なので結果はOKになるはずです。しかしテスト結果はFAILED (failures=1)です。ということでバグがあるということになります。
models.pyのバグを修正してテストを実行すれば結果はOKになります。

tests.pyにTestCase を継承したサブクラスに「test_」ではじまるメソッドを書いておきます。ターミナルでpython manage.py test アプリ名(省略可)で、そのクラスが実行されます。

より包括的なテストで2つテスト関数を追加してます。これを実行するとokになるのですが、どれかのself.assertIsの第2引数を変更してテストするとテスト結果がfalseになって、どこがエラーか表示されます。
ビューをテストする
各テスト関数が何をやっているかの説明とわざとエラーを出して動作確認する方法です。
データベースにすでに存在するデータは関係ありません。

def test_no_questions(self):
クライアントがpollsにアクセスしたらindexが表示されるのだが、questionがないときの動作を確認しています。index.htmlの中の以下の<p>タグの文章を例えばYes polls are available.と変えてテストを実行するとself.assertContains(response, "No polls are available.")の第1引数に第2引数が含まれなくなるのでテスト結果がFailedになります。
{% else %}
    <p>No polls are available.</p>
{% endif %}

def test_past_question(self):
過去の日付で投稿したものだけが有効かのテスト
question = create_question(question_text="Past question.", days=-30)を実行することで日付が30日前で"Past question."と内容の投稿をする。
なので投稿が、その1個しかないのでself.assertQuerySetEqualで同じものを比較しているので結果OKになる。
, days=-30)を, days=30)と未来に変更すると、投稿はされるものの、views.pyで未来の投稿ははじくような処理を直上で追加しているので、このテストはFailedになります。

def test_future_question(self):
今度は逆に未来の日付で投稿した質問は表示されないのでself.assertContains(response, "No polls are available.")でresponseに"No polls are available."が含まれるのでテスト結果はOKになります。

def test_future_question_and_past_question(self):
これは上の2つのテストを合わせたもので過去と未来の日付で投稿し過去のみ有効であることをテストしています。

def test_two_past_questions(self):
過去の日付の投稿を別の日付で2つ行ったテストです。表示される順序が最新が先になっているかテストしています。例の場合question2、question1の順。question2 のdays=-31)にしてquestion1より投稿日を古くしてテストするとFailedになります。
DetailViewのテスト
ここは未来と過去の投稿日で過去の投稿のみ表示するかのテストです。
def test_future_question(self):でself.assertEqual(response.status_code, 404)で判定しています。これはclass DetailView(generic.DetailView):に以下を追加したので未来の日付の投稿は排除されています。なのでstatus_code404(not found)で判定できています。
    def get_queryset(self):
        return ACquestion.objects.filter(pub_date__lte=timezone.now())
補足:__lte(less than equalの略)を付けることで右辺以下になるようです。参考https://codelab.website/django-queryset-filter/

def test_future_question(self):を以下のように過去の日付の投稿にするとresponseのstaus_codeが正常通信の200になりテストはFailedになります。
# future_question = create_question(acquestion_text="Future question.", days=5)
# url = reverse("ac_polls:detail", args=(future_question.id,))
past_question = create_question(acquestion_text="Past Question.", days=-5)
url = reverse("ac_polls:detail", args=(past_question.id,))

目次へ

はじめての Django アプリ作成、その 6

はじめての Django アプリ作成、その 6
cssをあてています。指示通りに追加します。
index.htmlに追加する場所は以下です。

{% load static %}
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <title>My test page</title>
    <link rel="stylesheet" href="{% static 'polls/style.css' %}">
  </head>

vscodeで画像ファイルを追加する場合はpolls/static/polls/imagesを右クリックして「Exploreを表示」をクリックするとExploreが表示されるので、普通のExploreの操作と同じように画像ファイルを追加してください。
プログラムを実行しても変化がない場合cssが読み込まれていません。原因はわかりませんが以前のDjangoでは以下のように違っていたので/を付けたらうまくいきました。今回(Django4.2を使用)1回やったあとはもとに戻してもうまくいきます。

settings.pyの
STATIC_URL = 'static/'をSTATIC_URL = '/static/'という言う感じで/を追加する。

こんな感じになります。

完成品

その4までの部分です。(その6は入っていません。)
完成品Railwayデプロイ(毎月21日以降は停止してます)
完成品Renderにデプロイ(表示するまで数分かかることがあります)
データベース以外は
使い勝手は悪いですが、「その4」のプログラムそのままです。
ただしデータはPythonシェルで書き換えてます。
Herokuへのデプロイ方法は以下参照してください。(2022/11/末Herokuは有料になりました)

イチゲをOFUSEで応援する(御質問でもOKです)Vプリカでのお支払いがおすすめです。
MENTAやってます(ichige)
目次へ

コメント

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