SQLAlchemyとGenshiとToscaWidgetsでBBSみたいなものを作ってみるテスト

前回SQLAlchemyで超簡易コメントモデルを作ってみたのですが、他にもGenshiやらToscaWidgetsやらをいじってみるためにBBSライクなものを作成してみます。

準備する

事前準備としてTurboGears以外に

easy_install gsquickstart
easy_install ToscaWidgets
easy_install twForms

としておくとよさげなので、実行しました。

gsquickstartはGenshiでプロジェクトを作るのに便利です。quickstartに「-t tggenshi」を与えることで、デフォルトのテンプレートシステムが(Kidではなく)Genshiになります。

またToscaWidgets、twFormsはToscaWidgetsを使うために必要です。

プロジェクトを作る

SQLAlchemyとGenshiでプロジェクトを作成するために「-s」と「-t tggenshi」オプションを与えてquickstartします。

tg-admin tgbbs -s -t tggenshi

モデルを作る

前回とほぼ同じようにモデルを作成します。前回は名前の記入欄がなかったので名前のカラムも追加しています。ただし今回はidentityは用いていません。投稿日を記録するためにdatetimeをインポートしておきます。

from sqlalchemy import *
from turbogears.database import metadata, session
from sqlalchemy.ext.assignmapper import assign_mapper
from datetime import datetime

# comment schema

comments_table = Table('comment', metadata,
Column('comment_id', Integer, primary_key=True),
Column('comment_author', Unicode(64)),
Column('comment_title', Unicode(255)),
Column('comment_body', Unicode(1024)),
Column('posted', DateTime, nullable=False, default=datetime.now),
)

class Comment(object):
def __init__(self, comment_author, comment_title, comment_body):
self.comment_author, self.comment_title, self.comment_body = comment_author, comment_title, comment_body

assign_mapper(session.context, Comment, comments_table)

またテスト用に予めデータを投入しておきます。

tg-admin sql create

したあとで、tg-adminのシェルから

c1 = Comment('hoge', 'Test Comment 1', 'This is a test.')
c2 = Comment('fuga', 'Test Comment 2', 'This is a test, too.')
c1.flush()
c2.flush()

どんな感じの画面にするのか

BBSは1画面ですべて完結するものにしたいと思います。

画面上部には、投稿された複数のコメントがずらずら~っと並び、その下にコメント用のフォームがあるようにしてみます。フォームにはタイトル、名前、本文の入力欄があって、そこから投稿すると一覧にコメントが追加される、というような感じで。

とりあえず投稿されたコメントを表示してみる

コントローラを編集

まずは画面上部にコメント一覧を表示することを考えてみます。コメントを順番にリストに入れて、このリストを返すようなメソッドを用意してやることにします。テンプレートは「bbslist」という名前にすることにします。

from turbogears import controllers, expose, flash

# モデルをインポートする
from model import *

# import logging
# log = logging.getLogger("tgbbs.controllers")

class Root(controllers.RootController):
# テンプレートに「bbslist」を指定する
@expose(template="tgcapthcabbs.templates.bbslist")
def index(self):
# 空のリストを用意する
comment_list = []
# コメントを順番にリストに加える
for c in Comment.select():
comment_list.append(c)
# コメントが入ったリストを返す
return {"comment_list":comment_list}

テンプレートを作成

tgbbs/templatesディレクトリに以下のようなbbslist.htmlを作成しました。

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:py="http://genshi.edgewall.org/"
xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="master.html" />

<head>
<meta content="text/html; charset=UTF-8" http-equiv="content-type" py:replace="''"/>
<title>Welcome to TurboGears</title>
<link rel="stylesheet" type="text/css" media="screen" href="${tg.url('/static/css/style.css')}" />
</head>

<body>
<h1>TG BBS</h1>
<h2>Comments</h2>
<div py:for="comment in comment_list">
<h3 py:content="comment.comment_title">comment title</h3>
<p py:content="comment.comment_author">comment author</p>
<p py:content="comment.comment_body">comment body</p>
<p py:content="comment.posted">posted at</p>
</div>
</body>
</html>

この時点でstart-tgbbs.pyを実行し、「http://localhost:8080/」へアクセスしてみると…

コメント一覧の表示

表示されたよワショーイヾ(゜∀゜)/

フォームの作成

もちろんTurboGearsについてくるWidgetを使ってもよいのですが、いつもの病気の所為でToscaWidgetsを使うことにしました。ToscaWidgetsはTurboGears付属のWidgetを書き換えたもので、次期TurboGearsの標準Widgetになる予定です。

使い方がさっぱりわからないのですが、一応TurboGearsでToscaWidgetsを使うためのちょっとした説明が書いてあります。フォームをクラスとして定義してそのインスタンスを使えばよいのかな…。とりあえずサンプルアプリがあるようなので、これを参考に作りたいと思います。サンプルアプリのコントローラには

from toscawidgets.widgets.forms.samples import AddUserForm

(中略)

class Root(controllers.RootController):
add_user_form = AddUserForm('form', action='save', submit_text="Add")

などと書いてあります。

ToscaWidgetsのパッケージにはサンプルフォームが同梱されており、これを利用しているようです。そこで「C:\Python25\Lib\site-packages\twforms-0.1a2dev_r2861-py2.5.egg\toscawidgets\widgets\forms\samples.py」を見てみると、以下のような感じでフォームが定義されていました。

class AddUserForm(ListForm):
class fields(WidgetsList):
id = HiddenField(default="I'm hidden!")
name = TextField(
validator = UnicodeString(not_empty=True),
default = "Your name here"
)
gender = RadioButtonList(
options = "Male Female".split(),
)
age = SingleSelectField(
validator = Int,
options = range(100)
)

これをまねっこして考えれば

class CommentForm(ListForm):
class fields(WidgetsList):
name = TextField(
validator = UnicodeString(not_empty=True),
)
...

といった感じでコメント用のフォームを用意して、それを使ってやればいいのかしらん。

ということで見よう見まねのコメントフォームクラス。

from formencode.validators import *
from formencode.schema import Schema

from toscawidgets.api import WidgetsList
from toscawidgets.widgets.forms import *

class FilteringSchema(Schema):
    filter_extra_fields = True
    allow_extra_fields = True

class CommentForm(ListForm):
    class fields(WidgetsList):
        comment_title = TextField(
validator=UnicodeString(not_empty=True)
)
        comment_author = TextField(
validator=UnicodeString(not_empty=True)
)
        comment_body = TextArea(
validator=UnicodeString(not_empty=True)
)
    validator = FilteringSchema()

FormEncodeはPythonにおける様々な入出力のバリデーションを行うことができるユーティリティで、ToscaWidgetsではformencode.schema.Schemaを継承したクラスを用いることでバリデーションを行うことができるようになるようです(上記のFilteringSchemaクラス)。

ToscaWidgetsのフォームやフィールドは、toscawidgets.widgets.formsから利用できます。それぞれのWidgetを利用するにはToscaWidgetsだけではなくtwFormsもインストールする必要がありますので、not foundと言われたときにはとりあえずtwFormsがインストールされているかどうか確認した方がよさそうです。

ここではフォームとしてListFormを利用し、そのフォームにテキストフィールドとテキストエリアを配置することにします。ちなみにWidgetsListというのはwidgetsのリストを宣言するためのシンタックスシュガーらしいです。またバリデータとしてFilteringSchema()を指定しておきます。

フォームの表示

ToscaWidgetsのサンプルではルートコントローラのアトリビュートとしてフォームを定義し、indexメソッドでこれを利用しているようです。

class Root(controllers.RootController):
    add_user_form = AddUserForm('form', action='save', submit_text="Add")
    grid = DataGrid("grid", fields=fields_from_form(add_user_form))
    @expose(template="tgsample.templates.add_user")
    def index(self):
        form = self.add_user_form
...

ということでこれにならってコメントフォームを実装してみます。

from turbogears import controllers, expose, flash
from model import *
# import logging
# log = logging.getLogger("tgbbs.controllers")

# さっき作ったコメントフォームクラスを追加
# ------------------------------------------
from formencode.validators import *
from formencode.schema import Schema

from toscawidgets.api import WidgetsList
from toscawidgets.widgets.forms import *


class FilteringSchema(Schema):
    filter_extra_fields = True
    allow_extra_fields = True


class CommentForm(ListForm):
    class fields(WidgetsList):
        comment_title = TextField(
            validator=UnicodeString(not_empty=True)
        )
        comment_author = TextField(
            validator=UnicodeString(not_empty=True)
        )
        comment_body = TextArea(
            validator=UnicodeString(not_empty=True)
        )
    validator = FilteringSchema()
# ------------------------------------------


class Root(controllers.RootController):
# コメントフォームをアトリビュートとして追加
    comment_form = CommentForm("comment_form", action="add_comment", submit_text="add comment")

    @expose(template="tgbbs.templates.bbslist")
    def index(self):
# indexメソッドでコメントフォームを使う
        comment_form = self.comment_form
        comment_list = []
        for c in Comment.select():
            comment_list.append(c)
# コメントフォームをテンプレートに渡す
      return {"comment_list":comment_list, "comment_form":comment_form}

テンプレートでは、indexメソッドが返すcomment_formを利用します。コメント一覧の下にフォームを追加しました。

<body>
  <h1>TG BBS</h1>
  <h2>Comments</h2>
  <div py:for="comment in comment_list">
    <h3 py:content="comment.comment_title">comment title</h3>
    <p py:content="comment.comment_author">comment author</p>
    <p py:content="comment.comment_body">comment body</p>
    <p py:content="comment.posted">posted at</p>
  </div>
<!--コメントフォームを追加-->
  <h2>Post your comment</h2>
  <div py:replace="comment_form.display()"></div>
</body>

ここで「http://localhost:8080/」にアクセスしてみると、

コメントの一覧とフォームが表示された

表示されたよワショーイヾ(゜∀゜)/

※フォームのhtmlタグがエスケープされてしまう場合は、「tgbbs\tgbbs\config\app.config」に「toscawidgets.on = True」を記述してサーバをリスタートすると直ります。たぶん。

コメントの投稿

それでは実際にコメントを投稿することを考えます。

投稿されるコメントを処理するメソッドは「add_comment」とします。

というか、先ほどルートコントローラにcomment_formを追加したときにすでに

class Root(controllers.RootController):
    comment_form = CommentForm("comment_form", action="add_comment", submit_text="add comment")

と、actionを指定しているのでそうなるわけです(もちろん「action="save"」などと指定した場合にはsaveメソッドを作ることになります。お好きな名前でどうぞ)。

とりあえずここではadd_commentをコントローラに定義することにします。

フォームからタイトルと名前と本文を受け取って、これをデータベースに保存するので、以下のような感じになるでしょうか。リダイレクトを使うので、controllers.pyの冒頭部では、turbogearsからredirectもimportしておきます。

# redirectもimportしておく
from turbogears import controllers, expose, flash, redirect

(中略)

@expose()
def add_comment(self, comment_title, comment_author, comment_body):
    c = Comment(comment_title=comment_title, comment_author=comment_author, comment_body=comment_body)
    c.flush()
    return redirect("/")

ただしこのままではバリデーションが効かないので、以下のようにタイトルや名前、本文などの中身がないコメントもpostできてしまいます。

空っぽのコメントが追加されてしまった

デコレータでバリデーションを指定

TurboGearsでのバリデーションはデコレータを使うのが一般的ではないでしょうか。バリデーションを指定する「@validate」や、エラーが起こったときに何をするかを指定する「@error_handler」を使うのがよいでしょう。

バリデーションに関してはフォームを定義する段階で以下のように指定しましたので、

class FilteringSchema(Schema):
    filter_extra_fields = True
    allow_extra_fields = True


class CommentForm(ListForm):
    class fields(WidgetsList):
        comment_title = TextField(
            validator=UnicodeString(not_empty=True)
        )
        comment_author = TextField(
            validator=UnicodeString(not_empty=True)
        )
        comment_body = TextArea(
            validator=UnicodeString(not_empty=True)
        )
    validator = FilteringSchema()

あとは@validateにはフォームを渡してやればよいでしょう。

またエラーが検出された場合にはページ(indexメソッド)を再表示してやればよいですから、@error_handlerにはindexを渡してやることにします。ただし@validateや@error_handlerを使うためにはturbogearsからvalidateやerror_handlerをインポートする必要があります。

ということで結局add_commentメソッドは以下のようになりました。

# validateとerror_handlerをimportしておく
from turbogears import controllers, expose, flash, redirect, validate, error_handler

(中略)

# エラーが出たらindexページを再表示
@error_handler(index)
# バリデーションを有効にするフォームを指定
@validate(form=comment_form)
@expose()
def add_comment(self, comment_title, comment_author, comment_body):
    c = Comment(comment_title=comment_title, comment_author=comment_author, comment_body=comment_body)
    c.flush()
    return redirect("/")

これでバリデーションが有効になるはずです。

試しに何も入力せずに投稿しようとすると

バリデーションチェックに引っかかった

「Please enter a value」と表示されました。バリデーションが効いているみたいですね。

最後に実際にコメントを投稿してみます。

コメントの追加に成功

OK。成功です。タイポはスルーしてください。

そして眠いので寝ます。

カテゴリ
Turbogears Turbogears
トラックバック用URL:
http://nagosui.org:8080/Nagosui/COREBlog2/tgbbs-with-sqlalchemy-genshi-toscawidgets/tbping
コメントを追加

下のフォームに記入してコメントを追加できます。平文テキスト形式。

(必須)
(必須)
(必須)
(Required)
Enter the word

このBlogについて
Plone, Zope, Pythonなどのトピックについてのメモです。
カテゴリ
Plone (99)
Plone Products (23)
COREBlog2 (31)
COREBlog1 (29)
ReadingCOREBlog (7)
Zope (66)
Turbogears (18)
Django (12)
Python (26)
Linux (32)
Nagosui (13)
Design (34)
Misc (49)
moblog (5)
最近のエントリ
Plone3.2+その他もろもろのレシピ nyusuke 2009年01月07日
さらばファッキンKDDI nyusuke 2008年12月10日
Xoopsのテーマをいじる1 nyusuke 2008年12月08日
第13回名古屋大学吹奏楽団定期演奏会 nyusuke 2008年12月07日
最近のコメント
Re:WebデザイナーのためのDjangoはじめの一歩 nyusuke 2007年06月01日
Re:WebデザイナーのためのDjangoはじめの一歩 pateo 2007年05月31日
Re:東海Python Workshop 01終了 nyusuke 2007年05月31日
Re:東海Python Workshop 01終了 kfuruhata 2007年05月30日
Geek Test
I am 30% Geek.
Geek? Yes, but at least I got social skills.
You probably work in computers, or a history deptartment at a college. You never really fit in with the "normal" crowd. But you have friends, and this is a good thing.