dackdive's blog

新米webエンジニアによる技術ブログ。JavaScript(React), Salesforce, Python など

Google App Engine Python Tutorialのメモ(4)

第4回はChapter 7 Using Templatesです。

(追記)その他のメモ

はじめに

公式リファレンスではJinja2というライブラリを使っています。
しかし、個人的な事情により今回はDjangoというライブラリを使ってこのチャプターをやってみたいと思います。

参考にしたサイト

いつもはリファレンスを一番最後に書くんですが、今回はここに。

まずは公式リファレンス。

ただし、上記は2010年に書かれたもので、情報が少し古いです。

また、日本語で参考になりそうなのはこのあたりのサイト。

ただ、ここもチュートリアルについて書かれたものでなかったり、微妙に情報が違っていたりするので注意が必要。

そして、おそらく一番参考になったのがこのサイト(英語ですが...)。

GitHub

今回やってみたコードはGitHubで公開してます。

https://github.com/zaki-yama/gae.git

git checkout chapter-7

とするとコードが確認できます。

また、

git checkout chapter-6

とすると、前回までで作成したguestbookアプリになるので、比較することができます。

Djangoの基礎知識

Converting to Django's file structure

↑はリファレンス[1]。中盤のこのチャプターから読んだ方がわかりやすいような。
Djangoディレクトリ構成について書かれてます。

Django has the concept of a project, which is essentially a set of global files that dictate behavior and manage one or more applications.
You can think of a project as a single website with one or more apps that run underneath it like a blog, guestbook, etc.
You can read more about the differences between projects and apps by taking a look at the sidebar in the Creating Models section of the Django tutorial.
Note that the terminology differs from App Engine where the "app" is all-inclusive of everything you upload to Google, so in that sense, it's more like what you would consider a Django project.

(以下、訳)
Djangoディレクトリ構成における一番大きな単位はprojectと呼ばれ、1つないし複数のapplicationsを管理することができる。
例えば、ブログやゲストブックなどの複数のアプリケーションからなるウェブサイトを構築しようと考えた時、
そのウェブサイトが1つのprojectという単位になる。
projectapps(applications)の違いについてはDjangoチュートリアルCreating Modelsという部分を読むこと。

ここで注意点として、App Engineでappと呼んでいるのは(複数のアプリもろもろ全部ひっくるめて)GAEにアップロードするものの単位なので、
Djangoでいうところのprojectに相当すると考えた方がよい。

Djangoの構成

1つのprojectは次の4つのファイル+アプリケーションごとのディレクトリ(複数)からなります。

  • __init__.py: パッケージであることをpythonに認識させるために必要
  • urls.py: グローバルなURL設定(URL Conf)ファイル
  • settings.py: プロジェクト固有の設定
  • manage.py: command-line interface for apps

そして、これらのファイルと同じ階層に置かれているアプリケーションごとのディレクトリは
次のようなファイル構成になっています。

  • ___init__.py: プロジェクトと同じ役割
  • models.py: データモデル
  • views.py: リクエストハンドラ
  • tests.py: ユニットテスト

これら4つのファイルに加え、アプリケーションレベルでのURL設定ファイルおよびtemplatesディレクトリなどが含まれます。

projectを作成してみる(Getting Started with Django)

早速、Djangoのprojectを作成してみます。
いったん、これまでに作成したguestbookとは無関係のところに
新規プロジェクトとして作成します。

これまでのguestbookアプリケーションを~/workspace/gae以下に作成していたとすると、

~/workspace/gae $ django-admin.py startproject [project名]

と実行します。
django-admin.py

/usr/local/google_appengine/lib/django-1.5/django/bin/

ディレクトリにあるもの(SDKに付属のもの)を使用。
また、[project名]は任意ですが、今回はgae_django_appとします。

その結果、

.
├── gae_django_app
│   ├── gae_django_app
│   │   ├── __init__.py
│   │   ├── settings.py
│   │   ├── urls.py
│   │   └── wsgi.py
│   └── manage.py
└── guestbook(これまでに作成したもの)

このように、gae_django_appディレクトリが作成され、その中に
manage.pyというファイルと、同名のディレクトリ、
さらにその中にprojectの基本定義ファイルが作成されます。

次に、アプリケーションを作成します。
アプリケーション名はguestbookとします。

~/workspace/gae $ cd gae_django_app
~/workspace/gae/gae_django_app $ ./manage.py startapp guestbook

すると、以下のようなディレクトリ構成になります。

before

gae_django_app(root)
├── guestbook(Application)
│   ├── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── manage.py
└── gae_django_app(Project)
    ├── __init__.py
    ├── settings.py
    ├── urls.py
    └── wsgi.py

このまま、上のgae_django_app(root)直下にapp.yamlを作成して
GAEにデプロイできるようにしていってもいいのですが

  1. gae_django_appの下にもう1個gae_django_appというディレクトリがあるのは不自然(というかムダ?)
  2. gae_django_app/guestbook(プロジェクトの下にアプリケーション)というディレクトリ構成にしたい

という理由から、gae_django_app(Project)の下にguestbook(Application)をまるっと移動し、
さらにmanage.pygae_django_app(root)は不要なので削除します。
(※あくまでこれは好みです)

after

gae_django_app(Project)
├── __init__.py
├── guestbook(Application)
│   ├── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── settings.py
├── urls.py
└── wsgi.py

app.yamlを作成する

作成したファイル群をGAEにデプロイできるようにするため、
まずはapp.yamlを作成します。
公式チュートリアルChapter6. Using the Datastoreの時点では次のような内容だったと思います。

before

application: your-app-id
version: 1
runtime: python27
api_version: 1
threadsafe: TRUE


handlers:
- url: /.*
  script: guestbook.application

これを、次のように編集します。

after

application: your-app-id
version: 1
runtime: python27
api_version: 1
threadsafe: TRUE

libraries:
- name: django
  version: "1.5"

builtins:
- django_wsgi: on

handlers:
- url: /.*
  script: wsgi.application

モデル(models.py)

上にあげたリファレンス[1]には、 GAEのモデルをDjangoでも使えるようにするための設定が色々と書かれています。

が、ndbを使っている限りは
とりあえずそのままGAEのモデルが使えるようです。

ですので、Chapter6までで作成したguestbook.pyのモデル定義部分をそのままmodels.pyに移動します。

models.py

from google.appengine.ext import ndb

DEFAULT_GUESTBOOK_NAME = 'default_guestbook'

# We set a parent key on the 'Greetings' to ensure that they are all in the same
# entity group. Queries across the single entity group will be consistent.
# However, the write rate should be limited to ~1/second.


def guestbook_key(guestbook_name=DEFAULT_GUESTBOOK_NAME):
    """ Constructs a Datastore key for a Guestbook entity with guestbook_name."""
    return ndb.Key('Guestbook', guestbook_name)

class Greeting(ndb.Model):
    """ Models an individual Guestbook entry."""
    author = ndb.UserProperty()
    content = ndb.StringProperty(indexed=False)
    date = ndb.DateTimeProperty(auto_now_add=True)

ビュー(views.py)

guestbook.pyを修正してviews.pyを作成します。
まず、コード全体をこちらに。

views.py

# -*- encoding: utf-8 -*-
import urllib

from django.http import HttpResponse
from django.http import HttpResponseRedirect
from django.views.generic.base import TemplateView
from django.template import Context, loader
from django.shortcuts import render
from django.core.context_processors import csrf

from google.appengine.api import users

from .models import Greeting, guestbook_key, DEFAULT_GUESTBOOK_NAME


""" 変更点: class MainPageでなく普通のメソッドになった """
def main_page(request):
    guestbook_name = request.GET.get('guestbook_name',
            DEFAULT_GUESTBOOK_NAME)

    # Ancestor Queries, as shown here, are strongly consistent with the High
    # Replication Datastore. Queries that span entity groups are eventually
    # consistent. If we omitted the ancestor from this query there would be
    # a slight chance that Greeting that had just been written would not
    # show up in a query.
    greetings_query = Greeting.query(
            ancestor=guestbook_key(guestbook_name)).order(-Greeting.date)
    greetings = greetings_query.fetch(10)

    """ 変更点: for greeting in ...は不要 """

    if users.get_current_user():
        url = users.create_logout_url(request.get_full_path())
        url_linktext = 'Logout'
    else:
        url = users.create_login_url(request.get_full_path())
        url_linktext = 'Login'

    """ 変更点: templateに埋め込むcontextを定義する """
    template_values = Context({
            # 'user':      user,
            'greetings': greetings,
            'guestbook_name': guestbook_name,
            'url': url,
            'url_linktext': url_linktext,
            })
    template_values.update(csrf(request))
    return HttpResponse(loader.get_template('guestbook/main_page.html').render(template_values))


def sign_post(request):
    if request.method == 'POST':
        # We set the same parent key on the 'Greeting' to ensure each Greeting
        # is in the same entity group. Queries across the single entity group
        # will be consistent. However, the write rate to a single entity group
        # should be limited to ~1/second.
        guestbook_name = request.POST.get('guestbook_name', DEFAULT_GUESTBOOK_NAME)

        greeting = Greeting(parent=guestbook_key(guestbook_name))

        if users.get_current_user():
            greeting.author = users.get_current_user()

        greeting.content = request.POST.get('content')
        greeting.put()

        query_params = {'guestbook_name': guestbook_name}
        return HttpResponseRedirect('/?' + urllib.urlencode(query_params))

    return HttpResponseRedirect('/')

主な変更点はコメントとしてコード中に記載しました。

また、それ以外に参考にしたサイトと比較して個人的に気になった部分をいくつか。

その1

   template_values.update(csrf(request))

CSRFはリファレンス[2]にあったのでそのまま記載してます。
詳しいことはわかりませんがフォーム送信の際は必要みたいで、
この1行をコメントアウトするとPOST送信時に以下のようなエラーが出ます。

f:id:dackdive:20140828002812p:plain

その2

   return HttpResponse(loader.get_template('guestbook/main_page.html').render(template_values))

リファレンス[4]だとDjangodirect_to_templateメソッドを使ってますが、
これは関数ベース汎用ビューと呼ばれるもので、Djanogの現バージョン(1.5)では
クラスベース汎用ビューを使う方が良いそうです。

参考:http://docs.djangoproject.jp/en/latest/topics/generic-views-migration.html

テンプレート(templates)

ビュー内で使用するテンプレートを作成します。

まずは、テンプレートを配置するディレクトリをsettings.pyに書く必要があります。

settings.py

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.
    os.path.join(ROOT_PATH, 'templates'),
)

これで、Djangotemplatesというディレクトリからテンプレートファイルを検索するようになります。

続いて、templatesディレクトリと、その下に実際のテンプレートファイルmain_page.htmlを作成します。

配置する場所としては2通り考えられます。

選択肢1: Projectの下に配置する

gae_django_app(Project)
├── __init__.py
├── guestbook(Application)
│   ├── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├──templates
│   └── guestbook
│         └── main_page.html
├── settings.py
├── urls.py
└── wsgi.py

選択肢2: Applicationの下に配置する

gae_django_app(Project)
├── __init__.py
├── guestbook(Application)
│   ├──templates
│   │   └── guestbook
│   │         └── main_page.html
│   ├── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── settings.py
├── urls.py
└── wsgi.py

なぜこのように2種類選べるのか、という点については
Djangoの別の設定が影響しています。
これについては、また別の記事でまとめたいな...。

最後に、main_page.htmlを作成します。

main_page.html

<html>
<head>
</head>
<body>
    {% for greeting in greetings %}
    {% if greeting.author.nickname %}
    <b>{{ greeting.author }}</b> wrote:
    {% else %}
    An anonymous person wrote:
    {% endif %}
    <blockquote>{{ greeting.content|escape }}</blockquote>
    {% endfor %}

    <form action="/sign/" method="post">
        {% csrf_token %}
        <input type="hidden" name="guestbook_name" value="{{ guestbook_name }}" />
        <div><textarea name="content" rows="3" cols="60"></textarea></div>
        <div><input type="submit" value="Sign Guestbook"></div>
    </form>

    <hr>
    <form>Guestbook name:
        <input value="{{ guestbook_name }}" name="guestbook_name">
        <input type="submit" value="switch">
    </form>
    <a href="{{ url }}">{{ url_linktext }}</a>
</body>
</html>

{{ XXX }}または{% XXX %}で記述した箇所が、
views.py内でレンダリングすることによって置き換わるわけです。

URL(urls.py)

urls.pyというファイルは2つあります。
Project直下のものと、Application直下のものです。

urls.py(Project直下)

from django.conf.urls import patterns, include, url

urlpatterns = patterns('',
    url(r'^', include('guestbook.urls')),
)

urls.py(Application直下)

from django.conf.urls import patterns, include, url
from guestbook.views import main_page, sign_post

urlpatterns = patterns('',
    (r'^sign/$', sign_post),
    (r'^$', main_page),
)

wsgi.pyおよびsettings.pyの編集

最後に、設定ファイルを編集します。

まず、wsgi.pyについてはディレクトリ構成を変えたので
以下のように修正します。

before

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "gae_django_app.settings")

after

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")

Projectのrootディレクトリを基準として、settings.pyの場所を教えてあげれば良いみたいですね。

ちなみに、これを修正せずにディレクトリ構成だけ変えると次のようなエラーが。

ERROR    2014-08-19 16:37:51,897 wsgi.py:278]
Traceback (most recent call last):
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/google/appengine/runtime/wsgi.py", line 266, in Handle
    result = handler(dict(self._environ), self._StartResponse)
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/lib/django-1.5/django/core/handlers/wsgi.py", line 236, in __call__
    self.load_middleware()
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/lib/django-1.5/django/core/handlers/base.py", line 43, in load_middleware
    for middleware_path in settings.MIDDLEWARE_CLASSES:
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/lib/django-1.5/django/conf/__init__.py", line 53, in __getattr__
    self._setup(name)
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/lib/django-1.5/django/conf/__init__.py", line 48, in _setup
    self._wrapped = Settings(settings_module)
  File "/Applications/GoogleAppEngineLauncher.app/Contents/Resources/GoogleAppEngine-default.bundle/Contents/Resources/google_appengine/lib/django-1.5/django/conf/__init__.py", line 134, in __init__
    raise ImportError("Could not import settings '%s' (Is it on sys.path?): %s" % (self.SETTINGS_MODULE, e))
ImportError: Could not import settings 'gae_django_app.settings' (Is it on sys.path?): No module named gae_django_app.settings

続いて、settings.pyINSTALLED_APPSという変数に
アプリケーションguestbookを追加します。

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    # Uncomment the next line to enable the admin:
    # 'django.contrib.admin',
    # Uncomment the next line to enable admin documentation:
    # 'django.contrib.admindocs',
    'guestbook',
)

これでOK。

アプリを起動する

~/workspace/gae/gae_django_app $ dev_appserver.py .

を実行し、

http://localhost:8080

にアクセスしてちゃんと表示できれば成功です。

うまくいかない場合は上にも書きましたが
[https://github.com/zaki-yama/gae/tree/master/django_apps :title=GitHub]にコードを上げてるのでチェックアウトして試してください。

$ git clone git@github.com:zaki-yama/gae.git
$ cd gae
$ git checkout chapter-7