[Python][TurboGears] TurboGearsでBlog作ってみる その3 入力チェック

http://d.hatena.ne.jp/aodag/20060518/1147968387
http://d.hatena.ne.jp/aodag/20060521/1148210554

えらく長く時間が開いてしまった。
TurboGears勉強会で続きはどうなってるのだという突っ込みがありましたので、続き。
その1, その2 を書いた当時のソースが残っていなかったため微妙にメソッド名や処理が異なる場合があります。
ある程度書き終わったら、いったんまとめるのでご容赦を。

では入力チェックを入れよう。
今回チェックする内容は以下のとおり

  • 日付チェック
  • 必須入力チェック

TurboGearsの入力チェックはFormEncodeを使う。
また、TurboGearsで入力チェックが追加されているので、そちらも使う。
turbogears.validators はformencode.validatorsをインポートしているので、以下のようにcontroller にインポートする

from turbogears import validators

また、入力チェックをさせるためのデコレータにvalidate、エラー発生時の遷移を指定するためのerror_handlerをインポートする

from turbogears import validate, error_handler

ブログ保存時に入力チェックを行うため、saveBlog メソッドにバリデータの設定を付け加える

    @expose()
    @error_handler(editBlog)
    @validate(validators={'date':validators.DateTimeConverter(format="%Y/%m/%d"),
                          'title':validators.UnicodeString(not_empty=True),
                          'description':validators.UnicodeString(not_empty=True)})
    @identity.require(identity.not_anonymous())
    def saveBlog(self, id=None, date=None, title=None, description=None):

validatorsを通すと、メソッドで受け取る時に、文字列からPythonオブジェクトに変換されている。
DateTimeConverterはdatetime.datetime になる。
他はユニコード文字列のままだ。
SQLiteを使っている場合、DateCol にdatetime を保存することはできるが、読み出しでエラーになってしまう。(時刻部分の文字列をパースできなくなるため)
DateCol に代入するときには、dateメソッドを使ってdatetime.date オブジェクトにする。

        if id is not None:
            b = Blog.get(id)
            b.set(date=date.date(), title=title, description=description)
        else:
            b = Blog(date=date.date(), title=title, 
                     description=description, 
                     authorID=identity.current.user.id)

エラーが発生した場合は、error_handlerで設定したeditBlogが呼び出される。
このとき、元の入力内容とエラー内容も引数で渡されてくる。
これらを受け取るため、引数を追加する。

    def editBlog(self, id=None, title=None, date=None, description=None, tg_errors=None):

メソッド内の処理はこんな感じ。

        inputs = {}
        if tg_errors is None and id is not None:
            blog = Blog.get(id)
            inputs['id'] = blog.id
            inputs['title'] = blog.title
            inputs['date'] = blog.date.strftime('%Y/%m/%d')
            inputs['description'] = blog.description
        else:
            inputs['id'] = id
            inputs['title'] = title
            inputs['date'] = date
            inputs['description'] = description
        if tg_errors is None:
            tg_errors = {}
        return dict(inputs=inputs, tg_errors=tg_errors)

入力エラー処理時、更新時、新規時によって入力値をフォームに出すため、tg_errorsとidの有無で切り替える。

HTMLフォームは以下のように修正する。

  <form action="saveBlog" method="POST">
    <input type="hidden" name="id" value="${inputs['id']}" py:if="inputs.get('id', None)"/>
    <table>
      <tr>
        <td>
          <label for="date">日付</label>
        </td>
        <td>
          <input type="text" id="date" name="date" value="${inputs.get('date', None)}"/>
          <div py:if="tg_errors.has_key('date')" 
               py:content="tg_errors['date']"/>
        </td>
      </tr>
      <tr>
        <td>
          <label for="title">タイトル</label>
        </td>
        <td>
          <input type="text" name="title" value="${inputs.get('title', None)}"/>
          <div py:if="tg_errors.has_key('title')" 
               py:content="tg_errors['title']"/>
        </td>
      </tr>
      <tr>
        <td>
          <label for="description">内容</label>
        </td>
        <td>
          <textarea name="description" py:content="inputs.get('description', None)"/>
          <div py:if="tg_errors.has_key('description')" 
               py:content="tg_errors['description']"/>

        </td>
      </tr>
    </table>
    <button type="submit">保存</button>
    </form>

idを隠しフィールドにもたせるが、Noneの場合はエレメントごと表示させない。(フォームがある時点で、id='' と解釈されてしまうのが気持ち悪いため)
また、各入力フィールドの下にエラーメッセージを出す。
tg_errorsのキーがエラーのあったフィールド名になっている。

次は、ウィジェットか。

[Python][TurboGears] TurboGearsでBlog作ってみる その4 フォームウィジェット

TurboGearsでは再利用可能なビューをウィジェットとして取り扱うことができる。

widgets.TableForm などを使ってその3で作ったビューをウィジェットにしてみよう。

ウィジェットを使うには、turbogears.wigets モジュールが必要

from turbogears import wigets

ウィジェットの定義。
kidテンプレートの中でごちゃごちゃやるより、遥かに楽だ。

class BlogEditFields (widgets.WidgetsList):
    id = widgets.HiddenField('id')
    date = widgets.CalendarDatePicker("date", 
                                      label=unicode('日付', 'utf-8'),
                                      validator=validators.DateTimeConverter(format='%Y/%m/%d'),
                                      format='%Y/%m/%d')

    title = widgets.TextField("title",
                              label=unicode('タイトル', 'utf-8'),
                              validator=validators.UnicodeString(not_empty=True))
    description = widgets.TextArea("description",
                                   label=unicode('内容', 'utf-8'),
                                    validator=validators.UnicodeString(not_empty=True))

editBlogForm = widgets.TableForm(fields=BlogEditFields(),
                                 submit_text=unicode('保存', 'utf-8'))

TextFieldやHiddenFieldは名前のとおり。
CalendarDatePickerは入力フィールドと、選択可能なカレンダーを組み合わせたものだ。
使えるウィジェットはtoolboxのWidget Browserで確認できる。

$ tg_admin toolbox

TableForm の fields引数は、ウィジェットのリストでもよいので、WidgetsList は別に使わなくてもいいのだが、名前空間を切れるのと、リストの中でごちゃごちゃやらなくてすむので使っている。
editBlogForm がフォームィジェットになる。
kidテンプレート内で使うため、editBlog メソッドの戻り値に追加する。

        return dict(inputs=inputs, tg_errors=tg_errors,
                    editForm=editBlogForm)

kidテンプレートの中で、editBlogForm を呼び出す(__call__)とフォームウィジェットXHTMLを出力してくれる。
引数で値を指定すると、その値を各フィールドに設定してくれる。
入力エラー時には、自動で値保持をしていてくれるが、更新の場合を考慮してコントローラから入力値を渡すようにしている。

  <form py:replace="editForm(action='saveBlog', method='POST', value=inputs)"/>

フォームウィジェットはバリデータも持っている。
validate デコレータに渡すと、入力チェックをしてくれる。
saveBlogメソッドの入力チェックをeditBlogFormで行う場合は以下のようになる。

    @expose()
    @error_handler(editBlog)
    @validate(editBlogForm)
    @identity.require(identity.not_anonymous())
    def saveBlog(self, id=None, date=None, title=None, description=None):

いいね。DRYだね。

[Python][TurboGears] TurboGearsでBlog作ってみる その5 カスタムウィジェット

その4では既にあるウィジェットを組み合わせて新しいウィジェットを作った。
しかし、既にあるウィジェットだけでことが足りるわけではない。
最終的に生成されるXHTMLをコントロールするには、turbogears.wigets.Wdigetクラスを継承して、カスタムウィジェットを定義する。

Blog を表示する機能は多くのページで使用する。
BlogWidgetを作成して、再利用しよう。
そして作ったのが以下にあります。

class BlogWidget(widgets.Widget):
    params = ['showDescription']
    def __init__(self, showDescription=True):
        self.showDescription = showDescription
    template = unicode('''<div xmlns="http://www.w3.org/1999/xhtml"
    xmlns:py="http://purl.org/kid/ns#"
    class="blog" id="blog-${value.id}">
    <span class="blog-date" py:content="value.date.strftime('%Y/%m/%d')">YYYY/MM/DD</span>
    <span class="blog-title" py:content="value.title">Blog Title</span>
    <div class="blog-description" py:content="value.description" py:if="showDescription">Blog Description</div>
    <div class="blog-control">
    <a href="editBlog?id=${value.id}" py:if="value.author == tg.identity.user">編集</a>
    <a href="deleteBlog?id=${value.id}" py:if="value.author == tg.identity.user">削除</a>
    </div>
    </div>
    ''', 'utf-8')

クラス変数templateがポイント。
これ、kidテンプレートを直接書いてもいいし、kidテンプレートファイルを指定してもいい。
内部では、なにやら1文字めが<かどうかで処理を分けているご様子。
実際に呼び出すときは、displayメソッドを使うが、その第一引数がvalueという名前で参照可能。
ここでは、Blogオブジェクトが渡されることを前提としている。
また、追加で参照したいインスタンスメンバはparamクラス変数に名前を追加する。
本文を出さない使い方もある(トップページの最新ブログリストなど)ので、オプション引数として、showDescription を使えるようにしている。

使い方はこんな感じ

<div py:for="blog in blogs" py:replace="blogWidget.display(blog)"/>