BETA

Django チュートリアルやったメモ

投稿日:2018-11-06
最終更新:2019-02-13

Django チュートリアル | Django documentation | Django をやった作業 & 学習メモ

今回は、vagrant の CentOS7 + python3 の venv 上で Django の開発環境を構築し、お勉強する。

環境構築

CentOS7 に Python をインストールしよう。

# ius リポジトリをインストール  
yum install -y https://centos7.iuscommunity.org/ius-release.rpm  

# python3.6 をインストール  
yum install  python36*  

...  
# 以下のようなエラーが大量に発生  
Transaction check error:  
  file /usr/bin/python3.6 from install of python36-3.6.6-1.el7.x86_64 conflicts with file from package python36u-3.6.5-1.ius.centos7.x86_64  
  file /usr/bin/python3.6m from install of python36-3.6.6-1.el7.x86_64 conflicts with file from package python36u-3.6.5-1.ius.centos7.x86_64  
  file /usr/share/man/man1/python3.6.1.gz from install of python36-3.6.6-1.el7.x86_64 conflicts with file from package python36u-3.6.5-1.ius.centos7.x86_64  
  file /usr/include/python3.6m/pyconfig-64.h from install of python36-libs-3.6.6-1.el7.x86_64 conflicts with file from package python36u-libs-3.6.5-1.ius.centos7.x86_64  

# どうやらもともと入っている python3 系のモジュールと、ius で入れるモジュールが競合しているらしい?  
# なので、全部インストールせずに必要そうなパッケージだけ入れたることで回避  
yum install -y python36u python36u-devel python36u-libs  
yum install -y python36u-pip  

# シンボリックリンクを貼っておく  
ln -s /usr/bin/python3.6 /usr/bin/python3  
ln -s /usr/bin/pip3.6 /usr/bin/pip3  

# インストールできたか確認  
python3 --version  
Python 3.6.5  

pip3 --version  
pip 9.0.3 from /home/vagrant/python3/django_tutorial/lib64/python3.6/site-packages (python 3.6)

Django をインストールしよう。

# 適当なディレクトリに vnev を構築  
mkdir ~/python3/ && cd $_  
python3 -m venv django_tutorial  

# 起動  
source django_tutorial/bin/activate  

# django インストール  
pip install django  

# インストールされたか確認  
python3 -m django --version  
2.1.1

Django チュートリアルに倣って、新しいプロジェクト mysite を作成する。

# mysite プロジェクトの作成  
django-admin startproject mysite  

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

Django を動かす

準備が整ったので、開発用サーバーを立ち上げてみよう。

python3 manage.py runserver 0.0.0.0:8000  

# mygration 関連の警告はここでは無視する

http://localhost:8000
にアクセスして、ロケットが発射されていれば OK

うまくうごかないときに確認すること

ホストマシンのプロキシ設定

ローカルホストにはプロキシを使わないように設定する。

set  
...  
NO_PROXY=localhost,127.0.0.1

runserver のオプション

チュートリアルにあるとおりに

python3 manage.py runserver

を実行すると、 127.0.0.1:8000 でサーバーが立ち上がってしまい、仮想環境内で閉じた状態になっている。
そこで、runserver のオプションに 0.0.0.0:8000 を指定する。

python3 manage.py runserver 0.0.0.0:8000

ポートフォワーディングの設定

Django の runserver では、デフォルトで 8000 番ポートを利用するので、Vagrantfile の設定を以下のようにした。

config.vm.network "forwarded_port", guest: 8000, host: 8000

CentOS のファイアウォール設定

CentOS7 では、iptables ではなく firewalld を使っているので、firewall-cmd コマンドで8000ポートを開放する必要がある。

sudo firewall-cmd  --permanent --add-port=8000/tcp  
# success と出れば OK  

sudo firewall-cmd --reload  
# success と出れば OK

もしくは、 VM だし firewalld 自体を止めてしまう

sudo systemctl stop firewalld

その1のまとめ

mysite プロジェクト配下に polls というアプリケーションを作成する。

python manage.py startapp polls

polls/ というディレクトリが作られ、アプリケーションに必要なファイル・ディレクトリも作成される。

view と URL の対応づけ

polls/view.py に index という名前のメソッドを作成する。

polls/views.py

from django.http import HttpResponse  

def index(request):  

    return HttpResponse("Hello, world. You're at the polls index.")

これを URL に対応づけるためには、 URLconf というものを作成する必要がある。
URLconf には、アプリケーション側、プロジェクト側でそれぞれ設定する必要がある。

アプリケーション側

polls/urls.py

from django.urls import path  
from . import views  

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

path() の引数で name='index' とやることで、先程 view で作成した index メソッドと対応付けることができる。

プロジェクト側

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

include() で、polls アプリケーション内に作成した urls を参照することができる。
こうすることで、プロジェクト側の URLconf とアプリ側の URLconf が疎結合となるのでこのように書こう。

確認

一通りの設定が終わったら http://localhost:8000/polls/ にアクセスして
Hello, world. You're at the polls index. が表示されていれば OK.

その2のまとめ

プロジェクトの基本設定は、mysite/settings.py を編集することで可能。
DB の設定 (どの DB を利用するか) などもここに記述する。デフォルトは SQLite。

デフォルトで入っているアプリケーション (INSTALLED_APPS に記述されているもの) は、DB を用いるので、使い始める前にデータベースのテーブルを作成する。

python3 manage.py migrate

また、今回作成する polls アプリもプロジェクトに含めるために INSTALLED_APPS の中にアプリケーションの構成クラスを記述する。

mysite/settings.py

INSTALLED_APPS = [  
    'polls.apps.PollsConfig',  
    ...  
]

モデルの作成

以下のようなモデルを作成した。

polls/models.py

import datetime  

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

# Create your models here.  

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
  • 作成したいテーブルを class として記述する
    • その中に、作成するフィールドを記述していく。
    • 外部キーなど、RDB の思想に則って記述することが可能。
    • 詳しくは モデル | Django documentation を参照
  • _str_ メソッドを追加することで、インスタンスの (question|choice)_text を返すようにしている。
  • class の中に自作メソッドを作成することで、モデルの API としての機能を実現できる。

モデルの有効化

モデルを作成したら、django が理解できるようにするために、モデルを有効化する。
mysite/settings.py に、アプリケーションの構成クラスが追記されていることを確認したら、

python3 manage.py makemigrations polls

を実行することで、Django にモデルの変更があったこと (今回はモデルの新規作成) を伝え、マイグレーションの形で保存できる。

マイグレーションの結果は、polls/migrations/0001_initial.py のような形でファイルとして保存され、そのマイグレーションでどういう構成になったか確認できる。
直にファイルを見てもよいが、

python manage.py sqlmigrate polls 0001

とすることで、マイグレーションの結果をクエリとして見ることができる。
django でのマイグレーションの流れ

  1. モデルを変更する
  2. マイグレーションを作成するために python3 manage.py makemigrations を実行
  3. DB に変更を適用するため python3 manage.py migrate を実行

その3のまとめ

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

保守性を持たせるため、view と template を利用する。
最終的にこのようになった。

polls/views.py

from django.shortcuts import get_object_or_404, render  
from django.http import HttpResponse  

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):  
    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)

polls/templates/polls/index.html

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

polls/templates/polls/detail.html

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

polls/urls.py

from django.urls import path  

from . import views  

app_name = 'polls'  
urlpatterns = [  
    # ex: /polls/  
    path('', views.index, name='index'),  
    # ex: /polls/5/  
    # the 'name' value as called by the {% url %} template tag  
    path('<int:question_id>/', views.detail, name='detail'),  
    # ex: /polls/5/results/  
    path('<int:question_id>/results/', views.results, name='results'),  
    # ex: /polls/5/vote/  
    path('<int:question_id>/vote/', views.vote, name='vote'),  
]
  • views にメソッド追加 & urls に path を追加することで、簡単に対応付けができる。
    • <int:question_id> のように <> を用いることで URL の一部がキーワード引数として views に送信される。
  • django.shortcutsrender() を使うことで、html のテンプレートと views の対応づけが簡単にできる。
    • 第2引数にテンプレートのパスを指定する
      • テンプレートの名前空間を考慮して polls/templates/polls/ 配下にテンプレートを作成するのが良い。
    • 第3引数の context は、テンプレートの変数名と python オブジェクトをマッピングするための辞書。
  • get_object_or_404() ショートカットを使うことで「オブジェクトが取得できなかった場合は 404 を表示」というよくあるエラーハンドリングが簡単にできる。
  • テンプレート内での url は {% url 'detail' %} のように書くと、urls の name 属性に指定されたものと紐づけされるので、アクセスする url を変えたい場合などの保守性が向上する。
  • テンプレートの詳細、記法は テンプレート | Django documentation を参照

その4のまとめ

フォームを作成する

polls/templates/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>
  • question.id を外部キーとする choice を表示し、ラジオボタンで選択・submit する。
  • post form の CSRF 対策として、Django で用意された {% csrs_token %} を用いるとよい。
    • というか、POST フォームにはすべて適用しよう。

このテンプレートに対する view

polls/views.py

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

from .models import Choice, 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):  
        # Redisplay the question voting form.  
        return render(request, 'polls/detail.html', {  
            'question': question,  
            'error_message': "You didn't select a choice.",  
        })  
    else:  
        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.  
        return HttpResponseRedirect(reverse('polls:results', args=(question.id,)))
  • request.POST は、キーを指定すると送信したデータにアクセスできる。
    • POST データに 'choice' がなかった場合、request.POST['choice'] は KeyError になるので、例外処理を施す。
  • POST が成功した場合は HttpResponseRedirect コンストラクタの reverse() を用いてリダイレクト処理させると良い。
    • 今回は result ページにリダイレクトさせるようにした。

その result ページのテンプレート

polls/templates/polls/result.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>

view ついては、detail のページと同じ感じになるので、汎用ビュー を使おう。
汎用ビューに変換するには以下の3ステップ

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

polls/urls.py

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

polls/views.py

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'  
    context_object_name = 'latest_question_list'  

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


class DetailView(generic.DetailView):  
    model = Question  
    template_name = 'polls/detail.htm'  


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


def vote(request, question_id):  
    ...
  • ListView, DetailView は、リスト表示、詳細表示、の概念を抽象化したもの。
  • 汎用ビューは、どのモデルに対して動作するのかを知っておく必要があるので、model 属性で指定する。
  • DetailView は、pk という名前で URL からプライマリキーを渡す約束になっているので、URL conf の <int:question_id><int:pk> に変更した。
  • ビューとテンプレートの変数の対応付けに context を使用したが、汎用ビューの場合は context_object_name 属性で指定できる。
    • 何も書かなかった場合は、DetailView ではモデルの名前 (question) が context となる。

その5のまとめ

テストをしよう

django でユニットテストを行うのは至って簡単。

python3 manage.py test polls
  • このコマンドを実行すれば、polls アプリケーションのユニットテストができる。
  • nosetest と同じような結果になるので、わかりやすい。

テストを書こう

最終的に、こんな感じになった。

polls/tests.py

import datetime  

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

from .models import Question  


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 question 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 in 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_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'],  
            ['<Question: Past question.>']  
        )  

    def test_future_question(self):  
        """  
        Questions with a pub_date in the future aren't displayed on  
        the index page.  
        """  
        create_question(question_text="Furure 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'],  
            ['<Question: Past question.>']  
        )  

    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'],  
            ['<Question: Past question 2.>', '<Question: Past question 1.>']  
        )  


class QuestionDetailViewTests(TestCase):  

    def test_future_question(self):  
        """  
        The detail viewof a question with a pub_date in the future  
        return 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 witha pub_date in the past  
        displays the questions'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)  


class QuestionModelTests(TestCase):  

    def test_was_published_recently_with_old_question(self):  
        """  
        was_published_recentry() returns False for question 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 question 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)  

    def test_was_published_recently_with_future_question(self):  
        """  
        was_published_recently() returns False for question 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)

ポイント(長いので)

  • ユニットテストは、 django.test.TestCase クラスのサブクラスを発見する。
    • テストのための特別な DB を用いる。
    • テスト用メソッドとして、test から始まるメソッドを探す。
  • create_question() という question のショートカット関数を作成することで、テスト内で question 作成処理の重複をなくした。
  • アサーションについて
    • assertIs(a, b) : a と b が同じオブジェクトであるかを判定する
    • assertEqual(a, b) : a の値と b の値が等しいかを判定する
    • assertContains(a, b) : html レスポンス a に、文字列 b が含まれているか判定する
    • assertQuerysetEqual(a, b) : クエリの実行結果 a が b であるかを判定する

テストを通すための修正

モデル

polls/models.py

import datetime  

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

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() が、pub_date が未来の場合も Ture を返していたので、判別を厳しくした。

ビュー

polls/views.py

...  

class IndexView(generic.ListView):  
    ...  

    def get_queryset(self):  
        """  
        Return the last fice 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]  

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())  

...
  • index ページで未来の Question についても表示されるようになっていたのを修正。
    • Question.objects.filter(pub_date__lte=timezone.now()) とすることで pub_datetimezone.now 以前の Question を含んだクエリセットを返すようになった。
  • detail ページでも、未来の質問の詳細ページが見れるようになっていたのを修正。
    • pub_date が未来の場合は 404 を返すように。

テストを書くときに気にかけること

  • テストコードがアプリケーションのコードより大きくなったり、繰り返しが増えたりしても OK
  • テストが膨大になりそうなときのプラクティス
    • モデルやビューごとに TestCase を分割する。
    • テストしたい条件の集まりのそれぞれに対して、異なるテストメソッドを作る。
    • テストメソッドの名前は、その機能を説明できるようなものにする。

その6のまとめ

css を適用する

  • Django では、css や画像ファイル等を静的 (static) ファイルと呼ぶ。
  • template と同様に、static ディレクトリを作成しその子ディレクトリをそれぞれ作成。そこに配置する。
polls/static/  
└── polls  
      ├── images  
      │      └── background.gif  
      └── style.css

css と template はそれぞれこんな感じ

polls/static/polls/style.css

li a {  
    color: green;  
}  

body {  
    background: white url("images/background.gif") no-repeat;  
}
  • 静的ファイルをリンクする場合 (上記の例では背景画像を css に記述する)、相対パス で書くこと。
    • url が変わってもパスを修正する必要がないため

polls/templates/polls/index.html

{% load static %}  

<link rel="stylesheet" type="text/css" href="{% static 'polls/style.css' %}">  

{% 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 %}
  • {% load static %} することで、static 配下の静的ファイルをよしなに参照できる
    • href="{% static 'polls/style.css' %}" のように static 配下のどこの場所かだけを書けば良いので楽
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
駆け出しエンジニアからエキスパートまで全ての方々のアウトプットを歓迎しております!
or 外部アカウントで 登録 / ログイン する
クランチについてもっと詳しく

この記事が掲載されているブログ

よく一緒に読まれる記事

0件のコメント

ブログ開設 or ログイン してコメントを送ってみよう
目次をみる
技術ブログをはじめよう Qrunch(クランチ)は、プログラマの技術アプトプットに特化したブログサービスです
or 外部アカウントではじめる
10秒で技術ブログが作れます!