第4回はChapter 7 Using Templatesです。
(追記)その他のメモ
- Google App Engine Python Tutorialのメモ(1) - dackdive's blog
- Google App Engine Python Tutorialのメモ(2) - dackdive's blog
- Google App Engine Python Tutorialのメモ(3) - dackdive's blog
- Google App Engine Python Tutorialのメモ(5) - dackdive's blog
はじめに
公式リファレンスではJinja2というライブラリを使っています。
しかし、個人的な事情により今回はDjangoというライブラリを使ってこのチャプターをやってみたいと思います。
参考にしたサイト
いつもはリファレンスを一番最後に書くんですが、今回はここに。
まずは公式リファレンス。
ただし、上記は2010年に書かれたもので、情報が少し古いです。
また、日本語で参考になりそうなのはこのあたりのサイト。
- [2] Django1.5でGoogle App Engineのチュートリアルをやったときのメモ - Men talking over coffee with smoking Ark Royal.
- [3] 「Using Django with Appengine」邦訳 - WebOS Goodies
ただ、ここもチュートリアルについて書かれたものでなかったり、微妙に情報が違っていたりするので注意が必要。
そして、おそらく一番参考になったのがこのサイト(英語ですが...)。
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
という単位になる。
project
とapps(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
そして、これらのファイルと同じ階層に置かれているアプリケーションごとのディレクトリは
次のようなファイル構成になっています。
これら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にデプロイできるようにしていってもいいのですが
gae_django_app
の下にもう1個gae_django_app
というディレクトリがあるのは不自然(というかムダ?)gae_django_app/guestbook
(プロジェクトの下にアプリケーション)というディレクトリ構成にしたい
という理由から、gae_django_app(Project)
の下にguestbook(Application)
をまるっと移動し、
さらにmanage.py
とgae_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送信時に以下のようなエラーが出ます。
その2
return HttpResponse(loader.get_template('guestbook/main_page.html').render(template_values))
リファレンス[4]だとDjangoのdirect_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'), )
これで、Djangoはtemplates
というディレクトリからテンプレートファイルを検索するようになります。
続いて、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.py
のINSTALLED_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