フォーム入力した値の検証について気づいた事

ずっと、GETのリクエストハンドラとPOSTのリクエストハンドラで違うルーティングをするものだと思っていたので、POSTで検証が通らなかったときにどうやって、その情報を渡しつつ元のフォームに戻るんだろうか。

という疑問があったが、form validation frameworkのサンプルページを読んでいたら、どうもGETもPOSTも同じリクエストハンドラで処理するみたいだ。

だったら、話は簡単だと思うわけで、特にform validation frameworkを利用しなくても良くて、ただ

class ****#リクエストハンドラ
def get(self):
  初期表示
def post(self):
#入力値検証
#検証が通らなかったらgetのレンダリング+エラーメッセージ
#通れば、post処理をした後、getのレンダリング

といった感じでも良さそう。でも、後学の為にもframeworkを使ってみても良さそう。

そういえば、ASP.NET MVC Previe2でも同じサンプルアプリケーションを作成する事で、構成の違いが載せられたら良いなぁ。

まだ良いなぁ。でしかないが。

間に合えば、CLTで使えたら、面白いかも?

Google App EngineとVisual Studio2008+MVC Preview2を使ったネタは許されないかも?

Share

DJangoのテンプレートを利用したサンプルアプリの作成

最初に、サンプルアプリの仕様を説明

概要:
・1からインクリメントしながら番号を表示し、3の倍数と3の付く数字のときに別の文字列を、5の倍数のときにも更に別の文字列を表示する。
・終点となる番号が設定できる。
・3の倍数と3のつく数字のときに読み上げる文字列を設定できる。
・5の倍数のときに読み上げる文字列を設定できる。
・設定した値はユーザ毎に永続化される。
・アプリケーション名は世界の○○とする。○○のところはユーザのnicknameを設定したい。
・formのvalidationについては、今回は行わない。(本当は行う必要があるので、次回以降のエントリで機能追加します。

最終的に作成したい画面イメージ

f:id:NAL-6295:20080516130415j:image

それではユーザ毎の設定値を保持するためのモデルを定義したいと思う。

ユーザ毎の設定値なのでとりあえずクラス名は、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;
}

とりあえず、簡単なサンプルですが、用意した機能の基本的な部分を利用しながら作成する手順については追えるのではないでしょうか。

Share

formに入力した値の検証の仕方について

MVCの仕組みになれていなかったのでformに入力した値の検証の仕方が分からなかったが、DJangoのform validation frameworkという機能を利用するらしい。

後で使うので、説明しているページをメモ。

Introduction

In this article we show how to use Django’s form validation framework with the Google App Engine. This framework allows you to construct HTML forms from your data models, and handle the inputted information from the forms seamlessly when interacting with the datastore.

The Django Form Validation Framework on Google App Engine

Share

ユーザ管理どころかログオンページを自前で実装する必要が無いのは楽

from google.appengine.api import users

ってインポートしておいてやれば、

user = users.get_current_user()
if user:
print 'ログイン済み'
else:
print 'ログインしてない。'

と判断できるし、ログインしていないときはハンドラの中の実装しようとしているgetとかpostメソッド内で

self.redirect(users.create_login_url(self.request.uri))

とやってやれば、勝手にログインページが作られるので楽だ。

self.response.out.write("<a href="%s">ログアウト</a>" %
(users.create_logout_url(self.request.uri)))

としておけば、ログアウトするためのリンクもできるし。

その部分に気を使う必要が無い。

DataStoreにも、ちゃんとユーザ情報を登録するためのUserProperty型が用意されているので、データ管理も楽。

持ってこれるユーザ情報もnicknameとemailアドレスだけというのも個人的には嬉しい。

Share

DataStoreが手軽でいい

インフラを気にしないで良いという事が、これほど楽ちんなのかという感じ。

いちいち、接続先を指定しなくても良くて、インスタンスを生成してPutすればそれがInsertになるし、GQLでインスタンスのリストを取得して変更してPutすればUpdateになるし、Delete()すれば削除になる。

超簡単。

エンティティの定義も

from google.appengine.ext import db
class nabeatsuUserMaster(db.Model):
#ナベアツを利用するユーザのマスタ
author = db.UserProperty()
count = db.IntegerProperty()
date = db.DateTimeProperty(auto_now_add=True)
saidWhen3 = db.StringProperty()
saidWhen5 = db.StringProperty()

ってdb.Modelを継承したクラスを作成するだけで良い。

また、GQLってのがSQLに似ているんだけど、エンティティのプロパティの一部だけ持ってくるという事はできなくて、常に

  #パラメタライズドクエリみたいな感じで:authorを定義しておいて、author=userで割り当てを行う。
  #これもいろんなやり方があって、必ずしも名前が必要というわけではない。
datalist = nabeatsuUserMaster.gql("where author = :author",author=user)
または、
datalist = db.GqlQuery("select * from nabeatsuUserMaster where author = :author",author=user)

と言った風にちゃんと全項目指定でnabeatsuUserMaster型として持ってくる必要がある。

個人的には、前者が好み。

DataStoreの詳細については、http://code.google.com/appengine/docs/datastore/を読むといいです。

比較的サンプルが多いし説明が平易なので、英語が分からなくても、なんとなくイケル感じ。

Share