Toscawidgets の Widget の作成例

Toscawidgets のサンプルが少なくて、ちょっと凝ったWidget を作るのに苦労したので共有しときます。

TwoStepSelectField

1番目のSingleSelectField で絞り込み、2番目のSingleSelectField でデータを選択するフィールドセットを実装しました。


twostep.py

# -*- coding: utf-8 -*-
from tw.api import *
from tw.forms.fields import *
from formencode.schema import Schema
from formencode.validators import *

__all__ = ["TwoStepSelectField"]


class TwoStepSelectFieldSchema(Schema):
    """ NULL チェックのために ignore_key_missing は False にする """
    allow_extra_fields = True
    filter_extra_fields = False
    ignore_key_missing = False


class VoiceTextValidator(FormValidator):
    """ DB保存の前処理 """
    def _to_python(self, value, state):
        if "main" in value.keys():
            value = value["main"]
        return value


twostep_js = JSLink(modname=__name__, filename='static/twostep.js')
initialize_twostep = js_function("initialize_twostep")


class TwoStepSelectField(FieldSet):

    """ guide で絞り込みを行ってから main を選択する、リストボックスを2つ用いたフィールド
    field として代表して持つ値は main の値。
    絞り込み要素を与えるために、初期化時に2次元の辞書を与える。
    """

    template = "myproj.forms.templates.twostep"
    params = ["filter_dict", "main_attr", "guide_attr"]
    javascript = [twostep_js]
    include_dynamic_js_calls = True
    validator = TwoStepSelectFieldSchema(
        chained_validators = [VoiceTextValidator()]
        )

    def __new__(cls, id=None, parent=None, children=[], **kw):
        guide_attr = dict(label_text="guide", options=[], validator=Int)
        main_attr = dict(label_text="main", options=[], validator=Int)
        if "guide_attr" in kw.keys():
            guide_attr.update(kw["guide_attr"])
        if "main_attr" in kw.keys():
            main_attr.update(kw["main_attr"])

        class fields(WidgetsList):
            guide = SingleSelectField(**guide_attr)
            main = SingleSelectField(**main_attr)
        children = fields
        return super(TwoStepSelectField, cls).__new__(cls, id, parent, children, **kw)

    @property
    def groups(self):
        """ 初期化時に渡された filter_dict (dict or callable) を辞書化して返す """
        if callable(self.filter_dict):
            return self.filter_dict()
        return self.filter_dict

    def adapt_value(self, value):
        """ value の加工 (フォーム表示の前処理) """
        for k, v in self.groups.items():
            if value in dict(v).keys():
                return {"guide": k, "main": value}
        return value

    def update_params(self, d):
        """ template に渡す d の加工 """
        super(TwoStepSelectField, self).update_params(d)
        main_value = d["value_for"](d["c"]["main"])
        self.add_call(initialize_twostep(main_value, self.id, self.groups))  # "include_dynamic_js_calls = True" is required
        return d

twostep.js

function initialize_twostep(val, parent_id, groups) {

    var guide = document.getElementById(parent_id + "_guide");
    var main = document.getElementById(parent_id + "_main");

    function change_selects() {
        var idx = guide[guide.selectedIndex].value;
        for (var i = main.options.length - 1; i >= 0; i--) {
            main.options[i] = null;
        }
        if (groups[idx]) {
            for (var i = 0; i < groups[idx].length; i++) {
                var id = groups[idx][i][0];
                var label = groups[idx][i][1];
                main.options[i] = new Option(label, id);
            }
        }
    }

    // Add Event to guide field.
    if (guide.addEventListener) {
        guide.addEventListener("change", change_selects, false);  // Not IE
    } else {
        guide.attachEvent("onchange", change_selects);  // IE
    }

    // Initialize
    change_selects(guide[guide.selectedIndex].value);

    for (var i = 0; i < main.options.length; i++) {
        if (main.options[i].value == val) {
            main.selectedIndex = i;
            break;
        }
    }
}

twostep.mak

## -*- coding: utf-8 -*-
<%namespace name="tw" module="tw.core.mako_util"/>

% for field in ifields:
  ${ field.label_text}
  ${ field.display(value_for(field), **args_for(field)) }
% endfor

使い方

絞り込みのための辞書を用意して、以下のように使います。

foods = {"vegetable": [(1, "tomato"), (2, "")],
         "fruit": [(3, "orange"), (4, "apple")],
        }
food = TwoStepSelectField(
           label_text="Food",
           guide_attr=dict(label_text = "Category",
                           options = [(c, c) for c in foods.keys()],
                           validator = String),
           main_attr=dict(label_text = "Food", options = [], validator = Int(not_empty=True)),
           filter_dict=foods,
       )

参考

公開メソッドと非公開メソッドが入り乱れていて分かりづらいですが、tw.core:Widget を読むことをお勧めします。

個人的には、 adapt_value の使い方に気づくまで時間がかかりました。

posted by id:junya_hayashi