Pyramid + SQLAlchemy + FormAlchemy (3)

(承前)

METAL

ZPT のマクロ機能である METAL を Pyramid 上で使用するのは簡単で、 ここ に書かれている通りである。要するに、マクロファイルを pyramid.renderers.get_renderer() でパースさせて、 そのインスタンスをテンプレートにデータとして引き渡せば良い。 マクロの記法自体は ZPT と同様である。 Zope では named template など、マクロオプジェクトへの特別な参照方法があったが、 pyramid-chameleon にはない、ことが違いか。

リレーション

テーブル間のリレーションの設定には 2種類が必要で、

  • RDBMS 上のテーブル定義での外部キー制約の設定
  • Python コード上でリレーションを扱うための設定

を別々に指定する。これらはモデルに指定する。

alc/models.py に以下を追加する。

class Order(Base):
  __tablename__ = 'orders'
  id = Column(Integer, primary_key=True)
  fruit_id = Column(Integer, ForeignKey('fruits.id'))
  amount = Column(Integer)

  fruit = relation(Fruit, backref=backref('orders', order_by=id))

ここで、fruit_id が RDBMS 上での外部キーを指示しており、 fruit がリレーション先の Fruit オブジェクトを格納する変数である。 Python コード上で fruit 変数にアクセスすると自動的にデータベースに クエリが発行されて対応する Fruit オブジェクトが fruit 変数に 格納されるという動作をする。

backref 云々というのはリレーション先の Fruit クラス上に 逆方向のリレーションを自動的に作成するように指示するコードで、 Fruit.orders に関連する Order オブジェクトが自動的に格納されるようになる。

モデルを追加したのでテーブルを作成する。

$ initialize_alc_db development.ini#truemain

Order 用に URL パターンを追加する。__init__.main() に

config.add_route('order', '/order')

そしてビューも追加する。views.py に

@view_config(route_name='order', renderer='templates/fruit.pt')
def order_view(request):
  num = DBSession.query(Order).count()
  action_url = request.route_url('order')

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

  fs = FieldSet(Order, DBSession) # notice
  return {'number': num, 'form': fs.render(), 'action_url': action_url,
      'submit_label': 'Add New Order'}

ここで注意が必要なのは # notice の部分で、リレーションが無い場合は

fs = FieldSet(Order)

でも動作するのだが、リレーションを設定していると、リレーション先の テーブル内容を取得するために自動的にクエリが発行される場合があるため、 DBへのセッションを与えておく必要がある。上記の場合では return 文にある fs.render() を実行する時にリレーションを解決しようとするため、 セッションを指定しないと例外が出る。

Exception: No session found.  Either bind a session explicitly, or specify relation options manually so FormAlchemy doesn't try to autoload them.

さてアプリケーションを再起動して http://hogehoge/alc/order にアクセスすると TypeError が出る。

TypeError: lists/tuples are no longer allowed as elements in the 'options' arg: (u'', u'None')

これは WebHelpers2 の仕様変更に FormAlchemy が追従していないために 発生する例外である。PyPI 上にある最新の FormAlchemy は Ver 1.5.5 であるが、 GitHub には開発版の 1.6-dev があり、そちらのコードは WebHelpers2 の 仕様変更に追従している。変更点は 1行だけであるので、ローカルのファイルを 手作業で変更した方が早い。

修正が必要なファイルは formalchemy/helpers.py で、 diff は

@@ -173,7 +173,7 @@ def select(name, selected, select_options, **attrs):
     """
     if 'options' in attrs:
         del attrs['options']
-    select_options = _sanitize_select_options(select_options)
+    select_options = (tags.Option(*reversed(o)) for o in _sanitize_select_options(select_options))
     _update_fa(attrs, name)
     if six.PY3 and isinstance(selected,map): # this test fails with py2
         selected = tuple(selected)

アプリケーションを再起動して http://hogehoge/alc/order にアクセスすると このような画面が表示される。

../../../_images/order1.png

Fruit 欄にインスタンスのメタ情報が表示されてしまっているが、 これは Fruit クラスの __repr__() をオーバーライドしていないために デフォルトの __repr__() 函数が使用されているためである。 従って Fruit クラスの定義に __repr__() を追加する。

def __repr__(self):
  return self.name

アプリケーションを再起動するとドロップダウンメニューの表示が変わる。

../../../_images/order2.png

(続く)