TurboGearsでblog作ってみる(その2)

前後半では収まらんので、その2にします。


では、editBlogの内容だ。

    @expose(template="blog.templates.editBlog")
    @identity.require(identity.not_anonymous())
    def editBlog(self, blogId):
	blog = Blog.get(blogId)
	if blog.author != identity.current.user:
	    raise identity.IdentityException()
	return dict(blog=blog)

主キーによるアクセスは、クラスメソッドgetを使用する。
宣言的なアクセスコントロールは新規作成時と同じだが、
今回は更新対象のBlogのauthorであるという条件が加わる。
メソッドに宣言することはできないので、blog取得後にプログラム的アクセスコントロールを行う。
editBlogテンプレートは以下のようにしておく。

<body>
<form action="save" method="post">
<input type="hidden" name="blogId" value="${blog.id}"/>
<table>
<tr><td><label>Date:</label></td>
<td><input name="date" value="${blog.date}"/></td></tr>
<tr><td><label>Title:</label></td>
<td><input name="title" value="${blog.title}"/></td></tr>
<tr><td><label>Description:</label></td>
<td><textarea name="description">${blog.description}</textarea></td></tr>
</table>
<input type="submit" value="Update"/>
</form>
</body>

${...}内はpython式を書くことができる。
上では、コンテキスト中のblogオブジェクトにアクセスして、各プロパティを取得している。

mypageのEditリンクをたどると、そのblogオブジェクトの更新フォームに遷移する。
この状態のURLをどこかに記録しておこう。

再度tg-admin shellからユーザーを追加してそのユーザーでログインする。
さきほど記録しておいたURLにアクセスしてみよう。
更新フォームにはアクセスできず、ログインフォームが表示される。
ここでblogを作成したユーザーでログインすると、更新フォームが表示される。

実際の更新はsaveメソッドを利用する。

    @expose()
    @identity.require(identity.not_anonymous())
    def save(self, date, title, description, blogId=None):
	hub.begin()
	try:
	    if blogId is None:
		b = Blog(date=date, title=title, description=description, authorID=identity.current.user.id)
	    else:
		b = Blog.get(blogId)
		if b.author != identity.current.user:
		    raise identity.IdentityException()
		b.date = date
		b.title = title
		b.description = description
	    hub.commit()
	except:
	    hub.rollback()
	    raise
	hub.end()
	turbogears.redirect("mypage")

blogオブジェクトのプロパティを変更すると、その内容がDBに反映される。
hub.commitメソッドがそのトリガーだ。
ここでも、blog.authorによるアクセスコントロールをしなければならない。
削除時も同様だろう。
Blogクラスのクラスメソッドとしてしまおう。

    def identityGet(cls, id):
	b = cls.get(id)
	if b.author != identity.current.user:
	    raise identity.IdentityException()
	return b
    identityGet = classmethod(identityGet)

更新などの場合はこちらを使用する。

	blog = Blog.identityGet(blogId)

削除もできるようにする。

    @expose()
    @identity.require(identity.not_anonymous())
    def deleteBlog(self, blogId=None):
	hub.begin()
	try:
	    b = Blog.identityGet(blogId)
	    Blog.delete(b.id)
	    hub.commit()
	except:
	    hub.rollback()
	    raise
	hub.end()
	turbogears.redirect("mypage")

これで、mypageからBlogを作成、更新、削除することができるようになった。

閲覧用ページを作る。
閲覧用にblogメソッドを作成する

    from sqlobject import *

    ...

    @expose(template="blog.templates.blog")
    def blog(self, userName):
	blogs = Blog.select(AND(Blog.q.authorID==User.q.id,
			    User.q.user_name==userName), orderBy=Blog.q.date)
	
	return dict(blogs=blogs)

select内の検索条件は、AND関数などを使用して複合条件にすることができる。
また、順序指定のためにorderByキーワード引数にカラムを指定している。

表示するとともに、コメントも受け付けるようにする。
ログイン中はコメントのauthorにユーザー名をデフォルトで出す。

	<form action="addComment" method="post">
	  <input type="hidden" name="blogId" value="${blog.id}"/>
	  <table>
	    <tr>
	      <td>
		<input py:if="not tg.identity.anonymous" type="input" name="author" value="${tg.identity.user.user_name}"/>
		<input py:if="tg.identity.anonymous" type="input" name="author"/>
	      </td>
	    </tr>
	    <tr>
	      <td>
		<textarea name="data"/>
	      </td>
	    </tr>
	  </table>
	  <input type="submit" value="Add"/>
	</form>

また、コメントも表示する。

	<div py:for="comment in blog.comments">
	  <span py:content="comment.author">comment author
	  <span py:content="comment.data">Comment
	</div>

コメントの追加は以下のようにする。

    @expose()
    def addComment(self, blogId, data, author):
	print blogId, data, author

	hub.begin()
	try:
	    c = Comment(date=date.today(), blogID=blogId)
	    c.data=data
	    c.author=author
	    hub.commit()
	except:
	    hub.rollback()
	    raise
	hub.end()
	turbogears.redirect("blog?blogId=%s" % blogId)

最初は以下のようにしたのだが、codecエラーが出てしまった。
上記のようにするとなぜか上手くいく。

    @expose()
    def addComment(self, blogId, data, author):
	hub.begin()
	try:
	    c = Comment(blogID=blogId, data=data, author=author)
	    hub.commit()
	except:
	    hub.rollback()
	    raise
	hub.end()
	turbogears.redirect("blog?blogId=%s" % blogId)

上記の問題のため、Commentクラスのコンストラクタでdataとauthorを指定しなくてもよいように、Commentクラスの定義を少し修正した。

class Comment(SQLObject):
    date = DateCol()
    data = UnicodeCol(default=None)
    author = UnicodeCol(default=None)
    blog = ForeignKey("Blog")

テンプレートの修正
ログインやログアウトリンクはmaster.kidにある。
ログイン中は、ログアウトリンクとともに、MyPageへのリンクも表示させよう。

        <span py:if="not tg.identity.anonymous">
            Welcome ${tg.identity.user.display_name}.
            <a href="/logout">Logout
            <a href="/mypage">My Page
        </span>

トップページの作成
トップページには、最新情報を出すことにしよう。
Blogから日付の降順で、10件分の情報を出す。

    @expose(template="blog.templates.welcome")
    def index(self):
	blogs = Blog.select(orderBy=Blog.q.date)[:10]
	return dict(blogs=blogs)

[:10]というのはスライスと呼ばれる操作だ。
この場合は、シーケンスの先頭から10個分の要素を取得する。
sqliteを使用している場合は、この操作はオーバーライドされていて、
limitを使用したクエリの実行として実装されている。

welcome.kidではこのblogリストを表示する。
blogそのものへのリンクとauthorへのリンクを作成する。

<table>
<tr py:for="blog in blogs">
  <td>
    ${blog.date}
  </td>
  <td>
    <a href="blog?blogId=${blog.id}" py:content="blog.title">
    Blog Title 
    </a>
  </td>
  <td>
    <a py:content="blog.author.display_name" 
       href="blog?userName=${blog.author.user_name}">Blog Author</a>
  </td>
</tr>
</table>

次回は入力値チェックなど