WebOS Goodies

WebOS の未来を模索する、ゲームプログラマあがりの Web 開発者のブログ。

WebOS Goodies へようこそ! WebOS はインターネットの未来形。あらゆる Web サイトが繋がり、共有し、協力して創り上げる、ひとつの巨大な情報システムです。そこでは、あらゆる情報がネットワーク上に蓄積され、我々はいつでも、どこからでも、多彩なデバイスを使ってそれらにアクセスできます。 WebOS Goodies は、さまざまな情報提供やツール開発を通して、そんな世界の実現に少しでも貢献するべく活動していきます。
Subscribe       

「Using Django with Appengine」邦訳

ようやく少しずつ時間が取れるようになってきたので、 Google App Engine の勉強をはじめています。 Python の書き方や App Engine 自体の使い方を覚えるのは苦ではないのですが、大変なのはその先、ライブラリやフレームワークなどを把握するのが厄介なんですよね。

まあとりあえずは App Engine に付属している Web フレームワーク Django に挑戦しようと思い、良いドキュメントはないかと探してみたところ、「Using Django with Appengine」という、そのものズバリなページを発見しました。 Django のチュートリアルを App Engine 用に書き直したもののようです。素晴らしい。著者の Shabda Raaj 氏に感謝です。

そんなわけで、勉強ついでにそのページを翻訳してみました。いつものごとくフィーリングで訳しているので、あまり人前に出せるような出来ではないのですが、それでも無いよりはマシだろうということで公開します。これから App Engine を使ってみようという方は参考にしてください。誤訳などのご指摘も大歓迎です。

Using Django with Appengine

著者:Shabda Raaj
バージョン:1
Copyright:このドキュメントは Creative Commons Attribution license で配布されます。
翻訳:伊藤千光
原文:http://www.42topics.com/dumps/django/docs.html

Contents

About

Django を App Engine に統合するためのチュートリアルにようこそ。この文書は純粋な Django の代わりに App Engine を使用する Django チュートリアルの改変版です。 Djaongo チュートリアルと同様に、複数の質問を作成してユーザーがそれらに投票できる投票エンジンを構築します。 Django チュートリアルの 4 つのパートの代わりに、ここではただひとつのチュートリアルを使います。また、多くの Django の機能と同様に、傑出した素晴らしい管理インターフェースは動作しないので、それらも回避することにします。このチュートリアルは皆さんが Python をよく知っていることを前提にしていますが、 Django の予備知識は不要です。 Django のコンセプトから解説しています。

What will we build

ここではこれより、人々が質問を作成し、その選択肢に投票することができる投票エンジンを構築します。基本的にこのアプリケーションは以下の 4 つのタイプのページを持ちます。

  1. 最近作成された質問のリスト。
  2. 質問の新規作成ページ。
  3. 選択肢に投票するためのページ。
  4. 集計結果のページ。

ライブデモが blogango.appspot.com にて公開されています。完全なソースはここからダウンロードできます。

Downloading Django, and Appengine

Dojango のダウンロード方法はとてもわかりやすいので、ここでは繰り返しません。ましてや皆さんは既にApp Engine をダウンロードしていますよね?それならば、 Django のダウンロード方法はこのチュートリアルには必要ありません。 App Engine は Django 0.96 ビルドを含んでいるからです。

Getting Help

もしこのチュートリアルでトラブルに遭遇したら、 Django フォーラム で質問してみてください。 Django コミュニティーの誰かが助けてくれるでしょう。もしくは、 42topics.com blog にコメントを残してくれれば、私も力になれるよう努めます。

Creating a project

App Engine と Django のすべてのファイルを格納するディレクトリをお好みの場所に作成し、そのディレクトリの中で以下のコマンドを実行します:

django-admin.py startproject appproject

このコマンドは Django で使われるすべてのファイルを格納するディレクトリを appproject という名前で作成します。すぐに我々はその中身を見ていくことになりますが、その前にさらに 2 つのファイルを作成する必要があります。 appproject フォルダーと同じ階層に main.py というファイルを作成し、内容を以下のようにします:

import os,sys
os.environ['DJANGO_SETTINGS_MODULE'] = 'appproject.settings'

# Google App Engine imports.
from google.appengine.ext.webapp import util

# Force Django to reload its settings.
from django.conf import settings
settings._target = None

import django.core.handlers.wsgi
import django.core.signals
import django.db
import django.dispatch.dispatcher

# Log errors.
#django.dispatch.dispatcher.connect(
#   log_exception, django.core.signals.got_request_exception)

# Unregister the rollback event handler.
django.dispatch.dispatcher.disconnect(
    django.db._rollback_on_exception,
    django.core.signals.got_request_exception)

def main():
  # Create a Django application for WSGI.
  application = django.core.handlers.wsgi.WSGIHandler()

  # Run the WSGI CGI handler with that application.
  util.run_wsgi_app(application)

if __name__ == '__main__':
  main()

さらに app.yaml という名前で別のファイルを作成し、内容を以下のようにします:

application: appproject
version: 1
runtime: python
api_version: 1

handlers:
- url: /.*
  script: main.py

これら 2 つのファイルは App Engine に我々のアプリケーションに関するいくつかの事柄と、リクエストの処理に Django を利用することを伝えています。 url: /.* という行は、すべての URL のリクエストを main.py が処理することを示しています。

以下のコードは Django wsgi サーバーを作成し、それにリクエストを委譲しています。

def main():
          # Create a Django application for WSGI.
          application = django.core.handlers.wsgi.WSGIHandler()

          # Run the WSGI CGI handler with that application.
          util.run_wsgi_app(application)

appproject フォルダに移動して、このコマンドを実行してください:

python manage.py startapp poll

manage.pydjango-admin.py によって作られたファイルです。皆さんの Django アプリケーションを管理するためのコマンドを含んでいます。 python manage.py startapp poll はこのアプリケーションに関連するすべてのファイルを保持するディレクトリを作成します。このディレクトリにある、もうひとつの興味深いファイルが settings.py です。ここには皆さんのプロジェクトの設定が書かれています。

Editing the settings.py file

純粋な Django と Django + App Engine のもっとも大きな相違点は ORM です。 App Engine は従来のデータベースサーバーにはデータを保存しないので、 Django ORM は App Engine では動作しません。 Django にバンドルされている多くのアプリケーション(とくに admin, auth, sessions など)も動作しません。 django-admin.py startproject appproject で作成した settings.py は皆さんがこれらのアプリを使うことを想定しているので、それらのアプリケーションを settings.py から取り除かなければなりません。

settings.py を編集し、 MIDDLEWARE_CLASSESINSTALLED_APPS を以下のように変更します:

MIDDLEWARE_CLASSES = (
    'django.middleware.common.CommonMiddleware',
)

INSTALLED_APPS = (
    'appproject.poll'
)

さらに、ページを表示する土台として使うファイルがどこにあるのかを、 Django に教えてやらなければなりません。 TEMPLATE_DIRS を以下のように変更します:

import os
ROOT_PATH = os.path.dirname(__file__)
TEMPLATE_DIRS = (
    # Put strings here, like "/home/html/django_templates" or
    # "C:/www/django/templates".  Always use forward slashes, even on Windows.
    # Don't forget to use absolute paths, not relative paths.
    ROOT_PATH + '/templates',
)

これは HTML テンプレートをカレントディレクトリからの相対で templates という名前のディレクトリから探すように Django に指示しています。

Write the URL configuration file

Django は与えられた URL をどの関数が処理するのかを示すために、正規表現を使います。 urls.py ファイルを以下のように編集します:

from django.conf.urls.defaults import *

urlpatterns = patterns('',
    (r'^$', 'pollango.poll.views.index'),
    (r'^create/$', 'pollango.poll.views.create'),
    (r'^poll/(?P<poll_key>[^\.^/]+)/$', 'pollango.poll.views.poll_detail'),
    (r'^poll/(?P<poll_key>[^\.^/]+)/results/$', 'pollango.poll.views.poll_results'),
    )

我々は 4 つの異なったタイプのページを持っており、それぞれのページが個々にマップされています。 (r'^$', 'pollango.poll.views.index'), という行を見てみましょう。正規表現 r'^$' はサイトルートにマッチします(^ は URL のパス部分の先頭にマッチし、 $ は末尾にマッチするので、結果的に追加のパスを持たない URL がこの行でハンドルされます)。 'pollango.poll.views.index' はその URL が呼び出された際にこの関数が使われることを示しています。

同様に、 (r'^create/$', 'pollango.poll.views.create'),\create\ を含む URL が pollango.poll.views.create でハンドルされることを示しています。

Write the models.py file.

データモデルは models.py ファイルで定義する必要があります。今回の場合、質問を保存する Poll と関連する選択肢を保存する Choice の 2 つのエンティティーがあります。したがって、 models.py ファイルには以下のように記述します:

from google.appengine.ext import db

class Poll(db.Model):
    question = db.StringProperty()
    created_on = db.DateTimeProperty(auto_now_add = 1)
    created_by = db.UserProperty()

    def __str__(self):
        return '%s' %self.question

    def get_absolute_url(self):
        return '/poll/%s/' % self.key()


class Choice(db.Model):
    poll = db.ReferenceProperty(Poll)
    choice = db.StringProperty()
    votes = db.IntegerProperty(default = 0)

Poll のデータモデルに注目しましょう:

class Poll(db.Model):
            question = db.StringProperty()
            created_on = db.DateTimeProperty(auto_now_add = 1)
            created_by = db.UserProperty()

データベースへの保存を行うエンティティーは google.appengine.ext.db.Model を継承しなければいけないので、 Poll もそうしています。エンティティーのそれぞれのアトリビュートは google.appengine.ext.db.*Property 型である必要があります。例えば、整数を保存するアトリビュートは google.appengine.ext.db.IntegerProperty 型にします。

Poll の場合、

  1. 質問を保存する question アトリビュートは db.StringProperty
  2. 質問者を保存する created_bydb.UserProperty()
  3. 作成時刻を保存する created_ondb.DateTimeProperty

となっています。 created_on のキーワード引数 auto_now_add はオブジェクトが最初に作成された時刻を保存するように ORM に指示しています。

以下のメソッドは文字列表現と Poll の URL の取得に使われます。

def __str__(self):
    return '%s' %self.question

def get_absolute_url(self):
    return '/poll/%s/' % self.key()

Choice も同様に db.model を継承し、アトリビュートを定義します。

class Choice(db.Model):
        poll = db.ReferenceProperty(Poll)
        choice = db.StringProperty()
        votes = db.IntegerProperty(default = 0)

Writing the forms

Django は HTML フォームを django.newsforms.Form 型の Python オブジェクトで表現することで状態を追跡します。

今回の場合、 2 つのフォームが必要です。

  1. 質問の作成。
  2. 選択肢の作成。

それでは新規ファイル bforms.py を作成し、以下の内容にしてください:

from django import newforms as forms
import models
from google.appengine.ext.db import djangoforms

class PollForm(djangoforms.ModelForm):
    class Meta:
        model = models.Poll
        exclude = ['created_by']

class ChoiceForm(forms.Form):
    choice = forms.CharField(max_length = 100)

    def __init__(self, poll=None, *args, **kwargs):
        self.poll = poll
        super(ChoiceForm, self).__init__(*args, **kwargs)

    def save(self):
        choice = models.Choice(poll = self.poll, choice = self.clean_data['choice'])
        choice.put()

それぞれのフォームを順番に見ていきましょう:

class PollForm(djangoforms.ModelForm):
        class Meta:
            model = models.Poll
            exclude = ['created_by']

PollForm は新しい質問を作成するためのフォームです。 google.appengine.ext.db.djangoforms.ModelForm によって App Engine エンティティーに関連する Django フォームを作成できます。 model = models.Poll はどのエンティティーのためのフォームかを定義しています。 exclude = ['created_by'] は Django に created_by のためのフィールドを作らないように指示しています。

次は ChoiceForm です:

class ChoiceForm(forms.Form):
    choice = forms.CharField(max_length = 100)

    def __init__(self, poll=None, *args, **kwargs):
        self.poll = poll
        super(ChoiceForm, self).__init__(*args, **kwargs)

    def save(self):
        choice = models.Choice(poll = self.poll, choice = self.clean_data['choice'])
        choice.put()

特定の質問の選択肢のためのフォームなので、 forms.Form を継承して、必要なフィールドを定義しています。 choice = forms.CharField(max_length = 100) は Django に HTML の Textfield を作成するように指示しています。

def save(self): はこの質問に関連するデータが保存されたときのアクションを定義しています。 choice = models.Choice(poll = self.poll, choice = self.clean_data['choice']) で Choice オブジェクトを作成し、 choice.put() でデータベースに保存しています。 .clean_data はフィールドに関連するデータの検索方法です。

PollForm には .save メソッドを定義しませんでしたが、これは ModelForm が関連するオブジェクトにデータを保存するデフォルトの .save メソッドを持っているためです。

Write the view

作成中のアプリケーションには 4 つのタイプのページがあり、 urls.py には 4 つのエントリーがあります。従って、 4 つの URL パターンにマップされる 4 つの関数を作成する必要があります。 views.py を以下のように変更してください。もし理解できなくてもご心配なく。後で解説します。

from django.http import HttpResponse, HttpResponseRedirect
from pollango.poll import models
import bforms
from django.shortcuts import render_to_response

def render(template, payload):
    payload['recents'] = models.Poll.all().order('-created_on').fetch(5)
    return render_to_response(template, payload)

def index(request):
    polls = models.Poll.all().order('-created_on').fetch(20)
    payload = dict(polls = polls)
    return render('index.html', payload)

def create(request):
    if request.method == 'GET':
        pollform = bforms.PollForm()
        choiceforms = []
        for i in range(4):
            choiceforms.append(bforms.ChoiceForm(prefix = 'f%s'%i))
    if request.method == 'POST':
        pollform = bforms.PollForm(request.POST)
        choiceform = bforms.ChoiceForm()
        if pollform.is_valid():
            poll = pollform.save()
            choiceforms = []
            for i in range(4):
                choiceforms.append(bforms.ChoiceForm(poll=poll, prefix = 'f%s'%i, data=request.POST))
            for form in choiceforms:
                if form.is_valid():
                    form.save()
            return HttpResponseRedirect(poll.get_absolute_url())
    payload = dict(pollform=pollform, choiceforms=choiceforms)
    return render('create.html', payload)

def poll_detail(request, poll_key):
    poll = models.Poll.get(poll_key)
    choices = models.Choice.all().filter('poll = ', poll)
    if request.method == 'POST':
        choice_key = request.POST['value']
        choice = models.Choice.get(choice_key)
        choice.votes += 1
        choice.put()
        return HttpResponseRedirect('./results/')
    payload = dict(poll = poll, choices = choices)
    return render('poll_details.html', payload)

def poll_results(request, poll_key):
    poll = models.Poll.get(poll_key)
    choices = models.Choice.all().filter('poll = ', poll)
    payload = dict(poll = poll, choices = choices)
    return render('poll_results.html', payload)

def index(request) は URL にレスポンスする際に呼ばれる関数その 1 です。これは (r'^$', 'pollango.poll.views.index') という行で定義されています。

def index(request):
    polls = models.Poll.all().order('-created_on').fetch(20)
    payload = dict(polls = polls)
    return render('index.html', payload)

polls = models.Poll.all().order('-created_on').fetch(20) という行の中で、 models.Poll.all() はすべての Poll オブジェクトを取得し、 .order('-created_by') でそのクエリーセットを created_by の降順でソートし、そして .fetch(20) で最大 20 オブジェクトに制限します。これは以下の SQL と同等です:

SELECT *
FROM poll
ORDER BY created_on DESC
LIMIT 0, 30

このクエリーセットは遅延評価されるので、使用したオブジェクトのみがロードされます。

これを HTML に変換するために、表示に必要なオブジェクトの辞書を引数にして render を呼び出します。 render がさらに render_to_response(template, payload) へオブジェクトの辞書を引き渡し、そしてテンプレートを使用します。テンプレートがどのように動作するかは後述します。

二番目のビュー関数は create です:

def create(request):
    if request.method == 'GET':
        pollform = bforms.PollForm()
        choiceforms = []
        for i in range(4):
            choiceforms.append(bforms.ChoiceForm(prefix = 'f%s'%i))
    if request.method == 'POST':
        pollform = bforms.PollForm(request.POST)
        choiceform = bforms.ChoiceForm()
        if pollform.is_valid():
            poll = pollform.save()
            choiceforms = []
            for i in range(4):
                choiceforms.append(bforms.ChoiceForm(poll=poll, prefix = 'f%s'%i, data=request.POST))
            for form in choiceforms:
                if form.is_valid():
                    form.save()
            return HttpResponseRedirect(poll.get_absolute_url())
    payload = dict(pollform=pollform, choiceforms=choiceforms)
    return render('create.html', payload)

この関数は質問の新規作成フォームの表示とそれが送信された際の処理を担当します。 HTTP GET リクエストではフォームを表示し、 HTTP POST リクエストでは新しい質問を作成します:

if request.method == 'GET':
    pollform = bforms.PollForm()
    choiceforms = []
    for i in range(4):
        choiceforms.append(bforms.ChoiceForm(prefix = 'f%s'%i))

このコードは Poll エンティティーオブジェクトを作成するためのフォームと Choice に関連したフォームを作成します。それらのフォームは bforms.py で定義されています。

if request.method == 'POST':
    pollform = bforms.PollForm(request.POST)
    choiceform = bforms.ChoiceForm()
    if pollform.is_valid():
        poll = pollform.save()
        choiceforms = []
        for i in range(4):
            choiceforms.append(bforms.ChoiceForm(poll=poll, prefix = 'f%s'%i, data=request.POST))
        for form in choiceforms:
            if form.is_valid():
                form.save()
        return HttpResponseRedirect(poll.get_absolute_url())

POST リクエストが行われると、 request.method の値が POST になります。その場合はユーザーが値を入力したかどうかを pollform.is_valid()form.is_valid() でチェックします.値が入力されていた場合は form.save() が実行され、それが obj.put を呼び出してオブジェクトをデータベースに保存します。

そして表示されるべきオブジェクトがあれば、 render にそれらを引き渡します。

poll_detailpoll_resultsPoll とそれに関連する Choice を表示します。もし poll のキーが xxxyyyyzzzz なら、 poll_detail は /poll/xxxyyyyzzzz/ へのレスポンスとして呼び出されます。この場合 poll_details には xxxyyyyzzzzpoll_key として、リクエストデータについての HttpRequest オブジェクトが第一引数として渡されます。

def poll_detail(request, poll_key):
    poll = models.Poll.get(poll_key)
    choices = models.Choice.all().filter('poll = ', poll)
    if request.method == 'POST':
        choice_key = request.POST['value']
        choice = models.Choice.get(choice_key)
        choice.votes += 1
        choice.put()
        return HttpResponseRedirect('./results/')
    payload = dict(poll = poll, choices = choices)
    return render('poll_details.html', payload)

models.Poll.get(poll_key)poll_key をキーとするオブジェクトをひとつ取得します。これは以下の SQL クエリーと同等です:

SELECT * FROM poll
WHERE key = poll_key

choices = models.Choice.all().filter('poll = ', poll) はこの Poll オブジェクトに関するすべての Choice オブジェクトを取得します。これは以下と同じです:

SELECT *
FROM choice
WHERE poll_id = poll.id

直接 filter('poll = ', poll) と記述できることに注意してください。このキーの参照と逆参照は暗黙的に行われます。

Poll オブジェクトと Choice オブジェクトが取得できたら、それらを render に渡してページに表示します。

poll_results もほぼ同様に同じオブジェクトを取得しますが、それを別のページに表示します。

Writing the templates

もし PHP やそれに似た言語をご存知なら、 Django テンプレートは窮屈に感じるかもしれません。テンプレートはプレゼンテーションロジックを意味しているので、 python のすべての機能の代わりに単純なテンプレート言語を使います。

余談はこれくらいにして、以下がインデックスページのテンプレートです:

{% extends 'base.html' %}

{% block title %}
Pollengine - A polling app built with Django, and Appengine
{% endblock %}

{% block contents %}
<h2>Pollengine - A polling app built with Django, and Appengine </h2>

{% for poll in polls %}
<div class="poll">
<a href="{{poll.get_absolute_url}}">{{poll.question}}</a>
<br>
By {{poll.user}} on {{poll.created_on|date}}
</div>
{% endfor %}
{% endblock %}

2 つの構造が見て取れると思います。 {% … %} はタグと呼ばれ、ループのようなプログラミング構造を実現します。 {{ .. }} はテンプレートに渡されたオブジェクトへのアクセスを可能にします。例えば、関数 index の中では polls 変数をテンプレートに渡しているので、必要に応じて {{polls}} が使えます。そしてタグにはさらに興味深い事柄があります。

ほとんどのサイトにはナビゲーション項目のような共通の要素があります。それらをそれぞれのページに記述するのはとても無駄なことです。 Django ではテンプレートを拡張することができます。ベーステンプレートでは子テンプレートでオーバーライドできる要素を定義できます。 index.html の親テンプレートを見てみましょう。

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">

<html>
<head>
<title>
{% block title %}
Page title
{% endblock %}
</title>

<style type="text/css">
<!--
.contents{
  width: 70%;
        float: right;
}
.sidebar{
   width: 25%;
         float: right;
}
-->
</style>

</head>
<body>

<div class="contents">

{% block contents %}

asdf
{% endblock %}
</div>

<div class="sidebar">
{% block sidebar %}
<h3>Meta</h3>
<ul>
<li>
<a href="/create/"> Create new Poll</a>

</li>
</ul>

<h3>Recent Polls</h3>
<ul>
{% for poll in recents %}
<li>
<a href="{{poll.get_absolute_url}}">{{poll.question}}</a>

</li>
{% endfor %}
</ul>
{% endblock %}
</div>

</body>
</html>

これは単純な HTML ページです。しかし、子テンプレートがオーバーライドして個々の情報を挿入できるフックが 3 つ提供されています。 {% block contents %} がそのようなフックの開始を指示しています。ブロックは {% endblock %} で終了します。 Index.html{% block contents %} を再度定義することでオーバーライドします。 {% extends 'base.html' %} というステートメントは Django に index.html の親テンプレートを伝えます。

{% for poll in polls %} はループ構造で、このページに渡されたそれぞれの質問に対して働きます。このループの終端は {% endfor %} です。このループの内部では {{poll}} を使ってカレントオブジェクトにアクセスできます。

ほかにも index 以外のページに対応する 3 つのテンプレートがあります。それらはとても似通っているので、詳しく解説はしません。

create.html:

{% extends 'base.html' %}

{% block contents %}
<form action="." method="post">
{{pollform.as_p}}

{% for form in choiceforms %}
{{form.as_p}}
{% endfor %}

<input type="submit" name="createpoll" value="createpoll" />

</form>

{% endblock %}

poll_details.html:

{% extends 'base.html' %}

{% block title %}
{{poll.question}}
{% endblock %}

{% block contents %}
<div class="poll">
{{poll.question}}
<br />
{{poll.created_on|date}}
</div>

<br />
<form action="." method="post">
{% for choice in choices %}
<div class="choice">
{{ choice.choice }} <input type="radio" name="value" value="{{ choice.key }}">

</div>
{% endfor %}
<input type="submit" name="dovote" value="Choose" />
</form>
{% endblock %}

poll_results.html:

{% extends 'base.html' %}

{% block title %}
{{poll.question}} - results
{% endblock %}

{% block contents %}
<div class="poll">
{{poll.question}}
<br />
{{poll.created_on|date}}
</div>
<br />
{% for choice in choices %}

<div class="choice">
{{ choice.choice }}
{{choice.votes}}
</div>
{% endfor %}
{% endblock %}

What next

皆さんがこのチュートリアルを読んで Django の可能性に夢中になってくれることを望みます。 Django forum では Django についてさらに学び、そして行き詰まったときに助けを得ることができます。もしこのチュートリアルに関して疑問があれば 42topics.com blog にコメントしてください。力になれるよう努めます。

Links

皆さんはこれらのリンクにも興味を持つでしょう。

関連記事

この記事にコメントする

Recommendations
Books
「Closure Library」の入門書です。
詳しくはこちらの記事をどうぞ!
Categories
Recent Articles