[TurboGears] フォームのウィジェットをつくるときは、turbogears.widgets.Formを継承する。

環境

この記事の内容は、TurboGears 1.0.1で確認しました。

やりたいこと

フォーム全体をウィジェットとして作成したいとします。つまり、Kidテンプレートの中で、

${foo.display(value)}

とすると、

<form>
:
</form>

が出力されるようなウィジェットをつくることを考えます。

やること

そのウィジェットは、turbogears.widgets.Formクラスを継承して作成します。

詳細

Rapid Web Applications with TurboGears: Using Python to Create Ajax-Powered Sites (Prentice Hall Open Source Software Development Series)

Rapid Web Applications with TurboGears: Using Python to Create Ajax-Powered Sites (Prentice Hall Open Source Software Development Series)

上の本では、独自のウィジェットをつくるときはturbogears.widgets.CompoundWidgetを継承するように書かれています。しかし、CompoundWidgetを継承してフォームを作成すると、例えば以下のように@validateデコレータを使ったときにエラーになります。

@validate(form=foo)
def bar(self):
    pass

エラーの内容は、具体的には以下の通りです。

500 Internal error

The server encountered an unexpected condition which prevented it from fulfilling the request.

Page handler: >
Traceback (most recent call last):
  File "/usr/lib/python2.4/site-packages/CherryPy-2.2.1-py2.4.egg/cherrypy/_cphttptools.py", line 105, in _run
    self.main()
  File "/usr/lib/python2.4/site-packages/CherryPy-2.2.1-py2.4.egg/cherrypy/_cphttptools.py", line 254, in main
    body = page_handler(*virtual_path, **self.params)
  File "", line 3, in do
  File "/usr/lib/python2.4/site-packages/TurboGears-1.0.1-py2.4.egg/turbogears/controllers.py", line 334, in expose
    output = database.run_with_transaction(
  File "", line 5, in run_with_transaction
  File "/usr/lib/python2.4/site-packages/TurboGears-1.0.1-py2.4.egg/turbogears/database.py", line 302, in so_rwt
    retval = func(*args, **kw)
  File "", line 5, in _expose
  File "/usr/lib/python2.4/site-packages/TurboGears-1.0.1-py2.4.egg/turbogears/controllers.py", line 351, in 
    mapping, fragment, args, kw)))
  File "/usr/lib/python2.4/site-packages/TurboGears-1.0.1-py2.4.egg/turbogears/controllers.py", line 378, in _execute_func
    output = errorhandling.try_call(func, *args, **kw)
  File "/usr/lib/python2.4/site-packages/TurboGears-1.0.1-py2.4.egg/turbogears/errorhandling.py", line 73, in try_call
    return func(self, *args, **kw)
  File "", line 3, in do
  File "/usr/lib/python2.4/site-packages/TurboGears-1.0.1-py2.4.egg/turbogears/controllers.py", line 131, in validate
    form = init_form(args and args[0] or kw["self"])
  File "/usr/lib/python2.4/site-packages/TurboGears-1.0.1-py2.4.egg/turbogears/controllers.py", line 124, in 
    init_form = lambda self: form(self)
  File "/usr/lib/python2.4/site-packages/TurboGears-1.0.1-py2.4.egg/turbogears/widgets/base.py", line 226, in __call__
    return self.display(*args, **params)
  File "/usr/lib/python2.4/site-packages/TurboGears-1.0.1-py2.4.egg/turbogears/widgets/meta.py", line 107, in lockwidget
    output = self.__class__.display(self, *args, **kw)
  File "/usr/lib/python2.4/site-packages/TurboGears-1.0.1-py2.4.egg/turbogears/widgets/base.py", line 352, in display
    return super(CompoundWidget, self).display(value, **params)
  File "/usr/lib/python2.4/site-packages/TurboGears-1.0.1-py2.4.egg/turbogears/widgets/base.py", line 264, in display
    return view.engines.get('kid').transform(params, self.template_c)
  File "/usr/lib/python2.4/site-packages/TurboKid-0.9.8-py2.4.egg/turbokid/kidsupport.py", line 173, in transform
    return ElementStream(t.transform()).expand()
  File "/usr/lib/python2.4/site-packages/kid-0.9.3-py2.4.egg/kid/pull.py", line 99, in expand
    for ev, item in self._iter:
  File "/usr/lib/python2.4/site-packages/kid-0.9.3-py2.4.egg/kid/pull.py", line 168, in _track
    for p in stream:
  File "/usr/lib/python2.4/site-packages/kid-0.9.3-py2.4.egg/kid/filter.py", line 21, in transform_filter
    for ev, item in apply_matches(stream, template, templates, apply_func):
  File "/usr/lib/python2.4/site-packages/kid-0.9.3-py2.4.egg/kid/filter.py", line 25, in apply_matches
    for ev, item in stream:
  File "/usr/lib/python2.4/site-packages/kid-0.9.3-py2.4.egg/kid/pull.py", line 168, in _track
    for p in stream:
  File "/usr/lib/python2.4/site-packages/kid-0.9.3-py2.4.egg/kid/pull.py", line 210, in _coalesce
    for ev, item in stream:
  File "", line 53, in _pull
AttributeError: 'Add' object has no attribute 'id'

ここで、最後の行にある"Add"はコントローラの名前であり、idは作成したウィジェットのテンプレートの中で${value.id}として表示しようとしている属性名です。すなわち、上ではなぜかテンプレートを使ってコントローラを表示しようとしていることになります。

この原因を探るため、@validateデコレータの中身を調べてみます。ファイルは、turbogears/controllers.pyになります。重要ではない箇所は省略します。

def validate(form=None, validators=None,
             failsafe_schema=errorhandling.FailsafeSchema.none,
             failsafe_values=None, state_factory=None):
(略)
    def entangle(func):
        recursion_guard = dict(func=func)
        if callable(form) and not hasattr(form, "validate"):
            init_form = lambda self: form(self)
        else:
            init_form = lambda self: form

        def validate(func, *args, **kw):
            if tg_util.call_on_stack("validate", recursion_guard, 4):
                return func(*args, **kw)
            form = init_form(args and args[0] or kw["self"])
(略)
        return validate
    return weak_signature_decorator(entangle)

まず着目するのは、以下の部分です。

        if callable(form) and not hasattr(form, "validate"):
            init_form = lambda self: form(self)
        else:
            init_form = lambda self: form

すべてのウィジェットの基底クラスであるturbogears.widgets.Widgetは__call__メソッドを実装しており(callable(form)は真となる)、validateメソッドは持っていないので、

            init_form = lambda self: form(self)

となります。このとき、ヴァリデータの本体となるvalidate関数の、

            form = init_form(args and args[0] or kw["self"])

が実行されると、turbogears.widgets.Widgetの__call__メソッドが呼び出されます。これは、turbogears/widgets/base.pyで以下のようになっています。

    def __call__(self, *args, **params):
        """
        Delegate to display. Used as an alias to avoid tiresome typing
        """
        return self.display(*args, **params)

すなわち、ウィジェットを表示しようとします。これがエラーの原因です。

さて、toolboxのWidget Browserで見ることができるTableFormなどはturbogears.widgets.Formクラスを継承していますので、それにならってFormクラスを継承するように変更すると、上のエラーは解消されます。なぜならば、Formクラスはturbogears/widgets/form.pyで、

    def validate(self, value, state=None):
        if self.validator:
            return self.validator.to_python(value, state)

のようにvalidateメソッドを実装しているからです。これにより、先ほどのコードの中の、

            init_form = lambda self: form

が実行されるようになります。そして、先ほどはエラーとなった、

            form = init_form(args and args[0] or kw["self"])

が無事に実行されます。