プログラミング

Djangoのチュートリアルまとめ

Pythonのフレームワークである「Django」でWEBアプリを作成するチュートリアルについて書きたいと思います。

基本的にはチュートリアルをそのまま進めます。

日本語のドキュメントがあるのですが、何回か途中で行き詰まったところもあるので、自分の理解を深めるという意味でも要点を抑えておきたいと思いました。

初めてチュートリアルに取り組む人はまず、公式ドキュメントに沿って進めてみてください。

はじめての Django アプリ作成 | Django ドキュメント

なお、長いのでかなり省略している部分があります。(それでも長いですが…)

プロジェクトの作成

まず最初にDjangoのプロジェクトを作成します。

コマンドラインから、コードを置きたい場所に移動(cd)して、以下のコマンドを実行。

django-admin startproject mysite

これを実行すると、「mysite」というディレクトリが作られます。「mysite」の部分は任意でつけられるプロジェクト名なので、「mysite」じゃなくても構いません。ただ、PythonモジュールやDjangoのコンポーネントの名前は使わないようにとされています。

先ほど作成した「mysite」の中身は以下のようになっているはずです。

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

上から順番に、次のような役割を担っています。

  • mysite/ – プロジェクトの箱のようなもの。任意の名前に変更できる。
  • manage.py – プロジェクトに対する色々な操作を行うためのコマンドラインユーティリティ。
  • mysite/ – このプロジェクトにおけるPythonパッケージの名前。importの際に使用する。(例.import mysite.urls)
  • __init__.py – このディレクトリがPythonパッケージであることをPythonに知らせるための空のファイル。
  • settings.py – Djangoプロジェクトの設定ファイル
  • urls.py – DjangoプロジェクトのURL宣言
  • wsgi.py – プロジェクトをサーブするためのWSGI互換Webサーバーとのエントリーポイント

開発用サーバーを起動

作成したDjangoのプロジェクトがうまく動作するか確認するために、開発用サーバーを起動します。

外側のmysiteディレクトリに移動して、以下のコマンドを実行。

python manage.py runserver

ブラウザで「http://127.0.0.1:8000/」にアクセスしてみて、”Congratulations!” と表示された、ロケットが離陸しているページが出れば成功です。

また、「control + C」でサーバーが閉じます。

アプリケーションの作成

アプリケーションを作るには、manage.pyと同じディレクトリに入って、以下のコマンドを実行します。

python manage.py startapp polls

「polls」は任意でつけられるアプリケーションの名前です。

今回作成した「polls」の中身は以下のようになります。

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

ビューの作成

polls/views.pyに以下のPythonコードを書いてみます。

from django.http import HttpResponse

def index(request):
  return HttpResponse(“Hello, world. You’re at the polls index.”)

このビューを呼び出すために、URLを対応付ける必要があり、そのためにはURLconfというものが必要です。

polls/urls.py(新規作成する必要あり)に以下のコードを書きます。

from django.urls import path

from . import views

urlpatterns = [
  path(”, views.index, name=’index’),
]

次に、mysite/urls.pyに以下のコードを書きます。

from django.contrib import admin
from django.urls import include, path #includeを追加

urlpatterns = [
  path(‘polls/’, include(‘polls.urls’)), #追加
  path(‘admin/’, admin.site.urls),
]

include()関数を使うことで、他のURLconfへ参照することができます。

ここまでで、もう一度サーバーを起動し、今度は「http://127.0.0.1:8000/polls」にアクセスすると、ビューで入力した「Hello, world. You’re at the polls index.」が表示されるはずです。

データベースの設定

データベースの設定といっても、SQLiteが標準で組み込まれているため、データベースに詳しくなかったり、Djangoを少し試してみたい場合は、デフォルトのSQLiteを使うことが推奨されています。

データベースの設定に関しては、mysite/settings.pyに書かれています。

同じファイルには、デフォルトでインストールされている以下のアプリケーションが入っています。

  • django.contrib.admin – 管理(admin)サイト。
  • django.contrib.auth – 認証システム
  • django.contrib.contenttypes – コンテンツタイプフレームワーク
  • django.contrib.sessions – セッションフレームワーク
  • django.contrib.messages – メッセージフレームワーク
  • django.contrib.staticfiles – 静的ファイルの管理フレームワーク

これらは最低1つのデータベースのテーブルを使うため、以下のコマンドを実行します。

python manage.py migrate

「migrate」は、mysite/settings.pyの「INSTALLED_APPS」を参照し、同じファイルに書かれているデータベース設定に従って、データベースのテーブルを作成します。

モデルの作成

モデルはデータベースのデータを操作する部分で、今回のチュートリアルでは、polls/models.pyに以下のPythonコードを書きます。

from django.db import models

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

class Choice(models.Model):
  question = models.ForeignKey(Question, on_delete=models.CASCADE)
  choice_text = models.CharField(max_length=200)
  votes = models.IntegerField(default=0)

このチュートリアルでは投票アプリを作るのが目的のため、投票項目 (Question) と選択肢 (Choice) の二つのモデルを作成しています。

モデルを有効にする

まず、pollsアプリケーションをインストールしたことをDjangoのプロジェクトに認識してもらう必要があるため、mysite/settings.pyの「INSTALLED_APPS」に一文を追加します。

INSTALLED_APPS = [
  ’polls.apps.PollsConfig’, #追加
  ’django.contrib.admin’,
  ’django.contrib.auth’,
  ’django.contrib.contenttypes’,
  ’django.contrib.sessions’,
  ’django.contrib.messages’,
  ’django.contrib.staticfiles’,
]

次に、「makemigration」コマンドを実行します。

python manage.py makemigrations polls

これは、Djangoのモデルに変更があったことを伝え、保存するためのコマンドです。

そして、もう一度「migrate」コマンドを実行し、モデルのテーブルをデータベースに作成します。

python manage.py migrate

つまり、モデルを変更した場合は、以下の流れで実施します。

  1. モデルを変更する (models.py の中の)
  2. マイグレーションを作成するためにpython manage.py makemigrationsを実行する。
  3. データベースにこれらの変更を適用するためにpython manage.py migrateを実行する。

APIを使ってみる

下記のコマンドを実行して、Python対話シェルを起動して、Djangoが提供するAPIを使ってみます。

python manage.py shell

以下はシェル内でのやりとりです。

>>> from polls.models import Choice, Question
>>> Question.objects.all()
<QuerySet []>

>>> from django.utils import timezone
>>> q = Question(question_text=”What’s new?”, pub_date=timezone.now())

>>> q.save()

>>> q.id
1

>>> q.question_text
“What’s new?”

>>> q.pub_date
datetime.datetime(2012, 2, 26, 13, 0, 0, 775217, tzinfo=)

>>> q.question_text = “What’s up?”
>>> q.save()

>>> Question.objects.all()
<QuerySet [<Question: Question object (1)>]>

<QuerySet [<Question: Question object (1)>]>は、オブジェクトの表現として正しくないため、polls/models.pyを編集し、__str__()メソッドを追加します。

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): #追加
    return self.pub_date >= timezone.now() – datetime.timedelta(days=1) #追加

class Choice(models.Model):
  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 #追加

変更を保存して、もう一度python manage.py shellを実行します。

>>> from polls.models import Choice, Question

>>> Question.objects.all()
<QuerySet [<Question: What’s up?>]>

>>> Question.objects.filter(id=1)
<QuerySet [<Question: What’s up?>]>

>>> 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)
<Question: What’s up?>

>>> Question.objects.get(id=2)
Traceback (most recent call last):

DoesNotExist: Question matching query does not exist.

>>> Question.objects.get(pk=1)
<Question: What’s up?>

>>> q = Question.objects.get(pk=1)
>>> q.was_published_recently()
True

>>> q = Question.objects.get(pk=1)
>>> q.choice_set.all()
<QuerySet []>

>>> q.choice_set.create(choice_text=’Not much’, votes=0)
<Choice: Not much>

>>> q.choice_set.create(choice_text=’The sky’, votes=0)
<Choice: The sky>

>>> c = q.choice_set.create(choice_text=’Just hacking again’, votes=0)

>>> c.question
<Question: What’s up?>

>>> q.choice_set.all()
<QuerySet [<Choice: Not much>, <Choice: The sky>, <Choice: Just hacking again>]>

>>> q.choice_set.count()
3

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

>>> c = q.choice_set.filter(choice_text__startswith=’Just hacking’)
>>> c.delete()

管理ユーザーを作成

まず、以下のコマンドを実行して、adminサイトにログインするための管理ユーザーを作成します。

python manage.py createsuperuser

好きなユーザー名を入力します。

Username: admin

次にメールアドレスを入力します。

Email address: admin@example.com

最後にパスワードを2回入力します。

Password: **********
Password (again): *********
Superuser created successfully.

Django adminサイトはデフォルトで有効化されるので、管理ユーザーを作成したら、開発サーバーを起動し、「 http://127.0.0.1:8000/admin/」にアクセスします。

ログイン画面が表示されるので、先ほど作成した管理ユーザーのアカウントでログインすることができます。

現状、管理画面にはpollsアプリケーションが表示されていないので、polls/admin.pyに以下を書きます。

from django.contrib import admin

from .models import Question

admin.site.register(Question)

ビューをさらに編集

投票アプリケーションでは、以下4つのビューを作成します。

  • 質問 “インデックス” ページ – 最新の質問をいくつか表示
  • 質問 “詳細” ページ – 結果を表示せず、質問テキストと投票フォームを表示
  • 質問 “結果” ページ – 特定の質問の結果を表示
  • 投票ページ — 特定の質問の選択を投票として受付

現時点では、インデックスページとして仮に「Hello, world. You’re at the polls index.」を表示させています。

polls/views.pyに以下のコードを追加して、詳細・結果・投票ページを追加します。

from django.http import HttpResponse

def index(request):
  return HttpResponse(“Hello, world. You’re at the polls index.”)

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) #追加

polls.urlsモジュールと結びつけないと表示されないので、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’), #追加
]

ブラウザで「http://127.0.0.1:8000/polls/34/」にアクセスしてみると、「You’re looking at question 34.」と表示されます。

「http://127.0.0.1:8000/polls/34/results/」とすれば「You’re looking at the results of question 34.」が、「http://127.0.0.1:8000/polls/34/vote/」とすれば「You’re voting on question 34.」が表示されます。

ユーザーが「/polls/34/」をリクエストすると、Djangoはmysite.urlsを読み込み、urlpatternsの中からpolls/を見つけたら、polls/をpolls.urlsを読み込みます。この際、polls/を除いた文字列”34/”だけが「polls.urls」のURLconfに渡されます。

そしてこれが「polls.urls」の「<int:question_id>/」に一致するため、下記のようにdetail()が呼び出されるという仕組みです。

detail(request=, question_id=34)

もう少し、ビューを編集します。

polls/views.pyのindex()ビューの部分を以下のように書き換えます。

from django.http import HttpResponse

from .models import Question #追加

#変更箇所
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)
#変更箇所

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)

ただし、デザインもこのコードに含まれていると、なにかと利便性が悪いので、Djangoのテンプレートシステムを使ってデザインを分離する必要があります。

Djangoではテンプレートを読み込み場所が決まっているので、pollsディレクトリの中に「templates」フォルダを作り、さらにその中に「polls」フォルダを作り、そしてその中に「index.html」を作ります。

つまり「polls/templates/polls/index.html」というパスになります。

「polls/templates/polls/index.html」には、次のように書きます。

{% 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 %}

このテンプレートを使うために、polls/viewsを更新します。

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))
#変更箇所

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)

render()を使ってビューを次のように書き換えると、loaderやHttpResponseをimportする必要はありません。

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)

ただし、(detail、results、vote)も同じように書き換える必要があるため、ここでは一旦書き換え前の状態で進めたいと思います。

質問詳細ビュー

質問詳細ビューは、指定された投票の質問文を表示するもので、次のようなコードになります。

from django.http import HttpResponse
from django.template import loader
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] template = loader.get_template(‘polls/index.html’)
context = {‘latest_question_list’: latest_question_list,}
return HttpResponse(template.render(context, request))

#変更箇所
def detail(request, question_id):
  try:
    question = Question.objects.get(pk=question_id)
  except Question.DoesNotExist:
    raise Http404(“Question does not exist”)
  return render(request, ‘polls/detail.html’, {‘question’: question})
#変更箇所

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)

このビューは、リクエストしたIDを持つ質問が存在しないときには、Http404を返します。

polls/detail.htmlには、とりあえず以下のように書いておきます。

{{ question }}

さらに、「ショートカット: get_object_or_404()」を使ってdetail部分を以下のように書き換えます。

from django.http import HttpResponse
from django.template import loader
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] template = loader.get_template(‘polls/index.html’)
context = {‘latest_question_list’: latest_question_list,}
return HttpResponse(template.render(context, request))

#変更箇所
def detail(request, question_id):
  question = get_object_or_404(Question, pk=question_id)
  return render(request, ‘polls/detail.html’, {‘question’: question})
#変更箇所

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)

「detail.html」をもう少し書き換えます。

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

次に、「index.html」を修正します。

現状、以下のようになっていますが、プロジェクト内にテンプレートが多くある場合、URLの変更が難しくなってしまいます。

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

そのため、テンプレートタグの{%url%}を使って以下のように書き換えます。

<li><a href=”{% url ‘detail’ question.id %}”>{{ question.question_text }}</a></li>

このチュートリアルでは、プロジェクト内にアプリを1つしか作りませんが、実際にWebアプリケーションを開発するときには、プロジェクト内に複数のアプリケーションを持つことが一般的です。

そのときにDjangoがURLを区別するために、以下のようにアプリケーションの名前空間を設定します。

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’),
]

そして、先ほど修正した「index.html」のリンク部分をさらに修正し、名前空間つきの詳細ビューを指すようにします。

<li><a href=”{% url ‘polls:detail’ question.id %}”>{{ question.question_text }}</a></li>

フォームを実装

「polls/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 %}
<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>

このテンプレートによって、選択肢をラジオボタンで表示します。

各ラジオボタンのvalueは選択肢のIDで、nameは”choice”なので、ユーザーが選択してフォームを送信すると、POSTデータ(choice=選んだ選択肢id)が送信されます。

また、forloop.counterは、forタグのループが何度実行されたかを表す値です。

次に、polls/views.pyのvote()関数とresults()関数の部分を以下のように編集します。

from django.http import HttpResponse, HttpResponseRedirect
from django.template import loader
from django.shortcuts import get_object_or_404, render
from django.urls import reverse #追加

from .models import Choice, 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))

def detail(request, question_id):
  question = get_object_or_404(Question, pk=question_id)
  return render(request, ‘polls/detail.html’, {‘question’: question})

#変更箇所
def results(request, question_id):
  question = get_object_or_404(Question, pk=question_id)
  return render(request, ‘polls/results.html’, {‘question’: question})

def vote(request, question_id):
  question = get_object_or_404(Question, pk=question_id)
  try:
    selected_choice = question.choice_set.get(pk=request.POST[‘choice’])
  except (KeyError, Choice.DoesNotExist):
    return render(request, ‘polls/detail.html’, {
      ’question’: question,
      ’error_message’: “You didn’t select a choice.”,
    })
  else:
    selected_choice.votes += 1
    selected_choice.save()
    return HttpResponseRedirect(reverse(‘polls:results’, args=(question.id,)))
#変更箇所

results()はdetail()とほとんど同じです。

また、vote()ビューは質問の結果ページにリダイレクトします。

次に「polls/results.html」を作成して、以下のコードを書きます。

<h1>{{ question.question_text }}</h1>

<ul>
{% for choice in question.choice_set.all %}
<li>{{ choice.choice_text }} — {{ choice.votes }} vote{{ choice.votes|pluralize }}</li>
{% endfor %}
</ul>

<a href=”{% url ‘polls:detail’ question.id %}”>Vote again?</a>

ブラウザで「/polls/1/」にアクセスし、投票してみると、投票するたびに結果ページが更新されます。

なお、選択せずにフォームを送信した場合は、エラーメッセージ「You didn’t select a choice.」が表示されます。

汎用ビュー

Djangoでは、よく使われるビューは汎用ビューとして用意されているため、汎用ビューを使えばもっと短く簡単なコードで実装することができます。

これまで作成してきたものを汎用ビューに変換するには、以下のステップで行います。

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

今回は、index, detail, resultsを汎用ビューに変換します。

まず「polls/urls.py」は以下のように変更します。

from django.urls import path

from . import views

app_name = ‘polls’
urlpatterns = [
  path(”, views.IndexView.as_view(), name=’index’), #変更
  path(‘/’, views.DetailView.as_view(), name=’detail’), #変更
  path(‘/results/’, views.ResultsView.as_view(), name=’results’), #変更
  path(‘/vote/’, views.vote, name=’vote’),
]

次に「polls/views.py」の古いindex, detail, resultsビューのコードを削除して、Djangoの汎用ビューを使って、以下のように書き換えます。

from django.http import HttpResponse, HttpResponseRedirect
from django.template import loader
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’
  context_object_name = ‘latest_question_list’

  def get_queryset(self):
    return Question.objects.order_by(‘-pub_date’)[:5]

class DetailView(generic.DetailView):
  model = Question
  template_name = ‘polls/detail.html’

class ResultsView(generic.DetailView):
  model = Question
  template_name = ‘polls/results.html’
#変更箇所

def vote(request, question_id):
…以下は同じ

indexにListViewを、detailとresultsにはDetailViewを使っています。

これでWEB投票アプリは完成です。

自動テスト

このチュートリアルでは、一部のテストについて紹介されています。

Question.was_published_recently()のメソッドは、質問が過去に作成されたものはTrueを返すのですが、Questionの pub_dateが未来の日付になっている場合にもTrueを返してしまうというバグがあります。

そこでshellを使ってバグを確認してみます。

>>> import datetime
>>> from django.utils import timezone
>>> from polls.models import Question
>>> future_question = Question(pub_date=timezone.now() + datetime.timedelta(days=30))
>>> future_question.was_published_recently()
True

Trueが返っているので、これは間違いです。

バグに気づいたときは、こういう方法もできますが、わからない場合に自動テストでバグを見つけます。

そこで、polls/tests.pyに以下のようにアプリケーションのテストを書きます。

import datetime

from django.test import TestCase
from django.utils import timezone

from .models import Question

class QuestionModelTests(TestCase):

  def test_was_published_recently_with_future_question(self):
    ”””
    was_published_recently() returns False for questions whose pub_date
    is in the future.
    ”””
    time = timezone.now() + datetime.timedelta(days=30)
    future_question = Question(pub_date=time)
    self.assertIs(future_question.was_published_recently(), False)

このテストは、以下のコマンドで実行します。

python3 manage.py test polls

すると次のような結果になり、バグが1件あることだけでなく、バグのあるコードの行もわかります。

Creating test database for alias ‘default’…
System check identified no issues (0 silenced).
F
======================================================================
FAIL: test_was_published_recently_with_future_question (polls.tests.QuestionModelTests)
———————————————————————-
Traceback (most recent call last):
File “/path/to/mysite/polls/tests.py”, line 16, in test_was_published_recently_with_future_question
self.assertIs(future_question.was_published_recently(), False)
AssertionError: True is not False

———————————————————————-
Ran 1 test in 0.001s

FAILED (failures=1)
Destroying test database for alias ‘default’…

そこで、models.pyのバグを以下のように修正して、過去の日付のものだけを表示するようにします。

def was_published_recently(self):
  now = timezone.now()
  return now – datetime.timedelta(days=1) <= self.pub_date <= now

そして、もう一度テストを実行すると、以下のような結果になります。

Creating test database for alias ‘default’…
System check identified no issues (0 silenced).
.
———————————————————————-
Ran 1 test in 0.001s

OK
Destroying test database for alias ‘default’…

polls/tests.pyの同じクラスに、さらに以下の2つのテストを追加します。

def test_was_published_recently_with_old_question(self):
  ”””
  was_published_recently() returns False for questions whose pub_date
  is older than 1 day.
  ”””
  time = timezone.now() – datetime.timedelta(days=1, seconds=1)
  old_question = Question(pub_date=time)
  self.assertIs(old_question.was_published_recently(), False)

def test_was_published_recently_with_recent_question(self):
  ”””
  was_published_recently() returns True for questions whose pub_date
  is within the last day.
  ”””
  time = timezone.now() – datetime.timedelta(hours=23, minutes=59, seconds=59)
  recent_question = Question(pub_date=time)
  self.assertIs(recent_question.was_published_recently(), True)

こうすることで、Question.was_published_recently()が過去、現在、未来の質問に対して意味のある値を返しているかどうかを確認することができます。

ビューのテストをする

このアプリケーションは、現時点ではまだ未来の日付のものも表示されてしまいます。

DjangoのClientを使ってテストします。

まずは、もう一度shellで試してみます。

python manage.py shell
>>> from django.test.utils import setup_test_environment
>>> setup_test_environment()
>>> from django.test import Client
>>> client = Client()

>>> response = client.get(‘/’)
Not Found: /

>>> response.status_code
404

>>> from django.urls import reverse
>>> response = client.get(reverse(‘polls:index’))
>>> response.status_code
200

>>> response.content
b’\n <ul>\n \n <li><a href=”/polls/1/”>What's up?</a></li>\n \n </ul>\n\n’

>>> response.context[‘latest_question_list’] <QuerySet [<Question: What’s up?>]>

次にpolls/views.pyを編集して、バグを直します。

まず、インポート文を追加します。

from django.utils import timezone

次にIndexViewクラスのget_querysetメソッドを以下のように修正します。

def get_queryset(self):
  ”””
  Return the last five published questions (not including those set to be
  published in the future).
  ”””
  return Question.objects.filter(
    pub_date__lte=timezone.now()
  ).order_by(‘-pub_date’)[:5]

では、新しいビューをテストしてみます。サーバーを立ち上げて手動で確認するのではなく、ここではテストで確認します。

まず、polls/tests.pyに以下の一文をインポート。

from django.urls import reverse

そして、以下のようなコードを書いて、ショートカット関数と、新しいテストクラスを作ります。

def create_question(question_text, days):
  ”””
  Create a question with the given `question_text` and published the
  given number of `days` offset to now (negative for questions published
  in the past, positive for questions that have yet to be published).
  ”””
  time = timezone.now() + datetime.timedelta(days=days)
  return Question.objects.create(question_text=question_text, pub_date=time)

class QuestionIndexViewTests(TestCase):
  def test_no_questions(self):
    ”””
    If no questions exist, an appropriate message is displayed.
    ”””
    response = self.client.get(reverse(‘polls:index’))
    self.assertEqual(response.status_code, 200)
    self.assertContains(response, “No polls are available.”)
    self.assertQuerysetEqual(response.context[‘latest_question_list’], [])

  def test_past_question(self):
    ”””
    Questions with a pub_date in the past are displayed on the
    index page.
    ”””
    create_question(question_text=”Past question.”, days=-30)
    response = self.client.get(reverse(‘polls:index’))
    self.assertQuerysetEqual(
      response.context[‘latest_question_list’],
      [‘‘]     )

  def test_future_question(self):
    ”””
    Questions with a pub_date in the future aren’t displayed on
    the index page.
    ”””
    create_question(question_text=”Future question.”, days=30)
    response = self.client.get(reverse(‘polls:index’))
    self.assertContains(response, “No polls are available.”)
    self.assertQuerysetEqual(response.context[‘latest_question_list’], [])

  def test_future_question_and_past_question(self):
    ”””
    Even if both past and future questions exist, only past questions
    are displayed.
    ”””
    create_question(question_text=”Past question.”, days=-30)
    create_question(question_text=”Future question.”, days=30)
    response = self.client.get(reverse(‘polls:index’))
    self.assertQuerysetEqual(
      response.context[‘latest_question_list’],
      [‘‘]     )

  def test_two_past_questions(self):
    ”””
    The questions index page may display multiple questions.
    ”””
    create_question(question_text=”Past question 1.”, days=-30)
    create_question(question_text=”Past question 2.”, days=-5)
    response = self.client.get(reverse(‘polls:index’))
    self.assertQuerysetEqual(
      response.context[‘latest_question_list’],
      [‘‘, ‘‘]     )

ここまでで、未来の質問はindexに表示されないようになりますが、URLを直接入力した場合は、まだ表示されてしまいます。

そこで、DetailViewにも同じように修正を加えます。

class DetailView(generic.DetailView):
  …
  def get_queryset(self):
    ”””
    Excludes any questions that aren’t published yet.
    ”””
    return Question.objects.filter(pub_date__lte=timezone.now())

そして、過去の質問が表示されることを確認するテストと、が未来の質問が表示されないことを確認するテストを加えて、テストを実施します。

class QuestionDetailViewTests(TestCase):
  def test_future_question(self):
    ”””
    The detail view of a question with a pub_date in the future
    returns a 404 not found.
    ”””
    future_question = create_question(question_text=’Future question.’, days=5)
    url = reverse(‘polls:detail’, args=(future_question.id,))
    response = self.client.get(url)
    self.assertEqual(response.status_code, 404)

  def test_past_question(self):
    ”””
    The detail view of a question with a pub_date in the past
    displays the question’s text.
    ”””
    past_question = create_question(question_text=’Past Question.’, days=-5)
    url = reverse(‘polls:detail’, args=(past_question.id,))
    response = self.client.get(url)
    self.assertContains(response, past_question.question_text)

静的ファイルの管理

最後に静的ファイルの管理についてです。

Djangoでは、javascriptやcssなどのファイルを静的(static)ファイルと呼びます。

templatesディレクトリの時と同様、pollsディレクトリにstaticフォルダを作成し、さらにその中にpollsフォルダを作成します。

そしてそこにstyle.cssを配置します。(polls/static/polls/style.css)

style.cssに以下のコードを書きます。

li a {
  color: green;
}

次にindex.html上部に以下のコードを追加します。

{% load static %}

サーバーを起動 (すでに起動済みの場合は再起動)すれば、リンクの文字色が緑色に変わっているのを確認できると思います。

次に背景画像の追加です。

polls/static/polls/の中に、imageフォルダを作成し、その中に画像を配置します。

そしてstyle.cssに以下のコードを書くと、背景画像を表示されます。

body {
  background: white url(“images/background.gif”) no-repeat;
}

adminサイトのカスタマイズ

ここからは管理画面の編集についてです。

たとえば、polls/admin.pyを以下のように書き換えることで、編集フォームでのフィールドの並び順が入れ替わります。

from django.contrib import admin

from .models import Question

class QuestionAdmin(admin.ModelAdmin):
  fields = [‘pub_date’, ‘question_text’]

admin.site.register(Question, QuestionAdmin)

さらに、フォームを複数のフィールドセットにするには、以下のようにします。

from django.contrib import admin

from .models import Question

class QuestionAdmin(admin.ModelAdmin):
  fieldsets = [
    (None, {‘fields’: [‘question_text’]}),
    (‘Date information’, {‘fields’: [‘pub_date’]}),
  ]

admin.site.register(Question, QuestionAdmin)

Questionの管理ページにChoiceも表示させる場合には、Questionと同様、モデルを登録します。

from django.contrib import admin

from .models import Choice, Question #変更

class QuestionAdmin(admin.ModelAdmin):
  fieldsets = [
    (None, {‘fields’: [‘question_text’]}),
    (‘Date information’, {‘fields’: [‘pub_date’]}),
  ]

admin.site.register(Question, QuestionAdmin)
admin.site.register(Choice) #追加

ただ、Questionを追加するときに、Choiceも追加できた方が便利なので、さきほどのコードをさらに編集します。(admin.site.register(Choice)の一文は削除する)

from django.contrib import admin

from .models import Choice, Question

class ChoiceInline(admin.StackedInline): #追加
  model = Choice #追加
  extra = 3 #追加

#変更箇所
class QuestionAdmin(admin.ModelAdmin):
  fieldsets = [
    (None, {‘fields’: [‘question_text’]}),
    (‘Date information’, {‘fields’: [‘pub_date’], ‘classes’: [‘collapse’]}),
  ]   inlines = [ChoiceInline] #変更箇所

admin.site.register(Question, QuestionAdmin)

さらに見やすくするために、「class ChoiceInline(admin.StackedInline):」を以下のように書き換えて、見え方を確認してみてください。よりコンパクトなテーブル形式で表示されます。

class ChoiceInline(admin.TabularInline):

これでQuestionの管理ページはOKなので、次はチェンジリストのページです。

下記のようにlist_displayオプションを追加することで、日付などの値も表示されるようになります。

class QuestionAdmin(admin.ModelAdmin):
  # …
  list_display = (‘question_text’, ‘pub_date’)

カラムのヘッダをクリックすると、カラムの値に応じてエントリを並べ換えることができますが、was_published_recently ヘッダは対応していないので、polls/models.pyにあるこのメソッドに少し書き加えます。

class Question(models.Model):
  # …
  def was_published_recently(self):
    now = timezone.now()
    return now – datetime.timedelta(days=1) <= self.pub_date <= now   was_published_recently.admin_order_field = 'pub_date' #追加   was_published_recently.boolean = True #追加   was_published_recently.short_description = 'Published recently?' #追加

さらに、polls/admin.pyを編集して、次の一行を加えます。

list_filter = [‘pub_date’]

これによってフィルタができるようになります。

また、次の一行を加えることで、検索ボックスが表示されます。

search_fields = [‘question_text’]

最後に、管理画面上部に書かれている「Django adminstration」というタイトルの変更についてです。

まず、プロジェクトディレクトリ(manage.py が置かれているディレクトリ)に「templates」ディレクトリを作成し、その中に、「admin」ディレクトリを作成します。

mysite/settings.pyのTEMPLATES設定オプションの中に、次のようにDIRSオプションを追加します。

TEMPLATES = [
  {
    ’BACKEND’: ‘django.template.backends.django.DjangoTemplates’,
    ’DIRS’: [os.path.join(BASE_DIR, ‘templates’)], #追加
    ’APP_DIRS’: True,
    ’OPTIONS’: {
      ’context_processors’: [
        ’django.template.context_processors.debug’,
        ’django.template.context_processors.request’,
        ’django.contrib.auth.context_processors.auth’,
        ’django.contrib.messages.context_processors.messages’,
      ],
    },
  },
]

そして、Django自体のソースコード内にある、デフォルトのDjango adminテンプレートディレクトリ (django/contrib/admin/templates)を探して、admin/base_site.htmlというテンプレートを、新しく作ったadminディレクトリにコピーします。

Django のソースファイルがシステム中のどこにあるのか分からない場合は、以下のコマンドを実行して確認します。

python -c “import django; print(django.__path__)”

あとは、任意のタイトルに変更するだけ。以下のように編集すると、「Django adminstration」が「Polls Administration」に変わります。

{% block branding %}
<h1 id=”site-name”><a href=”{% url ‘admin:index’ %}”>Polls Administration</a></h1>
{% endblock %}

チュートリアルは以上となります。

長かったですが、チュートリアルを一通りすると、Djangoの基本的なことはわかりました。

Leave a Comment