[TurboGears] ブログを作成する。その2

注意

この記事は、id:SumiTomohiko:20070115:1168855922の続きです。

ユースケース

ここで、これから作るブログの機能を整理します。具体的には、ユースケースを作ることで、必要な機能をまとめます。

このブログは、個人的に使用するものですが、それでも記事の追加や削除は、登録されているユーザのみに限定したいものです。しかし、記事を閲覧するのにわざわざログインしたくはありません。よって、アクターは、匿名ユーザとログインユーザの2種類を考えます。ここで、ログインしていないユーザを匿名ユーザ、ログインしているユーザをログインユーザとします。

ユーザを区別するとなると、ユーザを管理(追加、編集、削除)する機能も必要になりますが、このブログではこの機能は持たないことにします。ユーザの追加は最初の1回しかしないのに、管理画面を作るのは手間です。ユーザの編集も削除も基本的には行いません。ユーザの管理は、Catwalkか、tg-admin shellコマンドで行うことにします。

これをふまえると、ユースケースは以下のようになります。

アクター アクション
匿名ユーザ 記事を閲覧する。
匿名ユーザ ログインする。
ログインユーザ 記事を閲覧する。
ログインユーザ 自分の記事を追加する。
ログインユーザ 自分の記事を編集する。
ログインユーザ 自分の記事を削除する。
ログインユーザ ログアウトする。

URL

次に、このブログの機能を実行するURLを設計します。ここでは、以下のようにします。

URL 説明
/ エラーとします。
/date?user=&date=&span=<span> ユーザの、日付から<span>日間の記事をさかのぼって表示します。
/article?article=&count= の記事から、同じユーザの、数の記事を、さかのぼって表示します。
/add 記事の追加画面を表示します。ユーザが匿名ユーザであれば、エラーにします。
/doadd 記事を追加します。追加したら、"/date?user=<追加した記事のユーザID>&date=<追加した記事の日付>&span=1"にリダイレクトします。ユーザが匿名ユーザであれば、エラーにします。
/edit?article_id= の記事の編集画面を表示します。ユーザが匿名ユーザであれば、エラーにします。また、他のユーザの記事を編集しようとしたら、エラーにします。
/doedit?article_id= の記事を更新します。更新したら、"/date?user=<更新した記事のユーザID>&date=<更新した記事の日付>&span=1"にリダイレクトします。また、他のユーザの記事を編集しようとしたら、エラーにします。
/delete?article_id= の記事を削除します。また、他のユーザの記事を削除しようとしたら、エラーにします。
/login ログイン画面を表示します。
/dologin ログインします。ログインしたら、ログイン画面の直前に表示していた画面にリダイレクトします。
/logout ログアウトします。ログアウトしたら、直前に表示していた画面にリダイレクトします。ユーザが匿名ユーザならば、エラーにします。

画面

本来なら、この辺りで画面を設計しなければならないのですが、これは作りながら適当に決めることとし、ここでは省略します。

モデル

id:SumiTomohiko:20070115:1168855922で、tg-admin quickstartコマンドに--idenityオプションをつけたので、tgdiary/model.pyには、既に以下のクラスが作られています。

  1. Visit
  2. VisitIdentity
  3. Group
  4. User
  5. Permission

このブログでは、ユーザはグループ分けしませんので、Groupクラスは不要です。また、権限もログインしているユーザのIDでしか判定しないので、Permissionクラスも不要です。よって、この2つのクラスは削除します。

Visitクラスは、セッションを区別するのに使われているようです(ログインすると、このテーブルにレコードが追加され、tg-visitという名前で、追加されたレコードの値を持つクッキーが送信されます)。VisitIdenityクラスも、どのように使われるのかわかりませんが、名前からするとVisitクラスと同様、セッションの管理に必要な気がするので、残します(VisitIdentityの使いかたは、調査中です)。

ブログの記事は、Articleクラスで表します。このクラスは、Userクラスと1体多の関係にあり、記事を書いた日時 (created) と題名 (title) 、本文 (body) を属性に持ちます。

Userクラスも、メールアドレスなどは不要なので、整理します。

結局、tgdiary/model.pyは、以下のようになりました。

from datetime import datetime
from turbogears.database import PackageHub
from sqlobject import *
from turbogears import identity 

hub = PackageHub("tgdiary")
__connection__ = hub

# identity models.
class Visit(SQLObject):
    class sqlmeta:
        table = "visit"

    visit_key = StringCol(length=40, alternateID=True,
                          alternateMethodName="by_visit_key")
    created = DateTimeCol(default=datetime.now)
    expiry = DateTimeCol()

    def lookup_visit(cls, visit_key):
        try:
            return cls.by_visit_key(visit_key)
        except SQLObjectNotFound:
            return None
    lookup_visit = classmethod(lookup_visit)

class VisitIdentity(SQLObject):
    visit_key = StringCol(length=40, alternateID=True,
                          alternateMethodName="by_visit_key")
    user_id = IntCol()

class User(SQLObject):
    """
    Reasonably basic User definition. Probably would want additional attributes.
    """
    # names like "Group", "Order" and "User" are reserved words in SQL
    # so we set the name to something safe for SQL
    class sqlmeta:
        table = "tg_user"

    user_name = UnicodeCol(length=16, alternateID=True,
                           alternateMethodName="by_user_name", notNone=True)
    password = UnicodeCol(length=40, notNone=True)
    articles = MultipleJoin('Article')
    created = DateTimeCol(default=datetime.now, notNone=True)

    def _set_password(self, cleartext_password):
        "Runs cleartext_password through the hash algorithm before saving."
        hash = identity.encrypt_password(cleartext_password)
        self._SO_set_password(hash)

    def set_password_raw(self, password):
        "Saves the password as-is to the database."
        self._SO_set_password(password)

class Article(SQLObject):
    title = UnicodeCol(length=255, notNone=True)
    body = UnicodeCol(length=8192, notNone=True)
    user = ForeignKey('User', notNone=True)
    created = DateTimeCol(default=datetime.now, notNone=True)

# vim: tabstop=4 shiftwidth=4 expandtab

tg-admin sqlコマンドで、データベースを生成します。

$ tg-admin sql create                       [~/projects/tgdiary]
Using database URI sqlite:///home/tom/projects/tgdiary/devdata.sqlite

sqlite3コマンドで、どんなスキーマになったか確認します。

$ sqlite3 devdata.sqlite                    [~/projects/tgdiary]
SQLite version 3.3.5
Enter ".help" for instructions
sqlite> .schema
CREATE TABLE article (
    id INTEGER PRIMARY KEY,
    title VARCHAR(255) NOT NULL,
    body VARCHAR(8192) NOT NULL,
    user_id INT NOT NULL CONSTRAINT user_id_exists REFERENCES tg_user(id) ,
    created TIMESTAMP NOT NULL
);
CREATE TABLE tg_user (
    id INTEGER PRIMARY KEY,
    user_name VARCHAR(16) NOT NULL UNIQUE,
    password VARCHAR(40) NOT NULL,
    created TIMESTAMP NOT NULL
);
CREATE TABLE visit (
    id INTEGER PRIMARY KEY,
    visit_key VARCHAR(40) NOT NULL UNIQUE,
    created TIMESTAMP,
    expiry TIMESTAMP
);
CREATE TABLE visit_identity (
    id INTEGER PRIMARY KEY,
    visit_key VARCHAR(40) NOT NULL UNIQUE,
    user_id INT
);

また、tgdiary/config/app.cfgに、GroupクラスとPermissionクラスを設定している箇所があるので、これらを削除します。

$ diff -u tgdiary/config/app.cfg.orig tgdiary/config/app.cfg
--- tgdiary/config/app.cfg.orig 2007-01-17 20:32:34.489831750 +0900
+++ tgdiary/config/app.cfg      2007-01-17 20:32:42.906357750 +0900
@@ -97,8 +97,6 @@
 # SQL keywords for class names (at least unless you specify a different table
 # name using sqlmeta).
 identity.soprovider.model.user="tgdiary.model.User"
-identity.soprovider.model.group="tgdiary.model.Group"
-identity.soprovider.model.permission="tgdiary.model.Permission"
 
 # The password encryption algorithm used when comparing passwords against what's
 # stored in the database. Valid values are 'md5' or 'sha1'. If you do not