TurboGearsのチュートリアルを日本語訳して実行してみる(その2)

だいぶ日にちがあいてしまったが、第2弾ということで(その2)を公開してみる。
前回の話はこちらTurboGearsのチュートリアルを日本語訳して実行してみる(その1)

コンテンツ

  1. TurboGearsのインストール
  2. プロジェクトの骨組みの作成
  3. モデルの構築
  4. コントローラの構築
  5. リソーステンプレートの適用
  6. 独自テンプレートの作成
  7. フォーム処理
  8. 非同期JavaScriptの追加
  9. 高度な機能
  10. ライセンス

リソーステンプレートの適用

今のところ、デモの目的として、内部リクエストにくるのはすべてプレーンテキストを返していました。
これからどのように実物のテンプレートをコントローラーの中に埋め込むのかを見せましょう。
TurboGearsユーザは、マークアップの中の動的コンテンツをコントロールするためにKidテンプレートシステムが利用できます。

あなたのサイトのページ毎に、テンプレートコントローラの中でリソース対応が起こります。
これは特定のテンプレートの引数(turbogears.expose デコレータの引数)で行われます。

@turbogears.expose(template="tutorial.templates.welcome")
def index(self):
    ...

テンプレートは置き換える引数を取ります。テンプレート引数は、変数とほかの動的コンテンツに利用されます。コントローラーからのテンプレート引数は、テンプレート内でアクセス可能な変数(辞書キー)として返すことができます。

@turbogears.expose(template="tutorial.templates.users")
def index(self):
    users = model.User.select()
    return dict(users=users)

すべてのテンプレートが動的コンテンツを持っているとは限りません。それ故、引数が必要でないかもしれません。
このケースでは、空の辞書が返ります。

@turbogears.expose(template="tutorial.templates.welcome")
def index(self):
    return dict()

ここでは、アプリケーションのためにコントローラー修正をします。プレーンテキストの代わりにテンプレートを利用します。

class Users:
    @turbogears.expose(template="tutorial.templates.users")
    def index(self):
        users = model.User.select()
        return dict(users=users)

    @turbogears.expose(template="tutorial.templates.user")
    def default(self, userID):
        try:
            userID = int(userID)
            user = model.User.get(userID)
        except (ValueError, model.SQLObjectNotFound):
            raise cherrypy.NotFound
        else:
            return dict(user=user)

class Root(controllers.RootController):
    users = Users()

    @turbogears.expose(template="tutorial.templates.welcome")
    def index(self):
        return dict()

    @turbogears.expose(template="tutorial.templates.about")
    def about(self, author="Brian Beck"):
        return dict(author=author)
個人的メモ1

当然だけど、UsersクラスをRootクラスの前に書く必要がある。def defaultは適合するメソッド・アトリビュートが該当しなかった場合に呼び出される処理。URLを引数として持つ。例えばhttp://www.example.co.jp/Users/3 はdefaultの第2引数に"3"が入る。

このアプリケーションにアクセスしても、今は動きません。まだいくつかのテンプレートが存在していないし、作成されていないからです。

テンプレートの骨組みを作成します。プロジェクトが作られた時にできたデフォルトのwelcom.kidをコピーして作成します。

$ cp tutorial/templates/welcome.kid tutorial/templates/users.kid
$ cp tutorial/templates/welcome.kid tutorial/templates/user.kid
$ cp tutorial/templates/welcome.kid tutorial/templates/about.kid

独自のテンプレート作成

独自テンプレートの修正をします。

すべてのテンプレートは、master.kidと呼ばれるマスターテンプレートから継承したリソースを利用します。
もしサイト上のすべてのページがヘッダやナビゲーションリンク、フッターのような共通のエレメントを持っているとき、これらをmaster.kidの一部に所属します。これはサイト上のすべてのページで利用するCSSJavaScriptを入れ込むのにも最適です。

tutorial/templates/master.kidを開きます。サーバのスタートアップ時のwelcomスクリーンのパーツがありますが、高度なエレメント同様、このチュートリアルでは利用しません。

あなたのマスターテンプレートのエレメントに必要なのは2つだけです。

<div py:if="tg_flash" class="flash" py:content="tg_flash"></div>
<div py:replace="item[:]"/>

最初のエレメントは、何らかの処理が行われた後のページで表示ことができるためメッセージです。

個人的メモ2

1行目はtg_flashは、別のページなどでturbogears.flash("hogehoge")という風に使う

2番目のエレメントは、これから継承するテンプレートので出現するほかのエレメントの代替です。

例えば、dic(tg_flash="Test message.")をコントローラーから返したいとき、コントローラのテンプレートのの唯一のエレメントは
<p>Hello, world!</p>

フォーム処理

次の変更は、インタラクティブシェルの代わりにアプリケーション内でユーザとリストを管理するフォームです。

最初に、ユーザ削除と追加を加えます。両方の動作はユーザのユニークなe-mailアドレスを必要とするでしょう。
実行の動作はステータスメッセージをディスプレイへ表示させ、/users/indexへリダイレクトさせます。

これらのメソッドはPOSTリクエストの結果からです。そしてクライアントを直ぐにリダイレクトします。
新しいテンプレートは必要ないです。ここでは新しい2つのメソッドをUsersコントローラに追加します。

class Users:
    ...

    @turbogears.expose()
    def add(self, email):
        # Remove extra spaces
        email = email.strip()
        # Remove null bytes (they can seriously screw up the database)
        email = email.replace('\x00', '')
        if email:
            try:
                user = model.User.byEmail(email)
            except model.SQLObjectNotFound:
                user = model.User(email=email)
                turbogears.flash("User %s added!" % email)
            else:
                turbogears.flash("User %s already exists!" % email)
        else:
            turbogears.flash("E-mail must be non-empty!")
        raise cherrypy.HTTPRedirect('index')

    @turbogears.expose()
    def remove(self, email):
        try:
            user = model.User.byEmail(email)
        except model.SQLObjectNotFound:
            turbogears.flash("The user %s does not exist. (Did someone else remove it?)" % email)
        else:
            for l in user.lists:
                for item in l.items:
                    item.destroySelf()
                l.destroySelf()
            user.destroySelf()
            turbogears.flash("User %s removed!" % email)
        raise cherrypy.HTTPRedirect('index')

メソッドを追加のための注意。ここでは入力されたemailが空ではなく正しく作られたかを非常に簡単に検証した。
現実では、ここには正規表現を使う。ユーザの重複登録や存在しないユーザの変更の安全のために、すべてtry/exceptブロックします。
いくつか検証は一般のWebサービスには必要です。

ここで、フォームに複数のメソッドを加えます。actionはUsersコントローラに加えた対応するメソッドの名前です。
tutorial/templates/users.kidののhtmlの最後にこれを加えます

<form action="add" method="POST">
    <fieldset>
        <legend>Add a User</legend>
        <label for="email">Email:</label>
        <input type="text" name="email"/>
        <input type="submit" value="Add"/>
    </fieldset>
</form>

次に、リストユーザの削除ボタンを加えます。リストアイテムにボタンを加えるために、users.kidの中のリストを変更します。

<ol>
    <li py:for="user in users">
        <form style="float: right" action="remove" method="POST">
            <input type="hidden" name="email" value="${user.email}"/>
            <input type="submit" value="Remove"/>
        </form>
        <span>${user.email} (<a href="${user.id}">View lists</a>)</span>
    </li>
</ol>


Usersページを再読込して、ユーザの追加と削除を試します。turbogears.flashで呼ばれたステータスメッセージが結果として表示されます。

この処理はフォームへのリストとアイテムの修正に似ている。したがって、多くの説明しません。代わりに次のコードを見ください。

これは新しいtutorial/templates/user.kidのです。

<h2>${user.email}'s Lists</h2>
<div py:for="userList in user.lists">
    <form style="float: right" action="/removeList" method="POST">
        <input type="hidden" name="userID" value="${user.id}"/>
        <input type="hidden" name="listID" value="${userList.id}"/>
        <input type="submit" value="Delete This List"/>
    </form>
    <h3>${userList.title} </h3>
    <ol>
        <li py:for="item in userList.items">
            <form style="float: right" action="/removeItem" method="POST">
                <input type="hidden" name="userID" value="${user.id}"/>
                <input type="hidden" name="itemID" value="${item.id}"/>
                <input type="submit" value="Remove"/>
            </form>
            <span>${item.value}</span>
        </li>
    </ol>
    <form action="/addItem" method="POST">
        <input type="hidden" name="userID" value="${user.id}"/>
        <input type="hidden" id="listID" name="listID" value="${userList.id}"/>
        <label>Add Item: <input type="text" name="value"/></label>
        <input type="submit" value="Add"/>
    </form>
</div>
<form action="/addList" method="POST">
    <fieldset>
        <legend>Create a List</legend>
        <input type="hidden" name="userID" value="${user.id}"/>
        <label for="title">Title:</label>
        <input type="text" id="title" name="title"/>
        <input type="submit" value="Create"/>
    </fieldset>
</form>
個人的メモ7

from turbogears import controllers, expose, flashとしてやると、turbogearsをいれなくてもそれぞれ呼び出せる。
from model import * で modelがなくなる

ルートコントローラーを加えます。

class Root(controllers.RootController):
    ...

    @turbogears.expose()
    def addList(self, userID, title):
        try:
            userID = int(userID)
            user = model.User.get(userID)
        except (ValueError, model.SQLObjectNotFound):
            raise cherrypy.NotFound
        else:
            # Remove extra spaces
            title = title.strip()
            # Remove null bytes (they can seriously screw up the database)
            title = title.replace('\x00', '')
            if title:
                l = model.List(title=title, user=user)
                turbogears.flash("List created!")
            else:
                turbogears.flash("Title must be non-empty!")
        raise cherrypy.HTTPRedirect('/users/%s' % userID)

    @turbogears.expose()
    def removeList(self, userID, listID):
        try:
            listID = int(listID)
            l = model.List.get(listID)
        except ValueError:
            turbogears.flash("Invalid list!")
        except model.SQLObjectNotFound:
            turbogears.flash("List not found! (Did someone else remove it?)")
        else:
            for item in l.items:
                item.destroySelf()
            l.destroySelf()
            turbogears.flash("List deleted!")
        raise cherrypy.HTTPRedirect('/users/%s' % userID)

    @turbogears.expose()
    def addItem(self, userID, listID, value):
        try:
            listID = int(listID)
            l = model.List.get(listID)
        except ValueError:
            turbogears.flash("Invalid list!")
        except model.SQLObjectNotFound:
            turbogears.flash("List not found! (Did someone else remove it?)")
        else:
            # Remove extra spaces
            value = value.strip()
            # Remove null bytes (they can seriously screw up the database)
            value = value.replace('\x00', '')
            if value:
                item = model.Item(listID=listID, value=value)
                turbogears.flash("Item added!")
            else:
                turbogears.flash("Item must be non-empty!")
        raise cherrypy.HTTPRedirect('/users/%s' % userID)

    @turbogears.expose()
    def removeItem(self, userID, itemID):
        try:
            itemID = int(itemID)
            item = model.Item.get(itemID)
        except ValueError:
            turbogears.flash("Invalid item!")
        except model.SQLObjectNotFound:
            turbogears.flash("No such item! (Did someone else remove it?)")
        else:
            item.destroySelf()
            turbogears.flash("Item removed!")
        raise cherrypy.HTTPRedirect('/users/%s' % userID)
個人的メモ8

行頭にimport cherrypy が必要だった。

これは機能中に大きな変更を与えます。しかし、このページスタイルは悪くない。
今は、マークアップを見て、/tutorial/staitc/css/style.cssの中にどのように多くのスタイルを適応させるかを決める時間です。

非同期Javascriptの追加

テンプレートにJavaScriptを追加したかどうか思い出してください。この章ではMochiKit Javascriptライブラリを利用してアプリケーションにAJAXを追加します。(実際には、JSONを使うだけでAJAXではないです。TurboGearsではそれは簡単に作れます。)

よく使う機能を一つ、ページリロードをしないでインラインでアイテムリストを編集できる機能をアプリケーションに追加します。
コーディングを始める前に、ここに必要な概要を上げます。

  • 各アイテムへの編集ボタンを加える。
  • ボタンを押したときに、textアイテムをinputフィールドに置き換える。
  • ボタンを押したときに、editボタンを置き換えてsaveボタンにもする。
  • 変更してsaveボタンを押したとき、サーバに変更の値を送る。
  • サーバ上で、コントロールメソッドはデータベースへ内部アイテムの修正と書き込みを処理する。
  • inputフィールドが値の変更と同時にテキストに戻ったとき変更する。


最初は簡単です。それぞれのリストアイテムの横にEditボタンを置きます。 tutorial/templates/user.kidに、それぞれのアイテムリストに新しいフォームを追加します。フォームとJavascripを一緒にサブミットするようにしてからは、actionやmethodは必要はなく、代わりに、フォームにonsubmit属性、ボタンにonclick属性を加えます。

<ol>
    <li py:for="item in userList.items">
        <form style="float: right" action="/removeItem" method="POST">
            <input type="hidden" name="userID" value="${user.id}"/>
            <input type="hidden" name="itemID" value="${item.id}"/>
            <input type="submit" value="Remove"/>
        </form>
        <form onsubmit="return saveItem(this)">
            <input type="button" style="float: right" value="Edit"
                   onclick="editItem(this)"/>
            <span>${item.value}</span>
            <input type="hidden" name="itemID" value="${item.id}"/>
        </form>
    </li>
</ol>

見てわかるように、2つのまだ実在しないJavaScript関数を呼び出しました。これから、一つのページだけで、先ほどの機能を実現します。tutorial/staitc/javascript/user.jsに新しくファイルを作ります。そしてそれをuser.kidので読み込みます。

<script type="text/javascript" src="/static/javascript/user.js"></script>

はじめに、前述のinputフィールドと一緒にitemテキストを置き換えるEditボタン(editItemへ呼ぶ)を作成します。
user.jsに加えます。

var editItem = function(button) {
    var item = getElementsByTagAndClassName('span', null, button.parentNode)[0];
    var editBox = INPUT({'type': 'text', 'value': item.innerHTML});
    swapDOM(item, editBox);
    swapDOM(button, INPUT({'type': 'submit', 'style': 'float: right',
                           'value': 'Save'}));
}

このJavaScripthaは、MochiKitを少し利用します。より明確にはINPUTです。"input"エレメントは特別な属性とともに作られます。INPUTはeditフィールドとSaveボタンを作るために、ここで利用されます。

今、Saveボタンがあり、押すとフォームを送信します。フォームのonsubmit属性はこのようにsaveItemを呼びます。

var saveItem = function(form) {
    var fields = getElementsByTagAndClassName('input', null, form);
    var editBox = fields[1];
    var itemID = fields[2].value;
    var button = fields[0];
    var d = postJSON('/editItem', {'itemID': itemID, 'value': editBox.value});
    d.addCallback(showChanges, editBox, button);
    return false;
}

関数戻り値はfalseです。
なぜならonsubmitの戻り値がfalseに遭遇した時は、フォームactionのトリガーではないからです。ふつう、この原因はページのリロードです。
(フォームにactionを与えておらず、代わりにJavaScriptを一緒にしてるということを思い出してください)

この関数の最も重要なのは、postJSONへ呼ぶことです。JSONとはJavascript Object Notationで、データをとり回す軽量フォーマットです。
XML構造のパースと組み立てを要求します。ここではXMLの代わりとして使います。MochiKitTurboGearsは、必要な箇所でのJavaScriptの中へJSONの動的な変換ルーチンを提供します。そして、コントローラーの中のPythonオブジェクトへ代入します。
残念ながら、postJSONはMochiKitの一部ではありませんが、このコードで実行できます。

var postJSON = function(url, postVars) {
    var req = getXMLHttpRequest();
    req.open("POST", url, true);
    req.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    var data = queryString(postVars);
    var d = sendXMLHttpRequest(req, data);
    return d.addCallback(evalJSONRequest);
}

When postJSON is called, it immediately returns a Deferred object. If you're familiar with the Twisted library for Python, the concept is the same.
postJSONを呼び出したとき、直ぐに違うオブジェクトが帰ってきます。PythonのTwistedライブラリに精通していたなら、コンセプトは同じです。

Adding callbacks to a Deferred causes them to be invoked when the result of the call is ready.
遅延箇所に追加したコールバックは、呼び出し準備ができた時に呼び出されます。

In saveItem, we add the showChanges function as a callback.
saveItem内で、コールバックとしての機能をshowChangesに追加します。

showChanges will change the client's display of the modified item once it is sure that the server received the changes.
サーバの変化の確認を受け取ると、showChangesはクライアントの変更された項目の表示を変更します。

This requires replacing the edit field with the new item text, and replacing the Save button with the Edit button:
この要求は新しいitemと一緒に編集フィールドを置き換えます。そして、Editボタンと一緒にSaveボタンへ変更します。

var showChanges = function(editBox, button, data) {
    swapDOM(editBox, SPAN(null, data.value));
    swapDOM(button, INPUT({'type': 'button', 'style': 'float: right', 'value': 'Edit',
                           'onclick': function() { editItem(this); }}));
}

That's all the JavaScript needed. One more addition to the Root controller and you're done. Notice that our postJSON call sent its input data to /editItem. This is the new resource to add:

すべてのJavaScriptには必要です。Rootコントローラにもう一度加えると終了です。
注意:postJSONはinputデータを/editItemに送ります。

class Root(controllers.RootController):
    ...

    @turbogears.expose(format="json")
    def editItem(self, itemID, value):
        try:
            itemID = int(itemID)
            item = model.Item.get(itemID)
        except (ValueError, model.SQLObjectNotFound):
            raise CherryPy.NotFound
        else:
            # Remove extra spaces
            value = value.strip()
            # Remove null bytes (they can seriously screw up the database)
            value = value.replace('\x00', '')
            if value:
                item.value = value
            else:
                value = item.value
        return dict(value=value)

新しいことは、turbogears.exposeへ通過するformat="jsonだけです。
このメソッドから返ってくる引数は、控えているJSONJavaScriptへと送られます。
TurboGearsJSONへの変換します。私たちには中のJSONobjectが返ってきます。

必要ならばサーバを起動します。そしていくつかitemを編集します。localhostからアクセスしているならば、itemの保存はとても敏感に動きます

MochiKitを巧みに使うの非同期JavaScriptとDOMのヘルプはここにあります。
http://mochikit.com/doc/html/MochiKit/index.html

高度な機能

TurboGearsの多くの特別な機能によってアプリケーションに必要なことを簡単に取り込むことがでました。。
そのようなものの一つがログインアカウント操作です。たった今から誰かがユーザやユーザリストの修正ができます。
実行するとパーミッションの種類はセッションの利用や認証フレームワークに要求されます。

いくつかのエラーチェックとフォーム作成はTurboGearsの検証部分とウィジェットを利用すると自動作成も可能でした。
これらの機能はチュートリアルのでデモンストレーションです。それまでには、オフィシャルTurboGearsドキュメントを読んでください。

ここで、2行のコードを加えて一つの機能を追加します。
最後まで読んだあなたへのご褒美と考えてください。
tutorial/controllers.pyを開きimport構文をトップにこれを追加します。

from turbogears.toolbox.catwalk import CatWalk

Rootコントローラに次を加えます。

class Root(controllers.RootController):
    catwalk = CatWalk(model)
    ...

http://localhost:8080/catwalkにアクセスします。ナイスなwebインターフェイスを通して、データベースのビューや行の変更が行えます。

ライセンス

This tutorial and accompanying application are available under the MIT license.

Copyright (c) 2006 Brian Beck

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

最後に

最後の方の訳はなんだか気力が持たずに、ぐだぐだの訳になってしまった。
間違ってはいないと思うけど、直訳のためちょっと分りにくいかもしかです。。
ごめんなさい。
さて、次はThe 20 Minutes Wikiでもやってみようかな