Chapter 7: フォームとバリデータ

フォームとバリデータ

web2pyでフォームを構築するには以下の4通りの方法があります:

  • FORM はHTMLヘルパに関して低レベルの実装を提供します。FORMオブジェクトはHTMLへとシリアライズすることができ、そこに含まれるフィールドについて把握しています。FORMオブジェクトは送信フォームの値を検証することができます。
  • SQLFORM は、作成、更新、削除のフォームを、既存のデータベーステーブルから構築するための高レベルのAPIを提供します。
  • SQLFORM.factorySQLFORMの上にある抽象化レイヤです。データベースが用意されてない場合でもフォーム生成機能を活用できるようにしています。これは、テーブルの記述からSQLFORMととても良く似たフォームを生成します。ただしデータベース・テーブルを作成する必要はありません。
  • CRUDメソッド。SQLFORMと同等でSQLFORMに基づく関数が用意されていますが、よりコンパクトな表記が可能になります。

これらすべてのフォームは自分自身を把握しており、入力が検証を通らなかった場合、自分自身を修正してエラーメッセージを加えることができます。フォームでは、検証によって生成された検証済み変数とエラーメッセージを問い合わせることができます。

ヘルパを用いて、任意のHTMLコードをフォームへ挿入、また、フォームから抽出することができます。

FORMSQLFORMはヘルパで、DIVのように扱えます。例えば、フォームスタイルを指定できます:

1
2
form = SQLFORM(..)
form['_style']='border:1px solid black'

FORM

form
accepts
formname

次のような"default.py"コントローラを持つtestアプリケーションを考えます:

1
2
def display_form():
    return dict()

関連付けるビュー"default/display_form.html"は以下のようにします:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{{extend 'layout.html'}}
<h2>Input form</h2>
<form enctype="multipart/form-data"
      action="{{=URL()}}" method="post">
Your name:
<input name="name" />
<input type="submit" />
</form>
<h2>Submitted variables</h2>
{{=BEAUTIFY(request.vars)}}

これは、ユーザー名を問い合わせる一般的なHTMLフォームです。このフォームを入力しサブミット・ボタンをクリックすると、フォームは自分自身をサブミットし、request.vars.name変数がその準備された値といっしょに下部に表示されます。

同じフォームをヘルパを用いて生成することができます。これは、ビュー、または、アクションにおいて行うことができます。web2pyはフォームの処理をアクションにおいて行うので、フォームをアクションで定義する方が良いのです。

さあ、新しいコントローラです:

1
2
3
def display_form():
   form=FORM('Your name:', INPUT(_name='name'), INPUT(_type='submit'))
   return dict(form=form)

関連付けるビュー"default/desplay_form.html"は以下のようにします:

1
2
3
4
5
{{extend 'layout.html'}}
<h2>Input form</h2>
{{=form}}
<h2>Submitted variables</h2>
{{=BEAUTIFY(request.vars)}}

以前のコードは上のコードと等しいです。しかし、フォームは、FORMオブジェクトをシリアライズする{{=form}}という文によって生成されています。

次に、フォームの検証と処理を追加して、一段複雑なものを加えます。

コントローラを以下のように変更します:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def display_form():
    form=FORM('Your name:',
              INPUT(_name='name', requires=IS_NOT_EMPTY()),
              INPUT(_type='submit'))
    if form.accepts(request,session):
        response.flash = 'form accepted'
    elif form.errors:
        response.flash = 'form has errors'
    else:
        response.flash = 'please fill the form'
    return dict(form=form)

関連付けるビュー"default/desplay_form.html"は以下のようにします:

1
2
3
4
5
6
7
8
9
{{extend 'layout.html'}}
<h2>Input form</h2>
{{=form}}
<h2>Submitted variables</h2>
{{=BEAUTIFY(request.vars)}}
<h2>Accepted variables</h2>
{{=BEAUTIFY(form.vars)}}
<h2>Errors in form</h2>
{{=BEAUTIFY(form.errors)}}

以下のことに注意してください:

  • アクションでは、入力フィールド"name"に対してrequires=IS_NOT_EMPTY()バリデータを加えています。
  • アクションでは、form.accepts(...)の呼び出しを加えています。
  • ビューでは、フォームとrequest.varsとともにform.varsform.errorsを表示しています。

すべての作業は、formオブジェクトのacceptsメソッドによって行われます。これは、request.varsを、(バリデータによって表現された)宣言された要求に従って、フィルタします。acceptsは、検証を通ったこれらの変数をform.varsに格納します。フィールドの値が何かしら要求を満たさない場合は、失敗したバリデータがエラーを返し、そのエラーがform.errorsに格納されます。form.varsform.errorsは両方とも、request.varsに似たgluon.storage.Storageオブジェクトです。前者は、次のように検証を通った値を保持します:

1
form.vars.name = "Max"

後者は、次のようにエラーを保持します:

1
form.errors.name = "Cannot be empty!"

acceptsメソッドのすべての用法は以下の通りです:

onvalidation
1
2
3
form.accepts(vars, session=None, formname='default',
             keepvalues=False, onvalidation=None,
             dbio=True, hideerror=False):

これらオプション・パラメータの意味は、次の小節で説明します。

最初の引数にはrequest.varsrequest.get_varsrequest.post_varsや、単にrequestと指定できます。後者はrequest.post_varsと入力したのと等しく処理されます。

accepts関数はフォームが受理されたときにTrueを返し、そうでない場合はFalseを返します。フォームは、エラーがある場合か、サブミットされてない場合(たとえば、最初に表示されるとき)には受理されません。

次に示すのは、このページが最初に表示されたときの様子です:

image

無効なサブミットをしたときの様子です:

image

有効なサブミットをしたときの様子です:

image

processvalidateメソッド

以下のショートカットは

1
form.accepts(request.post_vars,session,...)

このようになります

1
form.process(...).accepted

後者はrequestsession引数を必要としません(任意で指定は可能ですが)。またフォーム自身を返すため、acceptsとは異なります。内部的には、processがacceptsを呼び出し、その引数を渡します。acceptsによって返された値がform.acceptedに保存されます。

process関数はacceptsに存在しない追加の引数を受け取れます。

  • message_onsuccess
  • onsuccess: 'flash' (既定)に相当し、フォームが受理されると上記のmessage_onsuccessを表示します。
  • message_onfailure
  • onfailure: 'flash' (既定)に相当し、フォームが検証を通らなかった場合、上記のmessage_onfailureを表示します。
  • next フォームが受理された後に、ユーザーをリダイレクトする場所を指定します。

onsuccessonfailure にはlambda form: do_something(form)のような関数も使用できます。

1
form.validate(...)

は以下のショートカットです。

1
form.process(...,dbio=False).accepted

隠しフィールド

上記のフォーム・オブジェクトが{{=form}}によってシリアライズされたとき、acceptsメソッドに対する前述の呼び出しがあるために、フォームは次のようになります:

1
2
3
4
5
6
7
<form enctype="multipart/form-data" action="" method="post">
your name:
<input name="name" />
<input type="submit" />
<input value="783531473471" type="hidden" name="_formkey" />
<input value="default" type="hidden" name="_formname" />
</form>

注意する点は、2つの隠しフィールド"_formkey"と"_formname"があることです。これらの存在は、acceptsの呼び出しによって引き起こされたもので、2つの異なる重要な役割を果たします:

  • "_formkey"という隠しフィールドは一度限りのトークンで、web2pyがフォームの二重投稿を防ぐために用いられます。このキーの値はフォームがシリアライズされたときに生成され、sessionに保存されます。フォームがサブミットされたときに、この値が一致する必要があります。そうでないとacceptsは、フォームが全くサブミットされてないかのように、エラーなしでFalseを返します。これは、フォームが正しくサブミットされたかどうかをweb2pyが判断できないためです。
  • "_formname"という隠しフィールドは、フォームの名前としてweb2pyによって生成されますが、その名前は上書きすることができます。このフィールドは、ページが複数のフォームを含んで処理することを可能にするために必要です。web2pyは、この名前によって異なるサブミットされたフォームを区別します。
  • オプション的な隠しフィールドはFORM(..,hidden=dict(...))のように指定します。

これらの隠しフィールドの役割と、カスタムフォームと複数のフォームを持つページにおける使用方法は、本章の後半で詳しく説明します。

上記のフォームを空の"name"フィールドでサブミットした場合、フォームは検証を通過しません。フォームが再びシリアライズされるときは、次のように表示されます:

1
2
3
4
5
6
7
8
<form enctype="multipart/form-data" action="" method="post">
your name:
<input value="" name="name" />
<div class="error">cannot be empty!</div>
<input type="submit" />
<input value="783531473471" type="hidden" name="_formkey" />
<input value="default" type="hidden" name="_formname" />
</form>

シリアライズしたフォームにあるDIVのクラス"error"の存在に注意してください。web2pyはこのエラーメッセージをフォームに挿入し、検証を通過しなかったフィールドについて訪問者に知らせます。サブミットの際のacceptsメソッドは、フォームがサブミットされたかどうかを判断し、フィールド"name"が空でないか、また、それが要求されているかをチェックし、最終的に、バリデータからフォームにエラーメッセージを挿入します。

基底の"layout.html"ビューは、DIVクラスの"error"を処理することが想定されています。デフォルトのレイアウトはjQueryのエフェクトを使用して、エラーを可視化し、赤い背景とともにスライドダウンさせます。詳細は第11章を参照してください。

keepvalues

keepvalues

オプション引数keepvaluesは、フォームが受理され、かつ、リダイレクトがないときに、web2pyに何をするか知らせ、同じフォームが再び表示されるようにします。デフォルトではすべてクリアされます。keepvaluesTrueの場合、フォームは前回挿入した値を事前に入力します。これは、複数の似たレコードを繰り返し挿入するために使用することを想定したフォームがあるときに便利です。dbio引数がFalseの場合、web2pyは、フォームを受理した後、いかなるDBの挿入/更新も行いません。hideerrorTrueでフォームにエラーが含まれている場合、フォームがレンダリングされたときにエラーは表示されません(form.errorsをどのように表示するかは開発者次第です)。onvalidation引数は以下に説明します。

onvalidation

onvalidation引数はNoneもしくは、フォームを受け取り何も返さない関数をとることができます。そのような関数は、検証(が通った)直後に、かつ、それ以外のことが発生する前に呼ばれ、フォームを渡します。この関数の目的は複数あります。これは、たとえば、追加的なフォームのチェックを実行したり、最終的にフォームにエラーを加えたりすることができます。これはまた、いくつかのフィールドの値を、他のフィールドの値に基づいて計算するのに使用することもできます。これを用いて、レコードが作成/更新される前にいくつかのアクション(emailの送信など)を引き起こすことも可能です。

以下がその例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
db.define_table('numbers',
    Field('a', 'integer'),
    Field('b', 'integer'),
    Field('c', 'integer', readable=False, writable=False))

def my_form_processing(form):
    c = form.vars.a * form.vars.b
    if c < 0:
       form.errors.b = 'a*b cannot be negative'
    else:
       form.vars.c = c

def insert_numbers():
   form = SQLFORM(db.numbers)
   if form.process(onvalidation=my_form_processing).accepted:
       session.flash = 'record inserted'
       redirect(URL())
   return dict(form=form)

レコード変更の検知

レコードを編集するためにフォームを入力している際に、わずかですが他のユーザーが同じレコードを同時に変更している可能性があります。そこでレコードを保存する際に競合していないかチェックしたいです。これは以下のように実施できます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
db.define_table('dog',Field('name'))

def edit_dog():
    dog = db.dog(request.args(0)) or redirect(URL('error'))
    form=SQLFORM(db.dog,dog)
    form.process(detect_record_change=True)
    if form.record_changed:
        # do something
    elif form.accepted:
        # do something else
    else:
        # do nothing
    return dict(form=form)

フォームとリダイレクト

フォームを使用する最も一般的な方法は、自己サブミットを介して、サブミットされたフィールドの変数が、フォームを生成したものと同じアクションによって処理されるようにすることです。フォームが一旦受理されれば、現在のページを再び表示することはあまりありません(ここでは説明を単純にするためいくつか行っています)。訪問者を"next"ページへリダイレクトさせるのがより一般的です。

ここでは新しいコントローラの例を示します:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def display_form():
    form = FORM('Your name:',
              INPUT(_name='name', requires=IS_NOT_EMPTY()),
              INPUT(_type='submit'))
    if form.process().accepted:
        session.flash = 'form accepted'
        redirect(URL('next'))
    elif form.errors:
        response.flash = 'form has errors'
    else:
        response.flash = 'please fill the form'
    return dict(form=form)

def next():
    return dict()

現在のページの代わりにnextページでflashを設定するために、session.flashresponse.flashの代わりに設定する必要があります。web2pyはリダイレクト後、前者を後者に移します。session.flashsession.forget()を使用していないことが前提であることに注意してください。

ページ毎に複数のフォーム

この節の内容は、FORMとSQLFORMオブジェクトどちらにも適用されます。 The content of this section applies to both FORM and SQLFORM objects. ページ毎に複数のフォームを持つことが可能です。しかし、web2pyにそれらを区別できるようにしなければなりません。異なるテーブルのSQLFORMによって生成されたものならば、web2pyは異なる名前を自動的にそれらに与えます。それ以外の場合は、異なるフォームの名前を明示的に与えなければなりません。以下がその例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def two_forms():
    form1 = FORM(INPUT(_name='name', requires=IS_NOT_EMPTY()),
               INPUT(_type='submit'))
    form2 = FORM(INPUT(_name='name', requires=IS_NOT_EMPTY()),
               INPUT(_type='submit'))
    if form1.process(formname='form_one').accepted:
        response.flash = 'form one accepted'
    if form2.process(formname='form_two').accepted:
        response.flash = 'form two accepted'
    return dict(form1=form1, form2=form2)

ここに生成された出力を示します:

image

訪問者が空のform1をサブミットした場合、form1のみがエラーを表示します。一方、訪問者が空のform2をサブミットした場合、form2のみがエラーメッセージを表示します。

フォームの共有

この節の内容は、FORMSQLFORMオブジェクトどちらにも適用されます。ここで説明することは可能なことですが推奨されません、なぜなら、自己サブミットするフォームを持つことがベストプラクティスだからです。しかし、場合によっては、フォームを送受信するアクションが異なるアプリケーションに属していて、選択肢がないことがあります。

異なるアクションへサブミットするフォームを生成することは可能です。これは、FORMまたはSQLFORMオブジェクトの属性において、処理するアクションのURLを指定することで行われます。例えば:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
form = FORM(INPUT(_name='name', requires=IS_NOT_EMPTY()),
        INPUT(_type='submit'), _action=URL('page_two'))

def page_one():
    return dict(form=form)

def page_two():
    if form.process(session=None, formname=None).accepted:
         response.flash = 'form accepted'
    else:
         response.flash = 'there was an error in the form'
    return dict()

"page_one"と"page_two"は両者とも同じformを利用しているので、同じことの繰り返しを避けるために、そのフォームをすべてのアクションの外側で一度だけ定義していることに注意してください。コントローラの冒頭にある共通のコード部分は、アクションの呼び出しに制御を渡す前に毎回実行されます。

"page_one"はprocess(または、accepts)を呼び出さないため、formには名前がなくキーもありません。そのため、session=Noneを渡し、formname=Noneを設定する必要があります。そうでないと、フォームは"page_two"に受け取られたときに検証を行いません。

FORMにボタンを追加

通常、フォームはひとつのサブミットボタンを用意します。サブミットの代わりに他のページへ導く"back"ボタンを加えたいことはよくあります。

add_button

これはadd_buttonメソッドで行えます。

1
form.add_button('Back', URL('other_page'))

フォームに2つ以上のボタンを加えることができます。add_buttonの引数はボタンの値(テキスト)と、リダイレクト先のurlです。

FORMのより多くの操作について

Viewerの章で説明したように、FORMはHTMLヘルパーです。ヘルパーはPythonのリスト型や辞書型で操作することができ、実行時作成や修正が行えます。

SQLFORM

次の段階に進んで、以下のようなアプリケーションのモデルファイルを用意します:

1
2
db = DAL('sqlite://storage.sqlite')
db.define_table('person', Field('name', requires=IS_NOT_EMPTY()))

コントローラを以下のように変更します:

1
2
3
4
5
6
7
8
9
def display_form():
   form = SQLFORM(db.person)
   if form.process().accepted:
       response.flash = 'form accepted'
   elif form.errors:
       response.flash = 'form has errors'
   else:
       response.flash = 'please fill out the form'
   return dict(form=form)

ビューを変更する必要はありません。

この新しいコントローラでは、FORMを構築する必要はありません。なぜなら、SQLFORMコンストラクタは、モデルで定義されたdb.personテーブルからそれを構築するからです。この新しいフォームがシリアライズされると次のように表示されます:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<form enctype="multipart/form-data" action="" method="post">
  <table>
    <tr id="person_name__row">
       <td><label id="person_name__label"
                  for="person_name">Your name: </label></td>
       <td><input type="text" class="string"
                  name="name" value="" id="person_name" /></td>
       <td></td>
    </tr>
    <tr id="submit_record__row">
       <td></td>
       <td><input value="Submit" type="submit" /></td>
       <td></td>
    </tr>
  </table>
  <input value="9038845529" type="hidden" name="_formkey" />
  <input value="person" type="hidden" name="_formname" />
</form>

この自動的に生成されたフォームは、前述の低レベルのフォームよりも複雑です。第1に、これはテーブルの行を含み、各行は3つのカラムを持っています。最初のカラムはフィールドの名前を保持しています(db.personから決定されます)。第2のカラムは入力フィールドを保持します(最終的にエラーメッセージも保持します)。第3のカラムは省略可能で空になっています(SQLFORMのコンストラクタにおいてフィールドを用いて入力される可能性があります)。

フォームのすべてのタグには、テーブルとフィールドの名前に由来する名前が付けられています。これによりCSSとJavaScriptを用いてフォームをカスタマイズするのが容易になります。この機能については、第11章で詳しく説明します。

ここでより重要なのは、acceptsメソッドがより多くの仕事をすることです。前回の場合と同様、入力の検証を行いますが、加えて、入力が検証を通ったら、データベースに対して新規レコードの挿入を実行し、form.vars.idに新規レコードのユニークな"id"を格納します。

SQLFORMオブジェクトはまた、自動的に"upload"フィールドを処理し、アップロードしたファイルを"uploads"フォルダに保存します(競合を避け、ディレクトリ・トラバーサル攻撃を防ぐために安全にリネームした後に保存します)。そして、(新しい)ファイル名をデータベースの適切なフィールドに保存します。フォームが処理されると、新しいファイル名はform.vars.fieldname(つまり、cgi.FieldStorageオブジェクトの値をrequest.vars.fieldnameに置き換える)で参照可能なので、更新後、簡単にその新しい名前を参照することができます。

SQLFORMは、"boolean"の値をチェックボックスで、"text"の値をテキストエリアで、限定したセットまたはデータベース内に含まれることを要求された値をドロップボックスで、"upload"フィールドをアップロードしたファイルをダウンロードできるようにしたリンクで、表示します。"blob"フィールドは非表示にします。後述しますが、異なる方法で処理されることになるからです。

たとえば、次のモデルを考えてください:

1
2
3
4
5
6
db.define_table('person',
    Field('name', requires=IS_NOT_EMPTY()),
    Field('married', 'boolean'),
    Field('gender', requires=IS_IN_SET(['Male', 'Female', 'Other'])),
    Field('profile', 'text'),
    Field('image', 'upload'))

この場合、SQLFORM(db.person)は次に表示されるようなフォームを生成します:

image

SQLFORMのコンストラクタは、さまざまなカスタマイズを可能にします。たとえば、フィールドの一部のみを表示したり、ラベルを変更したり、オプションの第3のカラムに値を加えたり、現在のINSERTフォームとは対照的にUPDATEやDELETEフォームを作成したりすることができます。SQLFORMはweb2pyにおいて、たったひとつで最も大きく時間を節約できるオブジェクトです。

SQLFORMクラスは"gluon/sqlhtml.py"に定義されています。これは、xmlメソッドをオーバライドして簡単に拡張することができます。このメソッドはこのオブジェクトをシリアライズして、その出力を変更します。

fields
labels

SQLFORMのコンストラクタの用法は以下の通りです:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
SQLFORM(table, record=None,
        deletable=False, linkto=None,
        upload=None, fields=None, labels=None,
        col3={}, submit_button='Submit',
        delete_label='Check to delete:',
        showid=True, readonly=False,
        comments=True, keepopts=[],
        ignore_rw=False, record_id=None,
        formstyle='table3cols',
        buttons=['submit'], separator=': ',
        **attributes)
  • オプション的な第2の引数は、INSERTフォームから、指定したレコードに対するUPDATEフォームに切り替えます(次の小節を参照してください)。
    showid
    delete_label
    id_label
    submit_button

deletableTrueの場合、UPDATEフォームは"Chceck to delete"というチェックボックスを表示します。フィールドがある場合、このラベルの値はdelete_label引数を介して設定されます。

  • submit_buttonはサブミット・ボタンの値を設定します。
  • id_labelはレコードの"id"のラベルを設定します。
  • showidFalseの場合、レコードの"id"は表示されません。

fieldsは表示したいフィールド名のオプション的なリストです。リストが提供されている場合、リスト内のフィールドしか表示されません。例:

1
fields = ['name']
  • labelsはフィールドラベルの辞書です。辞書のキーはフィールド名で、対応する値はラベルとして表示されます。ラベルが提供されていない場合、web2pyはラベルをフィールド名から生成します(フィールド名を大文字で書き始めアンダースコアをスペースに置換します)。例:
1
labels = {'name':'Your Full Name:'}
  • col3は第3のカラム用の辞書の値です。例:
1
2
col3 = {'name':A('what is this?',
      _href='http://www.google.com/search?q=define:name')}
  • linktouploadは、ユーザー定義コントローラへのオプション的なURLです。これにより、フォームで参照フィールドを扱うことが可能になります。これについてはこの節の後の方で詳しく解説します。
  • readonly。Trueの場合、読み取り専用のフォームを表示します。
  • comments。Falseの場合、col3のコメントを表示しません。
  • ignore_rw。通常は、作成/更新フォームに対して、writable=Trueでマークされたフィールドしか表示されず、読み取り専用フォームに対しては、readable=Trueでマークしたフィールドしか表示されません。ignore_rw=Trueに設定すると、これらの制約は無視され、すべてのフィールドが表示されます。これは主に、appadminインターフェースにおいて、モデルの指示をオーバーライドして、各テーブルのすべてのフィールドを表示するために使用されます。
  • formstyle
    formstyleフォームをhtmlにシリアライズするときに使用されるスタイルを決めます。次の値をとることができます:"table3cols"(デフォルト)、"table2cols"(一行にラベルとコメントを、もう1つの行に入力を表示します)、"ul"(入力フィールドの順序なしリストを作成します)、"divs"(フォームをcssフレンドリなdivで表現します)。 formystyleはまた、(record_id, field_label, field_widget, field_comment)を属性として受け取り、TR()オブジェクトを返す関数をとることもできます。
  • buttons
    buttonsINPUTTAG.BUTTON(技術的にはあらゆるヘルパの組み合わせを使用できるが)のリストで、submitボタンが配置されるDIVに追加されます。
  • separator
    separatorはフォームのラベルと入力フィールドの間に区切り文字を設定します。
  • オプション的なattributesは、SQLFORMオブジェクトをレンダリングするFORMタグに対して渡したいアンダースコアで始まる引数群です。たとえば次のようになります:
1
2
_action = '.'
_method = 'POST'

特別なhidden属性があります。辞書がhiddenとして渡されたとき、その項目は"hidden"INPUTフィールドに変換されます(第5章のFORMヘルパの例を参照してください)。

1
form = SQLFORM(....,hidden=...)

この隠しフィールドはサブミットによって渡されますが、form.accepts(...)は受信した隠しフィールドを読み込み、form.varsに値を渡すといっと動作は行いません。これはセキュリティ上の理由です。隠しフィールドは改ざんされる可能性があるからです。このため、隠しフィールドをrequestからフォームに明示的に移動する必要があります。

1
2
form.vars.a = request.vars.a
form = SQLFORM(..., hidden=dict(a='b'))

SQLFORMinsert/update/delete

SQLFORMはフォームが受理されると新しいレコードを作成します。以下の例、

1
form=SQLFORM(db.test)
 を考えてみると、このとき最後に作成されたレコードのidはform.vars.idで参照できます。

delete record

レコードをSQLFORMコンストラクタのオプション的な第2の引数に渡した場合、フォームはそのレコードに対するUPDATEフォームになります。つまり、フォームがサブミットされると既存のレコードが更新され新しいレコードは挿入されません。deletable=True属性を設定して場合は、UPDATEフォームは"Check to delete"というチェックボックスを表示します。チェックされると、レコードは削除されます。

フォームがサブミットされ、削除チェックボックスがチェックされている場合は、form.deleted属性にTrueが設定されます。

たとえば、前述の例のコントローラを修正し、次のように、URLのパスに追加の整数引数を渡すことができます。

1
/test/default/display_form/2

これに対応するidを持つレコードがあると、SQLFORMは、このレコードのためのUPDATE/DELETEフォームを生成します:

1
2
3
4
5
6
7
8
def display_form():
   record = db.person(request.args(0)) or redirect(URL('index'))
   form = SQLFORM(db.person, record)
   if form.process().accepted:
       response.flash = 'form accepted'
   elif form.errors:
       response.flash = 'form has errors'
   return dict(form=form)

2行目でレコードを見つけ、3行目でUPDATE/DELETEフォームを作り、4行目はすべての対応するフォームの処理を行います。

更新フォームは作成フォームにとても似ていますが、現在のレコードによって事前入力され、プレビュー画像も表示します。デフォルトでは、deletable = Trueになっていて、"delete record"オプションが更新フォームに表示されます。

編集フォームはまた、name="id"という隠れINPUTフィールドを保持しています。これによりレコードが特定できるようになります。このidはまた、追加のセキュリティのためサーバーサイドで保存され、訪問者がフィールドの値を改ざんした場合、UPDATEは実行されず、web2pyは"user is tampering with form(訳注:ユーザーはフォームを改ざんした形跡がある)"というSyntaxErrorを発生させます。

フィールドがwritable=Falseとしてマークされていると、フィールドは作成フォームに表示されず、読み込み専用の更新フォームで表示されます。フィールドがwritable=False、かつ、readable=Falseとしてマークされている場合、フィールドは、更新フォームを含むすべてのフォーム上で表示されません。

次のように作成されたフォームは、

1
form = SQLFORM(...,ignore_rw=True)

readablewritableの属性を無視して、常にすべてのフィールドを表示します。appadminのフォームはデフォルトでそれらを無視します。

次のように作成されたフォームは、

1
form = SQLFORM(table,record_id,readonly=True)

常に、読み取り専用モードですべてのフィールドを表示し、フォームが受理されることはありません。

writable=Falseでフィールドをマークすると、フォームの一部となることを防ぎ、フォームが処理されるときにrequest.vars.fieldの値を無視するようにします。でも、もし、form.vars.fieldに値を割り当てるなら、この値はフォームが処理される際に、インサートやアップデートの一部と なるでしょう

これはフォームに含むことを望まない理由がある場合に、フォールドの値を変えることを可能にします。

HTMLにおけるSQLFORM

SQLFORMのフォームをその生成と処理機能の利便を得るため使用したいが、SQLFORMオブジェクトのパラメタではできなくらいのHTMLのカスタマイズが必要で、HTMLを用いてフォームを設計しなければならないことがあります。 では、前回のコントローラを編集し新しいアクションを追加してみます:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
def display_manual_form():
   form = SQLFORM(db.person)
   if form.process(session=None, formname='test').accepted:
       response.flash = 'form accepted'
   elif form.errors:
       response.flash = 'form has errors'
   else:
       response.flash = 'please fill the form'
   # Note: no form instance is passed to the view
   return dict()

そして、関連付けられたビュー"default/display_manual_form.html"にフォームを入れます:

1
2
3
4
5
6
7
8
{{extend 'layout.html'}}
<form>
<ul>
  <li>Your name is <input name="name" /></li>
</ul>
  <input type="submit" />
  <input type="hidden" name="_formname" value="test" />
</form>

ここで、このアクションはフォームを返していないことに注意してください。なぜならビューに渡す必要がないからです。ビューはHTMLにおいて手動で作成されたフォームを含んでいます。フォームは、隠れフィールド"_formname"を含んでいて、それはアクションのacceptsの引数で指定されたformnameと必ず同じにしなければなりません。web2pyは同じページに複数のフォームがある場合に、どれからサブミットされたかを判断するのに、フォームの名前を使用します。ページが単一のフォームからなる場合、formname=Noneと設定して、ビューにおける隠れフィールドを見送ることができます。

form.acceptsはデータベーステーブルdb.personのフィールドに適合するデータをresponse.vars内から検索します。これらのフィールドは以下のようにHTMLで宣言することができます。

1
<input name="field_name_goes_here" />

上記の例では、フォーム変数がURLの引数として渡される点に注意してください。そうしたくない場合、POSTプロトコルが指定される必要があります。さらに、uploadフィールドが指定された場合は、フォーム上でそれを許可するように設定しなければなりません。両方のオプションを以下に示します。

1
<form enctype="multipart/form-data" method="post">

SQLFORMとアップロード

"upload"型のフィールドは特殊です。それらは、type="file"のINPUTフィールドでレンダリングされます。特に指定がない限り、アップロードしたファイルはバッファにストリームされ、自動的に割り当てられる新しい安全な名前を用いて、アプリケーションの"upload"フォルダに保存されます。このファイルの名前はこのとき、アップロード型のフィールドに保存されます。

例として、次のモデルを考えてください:

1
2
3
db.define_table('person',
    Field('name', requires=IS_NOT_EMPTY()),
    Field('image', 'upload'))

先ほどのコントローラアクション"display_form"と同じものを利用することができます。

新規のレコードを挿入するとき、フォームはファイルに対する閲覧を可能にします。たとえば、jpg画像を選択してください。このファイルはアップロードされ、次のように保存されます:

1
applications/test/uploads/person.image.XXXXX.jpg

"XXXXXX"は、web2pyによってこのファイルに割り当てられるランダムな識別子になります。

content-disposition
デフォルトでは、アップロードしたファイルの元のファイル名はb16encoded(訳注:PythonのBase16参照)され、そのファイルに対する新しい名前の構築に使用されることに注意してください。この名前は、デフォルトの"download"アクションによって取り出され、元のファイルへのContent-Dispositionヘッダを設定するのに使用されます。

拡張子だけがそのままになります。これはセキュリティ上の理由です。なぜなら、ファイル名は特別な文字列を含む可能性があり、訪問者にディレクトリ・トラバーサル攻撃や他の悪意のある操作を許してしまうからです。

新しいファイル名はform.vars.imageにも格納されます。

UPDATEフォームを使用してレコードを編集するとき、既存のアップロードしたファイルへのリンクを表示するのは便利で、web2pyはその方法を提供しています。

URLをupload引数を介してSQLFORMのコンストラクタに渡す場合、web2pyは、ファイルをダウンロードするために、そのURLのアクションを用います。次のアクションを考えてください:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def display_form():
   record = db.person(request.args(0)) or redirect(URL('index'))
   form = SQLFORM(db.person, record, deletable=True,
                  upload=URL('download'))
   if form.process().accepted:
       response.flash = 'form accepted'
   elif form.errors:
       response.flash = 'form has errors'
   return dict(form=form)

def download():
    return response.download(request, db)

さて、次のURLにて新規のレコードを挿入してみます:

1
http://127.0.0.1:8000/test/default/display_form

画像をアップロード、フォームをサブミットして、次のURLを訪れて新しく作られたレコードを編集します:

1
http://127.0.0.1:8000/test/default/display_form/3

(ここでは最新のレコードがid=3をもつと仮定します)。フォームは以下に示すように画像のプレビューを表示します:

image

このフォームは、シリアライズされるときに、次のようなHTMLを生成します:

1
2
3
4
5
6
7
<td><label id="person_image__label" for="person_image">Image: </label></td>
<td><div><input type="file" id="person_image" class="upload" name="image"
/>[<a href="/test/default/download/person.image.0246683463831.jpg">file</a>|
<input type="checkbox" name="image__delete" />delete]</div></td><td></td></tr>
<tr id="delete_record__row"><td><label id="delete_record__label" for="delete_record"
>Check to delete:</label></td><td><input type="checkbox" id="delete_record"
class="delete" name="delete_this_record" /></td>

これは、アップロードしたファイルをダウンロードするリンクと、データベースのレコードからこのファイルを削除するためのチェックボックスを含んでいます。したがって、"image"フィールドにはNULLが格納されています。

なぜ、このような機構が公開されているのでしょうか?なぜ、ダウンロード関数を書く必要があるのでしょうか?なぜなら、いくつかの認証メカニズムをダウンロード関数に課すことが必要になるかもしれないからです。例は、第9章を参照してください。

通常アップロードファイルは"app/uploads"の中に保存されますが、別の場所を指定することもできます。

Field('image', 'upload', uploadfolder='...')

多くのオペレーティングシステムにおいて、同一のフォルダ内に大量のファイルがある場合はファイルシステムへの接続が遅くなる場合があります。もし1000以上のファイルをアップロードする予定があるならば、web2pyにサブフォルダでアップロードファイルを整理するように指示できます。

Field('image', 'upload', uploadseparate=True)

元のファイル名の保存

web2pyは自動的に元のファイル名を新しいUUIDのファイル名の中に保存し、ファイルがダウンロードされたときにそれを取り出します。ダウンロードの際、オリジナルのファイル名は、HTTPレスポンスのContent-Dispositionヘッダに格納されます。これはすべて、プログラミングの必要なしに透過的に行われます。

時には、オリジナルのファイル名をデータベースのフィールドに保存したい場合もあります。この場合、モデルを修正し、それを保存するフィールドを加える必要があります:

1
2
3
4
db.define_table('person',
    Field('name', requires=IS_NOT_EMPTY()),
    Field('image_filename'),
    Field('image', 'upload'))

そして、それを処理するようにコントローラーを修正する必要があります:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
def display_form():
    record = db.person(request.args(0)) or redirect(URL('index'))
    url = URL('download')
    form = SQLFORM(db.person, record, deletable=True,
                   upload=url, fields=['name', 'image'])
    if request.vars.image!=None:
        form.vars.image_filename = request.vars.image.filename
    if form.process().accepted:
        response.flash = 'form accepted'
    elif form.errors:
        response.flash = 'form has errors'
    return dict(form=form)

SQLFORMは"image_filename"フィールドを表示していないことに注意してください。"display_form"アクションは、request.vars.imageのファイル名をform.vars.image_filenameに移動します。これにより、acceptsにおいてファイル名を処理し、データベースに保存することができるようになります。ダウンロード関数は、そのファイルを配信する前に、元のファイル名をデータベースにおいてチェックし、Content-Dispositionヘッダにおいて使用します。

autodelete

autodelete

レコードを削除する際に、SQLFORMはレコードによって参照された物理的なアップロード・ファイルを削除することはありません。その理由は、web2pyがそのファイルが他のテーブルによって使用/リンクされているか、また、他の目的で使用されているかどうか知ることができないからです。対応するレコードが削除されたとき、実際のファイルを削除しても安全だと判断できる場合は、次のようにして削除できます:

1
2
3
db.define_table('image',
    Field('name', requires=IS_NOT_EMPTY()),
    Field('source','upload',autodelete=True))

autodelete属性はデフォルトではFalseです。Trueに設定すると、レコードが削除されるとき、ファイルも削除されるようになります。

参照レコードへのリンク

今度は、参照フィールドによってリンクされた2つのテーブルを考えます。

1
2
3
4
5
6
db.define_table('person',
    Field('name', requires=IS_NOT_EMPTY()))
db.define_table('dog',
    Field('owner', 'reference person'),
    Field('name', requires=IS_NOT_EMPTY()))
db.dog.owner.requires = IS_IN_DB(db,db.person.id,'%(name)s')

飼い主は犬を飼い、犬は所有者(owner)、つまり、飼い主に所属しています。犬の所有者(owner)には、有効なdb.person.id'%(name)s'を用いて参照することが要求されます。

このアプリケーションのappdadminインターフェースを用いて、何人かの飼い主と彼らの犬を加えましょう。

既存の飼い主を編集するとき、appadminのUPDATEフォームは、この飼い主に属する犬の一覧を表示するページへのリンクを表示します。この挙動は、SQLFORMlinkto引数を用いて真似することができます。linktoは、SQLFORMからのクエリ文字列を受け取って対応するレコードを返す新規のアクションのURLを指す必要があります。以下がその例です。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def display_form():
   record = db.person(request.args(0)) or redirect(URL('index'))
   url = URL('download')
   link = URL('list_records', args='db')
   form = SQLFORM(db.person, record, deletable=True,
                  upload=url, linkto=link)
   if form.process().accepted:
       response.flash = 'form accepted'
   elif form.errors:
       response.flash = 'form has errors'
   return dict(form=form)

これがそのページです:

image

"dog.owner"というリンクがあります。このリンクの名前は、次のようなSQLFORMlabels引数を介して変更することができます:

1
labels = {'dog.owner':"This person's dogs"}

リンクをクリックすると、次の場所に向かいます:

1
/test/default/list_records/dog?query=db.dog.owner%3D%3D5

"list_records"は指定されたアクションで、request.args(0)に参照するテーブルの名前が設定され、request.vars.queryにSQLのクエリ文字列が設定されています。URLのクエリ文字列は適切にurlエンコードされた"dog.owner=5"の値を含んでいます(web2pyはURLを解析するときに自動的にこれをデコードします)。

とても汎用的な"list_records"アクションを次のように簡単に実装することができます:

1
2
3
4
5
6
7
8
def list_records():
    REGEX = re.compile('^(\w+).(\w+).(\w+)\=\=(\d+)$')
    match = REGEX.match(request.vars.query)
    if not match:
        redirect(URL('error'))
    table, field, id = match.group(2), match.group(3), match.group(4)
    records = db(db[table][field]==id).select()
    return dict(records=records)

関連付けられるビュー"default/list_records.html"は次のようにします:

1
2
{{extend 'layout.html'}}
{{=records}}

選択によってレコードセットが返され、ビューでシリアライズされるとき、これは最初にSQLTABLEオブジェクト(Tableと同じではありません)に変換された後、各フィールドがテーブルのカラムと対応するHTMLテーブルへとシリアライズされます。

フォームの事前入力

次の構文を用いて、フォームを事前入力することは常に可能です:

1
form.vars.name = 'fieldvalue'

上記のような文は、フィールド(この例では"name")が明示的にフォームで表示されているかにかかわらず、フォームの宣言の後、かつ、フォームの受理の前に挿入される必要があります。

SQLFORMにフォーム要素の追加

フォーム作成後に要素を追加したい場合があります。例えば、あなたのウェブサイトの会員規約に同意するかどうかのチェックボックスを追加したい場合です。

1
2
3
form = SQLFORM(db.yourtable)
my_extra_element = TR(LABEL('I agree to the terms and conditions'),                       INPUT(_name='agree',value=True,_type='checkbox'))
form[0].insert(-1,my_extra_element)

my_extra_element変数はフォームスタイルに適合している必要があります。この例では、デフォルトのformstyle='table3cols'を想定しています。

サブミット後、form.vars.agreeはチェックボックスのステータス値を持ち、onvalidation関数などで使用できます。

データベースIOなしのSQLFORM

SQLFORMを使用してデータベーステーブルからフォームを生成し、サブミットしたフォームをそのまま検証したいが、データベースにおいて自動的なINSERT/UPDATE/DELETEを行いたくない場合があります。たとえば、1つのフィールドが他の入力フィールドから計算される必要がある場合です。また、挿入されたデータに対して標準の検証では達成できない追加の検証を行いたい場合です。

これは、次のものを:

1
2
3
form = SQLFORM(db.person)
if form.process().accepted:
    response.flash = 'record inserted'

以下のように分解して簡単に行うことができます。

1
2
3
4
5
form = SQLFORM(db.person)
if form.validate():
    ### deal with uploads explicitly
    form.vars.id = db.person.insert(**dict(form.vars))
    response.flash = 'record inserted'

同じことはUPDATE/DELETEフォームでも行うことができます。次のものを:

1
2
3
form = SQLFORM(db.person,record)
if form.process().accepted:
    response.flash = 'record updated'

以下のように分解します。

1
2
3
4
5
6
7
form = SQLFORM(db.person,record)
if form.validate():
    if form.deleted:
        db(db.person.id==record.id).delete()
    else:
        record.update_record(**dict(form.vars))
    response.flash = 'record updated'

"upload"型フィールド("fieldname")を持つテーブルの場合でも、process(dbio=False)validate()の両方はアップロードファイルの保存を、dbio=Trueのように、つまり既定の振る舞いのように、処理します。

web2pyによってアップロードされたファイルに割り当てられたファイル名は以下にあります:

1
form.vars.fieldname

フォームの他の種類

SQLFORM.factory

データベース・テーブルをあたかも持っているかのようにフォームを生成したいが、データベース・テーブルはいらないような場面があります。見栄えのよいCSSフレンドリなフォームの生成や、まれにファイルのアップロードとリネームの実行のために、SQLFORMの能力をシンプルに活用したい場面です。

これはform_factoryによって行うことができます。ここに、フォームを生成し、検証を行い、ファイルをアップロードし、すべてをsessionに保存するような例を示します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
def form_from_factory():
    form = SQLFORM.factory(
        Field('your_name', requires=IS_NOT_EMPTY()),
        Field('your_image', 'upload'))
    if form.process().accepted:
        response.flash = 'form accepted'
        session.your_name = form.vars.your_name
        session.your_image = form.vars.your_image
    elif form.errors:
        response.flash = 'form has errors'
    return dict(form=form)

SQLFORM.factory()のフィールドオブジェクトはDALの章で説明しました。 SQLFORM.factory()の実行時の作成手順は次のようです。

1
2
3
fields = []
fields.append(Field(...))
form=SQLFORM.factory(*fields)

次にビュー"default/form_from_factory.html"を示します:

1
2
{{extend 'layout.html'}}
{{=form}}

フィールドのラベルにおいてスペースの代わりにアンダースコアを使用するか、SQLFORMで行ったのと同様に、labelsの辞書をform_factoryに明示的に渡す必要があります。デフォルトでは、SQLFORM.factoryは、あたかも"no_table"というテーブルから生成されたフォームのように生成されたhtmlの"id"属性を用いてフォームを生成します。このダミーテーブルの名前を変更したいときは、factoryのtable_name属性を用いてください:

1
form = SQLFORM.factory(...,table_name='other_dummy_name')

factoryから生成された2つのフォームを同じテーブルに配置する必要があり、かつ、CSSの競合を避けたい場合、table_nameの変更が必要になります。

SQLFORM.factoryでのファイルアップロード

複数テーブルでひとつのフォーム

参照によってリンクされた(例えば、'client'と'address'という)ふたつのテーブルが存在し、ある顧客とその住所レコードをひとつのフォームで挿入したいとします。これは以下のようにできます:

model:

1
2
3
4
5
6
db.define_table('client',
     Field('name'))
db.define_table('address',
    Field('client','reference client',
          writable=False,readable=False),
    Field('street'),Field('city'))

controller:

1
2
3
4
5
6
7
8
def register():
    form=SQLFORM.factory(db.client,db.address)
    if form.process().accepted:
        id = db.client.insert(**db.client._filter_fields(form.vars))
        form.vars.client=id
        id = db.address.insert(**db.address._filter_fields(form.vars))
        response.flash='Thanks for filling the form'
    return dict(form=form)

SQLFORM.factory(両方のテーブルで公開されたフィールドからひとつのフォームを作成しバリデータも継承している)に注意してください。ひとつのフォームの受理で、あるデータはひとつ目のテーブル、残りは別のテーブルからと、ふたつの挿入を実施しています。

これは複数テーブル間で共通のフィールド名が存在しない場合のみ動作します。

確認フォーム

confirm

しばしば確認選択のあるフォームが必要なことがあります。そのフォームは選択がされているときに受理され、それ以外は受理しないべきです。このフォームは他のページへのリンクする追加のオプションを持つでしょう。web2pyはこれを行う簡単な方法を提供します:

1
2
form = FORM.confirm('Are you sure?')
if form.accepted: do_what_needs_to_be_done()

確認フォームが.accepts.processを必要とせず、呼ばないことに注意してください。これは内部で行われます。確認フォームに辞書{'value':'link'}の形式でリンクボタンを追加できます。

1
2
form = FORM.confirm('Are you sure?',{'Back':URL('other_page')})
if form.accepted: do_what_needs_to_be_done()

辞書を編集するフォーム

辞書に設定オプションを保存するシステムをイメージしましょう。

1
config = dict(color='black', language='English')

そして、訪問者にこの辞書の変更を許すフォームを必要としているとします。 これは次のようにします:

1
2
form = SQLFORM.dictform(config)
if form.process().accepted: config.update(form.vars)

フォームは辞書のそれぞれの項目でINPUTフィールドに表示されます。辞書のキーはINPUTのnameとラベルに、現在の値は推測された型(string, int, double, date, datetime, boolean)に用いられます。

この働きは絶大ですが、設定辞書を維持するロジックを残しましょう。例えば、セッションに設定を保存しても良いでしょう。

1
2
3
4
session.config or dict(color='black', language='English')
form = SQLFORM.dictform(session.config)
if form.process().accepted:
    session.config.update(form.vars)

CRUD

CRUD
crud.create
crud.update
crud.select
crud.search
crud.tables
crud.delete

web2pyに最近追加されたものの1つは、SQLFORMの上にあるCreate/Read/Update/Delete (CRUD) APIです。CRUDはSQLFORMを作成しますが、フォームの作成、フォームの処理、通知、リダイレクトを、すべて1つの関数に合体させることで、コーディングを単純化します。

初めに注意する点は、CRUDが他のこれまで使用してきたweb2pyのAPIと異なり、APIがまだ公開されていないことです。これはインポートしなければなりません。また、特定のデータベースにリンクする必要があります。例:

1
2
from gluon.tools import Crud
crud = Crud(db)

上で定義したcrudオブジェクトは次のようなAPIを提供します:

crud.tables
crud.create
crud.read
crud.update
crud.delete
crud.select
.

  • crud.tables()は、データベースに定義されているテーブルのリストを返します。
  • crud.create(db.tablename)は、テーブルのtablenameに対する作成フォームを返します。
  • crud.read(db.tablename, id)は、tablenameとレコードidに対する読み取り専用のフォームを返します。
  • crud.update(db.tablename, id)は、tablenameとレコードidに対する更新フォームを返します。
  • crud.delete(db.tablename, id)は、レコードを削除します。
  • crud.select(db.tablename, query)は、テーブルから選択されたレコードのリストを返します。
  • crud.search(db.tablename)は、(form, records)のタプルを返します。formは検索フォームで、recordsはサブミットされた検索フォームに基づくレコードのリストです。
  • crud()は、request.args()に基づいて、上記のうちの1つを返します。

たとえば、次のアクションは:

1
def data(): return dict(form=crud())

次のようなURLを公開します:

1
2
3
4
5
6
7
http://.../[app]/[controller]/data/tables
http://.../[app]/[controller]/data/create/[tablename]
http://.../[app]/[controller]/data/read/[tablename]/[id]
http://.../[app]/[controller]/data/update/[tablename]/[id]
http://.../[app]/[controller]/data/delete/[tablename]/[id]
http://.../[app]/[controller]/data/select/[tablename]
http://.../[app]/[controller]/data/search/[tablename]

しかし、次のアクションは:

1
2
def create_tablename():
    return dict(form=crud.create(db.tablename))

以下の作成メソッドしか公開しません

1
http://.../[app]/[controller]/create_tablename

また、次のアクションは:

1
2
def update_tablename():
    return dict(form=crud.update(db.tablename, request.args(0)))

以下の更新メソッドしか公開しません

1
http://.../[app]/[controller]/update_tablename/[id]

他も同様です。

CRUDの挙動は二通りの方法でカスタマイズできます。1つは、crudオブジェクトにいくつかの属性を設定することです。もう1つは、各メソッドに追加のパラメータを渡すことです。

設定

ここに、現在のCRUD属性と、そのデフォルトの値と、意味のリスト一式を示します:

すべてのcrudのフォームに認証をかけます:

1
crud.settings.auth = auth

利用方法は第9章で説明します。

crudオブジェクトを返すdata関数を定義しているコントローラを指定します

1
crud.settings.controller = 'default'

レコードの"create"が成功した後のリダイレクト先のURLを指定します:

1
crud.settings.create_next = URL('index')

レコードの"update"が成功した後のリダイレクト先のURLを指定します:

1
crud.settings.update_next = URL('index')

レコードの"delete"が成功した後のリダイレクト先のURLを指定します:

1
crud.settings.delete_next = URL('index')

アップロードされたファイルへリンクするために使用するURLを指定します:

1
crud.settings.download_url = URL('download')

crud.createフォームに対する標準の検証処理の後に実行される追加の関数を指定します:

1
crud.settings.create_onvalidation = StorageList()

StorageListStorageオブジェクトと同様で、両者"gluon/storage.py"ファイルに定義されていますが、そのデフォルトはNoneではなく[]になります。これにより、次の構文が使用できます:

1
crud.settings.create_onvalidation.mytablename.append(lambda form:....)

crud.updateフォームに対する標準の検証処理の後に実行される追加の関数を指定します:

1
crud.settings.update_onvalidation = StorageList()

crud.createフォームの完了後に実行される追加の関数を指定します:

1
crud.settings.create_onaccept = StorageList()

crud.updateフォームの完了後に実行される追加の関数を指定します:

1
crud.settings.update_onaccept = StorageList()

レコードが削除される場合において、crud.updateの完了後に実行される追加の関数を指定します:

1
crud.settings.update_ondelete = StorageList()

crud.deleteの完了後に実行される追加の関数を指定します:

1
crud.settings.delete_onaccept = StorageList()

"update"フォームが"delete"ボタンを持つかどうかを決めます:

1
crud.settings.update_deletable = True

"update"フォームが編集レコードの"id"を表示するかどうかを決めます:

1
crud.settings.showid = False

フォームが前回挿入された値を維持するか、サブミット成功後デフォルトにリセットするかどうかを決めます:

1
crud.settings.keepvalues = False

crudは編集されているレコードがフォーム表示時からサブミットの間に第3者によって修正されていないかを検知します。これは以下と等しいです。

form.process(detect_record_change=True)

そしてこのように設定します:

1
crud.settings.detect_record_change = True

変数の値をFalseにすることで無効にすることができます。

フォームのスタイルは次のようにして変更することができます

1
crud.settings.formstyle = 'table3cols' or 'table2cols' or 'divs' or 'ul'

全てのcrudフォームに区切り文字を設定できます。

1
crud.settings.label_separator = ':'

authで説明されるのと同じやり方で、フォームにキャプチャを加えることができます:

1
2
3
crud.settings.create_captcha = None
crud.settings.update_captcha = None
crud.settings.captcha = None

メッセージ

カスタマイズ可能なメッセージのリストを以下に示します:

1
crud.messages.submit_button = 'Submit'

これは、create、updateフォーム両方に対して"submit"ボタンのテキストを設定します。

1
crud.messages.delete_label = 'Check to delete:'

これは、"update"フォームにおいて"delete"ボタンのラベルを設定します。

1
crud.messages.record_created = 'Record Created'

これは、レコードの作成が成功した際のflashメッセージを設定します。

1
crud.messages.record_updated = 'Record Updated'

これは、レコードの更新が成功した際のflashメッセージを設定します。

1
crud.messages.record_deleted = 'Record Deleted'

これは、レコードの削除が成功した際のflashメッセージを設定します。

1
crud.messages.update_log = 'Record %(id)s updated'

これは、レコードの更新が成功したときのログメッセージを設定します。

1
crud.messages.create_log = 'Record %(id)s created'

これは、レコードの作成が成功したときのログメッセージを設定します。

1
crud.messages.read_log = 'Record %(id)s read'

これは、レコードの読み取りアクセスが成功したときのログメッセージを設定します。

1
crud.messages.delete_log = 'Record %(id)s deleted'

これは、レコードの削除が成功したときのログメッセージを設定します。

なお、crud.messagesgluon.storage.Messageクラスに所属しています。これは、gluon.storage.Storageに似ていますが、T演算子の必要なしに、自動的にその値を翻訳します。

ログメッセージは、CRUDが第9章で説明するAuthに接続された場合のみ使用されます。イベントは、Authテーブルの"auth_events"にログとして記録されます。

メソッド

CRUDのメソッドの挙動は、原則呼び出し毎にカスタマイズできます。ここにその用法を示します:

1
2
3
4
5
6
7
crud.tables()
crud.create(table, next, onvalidation, onaccept, log, message)
crud.read(table, record)
crud.update(table, record, next, onvalidation, onaccept, ondelete, log, message, deletable)
crud.delete(table, record_id, next, message)
crud.select(table, query, fields, orderby, limitby, headers, **attr)
crud.search(table, query, queries, query_labels, fields, field_labels, zero, showall, chkall)
  • tableは、DALのテーブル、または、テーブル名です。メソッドはその上で動作します。
  • recordrecord_idは、レコードのidです。メソッドはその上で動作します。
  • nextは、成功後にリダイレクトする先のURLです。URLが部分文字列"[id]"を含む場合、これは、現在作成/更新されたレコードのidによって置換されます。
  • onvalidationは、SQLFORM(..., onvalidation)と同じ機能を持ちます。
  • onacceptは、フォームのサブミットが受理された後に呼ばれ、そこで、リダイレクトする前に、動作する関数です。
  • logはログのメッセージです。CRUDにおけるログのメッセージは、form.varsの辞書変数を"%(id)s"のように参照します。
  • messageはフォームが受理されたときのflashメッセージです。
  • ondeleteは、"update"フォームを介してレコードが削除されるときに、onacceptの場所で呼ばれます。
  • deletableは、"update"フォームがdeleteオプションを持つかどうかを決めます。
  • queryは、レコードを選択するために使用するクエリです。
  • fieldsは、レコードを選択するために使用するクエリです。
  • orderbyは、選択したレコードの順序を決めます(第6章を参照してください)。
  • limitbyは、表示される選択レコードの範囲を決めます(第6章を参照してください)。
  • headersは、テーブルのヘッダの名前からなる辞書です。
  • queriesは、['equals', 'not equal', 'contains']のようなリストです。検索フォームにおける使用可能なメソッドを保持します。
  • query_labelsは、query_labels=dict(equals='Equals')のような辞書です。検索メソッドに対する名前を与えます。
  • fieldsは、検索ウィジェットにおいて列挙されるフィールドのリストです。
  • field_labelsは、フィールド名をラベルにマッピングする辞書です。
  • zeroは、デフォルトでは"choose one"で、検索ウィジェットのドロップダウンのためのデフォルトのオプションとして使用されます。
  • showallは最初の呼び出し時にqueryで選択されたrowsを返したい場合はTrueを設定しておきます。(1.98.2以降に追加)
  • chkallは検索フォームのチェックボックスを全てチェックしたい時にTrueを設定しておきます。(1.98.2以降に追加)

ここでは、単一のコントローラ関数における使用例を示します:

1
2
3
4
5
6
7
## assuming db.define_table('person', Field('name'))
def people():
    form = crud.create(db.person, next=URL('index'),
           message=T("record created"))
    persons = crud.select(db.person, fields=['name'],
           headers={'person.name': 'Name'})
    return dict(form=form, persons=persons)

もう1つのとても汎用的なコントローラ関数を示します。これにより、任意のテーブルから任意のレコードを検索、作成、編集することができます。このとき、テーブル名はrequest.args(0)によって渡されます:

1
2
3
4
5
6
def manage():
    table=db[request.args(0)]
    form = crud.update(table,request.args(1))
    table.id.represent = lambda id, row:        A('edit:',id,_href=URL(args=(request.args(0),id)))
    search, rows = crud.search(table)
    return dict(form=form,search=search,rows=rows)

なお、table.id.represent=...の行は、web2pyに対して、idフィールドの表現を変更し、代わりに、自分自身のページへのリンクを表示し、作成ページを更新ページに切り替えるためにidをrequest.args(1)として渡すように指示します。

レコードのバージョニング

SQLFORMとCRUDは共にデータベースレコードのバージョニングを行うユーティリティを提供しています:

すべての改訂履歴を必要とするテーブル(db.mytable)を持ちたいなら、次のようにするだけです:

1
form = SQLFORM(db.mytable, myrecord).process(onsuccess=auth.archive)
1
form = crud.update(db.mytable, myrecord, onaccept=auth.archive)

auth.archivedb.mytable_archiveという新規のテーブルを定義します(この名前は参照するテーブルの名前から派生します)。そして、更新時に、(更新前の)レコードのコピーを、作成した記録用のテーブルに保存します。現在のレコードへの参照も含んでいます。

レコードは実際に更新されるので(その前回の状態のみが記録されます)、参照が壊れることはありません。

これはすべて内部で行われます。記録テーブルにアクセスしたいならば、モデルにおいてそれを定義しておく必要があります:

1
2
3
db.define_table('mytable_archive',
   Field('current_record', 'reference mytable'),
   db.mytable)

なお、テーブルはdb.mytableを拡張し(そのすべてのフィールドを含み)、current_recordへ参照を追加しています。

auth.archiveは、次のように元のテーブルがtimestampフィールドを持たない限り、保存したレコードのタイムスタンプを取りません。

1
2
3
4
5
db.define_table('mytable',
    Field('created_on', 'datetime',
          default=request.now, update=request.now, writable=False),
    Field('created_by', 'reference auth_user',
          default=auth.user_id, update=auth.user_id, writable=False),

これらのフィールドに関して何ら特別なことはなく、好きな名前を付けることが可能です。これらはレコードが記録される前に入力され、各レコードのコピーと共に記録されます。記録テーブルの名前、または/かつ、フィールドの名前は次のように変更することができます:

1
2
3
4
5
6
7
8
db.define_table('myhistory',
    Field('parent_record', 'reference mytable'),
    db.mytable)
## ...
form = SQLFORM(db.mytable,myrecord)
form.process(onsuccess = lambda form:auth.archive(form,
             archive_table=db.myhistory,
             current_record='parent_record'))

カスタムフォーム

フォームがSQLFORMやSQLFORM.factory、CRUDを利用して作られている場合、それをビューに埋め込む方法は複数あり、複数の度合いのカスタマイズができるようになります。たとえば次のモデルを考えてみます:

1
2
3
db.define_table('image',
    Field('name', requires=IS_NOT_EMPTY()),
    Field('source', 'upload'))

また、次のアップロード・アクションも考えます:

1
2
def upload_image():
    return dict(form=SQLFORM(db.image).process())

最も簡単に、upload_imageに対するビューにおいてフォームを埋め込む方法は次の通りです:

1
{{=form}}

これは標準のテーブル・レイアウトになります。別のレイアウトを使用したい場合、フォームを要素に分解することができます

1
2
3
4
5
{{=form.custom.begin}}
Image name: <div>{{=form.custom.widget.name}}</div>
Image file: <div>{{=form.custom.widget.source}}</div>
Click here to upload: {{=form.custom.submit}}
{{=form.custom.end}}

ここで、form.custom.widget[fieldname]は、そのフィールドに対して適切なウィジェットにシリアライズされます。フォームがサブミットされてエラーを含む場合、そのエラーは従来通りウィジェットの下に追加されます。

上記のサンプルフォームは下図のように表示されます。

image

同様の結果はカスタムフォームを使わずに得ることができました:

1
SQLFORM(...,formstyle='table2cols')

あるいは、CRUDフォームの場合には次のパラメータを伴います:

1
crud.settings.formstyle='table2cols'

他の可能なformstyleは、"table3cols" (デフォルト)、"divs"、"ul"です。

web2pyによってシリアライズされたウィジェットを使用したくない場合は、それをHTMLで置き換えることができます。このために有用ないくつかの変数があります:

  • form.custom.label[fieldname]はフィールドのラベルを含みます。
  • form.custom.comment[fieldname]はフィールドのコメントを含みます。
  • form.custom.dspval[fieldname]はフィールドの表示方法に関わるform-typeとfield-typeを含みます。
  • form.custom.inpval[fieldname]はフィールドの値に関するform-typeとfield-typeを含みます。

もし、フォームがdeletable=Trueを持つなら、次も挿入するべきです。

1
{{=form.custom.delete}}

これは削除チェックボックスを表示します。

以下に説明する慣例に従うことは重要です。

CSSの慣例

SQLFORM、SQLFORM.factory、CRUDによって生成されたフォーム内のタグは厳密なCSSの命名規則に従っており、そのCSSはフォームのさらなるカスタマイズに使用できます。

"mytable"テーブルと"string"型の"myfield"フィールドが与えられたとき、次によって既定でレンダリングされます。

1
SQLFORM.widgets.string.widget

このレンダリングは次のようになります:

1
2
<input type="text" name="myfield" id="mytable_myfield"
       class="string" />

以下のことに注意してください:

  • INPUTタグのクラスはフィールドの型と同じです。これは"web2py_ajax.html"におけるjQueryのコードが機能するのに非常に重要です。これは、"integer"か"double"のフィールドにおいて数値しか持たないようにし、"time"、"date"、"datetime"のフィールドではポップアップのカレンダーが表示されるようにします。
  • idは、クラスの名前とフィールドの名前をアンダースコアで結合したものです。これにより、たとえばjQuery('#mytable_myfield')のようにして一意に参照することができ、フィールドのスタイルシートを操作したり、フィールドのイベントに関連付けられたアクション(focus、blur、keyupなど)をバインドすることができるようになります。
  • nameは、想像のとおり、フィールド名になります。

エラーの非表示

hideerror

場合によっては、自動的なエラー配置を無効にして、フォームのエラーメッセージをデフォルトではないどこか別の場所に表示したいことがあります。これを行うのは簡単です。

  • FORMまたはSQLFORMの場合は、hideerror=Trueacceptsメソッドに渡してください。
  • CRUDの場合は、crud.settings.hideerror=Trueを設定してください。

エラーを表示するビューを変更したくなることもあります(もはや自動的に表示されないので)。

次の例では、エラーをフォームの中ではなく、フォームの上に表示されるようにしています。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{{if form.errors:}}
  Your submitted form contains the following errors:
  <ul>
  {{for fieldname in form.errors:}}
    <li>{{=fieldname}} error: {{=form.errors[fieldname]}}</li>
  {{pass}}
  </ul>
  {{form.errors.clear()}}
{{pass}}
{{=form}}

エラーは下図のように表示されます:

image

このメカニズムはカスタムフォームでも動作します。

バリデータ

validators

バリデータは入力フィールド(データベース・テーブルから生成されたフォームを含む)を検証するために使用されるクラスです。

FORMとともにバリデータを使用する例です:

1
INPUT(_name='a', requires=IS_INT_IN_RANGE(0, 10))

どのようにテーブルのフィールドに対するバリデータを要求するかの例です:

1
2
db.define_table('person', Field('name'))
db.person.name.requires = IS_NOT_EMPTY()

バリデータは常にフィールドのrequires属性を用いて割り当てられます。フィールドは、単一もしくは複数のバリデータを持つことができます。複数のバリデータはリストの要素とします:

1
2
db.person.name.requires = [IS_NOT_EMPTY(),
                           IS_NOT_IN_DB(db, 'person.name')]

バリデータはFORM上のacceptsprocess関数や、フォームを含む他のHTMLヘルパーオブジェクトによって呼ばれます。それらは、列挙されている順序で呼ばれます。

あるフィールドに対して明示的にバリデータを呼ぶこともできます。

db.person.name.validate(value)

これは(value,error)のタプルを返し、検証する値が無い場合はerrorNoneを返します。

組み込みのバリデータはオプション引数を取るコンストラクタを持っています:

1
IS_NOT_EMPTY(error_message='cannot be empty')

error_messageは、任意のバリデータに対してデフォルトのエラーメッセージをオーバーライドするようにします。

データベース・テーブルに対するバリデータの例です:

1
db.person.name.requires = IS_NOT_EMPTY(error_message='fill this!')

ここで、国際化対応のためTという翻訳演算子を使用しています。なお、デフォルトのエラーメッセージは翻訳されません。(訳注:上記のコードには翻訳演算子はありません。)

list:型のフィールドに対して使用されるバリデータは以下のみとなります。

  • IS_IN_DB(...,multiple=True)
  • IS_IN_SET(...,multiple=True)
  • IS_NOT_EMPTY()
  • IS_LIST_OF(...)

一番最後のバリデータはリスト中の個別要素に対してバリデータを適用します。

バリデータ

IS_ALPHANUMERIC
IS_ALPHANUMERIC

このバリデータは、フィールドの値がa~z、A~Z、0~9の範囲にある文字しか含まれていないことをチェックします。

1
requires = IS_ALPHANUMERIC(error_message='must be alphanumeric!')
IS_DATE
IS_DATE

このバリデータは、指定したフォーマットで有効な日付がフィールドの値に入っていることをチェックします。異なるロケールの異なるフォーマットをサポートするために、翻訳演算子を用いてフォーマットを指定するのは良いプラクティスです。

1
2
requires = IS_DATE(format=T('%Y-%m-%d'),
                   error_message='must be YYYY-MM-DD!')

%ディレクティブの詳細な説明はIS_DATETIMEバリデータの項目を参照してください。

IS_DATE_IN_RANGE
IS_DATE_IN_RANGE

前のバリデータと非常に似ていますが、範囲を指定することができます:

1
2
3
4
requires = IS_DATE_IN_RANGE(format=T('%Y-%m-%d'),
                   minimum=datetime.date(2008,1,1),
                   maximum=datetime.date(2009,12,31),
                   error_message='must be YYYY-MM-DD!')

%ディレクティブの詳細な説明はIS_DATETIMEバリデータの項目を参照してください。

IS_DATETIME
IS_DATETIME

このバリデータは、指定したフォーマットで有効な日時がフィールドの値に入っていることをチェックします。異なるロケールの異なるフォーマットをサポートするために、翻訳演算子を用いてフォーマットを指定するのは良いプラクティスです。

1
2
requires = IS_DATETIME(format=T('%Y-%m-%d %H:%M:%S'),
                       error_message='must be YYYY-MM-DD HH:MM:SS!')

以下のシンボルをフォーマット文字列に対して使用することができます(シンボルと例となる文字列を示します):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
%Y  '1963'
%y  '63'
%d  '28'
%m  '08'
%b  'Aug'
%b  'August'
%H  '14'
%I  '02'
%p  'PM'
%M  '30'
%S  '59'
IS_DATETIME_IN_RANGE
IS_DATETIME_IN_RANGE

前のバリデータと非常に似ていますが、範囲を指定することができます:

1
2
3
4
requires = IS_DATETIME_IN_RANGE(format=T('%Y-%m-%d %H:%M:%S'),
                       minimum=datetime.datetime(2008,1,1,10,30),
                       maximum=datetime.datetime(2009,12,31,11,45),
                       error_message='must be YYYY-MM-DD HH:MM::SS!')

%ディレクティブの詳細な説明はIS_DATETIMEバリデータの項目を参照してください。

IS_DECIMAL_IN_RANGE
IS_DECIMAL_IN_RANGE
1
INPUT(_type='text', _name='name', requires=IS_DECIMAL_IN_RANGE(0, 10, dot="."))

入力をPythonのDecimalへと変換します。もしくは、数値が指定した範囲に収まっていない場合はエラーを生成します。比較はPythonのDecimalの算術で行われます。

最小値と最大値にはNoneを設定することができ、それぞれ。下限なし、上限なしを意味します。

dot引数はオプションで小数を区切る記号を国際化できます。

IS_EMAIL
IS_EMAIL

フィールドの値がemailのアドレスのようになっているかをチェックします。確認なのでemailを送信することは試みません。

1
requires = IS_EMAIL(error_message='invalid email!')
IS_EQUAL_TO
IS_EQUEL_TO

検証された値が与えられた値(変数にすることもできます)と等しいかチェックします:

1
2
requires = IS_EQUAL_TO(request.vars.password,
                       error_message='passwords do not match')
IS_EXPR
IS_EXPR

最初の引数は、変数に値に関する論理的な表現を含む文字列です。フィールドの値を、その式がTrueに評価されるかどうかで検証します。例:

1
2
requires = IS_EXPR('int(value)%3==0',
                   error_message='not divisible by 3')

例外が発生しないように、初めに整数であることをチェックしたほうがよいでしょう。

1
requires = [IS_INT_IN_RANGE(0, 100), IS_EXPR('value%3==0')]
IS_FLOAT_IN_RANGE
IS_FLOAT_IN_RANGE

フィールドの値が範囲内の浮動小数点になっていることをチェックします。次の例では、0 <= value <= 100の範囲をチェックしています:

1
2
requires = IS_FLOAT_IN_RANGE(0, 100, dot=".",
         error_message='too small or too large!')

dot引数はオプションで小数を区切る記号を国際化できます。

IS_INT_IN_RANGE
IS_INT_IN_RANGE

フィールドの値が定義した範囲内の整数になっていることをチェックします。次の例では、0 <= value <= 100の範囲をチェックしています:

1
2
requires = IS_INT_IN_RANGE(0, 100,
         error_message='too small or too large!')
IS_IN_SET
IS_IN_SET
multiple

フィールドの値がセットに含まれていることをチェックします:

1
2
requires = IS_IN_SET(['a', 'b', 'c'],zero=T('choose one'),
         error_message='must be a or b or c')

zero引数は省略可能で、デフォルトで選択されたオプション、つまり、IS_IN_SETバリデータ自身によって受理されないオプション、のテキストを決めます。"choose one"オプションを望まない場合は、zero=Noneとしてください。

zeroオプションはリビジョン(1.67.1)において導入されました。これは、アプリケーションを壊さないという意味で後方互換性を破りませんでした。しかし、以前はzeroオプションがなかったので、その挙動は変化しました。

セットの要素は常に文字列でなければなりません。ただし、このバリデータがIS_INT_IN_RANGE(値をintに変換)かIS_FLOAT_RANGE(値をfloatに変換)の後に続く場合はその限りではありません。例:

1
2
requires = [IS_INT_IN_RANGE(0, 8), IS_IN_SET([2, 3, 5, 7],
          error_message='must be prime and less than 10')]

チェックボックスには次を使います:

1
requires=IS_IN_SET(['on'])

辞書型やタプルのリストを使ってより記述的なドロップダウンリストを作成することもできます。

1
2
3
4
#### Dictionary example:
requires = IS_IN_SET({'A':'Apple','B':'Banana','C':'Cherry'},zero=None)
#### List of tuples example:
requires = IS_IN_SET([('A','Apple'),('B','Banana'),('C','Cherry')])
IS_IN_SETとタグ付け

IS_IN_SETバリデータはmultiple=Falseというオプション属性を持ちます。これがTrueに設定されている場合、複数の値を1つのフィールドに格納することができます。フィールドの型は、list:integerlist:stringにしてください。multiple参照は、作成と更新フォームにおいて自動的に処理されます。しかし、DALに対して透過的ではありません。multipleフィールドをレンダリングするためには、jQueryのmultiselectプラグインを使用することを強く勧めます。

multiple=Trueの場合、IS_IN_SETzeroや他の値を許可します。つまり、何も選択していないフィールドを許可するということになります。multipleはそれぞれ選択できる最小と最大(排他的)であるabを持つ、フォーム(a,b)のタプルを使用することもできます。
IS_LENGTH
IS_LENGTH

フィールドの値の長さが与えられた境界の間に収まることをチェックします。テキストとファイルの入力の両方で機能します。

引数は次の通りです:

  • maxsize: 最大許容の長さ/サイズ(デフォルトは255)
  • minsize: 最小許容の長さ/サイズ

例: テキストの文字列が33文字よりも短いかをチェックします:

1
INPUT(_type='text', _name='name', requires=IS_LENGTH(32))

テキストの文字列が5文字よりも長いかをチェックします:

1
INPUT(_type='password', _name='name', requires=IS_LENGTH(minsize=6))

アップロードされたファイルのサイズが1KBと1MBの間にあるかをチェックします:

1
INPUT(_type='file', _name='name', requires=IS_LENGTH(1048576, 1024))

ファイルを除くすべてのフィールドの型に対して、値の長さをチェックします。ファイルの場合は、値はcookie.FieldStorageになります。したがって、直感的に予想できる挙動であるファイルのデータ長をチェックすることになります。

IS_LIST_OF
IS_LIST_OF

これは正確にはバリデータではありません。その使用目的は、複数の値を返すフィールドの検証を可能にすることです。フォームが同じ名前の複数のフィールドや複数選択ボックスを含む場合といった稀なケースにおいて使用されます。唯一の引数は、別のバリデータです。別のバリデータをリストの各要素に適用することしかしません。たとえば、次の式はリストの各項目が0~10の範囲にある整数であることをチェックします:

1
requires = IS_LIST_OF(IS_INT_IN_RANGE(0, 10))

これは、エラーを返すことはなく、エラーメッセージも含まれません。内部のバリデータがエラーの発生を制御します。

IS_LOWER
IS_LOWER

このバリデータは決してエラーを返しません。単に、値を小文字に変換します。

1
requires = IS_LOWER()
IS_MATCH
IS_MATCH

このバリデータは、値を正規表現と照合し、一致してない場合はエラーを返します。米国の郵便番号を検証する使用例を示します:

1
2
requires = IS_MATCH('^\d{5}(-\d{4})?$',
         error_message='not a zip code')

IPv4アドレスを検証する使用例です(ただし、IS_IPV4バリデータのほうがこの目的のためにはより妥当です):

1
2
requires = IS_MATCH('^\d{1,3}(.\d{1,3}){3}$',
         error_message='not an IP address')

米国の電話番号を検証するための使用例です:

1
2
requires = IS_MATCH('^1?((-)\d{3}-?|\(\d{3}\))\d{3}-?\d{4}$',
         error_message='not a phone number')

Pythonの正規表現の詳細については、公式のPythonのマニュアルを参照してください。

IS_MATCHはデフォルトでFalseに設定されているstrictというオプションの引数を受け取ります。Trueが設定されている場合は、最初の文字だけを照合します。

>>> IS_MATCH('a')('ba')
('ba', <lazyT 'invalid expression'>) # no pass
>>> IS_MATCH('a',strict=False)('ab')
('a', None)                          # pass!

IS_MATCHはデフォルトでFalseに設定されているsearchというオプションの引数を受け取ります。Trueが設定されている場合は、matchの代わりに正規表現のsearchを利用して文字を検証します。

IS_MATCH('...', extract=True) は元の値ではなく最初に一致した部分をフィルタして抜き出します。

IS_NOT_EMPTY
IS_NOT_EMPTY

このバリデータは、フィールドの値が空の文字列ではないことをチェックします。

1
requires = IS_NOT_EMPTY(error_message='cannot be empty!')
IS_TIME
IS_TIME

このバリデータは、指定したフォーマットでの有効な時間がフィールドの値に入力されていることをチェックします。

1
requires = IS_TIME(error_message='must be HH:MM:SS!')
IS_URL
IS_URL

次のいずれかに該当するURL文字列を拒否します:

  • 文字列が空またはNone
  • 文字列がURLで許可されていない文字を使用する
  • 文字列がHTTP構文規則のいずれかを破る
  • (指定した場合)URLのスキームが'http'か'https'でない
  • (ホスト名を指定した場合)トップレベルのドメインが存在しない

(これらの規則は、RFC 2616[RFC2616]に基づいています)

この関数はURLの構文をチェックすることしかしません。たとえば、URLが実際の文章を指しているか、語義的に理にかなっているかはチェックしません。この関数は、省略URL('google.ca'など)の場合、自動的にURLの前に'http://'を追加します。

mode='generic'というパラメータが使用されている場合、この関数の挙動は変化します。このときは、次のいずれかに該当するURL文字列を拒否します:

  • 文字列が空またはNone
  • 文字列がURLで許可されていない文字を使用する
  • (指定した場合)URLのスキームが有効でない

(これらの規則は、RFC 2396[RFC2396]に基づいています)

許可されたスキーマのリストはallowed_schemesパラメータを使用してカスタマイズすることができます。リストからNoneを取り除いた場合、('http'のようなスキームを欠く)省略URLは拒否されます。

デフォルトで先頭に追加されるスキーマはprepend_schemeパラメータでカスタマイズすることができます。prepend_schemeをNoneに設定した場合、先頭への追加は無効になります。それでも、解析のために先頭への追加が必要なURLは受け入れられますが、戻り値は変更されません。

IS_URLは、RFC3490[RFC3490]で指定されている国際化ドメイン名(IDN)の標準と互換性があります。その結果、URLには、通常の文字列またはUnicode文字列を指定できます。URLのドメイン・コンポーネント(たとえば、google.ca)が非US-ASCII文字を含んでいる場合、ドメインは(RFC3492[RFC3492]で定義された)Punycodeに変換されます。IS_URLは標準を少しだけ越えて、非US-ASCII文字がURLのパスとクエリのコンポーネントにも提示されることを許可しています。これらの非US-ASCII文字はエンコードされます。たとえば、スペースは、'%20'にエンコードされます。16進数のUnicode文字0x4e86は'%4e%86'になります。

例: Examples:

1
2
3
4
5
6
7
requires = IS_URL())
requires = IS_URL(mode='generic')
requires = IS_URL(allowed_schemes=['https'])
requires = IS_URL(prepend_scheme='https')
requires = IS_URL(mode='generic',
                  allowed_schemes=['ftps', 'https'],
                  prepend_scheme='https')
IS_SLUG
IS_SLUG
1
requires = IS_SLUG(maxlen=80, check=False, error_message='must be slug')

checkがTrueに設定されている場合、検証される値が(英数字と繰り返しなしのダッシュのみ許可する)スラグかどうかをチェックします。

checkがFalseの場合(デフォルト)、入力値をスラグに変換します。

IS_STRONG
IS_STRONG

フィールド(通常はパスワードフィールド)の複雑さの要求を強制します。

例:

1
requires = IS_STRONG(min=10, special=2, upper=2)

ここで、

  • minは値の最小の長さです
  • specialは要求される特殊文字の最小数です。特殊文字は、次のいずれかなります@#$%^&*(){}[]-+
  • upperは大文字の最小数です
IS_IMAGE
IS_IMAGE

このバリデータは、ファイル入力からアップロードされたファイルが選択した画像のフォーマットの1つで保存されているか、また、与えられた制約内の寸法(幅と高さ)を持っているかどうかをチェックします。

これは、最大ファイルサイズはチェックしていません(そのためにはIS_LENGTHを使用してください)。何もデータがアップロードされていない場合は、検証エラーを返します。BMP、GIF、JPEG、PNGのファイル形式をサポートしています。Python Imaging Libraryは必要ありません。

コードの一部は参照[source1]から取っています。

次の引数を取ります:

  • extensions: 許可する小文字の画像ファイル拡張子を保持する反復可能オブジェクト
  • maxsize: 画像の最大の幅と高さを保持する反復可能オブジェクト
  • minsize: 画像の最小の幅と高さを保持する反復可能オブジェクト

画像サイズのチェックを回避するには、minsizeとして(-1, -1)を使用してください。

いくつかの例を示します:

  • アップロードされたファイルがサポートされている画像フォーマットのいずれかに含まれるかをチェックします:
1
requires = IS_IMAGE()
  • アップロードされたファイルがJPEGまたはPNG形式かをチェックします:
1
requires = IS_IMAGE(extensions=('jpeg', 'png'))
  • アップロードされたファイルが最大200x200ピクセルのサイズのPNGであるかをチェックします:
1
requires = IS_IMAGE(extensions=('png'), maxsize=(200, 200))
  • 注記: requires = IS_IMAGE()を含むテーブルの編集フォームを表示している場合、ファイルが削除されると検証を通らないのでdeleteチェックボックスが表示されません。deleteチェックボックスを表示したい場合は次のバリデータを使用してください。
1
requires = IS_EMPTY_OR(IS_IMAGE())
IS_UPLOAD_FILENAME
IS_UPLOAD_FILENAME

このバリデータは、ファイル入力からアップロードされたファイルの名前と拡張子が与えられた条件に一致するかをチェックします。

どのような方法であれ、これはファイルの型を保証するものではありません。何もデータがアップロードされていない場合は、検証エラーを返します。

引数は次の通りです:

  • filename: (ドットの前の)ファイル名の正規表現です。
  • extension: (ドットの後の)拡張子の正規表現です。
  • lastdot: どのドットがファイル名/拡張子の区分に使用されるか:Trueの場合、最後のドットとなります(たとえば、"file.tar.gz"は"file.tar"+"gz"に分解されます)。一方Falseの場合、最初のドットになります(たとえば、"file.tar.gz"は"file"+"tar.gz"に分解されます)。
  • case: 0は大文字小文字を維持します。1は文字列を小文字に変換します(デフォルト)。2は文字列を大文字に変換します。

dotが存在しない場合、拡張子は空の文字列に対してチェックされ、ファイル名はすべての文字列に対してチェックされます。

例:

ファイルがpdfの拡張子を持つかをチェックします(大文字小文字は区別しません):

1
requires = IS_UPLOAD_FILENAME(extension='pdf')

ファイルがtar.gz拡張子を持ち、かつ、backupで始まる名前を持つかをチェックします:

1
requires = IS_UPLOAD_FILENAME(filename='backup.*', extension='tar.gz', lastdot=False)

ファイルが、拡張子を持たず、かつ、名前がREADMEに一致するかをチェックします(大文字小文字を区別します):

1
requires = IS_UPLOAD_FILENAME(filename='^README$', extension='^$', case=0)
IS_IPV4
IS_IPV4

このバリデータは、フィールドの値が10進数形式のIPバージョン4のアドレスかをチェックします。特定の範囲のアドレスに強制するように設定できます。

IPv4の正規表現は参照[regexlib]から取っています。 引数は以下の通りです。

  • minip許容する最下限のアドレス。str 例 192.168.0.1や、反復可能な数字 例 [192, 168, 0, 1])や、int 例 3232235521 を受け入れます。
  • maxip許容する最上限のアドレス。上と同様に受け入れます。

ここにある3つの例の値は同じです。アドレスは、次の関数で包含チェックをするために、整数に変換されるからです:

1
number = 16777216 * IP[0] + 65536 * IP[1] + 256 * IP[2] + IP[3]

例:

有効なIPv4のアドレスに対するチェックをします:

1
requires = IS_IPV4()

有効なプライベートネットワークのIPv4のアドレスに対するチェックをします:

1
requires = IS_IPV4(minip='192.168.0.1', maxip='192.168.255.255')
IS_UPPER
IS_UPPER

このバリデータはエラーを返すことはありません。値を大文字に変換します。

1
requires = IS_UPPER()
IS_NULL_OR
IS_NULL_OR

現在のバージョンでは使用されておらず、下に記述するIS_EMPTY_ORの別名です。

IS_EMPTY_OR
IS_EMPTY_OR

他の要求を満たしつつフィールドに空の値を許可したい場合があります。たとえば、フィールドは日付だが、空の値にもなりうるという場合です。IS_EMPTY_ORバリデータはこれを可能にします:

1
requires = IS_EMPTY_OR(IS_DATE())
CLEANUP
CLEANUP

これはフィルタです。失敗することはありません。単に、[10、13、32-127]のリストに含まれない10進数のASCIIコードを持つすべての文字を削除します。

1
requires = CLEANUP()
CRYPT
CRYPT

これもフィルタです。入力に対して安全なハッシュを実行します。パスワードがデータベースにそのまま渡されるのを防ぐのに使用されます。

1
requires = CRYPT()

デフォルトでは、CRYPT(訳注:暗号化処理)は20バイト長ハッシュを生成するSHA512を組み合わせたpbkdf2アルゴリズムの1000反復を使います。web2pyの古いバージョンではキーの定義されたり、されなかったりというような"md5"やHMAC+SHA512を使っていました。

keyが指定されている場合、CRYPTはHMACアルゴリズムを用います。keyには、HMACとともに使用するアルゴリズムを決める接頭辞を含めることも可能です。たとえば、SHA512は次のようになります:

1
requires = CRYPT(key='sha512:thisisthekey')

これは、推奨される構文です。keyは、使用されるデータベースに関連付けられた一意の文字列でなければなりません。keyは変更することはできません。keyを失うと、それ以前にハッシュ化された値は使用できなくなります。

既定では、CRYPTはランダムなsalt(訳注:ソルト、暗号化処理の際に付与されるデータ)を使い、各結果は違います。定数saltを使うことで、その値を定めます。

1
requires = CRYPT(salt='mysaltvalue')

あるいはsaltを使わない:

1
requires = CRYPT(salt=False)

CRYPTバリデータは入力をハッシュ化するので、ある意味特別な処理と言えます。ハッシュ化される前にパスワードフィールドを検証したい場合は、CRYPTをバリデータのリストに入れることで可能ですが、最後に呼ばれるようにするために、リストの一番最後に追加する必要があります。例:

1
requires = [IS_STRONG(),CRYPT(key='sha512:thisisthekey')]

CRYPTmin_length引数を取ることもできます。この引数のデフォルト値はゼロです。

結果のハッシュはalg$salt$hashから得ます。ここでalgはハッシュアルゴリズム、saltはsalt文字列(空文字可)、hashはアルゴリズムの出力です。必然的に、このハッシュは自己同一で、以前のハッシュを無効にせず変更するアルゴリズムを許可します。しかし、キーは同じにしておかないといけません。

データベースのバリデータ

IS_NOT_IN_DB
IS_NOT_IN_DB

次の例を考えます:

1
2
db.define_table('person', Field('name'))
db.person.name.requires = IS_NOT_IN_DB(db, 'person.name')

これは、新規のpersonを挿入したとき、彼/彼女の名前がデータベースdbのフィールドperson.nameにすでに存在していないことを要求します。他のバリデータと同様、この要求はフォーム処理のレベルで強制され、データベースレベルではされません。これには次のわずかな可能性があります。2人の訪問者が同時に、同じperson.nameを持つレコードを挿入しようとした場合、競合状態を引き起こし、両者のレコードが受け入れられてしまうことです。したがって、データベースに対しても、このフィールドが一意の値を持つということを知らせるほうが安全です:

1
2
db.define_table('person', Field('name', unique=True))
db.person.name.requires = IS_NOT_IN_DB(db, 'person.name')

このとき、競合状態が発生した場合、データベースはOperationalErrorを発生させ、2つのうちの1つの挿入が拒否されます。

IS_NOT_IN_DBの最初の引数は、データベース接続かSetにすることができます。後者の場合、Setで定義されたセットのみをチェックするようになります。

例えば次のコードは、10日以内に同じ名前を持つ2人のpersonsの登録を許可しません:

1
2
3
4
5
6
7
import datetime
now = datetime.datetime.today()
db.define_table('person',
    Field('name'),
    Field('registration_stamp', 'datetime', default=now))
recent = db(db.person.registration_stamp>now-datetime.timedelta(10))
db.person.name.requires = IS_NOT_IN_DB(recent, 'person.name')
IS_IN_DB
IS_IN_DB

次のテーブルと要求を考えてください:

1
2
3
4
db.define_table('person', Field('name', unique=True))
db.define_table('dog', Field('name'), Field('owner', db.person)
db.dog.owner.requires = IS_IN_DB(db, 'person.id', '%(name)s',
                                 zero=T('choose one'))

これは、dogの挿入/更新/削除フォームのレベルで強制されます。これは、dog.ownerdbデータベースのperson.idフィールドにおいて有効なidになっていることを要求します。このバリデータのおかげで、dog.ownerフィールドはドロップボックスによって表現されます。バリデータの3番目の引数は、ドロップボックスの内の要素を説明する文字列です。この例では、personの%(id)sの代わりに、personの%(name)sを見たいことになります。%(...)sは、各レコードに対して括弧内においてフィールドの値によって置き換えられます。

zeroオプションはIS_IN_SETバリデータに対するものと非常によく似た動作をします。

バリデータの最初の引数はIS_NOT_IN_DBのようにデータベース接続やDALセットも使用できます。これはドロップボックスのレコードを制限したい場合などに活用できます。次の例では、コントローラが呼ばれるたびに動的にレコードを制限するようにIS_IN_DBを使用しています。

1
2
3
4
5
6
7
def index():
    (...)
    query = (db.table.field == 'xyz') #in practice 'xyz' would be a variable
    db.table.field.requires=IS_IN_DB(db(query),....)
    form=SQLFORM(...)
    if form.process().accepted: ...
    (...)

フィールドの検証はしたいが、ドロップボックスを表示したくない場合、バリデータをリストの中に置いてください。

1
db.dog.owner.requires = [IS_IN_DB(db, 'person.id', '%(name)s')]
_and

場合によっては、ドロップボックスは使用したい(上のようにはリスト構文を用いたくない)が、追加のバリデータを使用したいときがあります。この目的のために、IS_IN_DBバリデータは_andという追加の引数をとります。これは、検証値がIS_IN_DBの検証を通った場合に適用される他のバリデータのリストを指します。たとえば、db内のすべてのdogのownersにおいて、あるサブセットにはないことを検証するためには次のようにします:

1
2
3
subset=db(db.person.id>100)
db.dog.owner.requires = IS_IN_DB(db, 'person.id', '%(name)s',
                                 _and=IS_NOT_IN_DB(subset,'person.id'))

IS_IN_DBはselectのcache引数のように働くcache引数も取ります。

IS_IN_DBとタグ付け
tags
multiple

IS_IN_DBバリデータは、multiple=Falseというオプション属性を持ちます。これがTrueに設定されている場合、複数の値が1つのフィールドに保存されます。このフィールドは、第6章で説明したlist:reference型にする必要があります。そこでは、明示的なタグ付けの例が説明されています。multipleの参照は作成と更新フォームにおいて自動的に処理されます。しかし、DALに対して透過的ではありません。multipleフィールドをレンダリングするためには、jQueryのmultiselectプラグインを使用することを強く勧めます。

カスタムバリデータ

custom validator

すべてのバリデータは、以下のプロトタイプに従っています:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
class sample_validator:
    def __init__(self, *a, error_message='error'):
        self.a = a
        self.e = error_message
    def __call__(self, value):
        if validate(value):
            return (parsed(value), None)
        return (value, self.e)
    def formatter(self, value):
        return format(value)

すなわち、値を検証するために呼ばれたとき、バリデータは(x, y)というタプルを返します。yNoneの場合、値は検証を通過し、xは通過した値を保持します。たとえば、バリデータが値に整数であることを要求する場合、xint(value)に変換されます。値が検証を通過しない場合は、xは入力値を保持し、yは検証の失敗を説明するエラーメッセージを保持します。このエラーメッセージは、値が妥当でないフォームにエラーを報告するために使用されます。

バリデータはformatterメソッドも持つことが可能です。これは、__call__が行うものと逆の変換を行う必要があります。たとえば、IS_DATEに対するソースコードを考えてみます:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
class IS_DATE(object):
    def __init__(self, format='%Y-%m-%d', error_message='must be YYYY-MM-DD!'):
        self.format = format
        self.error_message = error_message
    def __call__(self, value):
        try:
            y, m, d, hh, mm, ss, t0, t1, t2 = time.strptime(value, str(self.format))
            value = datetime.date(y, m, d)
            return (value, None)
        except:
            return (value, self.error_message)
    def formatter(self, value):
        return value.strftime(str(self.format))

成功した場合、__call__メソッドはフォームからデータ文字列を読み取り、コンストラクタで指定したフォーマット文字列を用いてそれをdatetime.dateオブジェクトに変換します。formatterオブジェクトは、datetime.dateオブジェクトを受け取り、同じフォーマットを用いてそれを文字列表現に変換します。formatterはフォームによって自動的に呼び出されます。しかし、明示的に使用して、オブジェクトを適切な表現に変換することもできます。例:

1
2
3
4
5
6
7
>>> db = DAL()
>>> db.define_table('atable',
       Field('birth', 'date', requires=IS_DATE('%m/%d/%Y')))
>>> id = db.atable.insert(birth=datetime.date(2008, 1, 1))
>>> row = db.atable[id]
>>> print db.atable.formatter(row.birth)
01/01/2008

複数のバリデータが要求され(そしてリストに格納され)たとき、それらは順序通りに実行され、1つの出力は入力として次のものへ渡されます。この連鎖は、バリデータのいずれかが失敗したときに中断されます。

反対に、フィールドのformatterメソッドを呼ぶとき、複数のバリデータに関連付けられたformattersもまた連鎖されますが、逆順になります。

カスタムバリデータの代わりにform.accepts(...)form.process(...)form.validate(...)の要素であるonvalidateを使用することもできます。

依存関係のバリデータ

通常、バリデータはモデルにおいて全てに一度だけ設定されます。

フィールドを検証する必要があり、その検証が別のフィールドの値に依存することがあります。これはいろいろな方法で実現できます。モデルで実現する方法とコントローラで実現する方法があります。

例として、ユーザー名と2度のパスワードを尋ねる登録フォームを生成するページを示します。どのフィールドも空にすることはできず、両者のパスワードは一致しなければなりません:

1
2
3
4
5
6
7
8
9
def index():
    form = SQLFORM.factory(
        Field('username', requires=IS_NOT_EMPTY()),
        Field('password', requires=IS_NOT_EMPTY()),
        Field('password_again',
              requires=IS_EQUAL_TO(request.vars.password)))
    if form.process().accepted:
        pass # or take some action
    return dict(form=form)

同じメカニズムは、FORMとSQLFORMオブジェクトに適用することができます。

ウィジェット

以下に利用可能なweb2pyのウィジェットの一覧を示します:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
SQLFORM.widgets.string.widget
SQLFORM.widgets.text.widget
SQLFORM.widgets.password.widget
SQLFORM.widgets.integer.widget
SQLFORM.widgets.double.widget
SQLFORM.widgets.time.widget
SQLFORM.widgets.date.widget
SQLFORM.widgets.datetime.widget
SQLFORM.widgets.upload.widget
SQLFORM.widgets.boolean.widget
SQLFORM.widgets.options.widget
SQLFORM.widgets.multiple.widget
SQLFORM.widgets.radio.widget
SQLFORM.widgets.checkboxes.widget
SQLFORM.widgets.autocomplete

最初の10個は対応するフィールド型のデフォルトになります。"options"ウィジェットは、フィールドの要求がIS_IN_SETIS_IN_DBmultiple=False(デフォルトの挙動)のときに使用されます。"multiple"ウィジェットはフィールドの要求がIS_IN_SETIS_IN_DBmultiple=Trueのときに使用されます。"radio"と"checkboxes"ウィジェットはデフォルトでは決して使用されませんが、手動で設定することができます。autocompleteウィジェットは特別で、それ自身のセクションで説明します。

たとえば、textareaで表示される"文字列"フィールドを持つには以下のようにします:

1
Field('comment', 'string', widget=SQLFORM.widgets.text.widget)

ウィジェットをフィールドに後天的に割り当てることもできます:

db.mytable.myfield.widget = SQLFORM.widgets.string.widget

ウィジェットは値が指定される必要がある追加の引数を取る場合があります。この場合、lambdaを使用できます。

db.mytable.myfield.widget = lambda field,value:     SQLFORM.widgets.string.widget(field,value,_style='color:blue')

ウィジェットはヘルパファクトリで、最初の2つの引数は常にfieldvalueです。他の引数には通常のヘルパの属性である_style_class等を含みます。特別な引数を取るウィジェットもあります。具体的にいうと、SQLFORM.widgets.radioSQLFORM.widgets.checkboxesはそれを格納するフォームのformstyleと適合するために、"table"、"ul"、"divs"を指定できるstyle引数(_styleと混同しないでください)を取ることができます。

新しいウィジェットを作成したり、既存のウィジェットを拡張したりすることができます。

SQLFORM.widgets[type]はクラスで、SQLFORM.widgets[type].widgetは対応するクラスの静的メンバ関数です。各ウィジェット関数は2つの引数をとります。フィールドオブジェクトと現在のフィールドの値です。これは、ウィジェットの表現を返します。例として、stringウィジェットは次のように再コード化することができます:

1
2
3
4
5
6
7
8
def my_string_widget(field, value):
    return INPUT(_name=field.name,
                 _id="%s_%s" % (field._tablename, field.name),
                 _class=field.type,
                 _value=value,
                 requires=field.requires)

Field('comment', 'string', widget=my_string_widget)

idとclassの値は、本章の後半で説明されている慣例に従う必要があります。ウィジェットは独自のバリデータを持つことが可能ですが、バリデータをフィールドの"requires"属性に関連付け、ウィジェットがそこからそれらを得るようにするのが良いプラクティスです。

オートコンプリート・ウィジェット

autocomplete

autocompleteウィジェットには2つの使い道があります:リストから値を受けてフィールドを自動補完するためと、参照フィールドを自動補完するためです(ここで自動補完される文字列はidのように実装された参照の表現です)。

最初のケースは簡単です:

1
2
3
4
db.define_table('category',Field('name'))
db.define_table('product',Field('name'),Field('category'))
db.product.category.widget = SQLFORM.widgets.autocomplete(
     request, db.category.name, limitby=(0,10), min_length=2)

ここで、limitbyは一度に10個までの候補しか表示しないようにウィジェットに指示します。min_lengthは、ユーザーが検索ボックスにおいて少なくとも2文字をタイプした後のみ、候補を取得するAjaxコールバックを実行するようにウィジェットに指示します。

2番目のケースはより複雑になります:

1
2
3
4
db.define_table('category',Field('name'))
db.define_table('product',Field('name'),Field('category'))
db.product.category.widget = SQLFORM.widgets.autocomplete(
     request, db.category.name, id_field=db.category.id)

この場合、id_fieldの値は、自動補完される値がdb.category.nameでも、保存される値は対応するdb.category.idになるようにウィジェットに指示します。オプションのパラメタorderbyは、候補をどのようにソートするかをウィジェットに指示します(デフォルトはアルファベット順です)。

このウィジェットは、Ajaxを介して動作します。ここで、Ajaxコールバックはどこにあるのでしょうか?いくつかの魔法が、このウィジェットで起こっています。コールバックはウィジェットオブジェクトのメソッドそのものです。どのように公開されているのでしょうか?web2pyにおいて、任意のコード断片はHTTP例外を発生させることによってレスポンスを生成することができます。このウィジェットは次の方法でこの可能性を利用しています:ウィジェットはAjax呼び出しを最初にウィジェットを生成したURLと同じところに送ります。そして、request.varsにおいて特別なトークンを置きます。ウィジェットは再びインスタンス化されるはずで、ウィジェットはそのトークンを見つけ、リクエストに応答するHTTP例外を発生させます。これらすべては内部で行われ、開発者に対して隠されています。

SQLFORM.gridSQLFORM.smartgrid

注意: グリッドとスマートグリッドはweb2py バージョン 2.0より以前は実験的で、情報の漏れに弱点がありました。そのグリッドとスマートグリッドはもう実験的ではありませんが、グリッドの表現レイヤ、そのただAPIにおいて後方互換性の約束はまだしていません。

複雑なCRUDコントロールを作成する2つの高機能なガジェットがあります。レコードのページ送り、表示、検索、ソート、作成、更新、削除を1つのガジェットから実行する機能を提供します。

SQLFORM.gridの方がシンプルです。ここに使用方法の例を挙げます:

1
2
3
4
@auth.requires_login()
def manage_users():
    grid = SQLFORM.grid(db.auth_user)
    return locals()

これは次のページを作成します。

image

SQLFORM.gridの最初の引数はテーブルかクエリです。gridガジェットはクエリに一致したレコードに対する接続を提供します。

gridガジェットの膨大な引数のリストの説明の前に、どのように動作するかを理解する必要があります。ガジェットはrequest.argsを参照し何の動作(表示、検索、作成、更新、削除等)をするか決定します。ガジェットで作成されたそれぞれのボタンは同じ関数(上記の場合はmanage_users)にリンクされますが、異なるrequest.argsを渡します。gridによって作成されたURLは全てデフォルトで電子署名され認証されています。これはユーザーがログインしていない場合、一部の機能(作成、更新、削除)が実行できないことを意味します。この制限については緩和することができます。

1
2
3
def manage_users():
    grid = SQLFORM.grid(db.auth_user,user_signature=False)
    return locals()

しかし、これは推奨しません。

LOADを利用してコンポーネントとして埋め込みでもしない限り、gridはコントローラの関数につきひとつしか使用できないからです。 Because of the way grid works one can only have one grid per controller function, unless they are embedded as components via LOAD. ひとつ以上のLOADしたグリッドを使ってグリッドに既定の検索をさせるには、そのそれぞれに異なるformnameを用いてください。

gridを含む関数自体がコマンドライン引数を操作する場合があるので、gridはどの引数をgridで処理し、どの引数をgrid以外で処理するかを把握する必要があります。次は任意のテーブルを処理できるコードの例です。

1
2
3
4
5
6
@auth.requires_login()
def manage():
    table = request.args(0)
    if not table in db.tables(): redirect(URL('error'))
    grid = SQLFORM.grid(db[table],args=request.args[:1])
    return locals()

gridargs引数はどのrequest.argsがガジェットに渡されるか若しくは無視されるかを指定します。私たちの例ではrequest.args[:1]は処理したいテーブル名を表し、ガジェットではなく、manage関数自体で扱われています

gridの完全な用法は次のようになります:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
SQLFORM.grid(
    query,
    fields=None,
    field_id=None,
    left=None,
    headers={},
    orderby=None,
    groupby=None,
    searchable=True,
    sortable=True,
    paginate=20,
    deletable=True,
    editable=True,
    details=True,
    selectable=None,
    create=True,
    csv=True,
    links=None,
    links_in_grid=True,
    upload='<default>',
    args=[],
    user_signature=True,
    maxtextlengths={},
    maxtextlength=20,
    onvalidation=None,
    oncreate=None,
    onupdate=None,
    ondelete=None,
    sorter_icons=(XML('&#x2191;'), XML('&#x2193;')),
    ui = 'web2py',
    showbuttontext=True,
    _class="web2py_grid",
    formname='web2py_grid',
    search_widget='default',
    ignore_rw = False,
    formstyle = 'table3cols',
    exportclasses = None,
    formargs={},
    createargs={},
    editargs={},
    viewargs={},
    buttons_placement = 'right',
    links_placement = 'right'
    )
  • fields はデータベースから取得されるフィールドのリストです。gridビューにどのフィールドを表示するかを決定するためにも使用されます。
  • field_iddb.mytable.idのように、IDとして使用されるテーブルのフィールドである必要があります。
  • left...select(left=...)によってオプションの左外部結合(原文:left join)を定義したい場合に利用する追加の記述です。
  • headers は'tablename.fieldname'を対応するヘッダーラベルにマッピングする辞書です。例えば{'auth_user.email' : 'Email Address'}
  • orderby はrowsのデフォルトでの表示順序に使用します。
  • groupby はセットをグループ化するために使用します。簡単にselect(groupby=...)で渡したのと同じシンタックスを使います。
  • searchable, sortable, deletable, editable, details, create は検索、ソート、削除、編集、詳細表示、新規レコード作成を実行できるかどうかそれぞれ決定します。
  • selectable は複数レコード(チェックボックスが全行で入る)でカスタム関数を呼ぶために使用できます。例えば、
1
   selectable = lambda ids : redirect(URL('default', 'mapping_multiple', vars=dict(id=ids)))
  • paginate はページ毎の行の最大の数を指定します。
  • csv trueの場合は各種のフォーマット(後で詳しく)でグリッドのダウンロードを許可します。
  • links は異なるページにリンクできる新しい項目を表示するのに使用されます。links変数はdict(header='name',body=lambda row: A(...))のリストを取る必要があります。headerは新しい項目のヘッダーで、bodyはrowを取得し値を返します。この例でいうとその値はA(...)ヘルパになります。
  • links_in_grid はFalseに設定すると、リンクは(メインのグリッド上ではなく)"詳細"と"編集"ページにのみ表示されます。
  • uploadはSQLFORMのuploadと同様です。web2pyは指定されたアクションのURLをダウンロード時に使用します。
  • maxtextlength はgridビュー上で表示される各フィールドの文字の最大長を設定します。この値は'tablename.fieldname':length、例えば{'auth_user.email' : 50}の辞書としてmaxtextlengthsを利用し上書きできます。
  • onvalidation, oncreate, onupdate,ondelete はコールバック関数です。ondeleteはフォームオブジェクトを入力値として受け取ります。
  • sorter_icons は各フィールドの昇順、降順ソートオプションを表示するふたつの文字(またはヘルパ)のリストです。
  • ui は'web2py'と同様にweb2py形式のクラス名を設定します。jquery-uiはjquery UI形式のクラス名を設定しますが、様々なgridコンポーネントにおいて独自のクラスも設定されます:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
ui = dict(
    widget='',
    header='',
    content='',
    default='',
    cornerall='',
    cornertop='',
    cornerbottom='',
    button='button',
    buttontext='buttontext button',
    buttonadd='icon plus',
    buttonback='icon leftarrow',
    buttonexport='icon downarrow',
    buttondelete='icon trash',
    buttonedit='icon pen',
    buttontable='icon rightarrow',
    buttonview='icon magnifier')
  • search_widget はデフォルトの検索ウィジェットを上書きできます。詳細は"gluon/sqlhtml.py"のソースコードを参照してください。
  • showbuttontext (アイコンのみ有効である)テキスト無しのボタンを許します。
  • _class はグリッドコンテナのクラスです。
  • showbutton 全てのボタンを非表示にできます。
  • exportclasses はタプルの辞書を取ります。デフォルトでは次のように定義されます。
1
2
3
4
5
6
csv_with_hidden_cols=(ExporterCSV, 'CSV (hidden cols)'),
csv=(ExporterCSV, 'CSV'),
xml=(ExporterXML, 'XML'),
html=(ExporterHTML, 'HTML'),
tsv_with_hidden_cols=(ExporterTSV, 'TSV (Excel compatible, hidden cols)'),
tsv=(ExporterTSV, 'TSV (Excel compatible)'))

ExporterCSV, ExporterXML, ExporterHTML や ExporterTSV はすべてgluon/sqlhtml.pyで定義されます。自分の書き出しツールを作るにはこれらをみてください。dict(xml=False, html=False)のような辞書を渡すと、xmlとhtml書き出しフォーマットは無効になります。

  • formargs はgridで使用される全てのSQLFORMオブジェクトに渡されます。ですが、createargs,editargsviewargs はSQLFORMの特定の作成、編集、詳細表示にのみ渡されます。
  • formname, ignore_rw, formstyle は作成/更新フォーム用のgridで使用されるSQLFORMオブジェクトに渡されます。
  • buttons_placementlinks_placement は両方ともパラメータ('right', 'left', 'both')を取ります。このパラメータはボタン(あるいはリンク)の並びがどこに配置されるかに影響します。
deletable, editable, details は一般的にブーリアンの値ですが、rowオブジェクトを取得し対応するボタンを表示・非表示するか決定するといった関数も使用できます。

SQLFORM.smartgridgridに非常によく似ています。実際にgridを含みますが、クエリではなくひとつのテーブルを入力として受け取るように設計されています。そして参照先のテーブルも表示します。

以下のテーブル構造を考えてみましょう:

1
2
db.define_table('parent',Field('name'))
db.define_table('child',Field('name'),Field('parent','reference parent'))

SQLFORM.gridで全ての親を一覧表示できます:

1
SQLFORM.grid(db.parent)

全ての子供:

1
SQLFORM.grid(db.child)

全ての親と子供をひとつのテーブル:

1
SQLFORM.grid(db.parent,left=db.child.on(db.child.parent==db.parent.id))

SQLFORM.smartgridで全てのデータをまとめたひとつのガジェットを作成し、両方のテーブルを出力できます。

1
2
3
4
@auth.requires_login()
def manage():
    grid = SQLFORM.smartgrid(db.parent,linked_tables=['child'])
    return locals()

以下のようになります:

image

"children"リンクが追加されています。通常のgridを利用して追加のlinksを作成することもできますがその場合は異なる機能を示しています。smartgridを利用することで自動的に作成され同じガジェットで処理されます。

また、ある親の"children"リンクをクリックするとその親に紐づく(これは明らかですが)子供だけのリストが取得できます。しかし、新しい子供を追加しようとすると、その子供の親の値は選択された親(ガジェットに関連するパンくずリストで表示)から自動的に設定されます。そのフィールドの親の値は上書くこともできます。読み取り専用にすることで上書きを防げます。

1
2
3
4
5
@auth.requires_login():
def manage():
    db.child.parent.writable = False
    grid = SQLFORM.smartgrid(db.parent,linked_tables=['child'])
    return locals()

linked_tables引数が指定されていない場合は全ての参照テーブルが自動的にリンクされます。どちらにせよ、誤ってデータが公開されないように、リンクされるべきテーブルの一覧を明示的に指定することを推奨します。

次のコードはシステム内の全てのテーブルの非常に強力な管理インターフェースを作成します。

1
2
3
4
5
6
@auth.requires_membership('managers'):
def manage():
    table = request.args(0) or 'auth_user'
    if not table in db.tables(): redirect(URL('error'))
    grid = SQLFORM.smartgrid(db[table],args=request.args[:1])
    return locals()

smartgridgridと同じ引数を取り、条件付で追加の引数も取ります。

  • 最初の引数はテーブルで、クエリではありません
  • 'tablename':queryの辞書であるconstraintsという追加の引数があります。これは'tablename'gridで表示されるレコードに対してさらなるアクセス制限をかけることができます。
  • smartgridから接続できるテーブルの名称リストであるlinked_tablesという追加の引数があります。
  • divider はパンくずリストで使うキャラクタを指定できます。breadcrumbs_classはパンくず要素にクラスを適用します。
  • 以下で説明するようにテーブル、args,linked_tables,user_signaturesに辞書型を使うことができます。

前回のgridを考えて見ましょう:

1
grid = SQLFORM.smartgrid(db.parent,linked_tables=['child'])

db.parentdb.childの両方に接続できます。ナビゲーションコントロールにとって、それぞれのテーブルは、スマートテーブルではなくただのgridです。この場合、これはひとつのsmartgridが親と子供のgridをひとつずつ作成できることを意味します。これらのgridに異なるパラメタのセットを渡すこともできます。例えば異なるsearchableパラメタのセットです。

gridではブーリアン型を渡せます:

1
grid = SQLFORM.grid(db.parent,searchable=True)

smartgridではブーリアン型の辞書を渡せます:

1
2
grid = SQLFORM.smartgrid(db.parent,linked_tables=['child'],
     searchable= dict(parent=True, child=False))

このように親は検索可能だが子供は検索不可(検索ウィジェットが必要な場合はあまり多くないです)にできます。

gridとsmartgridのガジェットは今後も残りますが実験的としています。これは新たな機能追加があった場合に、返される実際のhtmlレイアウトやパラメータのセットが変更される可能性があるからです。

gridsmartgridはcrudのように自動でアクセス権を強制しませんが、authと統合して明示的にパーミッションを確認することができます:

1
2
3
grid = SQLFORM.grid(db.auth_user,
     editable = auth.has_membership('managers'),
     deletable = auth.has_membership('managers'))

または

1
2
3
grid = SQLFORM.grid(db.auth_user,
     editable = auth.has_permission('edit','auth_user'),
     deletable = auth.has_permission('delete','auth_user'))

smartgridは単数形と複数形の両方のテーブル名を表示する唯一のweb2pyのガジェットです。例えばparentは一人の"Child"や、たくさんの"Children"を持てます。それゆえ、テーブルオブジェクトは自身の単数形と複数形の名称を知る必要があります。web2pyは通常これを予測しますが明示的に設定することもできます:

1
db.define_table('child', ..., singular="Child", plural="Children")

または:

singular
plural

1
2
3
db.define_table('child', ...)
db.child._singular = "Child"
db.child._plural = "Children"

T演算子を使用して国際化対応することもできます。

そして複数形と単数形の値はsmartgridで使用されるヘッダーとリンクの正しい名前として提供されます。

第3版 - 翻訳: 細田謙二 レビュー: Omi Chiba
第4版 - 翻訳: Omi Chiba レビュー: Fumito Mizuno
第5版 - 翻訳: Mitsuhiro Tsuda レビュー: Omi Chiba
 top