最初に、サンプルアプリの仕様を説明
概要: ・1からインクリメントしながら番号を表示し、3の倍数と3の付く数字のときに別の文字列を、5の倍数のときにも更に別の文字列を表示する。 ・終点となる番号が設定できる。 ・3の倍数と3のつく数字のときに読み上げる文字列を設定できる。 ・5の倍数のときに読み上げる文字列を設定できる。 ・設定した値はユーザ毎に永続化される。 ・アプリケーション名は世界の○○とする。○○のところはユーザのnicknameを設定したい。 ・formのvalidationについては、今回は行わない。(本当は行う必要があるので、次回以降のエントリで機能追加します。
最終的に作成したい画面イメージ
それではユーザ毎の設定値を保持するためのモデルを定義したいと思う。
ユーザ毎の設定値なのでとりあえずクラス名は、nabeatsuUserMasterとし、db.Modelを継承する。
継承することで、Google App EngineのDataStoreを利用して永続化するためのモデルと認識される。
あとは、サンプルのコメントで説明する。
#の行がコメント。
青色になっていると思う。
#DataStoreの機能を利用するためにimportする。 from google.appengine.ext import db #db.Modelを継承することで、永続化するためのモデルとして認識される。 #RDBのテーブルのようなものです。 #項目は項目名 = db.型Property(パラメータ)で定義される。 class nabeatsuUserMaster(db.Model): #この設定のユーザを識別するための項目でUser型の項目にします。 #GoogleにログインしているUser情報が保存される。 author = db.UserProperty() #終点となる番号をInteger型の項目とします。 count = db.IntegerProperty() #とりあえず、利用しませんが、このデータの更新日時をDateTime型の項目とします。 date = db.DateTimeProperty(auto_now_add=True) #3の倍数と3のつく数字の時に読み上げたい文字列をString型の項目とします。 saidWhen3 = db.StringProperty() #5の倍数の時に読み上げたい文字列をString型の項目とします。 saidWhen5 = db.StringProperty()
これで、とりあえず、nabeatsuUserMasterは完成です。
つづいて設定と読み上げを行うためのページを定義します。
ここで、DJangoのテンプレートを作成します。ファイル名は、なんでも良いのですが、とりあえずindex.htmlとします。
基本的な機能だけを利用していますので先に利用している機能を説明します。
{{ parameterName }}
で後で説明するwebのリクエストをハンドラで定義したパラメータを出力します。
{% 構文 %}
とすることである程度の構文が使えます。今回はfor value in valuesのみ利用していますので、
valuesからvalueを取り出して出力という処理を
{% for value in values %}
{{ value }}
{% endfor %}
といった感じで実装しています。
後で説明するリクエストハンドラから受け付けているパラメータ名の説明をしておきます。
userName=今ログインしているユーザのnickname
logoutUrl=ログアウトするためのUrlです。
count=読み上げる数字の終点です。
saidWhen3=3の倍数と3のつく数字のときに表示する文字列
saidWhen5=5の倍数のときに表示する文字列
outputValues=読み上げた文字列のリストです。
index.htmlの内容です。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html lang="ja" xml:lang="ja" xmlns="http://www.w3.org/1999/xhtml"> <head> <title>世界の{{ userName }}</title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <link href="/nabeatsu/styles/styles.css" type="text/css" rel="stylesheet" /> </head> <body> <div id="header"> 世界の{{ userName }} <a href="{{ logoutUrl }}">ログオフ</a> </div> <form action="/nabeatsu/set" method="post"> <div>読み上げてほしい数:<input type="text" name="count" value="{{ count }}" tabindex="1" accesskey="c" /></div> <div>3の倍数と3のつく数字のとき<input type="text" name="saidWhen3" value="{{ saidWhen3 }}" tabindex="2" accesskey="3" />になって、</div> <div>5の倍数のとき<input type="text" name="saidWhen5" value="{{ saidWhen5 }}" tabindex="3" accesskey="5" />になります。</div> <div><input type="submit" value="設定" tabindex="4" accesskey="s" /></div> </form> <div id="body" > {% for value in outputValues %} {{ value }} <br /> {% endfor %} </div> </body> </html>
つづいて設定を更新するためのリクエストハンドラを作成します。
ここで、前もって作成しておいたnabeatsuUserMasterにデータを追加・更新する部分の作成します。
前段のテンプレートどおり、/nabeatsu/setに対応したハンドラとして、webapp.RequestHandlerを継承してUserMasterUpdateというクラスを作成します。
今回は、nabeatsuUserMasterと1対1で対応していますので、同じスクリプトファイルに記述されている前提です。
例によってサンプル中のコメントで説明します。
#dataStoreを利用するためのインポート from google.appengine.ext import db #web applicationを作成するための機能をりようするためのインポート from google.appengine.ext import webapp #googleのユーザ機能を利用するためのインポート from google.appengine.api import users #nabeatsuUserMasterクラスは前のセクションで説明したものですので、読み飛ばしてください。 class nabeatsuUserMaster(db.Model): author = db.UserProperty() count = db.IntegerProperty() date = db.DateTimeProperty(auto_now_add=True) saidWhen3 = db.StringProperty() saidWhen5 = db.StringProperty() #webapp.RequestHandlerを継承して作成することで、リクエストハンドラとしての機能を持ちます。 class UserMasterUpdate(webapp.RequestHandler): #今回は、データの設定なのでpostメソッドを作成します。HTTP POSTに対応しています。 def post(self): #現在ログオン中のユーザを取得します。 user = users.get_current_user() #ユーザが存在しない場合、users.create_login_urlメソッドを利用して、ログインページにリダイレクトします。 if not user: self.redirect(users.create_login_url(self.request.uri)) return #index.htmlのテキストボックスcountの値が入力されていなかったら、何もしないで元のページにリダイレクトします。 #今回は、form validationを行わないので、この程度にしておきます。 if self.request.get('count').count == 0: self.redirect('/nabeatsu/') return #nabeatsuUserMasterに対象ユーザのユーザ情報を検索条件として、検索します。 #GQLというクエリに良く似たものを利用しています。 Counts = nabeatsuUserMaster.gql("where author = :author",author=user) #取得したインスタンスリストの件数が0件の場合は、新しいnabeatsuUserMasterインスタンスを作成します。 #そうで無い場合は、取得したインスタンスリストの先頭を取得します。 if Counts.count() <= 0: Count = nabeatsuUserMaster() else: Count = Counts[0] #各種フォームデータをインスタンスに設定します。 Count.author = user Count.count = int(self.request.get('count')) Count.saidWhen3 = self.request.get('saidWhen3') Count.saidWhen5 = self.request.get('saidWhen5') #putでdataStoreに永続化されます。 Count.put() #設定が終わったところで最初のページにリダイレクトして終了です。 self.redirect('/nabeatsu/')
つづいて、一番肝心なindex.htmlに対応したリクエストハンドラを作成します。
ここで、テンプレートであるindex.htmlに対応したリクエストハンドラを作成します。
このリクエストハンドラ内で、該当ユーザ毎のデータを持ってきて、読み上げた数字のリストを他のデータとともに、テンプレートに渡しています。
例によってサンプル内のコメントで説明します。
# -*- coding: utf-8 -*- import wsgiref.handlers import cgi import os from google.appengine.ext import db from google.appengine.ext import webapp from google.appengine.api import users #テンプレート機能をつかうために意ポート from google.appengine.ext.webapp import template #先ほど定義した、nabeatuUserMasterとそれを更新するためのリクエストハンドラをインポート import masters #MainPageという名前でwebapp.RequestHandlerを継承して実装します。 class MainPage(webapp.RequestHandler): #数字を読み上げるロジックを実装しています。 def nabeatsu(self,saidWhen3,saidWhen5): def reader(): isAho = reader.value % 3 == 0 or str(reader.value).find('3') >= 0 isDog = reader.value % 5 == 0 outputValue = '' if isAho: #引数で渡されたsaidWhen3をUTF-8にエンコードした上で、有害な文字列が実行されないようにエスケープ処理 outputValue += cgi.escape(saidWhen3.encode('UTF-8')) if isDog: #引数で渡されたsaidWhen5をUTF-8にエンコードした上で、有害な文字列が実行されないようにエスケープ処理 outputValue += cgi.escape(saidWhen5.encode('UTF-8')) if not isAho and not isDog: outputValue = cgi.escape(str(reader.value)) reader.value += 1 return outputValue reader.value= 1 return reader #HTTP GETに対応した、メソッドを実装します。 def get(self): #現在のユーザ情報を取得します。 user = users.get_current_user() #ログインしていなければ、ログインページにリダイレクトして終了します。 if not user: self.redirect(users.create_login_url(self.request.uri)) return #各種初期値を設定しておきます。 #読み上げる数字の初期値は40で、3の倍数と3のつく数字のときは'あほ!'になって、5の倍数のときは'わん!'となるのがデフォルトです。 count = 40 saidWhen3 = u'あほ!' saidWhen5 = u'わん!' #自分自身の設定データをクエリで取得します。 currentCount = masters.nabeatsuUserMaster.gql("where author = :author",author=user) #データが存在してれば、各種データを設定します。 if currentCount.count() > 0: count = currentCount[0].count saidWhen3 = currentCount[0].saidWhen3 saidWhen5 = currentCount[0].saidWhen5 #nabeatsuメソッドに引数を渡して、readerメソッドのインスタンスを取得します。 reader = self.nabeatsu(saidWhen3,saidWhen5) #出力データリストを初期化します。 outputValues = [] #設定された数字まで、reader()を実行し続けます。 for i in xrange(1,count + 1,1): #戻り値が出力する文字列になっていますので、outputValuesに対してappendで追加していきます。 outputValues.append(reader()) #ここでテンプレートに渡すパラメータ群を設定します。 #logoutUrlをcgi.escapeしているのは、url中に&が出現しているから。 template_values = { 'userName' : user.nickname(), 'logoutUrl': cgi.escape(users.create_logout_url("/nabeatsu/")), 'count' : count, 'saidWhen3' : saidWhen3, 'saidWhen5' : saidWhen5, 'outputValues': outputValues, } #このスクリプトとテンプレートが同じフォルダにおいてあるので、テンプレートの完全なパスを作成します。 path = os.path.join(os.path.dirname(__file__), 'index.html') #template.renderにテンプレートとテンプレートに渡すパラメータ群を設定することで、実際のhtmlが出力されます。 self.response.out.write(template.render(path, template_values)) #最初に実行されるmainメソッドです。 def main(): #ここで、パスとリクエストハンドラを紐付けています。debug=trueの間はエラーのときに、tracebackが表示されます。 application = webapp.WSGIApplication( [('/nabeatsu/', MainPage), ('/nabeatsu/set', masters.UserMasterUpdate)], debug=True) wsgiref.handlers.CGIHandler().run(application) if __name__ == '__main__': main()
最後にGoogle App Engineのアプリケーション定義をして終了です
上から、
アプリケーションの名前
バージョン
ランタイム(pythonしかないので固定)
apiのバージョン(いまのところ1)
handlersはurlとスクリプトの割り当て
/nabeatsu/とnabeatsu.pyを割り当て
/nabeatsu/setとnabeatsu.pyを割り当て
スタイルシートのパスを割り当て
application: nabeatsu version: 1 runtime: python api_version: 1 handlers: - url: /nabeatsu/ script: nabeatsu.py - url: /nabeatsu/set script: nabeatsu.py - url: /nabeatsu/styles/ static_dir: styles
以上で、完成です。
スタイルシートについては適当に設定してあげれば良いですが、一応サンプルとして添付しておきます。
#body { color:blue; font-weight:bold; height:200px; overflow-y:scroll; border:solid 1px black; } body { color:Black; margin: auto; FONT-FAMILY: "MS ゴシック"; FONT-SIZE: 9pt; FONT-WEIGHT: normal; LETTER-SPACING: normal; TEXT-TRANSFORM: none; background-color: #FFFFFF; WORD-SPACING: normal; }
とりあえず、簡単なサンプルですが、用意した機能の基本的な部分を利用しながら作成する手順については追えるのではないでしょうか。