ユーザがウェブサイトのフォームを送信すると、 <form> タグの action 属性で指定された URL にデータが送信されます。データは <form> タグの method 属性によって指定された HTTP GET または POST によって送信さ れます。フォームに action が指定されていなければ、それは現在の URL に 送信されます。一般に action を指定した方が良いでしょう。また、 <input type=”file” name=”file” /> のようなファイルアップロードフィー ルドが存在している場合、 HTML <form> タグで enctype=”multipart/form-data” を指定しなければならず、 method は POST でなければなりません。
このような 2 つのアクションを加えてください:
# in the controller
def form(self):
return render('/form.mako')
def email(self):
return 'Your email is: %s' % request.params['email']
templates ディレクトリに以下の内容で form.mako という名前の新しいテ ンプレートを加えてください:
<form name="test" method="GET" action="/hello/email">
Email Address: <input type="text" name="email" />
<input type="submit" name="submit" value="Submit" />
</form>
サーバが実行されているなら (Getting Started Guide を参照)、 http://localhost:5000/hello/form を訪問す ることができて、フォームが表示されるでしょう。 E メールアドレス test@example.com を入力して、 Submit をクリックしてみてください。URL は http://localhost:5000/hello/email?email=test%40example.com に変 化して、テキスト Your email is test@example.com が見られるはずです。
Pylons では、すべてのフォーム変数は辞書のように振る舞う request.params オブジェクトからアクセスできます。 そのキーは フォームのフィールドの名前で、値はすべての文字実体がデコードされた文字 列です。 例えば、 @ 文字列はブラウザによって URL の中で %40 に変換 されている一方、 request.params の中ではすぐに使用できるように 元に戻されていることに注意してください。
Note
request と response は WebOb ライブラリのオブジェクトです。そ の属性とメソッドの完全なドキュメントは ここ にあります。
フォームに同じ名前を持った 2 つのフィールドがある場合、辞書インタフェー スを使用すると、最初の文字列が返されます。 .getall() メソッドを使用す ることによって、すべての文字列をリストとして返させることができます。 1 つの値しか期待していなくて、これを強制したいなら .getone() を使用する べきです。これは同じ名前がある 1 つ以上の値が送信されたらエラーを発生し ます。
デフォルトでは、値のないフィールドが送信されると辞書インタフェースは空 の文字列を返します。つまり、 request.params の .get(key, default) を使用すると、フォームに値が存在しなかった場合にだけ default が返される ことを意味します。
もし form.mako テンプレートで method を POST に変えて例を再実行する と、同じメッセージがこれまでと同様に表示されるのを見るでしょう。 しかし、 ブラウザで表示された URL は、クエリ文字列のないただの http://localhost:5000/hello/email です。データは URL の代わりにリクエス トのボディーで送られますが、Pylons はそれを GET リクエストの場合と同様 request.params を使用して参照できるようにします。
Note
パスワードフィールドを含むフォームを書く場合、パスワードがユーザの 画面を見ているかもしれない誰かの目に入るのを防ぐのに、通常は POST を使用するべきです。
フォームベースのアプリケーションを書いていると、時折ユーザがフォームを 送信した直後に再読み込みを押すことがわかるでしょう。これには、最初に フォームが送信されたときに実行されたあらゆるアクションが繰り返されると いう効果がありますが、ユーザはしばしば現在のページが再表示されると予想 するでしょう。フォームが POST によって送信されたなら、ほとんどのブラウ ザがユーザにデータを再送信するかどうかを尋ねるメッセージを表示します。 これは GET を使った場合には起こらないので、この点で POST は GET より望 ましいです。
もちろん、この問題を解決する最も良い方法は、異なるやり方でコードを構造 化することです:
# in the controller
def form(self):
return render('/form.mako')
def email(self):
# Code to perform some action based on the form data
# ...
redirect_to(action='result')
def result(self):
return 'Your data was successfully submitted'
この場合、いったんフォームが送信されるとデータが保存されて HTTP リダイ レクトが起こり、ブラウザが http://localhost:5000/hello/result にリダイ レクトされます。次にユーザがページを再読み込みすると、それはアクション を再実行する代わりに単にメッセージを再度表示します。
また、フォームを作成するのに WebHelpers を使用することができます。それ は Pylons に付属しています。これは前のセクションで作成したのと同じフォー ムですが、今回は helpers を使用しています:
${h.form(h.url(action='email'), method='get')}
Email Address: ${h.text('email')}
${h.submit('Submit')}
${h.end_form()}
これをする前に、使用したい helper をプロジェクトの lib/helpers.py ファ イルの中にインポートする必要があるでしょう。そうすれば、それらは Pylons の h グローバル変数の下で利用可能になります。 ほとんどのプロ ジェクトでは少なくともこれらをインポートするとよいでしょう:
from webhelpers.html import escape, HTML, literal, url_escape
from webhelpers.html.tags import *
他にもテキスト整形やコンテナーオブジェクト、統計、および巨大なクエリ結 果をページに分割するための多くの helper があります。 WebHelpers のドキュメント を見て、あなたが必要とする helper を選んで ください。
ファイルアップロードフィールドは、入力フィールドのタイプ file を使用 することによって作成されます。 file_field ヘルパーは、これらのフォー ムフィールドを作成するための近道を提供します:
${h.file_field('myfile')}
HTML フォームはブラウザがファイルをアップロードできるように enctype 属性を multipart/form-data に設定しなければなりません。 form ヘルパー の multipart キーワード引数は、適切な enctype 値を設定するための近 道を提供します:
${h.form(h.url(action='upload'), multipart=True)}
Upload file: ${h.file_field('myfile')} <br />
File description: ${h.text_field('description')} <br />
${h.submit('Submit')}
${h.end_form()}
ファイルアップロードが成功したとき、 request.POST (または request.params) MultiDict は、フィールドの値として cgi.FieldStorage オブジェクトを含むでしょう。
FieldStorage オブジェクトには、ファイルアップロードのための3つの重要 な属性があります:
filename
アップロードしたユーザのファイルシステム上での、アップロードされた ファイルの名前
file
ファイルのデータを読むことができる file(-like) オブジェクト: Python tempfile か StringIO オブジェクト。
value
事前にファイルオブジェクトから直接読み込まれた、アップロードされた ファイルの中身
ファイルのデータへアクセスする最も簡単な方法は value 属性を使用するこ とです: それは文字列としてファイル全体の内容を返します:
def upload(self):
myfile = request.POST['myfile']
return 'Successfully uploaded: %s, size: %i, description: %s' % \
(myfile.filename, len(myfile.value), request.POST['description'])
しかしながら、特に大きなファイルのアップロードでは、メモリからファイル の全体のコンテンツを読み取ることは望ましくありません。ファイルアップロー ドの一般的な取り扱い手段は、ファイルをファイルシステムのどこかに保存す ることです。 FieldStorage は通常ファイルをファイルシステムへ読み込み ますが、それは Python tempfile オブジェクトを通して非永久的な位置に保 存されます (非常に小さいアップロードに対しては、代わりに StringIO オ ブジェクトが使われることもあります)。
Python tempfiles は、 close されるとき (ガベージコレクションによって 暗黙的に close される場合を含む) に自動的に破壊される、 secure なファイ ルオブジェクトです。それらのセキュリティ機能の 1 つは、それらのパスが決 定できないということです: tempfile のパスからは単純な os.rename が できません。代わりに、 shutil.copyfileobj はファイルデータを永久的な 位置へ効率的にコピーすることができます:
permanent_store = '/uploads/'
class Uploader(BaseController):
def upload(self):
myfile = request.POST['myfile']
permanent_file = open(os.path.join(permanent_store,
myfile.filename.lstrip(os.sep)),
'w')
shutil.copyfileobj(myfile.file, permanent_file)
myfile.file.close()
permanent_file.close()
return 'Successfully uploaded: %s, description: %s' % \
(myfile.filename, request.POST['description'])
Warning
前の基本的な例では、ファイルをアップロードするユーザは permanent_store ディレクトリ内でウェブアプリケーションがパーミッ ションを持っているあらゆるファイルを上書きすることができます。
また、ここで myfile.filename.lstrip(os.sep) を使用していることに注意 してください: それがなければ、 os.path.join は危険です。 os.path.join は (os.sep で始まる) 絶対パスを join しません。つまり、 os.path.join(‘/uploads/’, ‘/uploaded_file.txt’) == ‘/uploaded_file.txt’ です。ユーザが送信したデータを os.path.join と 共に使用する場合、常にチェックして下さい。
これまでのところ、フォームにどんな値でも入力することができます。そして、 有効な E メールアドレスではなかったとしても、それをメッセージに表示する でしょう。多くの場合、ユーザの入力に対してバリデーションを行う必要があ るので、これは許容できません。 Pylons でフォームのバリデーションを行う ためのお勧めのツールは FormEncode です。
また、作成した各フォームのためにバリデーションスキーマを作成します。今 の場合、これはかなり簡単です:
import formencode
class EmailForm(formencode.Schema):
allow_extra_fields = True
filter_extra_fields = True
email = formencode.validators.Email(not_empty=True)
Note
通常、フォームのスキーマを一緒にしておいて、スキーマを更新するため の単一の場所を持つことを勧めます。新しいフォームスキーマを既存のも のの上に作ることができるので、それは継承にも便利です。フォームを models/form.py ファイルに入れるなら、この例ではコントローラ中で model.form.EmailForm として容易にそれらを使用できます。
このフォームには、実際に 2 つのフィールド、メールテキストフィールドと送 信ボタンがあります。余分なフィールドが送信された場合 FormEncode のデフォ ルトの振舞いではフォームが無効とみなされるので、 allow_extra_fields = True を指定します。また、余分なフィールドの値を使用したいとは思わない ので、 filter_extra_fields = True を指定します。 最後の行は、メール フィールドが Email() バリデータでバリデーションされるべきであると指定 します。 また、バリデータを作成する際に、メールフィールドが入力を必要と するように not_empty=True を指定します。
Pylons は簡単に使える validate デコレータを含んでいます。それを使用し たければ、このように lib/base.py でそれをインポートしてください:
# other imports
from pylons.decorators import validate
コントローラでそれを使用するのはとても簡単です:
# in the controller
def form(self):
return render('/form.mako')
@validate(schema=EmailForm(), form='form')
def email(self):
return 'Your email is: %s' % self.form_result.get('email')
バリデーションは POST リクエストのときにだけ起こります。そのため、フォー ム定義を変更して method を POST にする必要があります:
${h.form(h.url(action='email'), method='post')}
バリデーションが成功すると、バリデーション結果の辞書がアクションで使用 できるように self.form_result として保存されます。 さもなければ、アク ションはまるでそれが form で指定されたコントローラアクションへの GET リクエストであるかのように再実行されるでしょう。そして、その出力は、 FormEncode の htmlfill によってフォームフィールドエラーが埋め込まれます。 簡単なケースでは、テンプレートに (存在しているなら) エラーメッセージを 表示するためのコードを書かなくても済むので、これは本当に便利です。
これは上記の例とまさに同じことをします。しかし、それはオリジナルのフォー ム定義と共に動いています。 validate デコレータは formencode.htmlfill を使用して HTML フィールドを見つけて、それらを元々送信された値に置き換 えているので、事実上 HTML フォームがどのように生成されたかにかかわらず、 それはどんなフォームとも共に動くでしょう。
Note
Python 2.3 はデコレータをサポートしていません。そのため、 @validate() 構文を使用する代わりに email = validate(schema=EmailForm(), form=’form’)(email) を email 関数の宣 言の後に置く必要があります。
validate デコレータは作業の一部を隠します。そして、必要に応じて隠され た FormEncode の能力に直接アクセスする必要があるかもしれません。
これは EmailForm スキーマを使用するもっと長い方法です:
# in the controller
def email(self):
schema = EmailForm()
try:
form_result = schema.to_python(request.params)
except formencode.validators.Invalid, error:
return 'Invalid: %s' % error
else:
return 'Your email is: %s' % form_result.get('email')
入力された値が有効なら、スキーマの to_python() メソッドはバリデーショ ンと型変換 (coerce) された form_result の辞書を返します。 これは、 form_result 辞書が期待するデータ型に対して有効で正しい Python オブジェ クトである値を含んでいることを信用できることを意味します。
この場合 E メールアドレスが文字列なので、 request.params[‘email’] は たまたま form_result[‘email’] と同じです。 もしフォームが年齢フィール ドを含んでいて、 formencode.validators.Int() バリデータを使用したなら、 年齢に対する form_result の値は正しい型になるでしょう。この場合は Python 整数型です。
FormEncode は役に立つバリデータのセットを含んでいますが、独自のバリ データを作成することも容易にできます。 独自のバリデータを作成する場 合、 FormEncode のすべての schemas の .to_python() メソッドが state という2番目の引数を取るのが非常に役に立つことがわかるでしょ う。これは、バリデータが特定のフィールドをバリデーションするために 必要とするどんな変数も c オブジェクトの属性として設定できるように、 Pylons c オブジェクトをバリデータに渡すことができることを意味しま す。そして、以下のようにそれを c オブジェクトとしてスキーマに渡す ことができます:
c.domain = 'example.com'
form_result = schema.to_python(request.params, c)
スキーマは c を各バリデータに順番に渡すので、このようなことができます:
class SimpleEmail(formencode.validators.Email):
def _to_python(self, value, c):
if not value.endswith(c.domain):
raise formencode.validators.Invalid(
'Email addresses must end in: %s' % \
c.domain, value, c)
return formencode.validators.Email._to_python(self, value, c)
これが動作するように、定義済みの EmailForm スキーマを新しい SimpleEmail バリデータを使用するように変えてください。言い換えれば、
email = formencode.validators.Email(not_empty=True)
# becomes:
email = SimpleEmail(not_empty=True)
実際には、有効な E メールアドレスを入力しなかった場合に得られる invalid エラーメッセージはそれほど役に立ちません。入力された値と生成さ れたエラーメッセージが入ったフォームを再表示したいと思うでしょう。次の 行を、
return 'Invalid: %s' % error
以下のように置き換えてください:
c.form_result = error.value
c.form_errors = error.error_dict or {}
return render('/form.mako')
次に、 form.mako にいくつかの修正をする必要があります。このようにして ください:
${h.form(h.url(action='email'), method='get')}
% if c.form_errors:
<h2>Please correct the errors</h2>
% else:
<h2>Enter Email Address</h2>
% endif
% if c.form_errors:
Email Address: ${h.text_field('email', value=c.form_result['email'] or '')}
<p>${c.form_errors['email']}</p>
% else:
Email Address: ${h.text_field('email')}
% endif
${h.submit('Submit')}
${h.end_form()}
これで、フォームが無効なときに form.mako テンプレートはエラーメッセー ジと共に再レンダリングされます。
大量のフォームを作成するつもりなら、フォームの作成を補助するために FormBuild を使用することを検討すると良いかも しれません。 FormBuild を使用するためには、カスタム Form オブジェクトを 作成します。そして、そのオブジェクトを使用してすべてのフォームを作成し てください。そうすると、カスタム Form の定義を変更することによって、 フォームテンプレートを少しも変更する必要なく、カスタム Form を使って構 築されたすべてのフォームの生成と使用のあらゆる側面を API を使用して変更 することができます。
これは、コントローラでフォーム送信を扱うのに FormBuild をどのように使用 するかに関する 1 つの例です:
# in the controller
def form(self):
results, errors, response = formbuild.handle(
schema=Schema(), # Your FormEncode schema for the form
# to be validated
template='form.mako', # The template containg the code
# that builds your form
form=Form # The FormBuild Form definition you wish to use
)
if response:
# The form validation failed so re-display
# the form with the auto-generted response
# containing submitted values and errors or
# do something with the errors
return response
else:
# The form validated, do something useful with results.
...
すべての特徴の完全なドキュメンテーションは FormBuild manual にあります。それを読んだら、次は Using FormBuild in Pylons を見る とよいでしょう。
将来的には、 Pylons はもうすぐ TurboGears ウィジェットシステムを使用で きるようになりそうです。それはおそらく Pylons でフォームを構築するお勧 めの方法になるでしょう。