Pyramid + SQLAlchemy + FormAlchemy (2)

(承前)

次に、モデル作成からフォーム処理までの作業の概略を確認しよう。

モデル作成

Pyramid では全般的に、ファイル名やファイルパスに暗黙のルールは無いようだ。 だからモデル定義もどこに書こうが問題ないのだが、差し当たり、スケルトンの 初期ファイルを流用しよう。

alc/models.py に Fruit モデルを以下のように定義する。

class Fruit(Base):
    __tablename__ = 'fruits'
    id = Column(Integer, primary_key=True)
    name = Column(Text)
    price = Column(Integer)

DB初期化スクリプト alc/scripts/initializedb.py に新モデルを記載する。

from ..models import (
    Fruit,
    )

スクリプトを実行すると PostgreSQL 上に fruits テーブルが作成される。

$ initialize_alc_db development.ini#truemain

URLディスパッチ

  • ‘/’ で新レコード追加ページを表示
  • ‘/{id}’ で ID が id のレコードの修正ページを表示

とする。

まず URL のパターンマッチを alc/__init__.py の main() に指定する。 ‘/’ 用は最初からスケルトンに含まれているのでそのまま残し、 ‘/{id}’ 用の指定を追加する。

config.add_route('home', '/')
config.add_route('item', '/{id}')

ここで、 home とか item とかいう文字列が URL パターンとビューを接続する キーワードとして働く。従ってアプリケーション全体でユニークでなければ ならない、はずだ。

‘{}’ はもちろんプレースフォルダとして機能する。

ビュー

ビューの登録には幾つかの方法があるらしいが、差し当たり、スケルトンをまねて view_config デコレータを使う。

たとえば以下のようなコードにしてみた。

from pyramid.view import view_config
from formalchemy import FieldSet

from .models import (
    DBSession,
    Fruit,
    )

@view_config(route_name='home', renderer='templates/fruit.pt')
def home_view(request):
  num = DBSession.query(Fruit).count()
  action_url = request.route_url('home')

  if 'form.submitted' in request.params:
    up = FieldSet(Fruit, DBSession, data=request.POST)
    if up.validate():
      up.sync()
      DBSession.add(up.model)

  fs = FieldSet(Fruit)
  return {'number': num, 'form': fs.render(), 'action_url': action_url,
      'submit_label': 'Add New Item'}

@view_config(route_name='item', renderer='templates/fruit.pt')
def item_view(request):
  item_id = request.matchdict['id']
  item = DBSession.query(Fruit).filter(Fruit.id == item_id).first()

  if 'form.submitted' in request.params:
    up = FieldSet(item, data=request.POST)
    if up.validate():
      up.sync()
      DBSession.add(up.model)

  action_url = request.route_url('item', id=item_id)
  fs = FieldSet(item)
  fs_ro = FieldSet(item)
  fs_ro.readonly = True
  return {'tgt': item, 'info': fs_ro.render(),
      'form': fs.render(), 'action_url': action_url,
      'submit_label': 'Update'}

要点としては、

  • ビュー函数の記述位置に制限はない(プロジェクトディレクトリの下であれば)。 __init__.main() の config.scan() が再帰的に探索するから。
  • ビュー函数は、テンプレートに渡すための辞書オブジェクトを返さねばならない。
  • URL ディスパッチのプレースフォルダは request.matchdict に格納される。

SQLAlchemy についてはここでは詳説しない。要するに、 SQL 文を Python 文法化 しただけのことだ。

さて問題は FormAlchemy の方だが、上記コードでは FieldSet を試している。 FieldSet にはセッションやフォームデータを渡すことができ、

  • モデルインスタンスの生成
  • フォームデータのバリデート
  • データベースの更新
  • フォームの HTML スニペットの生成

などの機能があり、便利そうではある。

ただし、すべてのデータ型に対応している訳ではなく、少なくとも ARRAY(TEXT) に対しては、

TypeError: No renderer found for field text_list. Type TEXT[] has no default renderer

というエラーが出て動作しない。 Zope では zope.formlib に sequencewidget というリスト型用のウィジェットが 最初からあって重宝したが、FormAlchemy ではそうはいかないようだ。 ORM では ARRAY など使わずに正規化する方が普通なのかも知れない。

テンプレート

Pyramid は Mako と Chameleon と2種類のテンプレートが使えるが、 このうち Chameleon は ZPT からの派生で、 TAL や METAL が使えるというので、 Chameleon を使う。

<!DOCTYPE html>
<html lang="${request.locale_name}">
  <head>
    <meta charset="utf-8">
    <title>Friut Database</title>
  </head>
  <body>
    <div tal:condition="number | nothing">
      Number of items: ${number}
    </div>
    <div tal:condition="tgt | nothing">
      ID: ${tgt.id}
      <table>
        ${info}
      </table>
    </div>
    <div>
      <form action="${action_url}" method="post">
        ${form}
        <input type="submit" name="form.submitted" value="${submit_label}"/>
      </form>
    </div>
  </body>
</html>

ZPT は ${} 構文が使えず、 tal:content や tal:attributes を使わなければ ならないところが煩雑だったが、 ${} が使えるだけで随分簡便になるものだ。

ブラウザ上の表示は、’home’ ビューが

../../../_images/home.png

‘item’ ビューが

../../../_images/item.png

のようになる。試験のため見栄えが良くないが、 METAL が使えるということはマクロが使えるということなので、 ページレイアウトをマクロ化したりすれば多少はマシなものが作れるだろう。

あと確認が必要なのは、

  • 外部キーを使ったリレーションに対する FormAlchemy の振る舞い
  • METAL
  • pyramid_formalchemy のビュー機能

あたりか。

(続く)