Google App Engineで落書きアプリ

Google App Engineのアカウントがあっさり手に入ったけどPythonはあんまり好みじゃないからしばらく寝かせておこう。とか思ってたんだけど、思いのほかレアなようで手に入らなかった人も多いみたい。これでアカウント放置というのも皆様に申し訳ないのでちょっと作ってみた。


IECanvasの使い方がおかしいのかうまく動いてくれないためFireFox限定。
若干おかしいところもありますが、今は一応動きます。(2008/4/12 追記)


http://susan.appspot.com/


チュートリアルを試すの含めて4時間くらいでやっちゃったものなので、おかしなところがあっても見逃してください。


Google App Engine使ってみての感想だけど、デプロイが楽。コマンド一発でできる。勝手にスケールしてくれるそうだし、ローカルの開発環境で動けばそのあとのことはなにも考える必要がない。今のところこれが一番の利点。アプリ作成自体はRailsになれちゃってると、逆に後戻りしてるようにも感じる。いや、まだチュートリアルレベルのことしかやってないので、何一つ使いこなしてないんだけど。


作業してて、大きくはまったのは2つ。タブ非表示にしてたらインデントが一部タブ・空白交じりになっちゃってて変なエラーでた。慣れてないからだろうけどエラー内容からそんなしょーもない理由だとは想像できず、対応に少し時間かかったので、タブは表示するようにしておくのが吉。


あと、チュートリアルに沿って作業してたんだけど、静的ファイルの読み込みはローカル環境にバグがあるっぽくてそのままだと動かない。こちらを参考に対応しましょう。
http://d.hatena.ne.jp/Aoba/20080410

これでなんとかなる対策:dev_appserver.pyを書き換える

\google\appengine\tools\dev_appserver.py の2369〜2370行を書き換える。

元のコードの2369〜2370行。

      regex = os.path.join(re.escape(regex), '(.*)')
      path = os.path.join(path, '\\1')

これ↑を↓に書き換える。

      regex = re.escape(regex) + '/(.*)'
      path = path + '/\\1'

Pythonなので、インデントに注意すること。




以下は暇な人用に作り方解説。
基本は本家のチュートリアルそのまま。

http://code.google.com/appengine/docs/gettingstarted/

アカウントを手に入れる

http://appengine.google.com/
登録してInvitationメールが届くのを待つのみ。

いろいろDL&インストール

Pythonが入ってなかったらEngineのインストールで注意される。Pythonは日本語化されてるのもあるんだけど、バージョンが推奨よりも古そうだったので本家のを使ってみた。

作成するアプリケーションの情報を登録

http://appengine.google.com/start/createapp?
「Application Identifier」がURLのサブドメインで、「Application Title」はアプリのタイトルらしいけど、タイトルってどこで使われるのかよく分からん。あと、サブドメインは他人と被ってたら当然却下される。technohippyは残ってなかった・・・。

登録した情報をローカルの設定ファイルに記述

まず、アプリケーションディレクトリは %GAE_ROOT%/susan ってことにする。
アプリケーションの設定は%GAE_ROOT%/susan/app.ymlに保存。拡張子はyamlでもOK

application: susan
version: 1
runtime: python
api_version: 1

handlers:
- url: /stylesheets
  static_dir: stylesheets

- url: /javascripts
  static_dir: javascripts

- url: /.*
  script: susan.py

一行目は先ほど登録した「Application Identifier」。
handlersってところでURLと動作の対応を設定する。上から順にマッチされるので「/.*」は最後に書くこと。
それ以外はまぁ固定と考えていいんだと思う。

コーディング

%GAE_ROOT%/susan/susan.py


上から順に。
インデントに注意。


とりあえずソースの文字コード設定

#!-*- coding:utf-8 -*-


いろいろインポート

import os
import cgi
import wsgiref.handlers

from google.appengine.ext.webapp import template
from google.appengine.ext import db
from google.appengine.api import users
from google.appengine.ext import webapp


データモデル定義

class Stroke(db.Model):
  author = db.UserProperty()
  points = db.TextProperty()
  date = db.DateTimeProperty(auto_now_add=True)
  strdate = db.StringProperty()

db.Modelを継承して自作のモデルを定義。
あとはお察しくださいと言うか、チュートリアル参考にしただけなので自分もよく分かってない。
StringPropertyは500文字までなので、それ以上になるときはTextPropertyを使わないと駄目らしい。
インタプリタに叱られた。


メインページのハンドラ定義

# webapp.RequestHandlerを継承
class MainPage(webapp.RequestHandler):
  # GETリクエストを処理
  def get(self):
    # Strokeモデルを全権検索してdateプロパティにしたがって降順にソート
    # たぶん "-" が付いたら降順、なかったら昇順
    strokes = Stroke.all().order('-date') 

    # 現在のログインユーザーを確認
    # usersはGAE組み込みっぽいのでここは丸暗記
    if users.get_current_user():
      url = users.create_logout_url(self.request.uri)
      url_linktext = 'Logout'
    else:
      url = users.create_login_url(self.request.uri)
      url_linktext = 'Login'

    # 日本語を表示するためにContentTypeにUTF-8を設定
    self.response.headers['Content-Type'] = 'text/html; charset=utf-8'

    # テンプレートファイルを読み込んで変数を設定して出力
    template_values = {
        'strokes': strokes,
        'url': url,
        'url_linktext': url_linktext,
      }
    path = os.path.join(os.path.dirname(__file__), 'index.html')
    self.response.out.write(template.render(path, template_values))

後で指定されるパスにGETリクエストが来た時の処理を定義。
ログインユーザーとこれまでに描かれた落書きを読み込んでテンプレートに渡して表示。


落書き保存用のハンドラ定義

class Sketchbook(webapp.RequestHandler):
  # POSTリクエストを処理
  def post(self):
    # Strokeモデルを生成して、受け取った値を元にプロパティを設定
    stroke = Stroke()
    if users.get_current_user():
      stroke.author = users.get_current_user()
    # リクエストパラメータの値を取得して設定
    stroke.points = self.request.get('strokes') 
    # stroke.dateにはデフォルトで現在時刻が入るので
    # 文字列に変換したものもとっておく
    stroke.strdate = stroke.date.strftime('%Y%m%d%H%M%S') 

    # strokeを保存
    stroke.put()

    # ルートにリダイレクト
    self.redirect('/')

後で指定されるパスにPOSTリクエストが来た時の処理を定義。
リクエストパラメータで指定される落書き情報をモデルに保存してMainPageにリダイレクト。


Main関数定義

def main():
  # パスとハンドラの対応を設定
  application = webapp.WSGIApplication([('/', MainPage), ('/save', Sketchbook)], debug=True)
  # CGI開始?
  wsgiref.handlers.CGIHandler().run(application)

多分アプリケーションの起点になる関数を定義。
最上位で定義するので、インデントに注意。最初ハンドラのメソッドかと思ってインデントしたせいで少しはまった。


Main関数実行

if __name__ == '__main__':
  main()

最後に上で定義したMain関数を実行。
条件式の意味は分からん。

テンプレートファイル

%GAE_ROOT%/susan/index.html

MainPage#getの出力に使用されるテンプレートを作成

<html>
  <head>
    <link type="text/css" rel="stylesheet" href="/stylesheets/main.css" />
    <!--[if IE]><script type="text/javascript" src="/javascripts/excanvas-compressed.js"></script><![endif]-->
    <script type="text/javascript" src="/javascripts/prototype.js"></script>
    <script type="text/javascript" src="/javascripts/sketch.js"></script>
  </head>
  <body>
    <canvas id="canvas" width="200" height="200" style="position:absolute; top:5px; left:5px;"></canvas>
    <div style="margin-top:210px">
      <form action="/save" method="post">
        <input type="hidden" name="strokes" id="strokes"></input>
        <input type="submit" value="保存"></input>
      </form>

      <a href="{{ url }}">{{ url_linktext }}</a>

      {% for stroke in strokes %}
        <hr />
        {% if stroke.author %}
          [{{ stroke.date }}] <b>{{ stroke.author.nickname }}</b>さんが書きました 
        {% else %}
          [{{ stroke.date }}] 匿名さんが書きました 
        {% endif %}
        <br />
        <canvas id="canvas{{ stroke.strdate }}" width="200" height="200"></canvas>
        <script>
          initializeCanvas("canvas{{ stroke.strdate }}", "{{ stroke.points }}");
        </script>
      {% endfor %}
    </div>
  </body>
</html>

{{ }}が変数の値の挿入で、{% %}が処理の挿入らしい。
Rails的に言えばそれぞれ <%= %> と <% %> か。
変数の値はMainPage#getの最後の方でtemplate_valuesに設定されてます。
テンプレートはさすがにインデントで判別するんじゃなくてendifとかendforとかいるみたい。少しかっこ悪いな。

最終的なディレクトリ構造

%GAE_ROOT%/susan
|   app.yaml
|   index.html
|   index.yaml        # なんか自動生成されたっぽい
|   susan.py
|
+---javascripts
|       excanvas-compressed.js
|       excanvas.js   # http://sourceforge.net/projects/iecanvas
|       prototype.js  # http://www.prototypejs.org/
|       sketch.js
|
\---stylesheets
        main.css

デプロイ

C:\Program Files\Google\google_appengine>appcfg.py update susan
Loaded authentication cookies from C:\Documents and Settings\ando/.appcfg_cookie
s
Scanning files on local disk.
Initiating update.
Cloning 5 static files.
Cloning 1 application files.
Uploading 2 files.
Closing update.
Uploading index definitions.

C:\Program Files\Google\google_appengine>

appcfg.py update susanを実行するだけ。
これはうれしい。

動作確認

どうぞ。

http://susan.appspot.com/