2012年9月15日土曜日

web2py SQLFORM あれこれ ② SQLFORMカスタマイズ

SQLFORM あれこれ ① の続きです。今回は SQLFORM で、少し複雑な入力値のチェック、モデルにないフィールドの追加、JavaScriptの利用、といったカスタマイズを試したいと思います。

SQLFORM で使用するモデル(テーブル)定義は前回のものと同じだ。しかし前回記事の最後で、アップロードフィールドにバリデータを追加設定した。またURLフィールドに値を設定した場合、正しいURLかどうか判断できるように、IS_URL バリデータを新しくセットすることにする。これらを合わせて、モデル定義は次の通りとなる。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
db.define_table('category',
    Field('name', 'string', length=32, notnull=True), format='%(name)s')

db.define_table('image',
    Field('name', 'string', length=64, notnull=True),
    Field('url', 'string', length=256),
    Field('file', 'upload', autodelete=True),
    Field('category', db.category))

db.image.category.requires=IS_IN_DB(db,'category.id','%(name)s',
                                        zero=T('choose one'))
db.image.file.requires=IS_EMPTY_OR([IS_IMAGE(extensions=('jpeg', 'png')), 
                                    IS_LENGTH(maxsize=262144,
                                    error_message=T('max %(max)g bytes'))])
db.image.url.requires=IS_EMPTY_OR(IS_URL())
onvalidation による入力値チェック

入力値のチェックは通常、モデルの定義のバリデータを使う。しかし中には、バリデータでは設定できないようなチェック項目がある。例えば、今回のモデル定義に、url と file というフィールドがある、このどちらか最低一方だけ値を設定したいという場合はバリデータでは不可能だ。

このような場合、前回 で紹介した processメソッドやacceptsメソッドの onvalidation パラメータを使用する。取り敢えず、サンプルコードを紹介してみよう。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
def check_image(form):
    file_delete = '%s__delete' % 'file'
    
    if form.vars.url is None and \
    form.vars.file is None and (form.vars.has_key(file_delete) is False or \
    form.vars.get(file_delete, False)):
        form.errors.url = T('must enter a url')
        form.errors.file = T('must select a file')
            
def image():
   form = SQLFORM(db.image, request.args(0), True, 
                  upload=URL(c='default', f='download'))\
                  .process(onvalidation=check_image)
   return dict(form=form)

13行目の processメソッドの onvalidation パラメータに、check_image 関数を指定している。実際に実行してみると次のイメージのようになる。実行例では、url及びfileフィールドには何も入力しないで、Submitボタンを押した。するとキチンとエラーが表示されるのがわかる。

onvalidation に設定する関数には、次の特徴がある。

  • FORMオブジェクトが関数のパラメータとして渡される(ここでは変数 form として受け取る)
  • フォームの入力値は、form.vars.フィールド名 (form.vars[フィールド名])でアクセス可能
  • エラーは、form.errors.フィールド名(form.errors[フィールド名])にメッセージをセットする

サンプルコードの check_image 関数に関してもう少し解説してみると、通常2つのフィールドが共に値がないというのは、フィールド値がブランクかどうかをチェックすればよい。今回チェックする url 及び file フィールドは、IS_EMPTY_OR バリデータが設定されているため、値がない場合 None が返る。このためフィールドが None かどうかを確認している。

また fileフィールドは uploadタイプだが、このタイプのフィールドに値がセットされているかどうかチェックするのは少し複雑だ。ロジックを箇条書きにすると、

  1. フィールド値(form.vars.file)が None かどうか
  2. 1 の場合、削除チェックボックス(form.vars.file__delete)が存在しないかどうか → 新規登録用チェック
  3. 1 の場合、削除チェックボックス(form.vars.file__delete)がチェックされているかどうか → 更新登録用チェック

新規登録の場合は 1 だけでもよいのだが更新登録も考えると、これだけのロジックが必要になる。ちなみに、削除チェックボックの名前は、フィールド名__delete となる。

モデル定義にないフィールドの追加

onvalidation で2つのフィールドの値をチェックできるようになったが、どうもユーザインターフェースが分かりにくいと思わないだろうか?。画面の説明をすれば理解してもらえると思うが、直感的ではない。

このため画面にラジオボタンを追加し、入力フィールドを url か file のどちらかに選んでもらうようにする。また選んだ方の入力値をチェックすると共に、選ばれなかった方のフィールド値を削除するように仕様変更する。

ラジオボタンのフィールドはモデル定義でテーブルに追加するのが一番簡単なのだが、画面ロジック用のフィールドをテーブルに追加するのは抵抗がある。今回はモデル定義を変更せず、ラジオボタンを追加してみたい。まず改善したサンプルソースを示してみる。onvalidationに設定する入力チェック用の check_image関数と、フォーム表示用の image関数だ。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
def check_image(form):
    file_delete = '%s__delete' % 'file'
    
    if form.vars.url_file == 'url': 
        if not form.vars.url:
            form.errors.url = T('must enter a url')
        else:
            form.vars[file_delete] = 'on'
    else:
        if form.vars.file is None and \
            (form.vars.has_key(file_delete) is False or 
             form.vars.get(file_delete, False)):
            form.errors.file = T('must select a file')
        else:
            form.vars.url = ''

 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
def image():
    record = db.image(request.args(0))
    if not record or record.url:
        checked = True 
    else:
        checked = False
    
    label = T('Image') + ':'
    radio = P(INPUT(_type='radio', _name='url_file', _value='url', 
            _checked=checked, _id='url'), 
            LABEL(T('url'), _for='url'), ' ', 
            INPUT(_type='radio', _name='url_file', _value='file', 
            _checked=not checked, _id='file'),
            LABEL(T('file'), _for='file'), _style="margin:0;padding:4px 0")
    position = -5 if record else -4
            
    extra_element = TR(TD(LABEL(label),_class="w2p_fl"), 
                    TD(radio, _class="w2p_fw"),
                    TD('', _class="w2p_fc"), _id='url_file__row')
               
    form = SQLFORM(db.image, record, True, 
                   upload=URL(c='default', f='download'))
    form[0].insert(position, extra_element)
    form = form.process(onvalidation=check_image)

    return dict(form=form)

2012/10/09
onvalidationの設定後にフィールドを挿入していたため、チェック関数が正常に動作していませんでした。このためフィールド挿入後、processメソッドを呼び出すようにコード変更を行いました。

次に実行した画面イメージも示す。ラジオボタンが urlフィールドの上に挿入されたことがわかる。

新規登録更新登録

check_image関数は仕様変更通り、ラジオボタンで選択していないフィールドの値をクリアする様に変更した。また、image 関数については以下、簡単に解説する。

まずラジオボタンのHTML部品だが、サンプルで定義し radio変数にセットしている(9行目)。またラジオボタンのラベルは、label変数にセットしている(8行目)。これらの部品をテーブルにセット(17行目)し、さらにそのテーブルをフォームに代入している(23行目)。これは既存のフォーム部品と同じ構成にするためだ。

また挿入位置はフォームの下部の部品から数えて挿入していくため、新規登録と更新登録では挿入位置が一つずれる。このためレコードが存在するかどうかで、判定し調整を行なっている(15行目)。

今までの更新処理では、SQLFORMコンストラクタの第二パラメータにID番号を渡していた。しかし更新登録かどうかを判定するため、Rowオブジェクトを取り出しているので、そのままSQLFORMコンストラクタに渡している(21行目)。これによって、一つ、DBアクセスを削減している。

JavaScript の利用

ラジオボタンの追加で、画面は分かり易くなった。しかしもう一工夫 JavaScript を使って、選択しない方のフィールドを隠すようにしてみたい。まずサンプルコードを示す。image 関数を改造して、JavaScriptコードを追加しているだけだ。

 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
def image():
    onchange_js = "jQuery.noConflict();\
                if(jQuery('#url').attr('checked')){\
                    jQuery('#image_url__row').show();\
                    jQuery('#image_file__row').fadeOut();}\
                else {\
                    jQuery('#image_file__row').show();\
                    jQuery('#image_url__row').fadeOut();}"    
    ready_js = "jQuery(document).ready(function(){" + onchange_js + ";});"

    record = db.image(request.args(0))
    if not record or record.url:
        checked = True 
    else:
        checked = False
    
    label = T('Image') + ':'
    radio = P(INPUT(_type='radio', _name='url_file', _value='url', 
            _checked=checked, _id='url', _onchange=onchange_js), 
            LABEL(T('url'), _for='url'), ' ', 
            INPUT(_type='radio', _name='url_file', _value='file', 
            _checked=not checked, _id='file', _onchange=onchange_js),
            LABEL(T('file'), _for='file'), _style="margin:0;padding:4px 0")
    position = -5 if record else -4
            
    extra_element = TR(TD(LABEL(label),_class="w2p_fl"), 
                    TD(radio, _class="w2p_fw"),
                    TD('', _class="w2p_fc"), _id='url_file__row')
               
    form = SQLFORM(db.image, record, True, 
                   upload=URL(c='default', f='download'))
    form[0].insert(position, extra_element)
    form = form.process(onvalidation=check_image)
    form = DIV(form, SCRIPT(ready_js, _type='text/javascript'))
    
    return dict(form=form)

2012/10/09
onvalidationの設定後にフィールドを挿入していたため、チェック関数が正常に動作していませんでした。このためフィールド挿入後、processメソッドを呼び出すようにコード変更を行いました。

実行すると画面イメージは次のようになる。ラジオボタンを選択することにより、項目が消えたり・現れたりする。

url を選択file を選択

コードを簡単に解説すると、JavaScriptのコードは onchange_js と ready_js と2つの変数に設定している。onchange_js は、ラジオボタンの onchange イベントハンドラに設定している(19及び22行目)。

ready_js は、jQuery の ready メソッドで、 onchange_js のコードを動かすものだ。これをフォームの HTML コードの最後に付け足している(34行目)。これにより、ページのロードが完了した時点で自動的に動作する。


今回の記事はここまでです。SQLFORMのカスタマイズ例を説明してみました。SQLFORMは他にもいろいろな機能があります。これらの詳細は web2py book を参照してください。

参考: web2py book - フォームとバリデータ

まだ次回に続く予定です。次回bootstrap を使ったカスタマイズを説明していきたいと思います。