[TurboGears] フォームの中にあるウィジェットのIDは、「_」となる。
環境
この記事の内容は、TurboGears 1.0.1で確認しました。
現象
例えば、以下のようなフォームのウィジェットを作成します。
from turbogears import widgets class Foo(widgets.Form): def __init__(self): self.text_widget = widgets.TextField(name="bar") template = """ ${text_widget.display()} """
すなわち、barという名前のテキストボックスを持つフォームです。これを、次のようにしてインスタンス化し、表示します。
foo = Foo(name="foo")
foo.display()
このとき、フォームの中のテキストボックスのid属性の値は、"bar"ではなく、"foo_bar"になります(name属性の値は"bar"です)。
なぜこんな仕様になっているのか?
おそらく、同一のクラスから複数のフォームを生成し、画面に出力する場合に、中の要素を一意に区別できるようにするためです。
詳細
このような動作をするコードを、調べてみたいと思います。
とりかかりとして、テキストボックスのid属性がどのようにして出力されるのかを調べます。TextFieldのソースコードは、turbogears/widgets/forms.pyにあります。
class TextField(FormField): "A standard, single-line text field." template = """ <input xmlns:py="http://purl.org/kid/ns#" type="text" name="${name}" class="${field_class}" id="${field_id}" value="${value}" py:attrs="attrs" /> """ params = ["attrs"] params_doc = {'attrs' : 'Dictionary containing extra (X)HTML attributes for' ' the input tag'} attrs = {}
どうやら、id属性はオブジェクトのfield_idの値となるようです。
そこで、field_idをgrepして探してみます。すると、turbogears/widgets/forms.pyのFormFieldクラス(TextFieldクラスの基底クラスです)の中に、定義している箇所が見つかりました。
class FormField(InputWidget): (略) def _get_field_id(self): return build_name_from_path(self.path, '_', '_') field_id = property(_get_field_id)
field_idの値は、path属性の値と、build_name_from_path関数から決定されるようです。pathをgrepすると、turbogears/widgets/forms.pyのInputWidgetクラス(FormFieldクラスの基底クラスです)で見つかりました。
class InputWidget(Widget): (略) def _get_path(self): return get_path(self, None)[:] _get_path = update_path(_get_path) path = property(_get_path)
path属性の値は、_get_pathメソッドから得られますが、_get_pathメソッドはupdate_path関数によってデコレートされています。まず、update_path関数の方から調べてみます。これは、turbogears/widgets/forms.pyで見つかりました。
def update_path(func): def _update_path(self, *args, **kw): update = append_to_path(self, None) returnval = func(self, *args, **kw) if update: pop_from_path() return returnval return _update_path
これを踏まえると、_get_pathメソッドというのは、
def _get_path(self): update = append_to_path(self, None) returnval = get_path(self, None)[:] if update: pop_from_path() return returnval
のように書き直すことができます。では、append_to_path関数とget_path関数、pop_from_path関数を見てみましょう。まずappend_to_path関数です。これはturbogears/widgets/forms.pyにあります。
from cherrypy import request (略) def append_to_path(widget, repetition): path = [] if request_available(): if hasattr(request, "tg_widgets_path"): path = request.tg_widgets_path else: request.tg_widgets_path = path if (not path) or (path[-1].widget is not widget): path.append(Bunch(widget=widget, repetition=repetition)) return True else: return False
ぱっと見ではよくわからないので、前半と後半に分けて考えてみます。request_available関数は以下のようにturbogears/util.pyにありますが、これは真になりそうなので省略します。
def request_available(): """Check if cherrypy.request is available.""" try: setattr(request, "tg_dumb_attribute", True) return True except AttributeError: return False
さらに、処理結果が変わらないように行を入れ換えたappend_to_path関数の前半部分が、以下です。
if hasattr(request, "tg_widgets_path"): path = request.tg_widgets_path else: path = [] request.tg_widgets_path = path
すなわち、
- requestオブジェクトにtg_widgets_path属性があれば、その値をpath変数に設定する。
- requestオブジェクトにtg_widgets_path属性がなければ、path変数を空のリストに設定し、さらにrequestオブジェクトのtg_widgets_path属性を同じように空のリストで初期化する。
ということになりますが、要するに最終的には、
- requestオブジェクトはtg_widgets_path属性を持つ。
- path変数の値はrequest.tg_widgets_pathの値に等しい。
となる、ということです。続いて後半部分(下記)です。
if (not path) or (path[-1].widget is not widget): path.append(Bunch(widget=widget, repetition=repetition)) return True else: return False
このif文は、現段階ではどちらが実行されるのか分かりません。ここは保留にしておきます。なお、ここでpathに追加されるBunchオブジェクトというのはturbogears/util.pyで定義されていて、
class Bunch(dict): __setattr__ = dict.__setitem__ def __delattr__(self, name): try: del self[name] except KeyError: raise AttributeError def __getattr__(self, name): try: return self[name] except KeyError: raise AttributeError
という、プロパティをキーとして使える辞書です。
さて、次にget_pathメソッドを探します。これはturbogears/widgets/forms.pyにありました。
def get_path(default_widget, default_repetition): default_path = [Bunch(widget=default_widget, repetition=default_repetition)] return getattr(request, "tg_widgets_path", default_path)
get_path関数は、get_path(self, None)として呼び出しているので、request.tg_widgets_pathがなければ、[Bunch(widget=self, repetition=None)]が返ることになります。
最後に、pop_from_path関数を見てみます。
def pop_from_path(): if request_available() and hasattr(request, "tg_widgets_path"): request.tg_widgets_path.pop()
この関数では、request.tg_widgets_pathの最後の要素を取り除いています。ここで、書き直した_get_path関数を思い出します(以下に再掲します)。
def _get_path(self): update = append_to_path(self, None) returnval = get_path(self, None)[:] if update: pop_from_path() return returnval
pop_from_path関数は、append_to_path関数がTrueを返したときのみ実行されます。そして、append_to_path関数がTrueを返すのは、request.tg_widgets_pathにBunchオブジェクトが追加されるときでした。以上より、request.tg_wigets_pathにappend_to_path関数で追加したものをpop_from_path関数で取り出す、という挙動が見えてきます。
ここまでの内容から考えると、_get_path関数(すなわちpathプロパティ)の値というのは、requestオブジェクトにtg_widgets_pathメンバがあるかどうかで変わってきそうです。まず、requestオブジェクトにtg_widgets_pathメンバがない場合の挙動を追ってみます。このとき、
- append_to_path関数で、request.tg_widgets_pathを空のリストとする。
- append_to_path関数で、request.tg_widgets_pathにBunch(widgets=self, repetition=None)オブジェクトを追加する。
- get_path関数が、[Bunch(widgets=self, repetition=None)]を返す。
- pop_from_path関数で、request.tg_widgets_pathからBunchオブジェクトを取り除く(空のリストにする)。
次に、request.tg_widgets_pathにself以外の要素を持つリストが既に設定されていた場合は、
- append_to_path関数で、request.tg_widgets_pathにBunch(widgets=self, repetition=None)オブジェクトを追加する。
- get_path関数が、request.tg_widgets_pathを返す。
- pop_from_path関数で、request.tg_widgets_pathからBunchオブジェクトを取り除く。
となります。また、tg_wigets_pathをgrepした結果を眺めてみたところ、get_path関数の戻り値となる(つまり、pathプロパティの値となる)request.tg_widgets_pathは、Bunchオブジェクトのリストとなっていると考えて間違いなさそうです。
これでpathプロパティの値(の性質)が分かりました。field_idの値を求めるのに必要なのは、あとはbuild_name_from_path関数だけです。これは、turbogears/widgets/forms.pyにあります。
def build_name_from_path(path, repeating_marker='-', nesting_marker='.'): name = [] for item in path: if item.repetition is not None: name.append(item.widget.name + repeating_marker + str(item.repetition)) else: name.append(item.widget.name) return nesting_marker.join(name)
build_name_from_path関数の第1引数のpathは、(pathプロパティの値だから)Bunchオブジェクトのリストです。また、これまで見てきた中では、BunchオブジェクトのrepetitionはすべてNoneになっていました。他の引数も踏まえると、上の関数の中身は、以下のようになります。
name = [] for item in path: name.append(item.widget.name) return "_".join(name)
以上をすべてひっくるめて考えてみると、request.tg_widgets_pathのリストにウィジェット(を持つBunchオブジェクト)を出し入れしながら、ウィジェットの名前をつなげていく、という挙動が浮かびます。
では、他にはどんなところでrequest.tg_widgets_pathに要素を追加しているのでしょうか? update_path関数がポイントになりそうなので、これをgrepしてみたら、turbogears/widgets/forms.pyのInputWidgetクラスのdisplayメソッドが、
class InputWidget(Widget): (略) def display(self, value=None, **params): return super(InputWidget, self).display(value, **params) display = update_path(display)
となっていました。このdisplayメソッドを書き直すと、以下のようになります。
def display(self, value=None, **params): update = append_to_path(self, None) returnval = super(InputWidget, self).display(value, **params) if update: pop_from_path() return returnval
super(InputWidet, self).displayメソッドというのは、turbogears/widgets/base.pyのWidgetクラスのdisplayメソッドで、
class Widget(object): (略) def display(self, value=None, **params): (略) return view.engines.get('kid').transform(params, self.template_c)
となっており、最終的にウィジェットを表示するメソッドです。すなわち、InputWidgetクラスを継承するウィジェットを表示すると、その中にあるウィジェットのid属性には、親ウィジェットの名前が追加されることになります。ここで継承関係を確認します。
InputWidget CompoundWidget ^ ^ | | CompoundInputWidget ^ | FormFieldsContainer ^ | Form
FormクラスはInputWidgetクラスを継承しています。よって、フォームの中のウィジェットのid属性の値には、フォームの名前が付加されます。
問題点
このTurboGearsの仕様は、オブジェクトを一意に決定できるidを生成しようという意図によるものです。それは理解できるのですが、問題があります。私が現在開発しているアプリケーションのある画面では、自分で作成したウィジェット(CompoundWidgetを継承しています)を含むフォームウィジェットを使っています。この画面ではある操作をすると、Ajaxによってそのウィジェットを置き換える新しいHTMLが返ってくるという仕組みになっています。
+----------------+ |ブラウザ | |+--------------+| ||フォーム || ||+------------+|| +------------+ +------+ |||ウィジェット|<---|ウィジェット|---|サーバ| ||+------------+|| +------------+ +------+ |+--------------+| +----------------+
ここで問題というのは、初期画面表示の段階ではフォームを表示するので、中のウィジェットのid属性の値にはフォームの名前が付加されていますが、Ajaxでウィジェットだけ送るときはウィジェットしか表示しないので、id属性の値にフォームの名前が付加されないのです。これによって、ある操作をするとウィジェットのidが変わってしまうという現象が起きてしまいます。
解決策
この問題を解決するため、ウィジェットだけを返すメソッドに、以下を記述しています。
from turbogears.widgets import forms from turbogears import widgets forms.append_to_path(widgets.Form(name="foo"), None)
すなわち、架空のフォームを追加して、あたかもウィジェットがフォームの中にあるかのようにするわけです。
所感
Ajaxを使うとき、ユーザインターフェースをどこで作成するかによって、2つの方法があると思います。
- 表示に必要なデータのみをJSONなどの形式でもらい、ブラウザ側でHTMLを組み立てる。
- 表示するHTMLをサーバで生成し、ブラウザではそれを表示するだけ。
1.の方法だと、HTMLを作成するコードをサーバ側とブラウザ側の2箇所で書かなくてはならず、二度手間になるので、私は2.の方法をとっています。しかし、TurboGearsは2.の方法とは相性が悪いようです。今回とった解決策も、TurboGearsの内部に踏み込んでいるので、後のバージョンでも可能かどうか、不安です。