2007年12月14日

wsgiでwiki作ってみる


とりあえず動かすまで



まず、pasteを使って、wsgiアプリケーションの土台を作る

paster create -t paste_deploy wsgiwiki
cd wsgiwiki



コンフィグのひな形をプロジェクトトップに移動。

mv docs/devel_config.ini .



とりあえず動かしてみる。

paster serve --reload devel_config.ini


ディスパッチャの導入とモデル作成



selectorを導入する。
wsgiwiki/wsgiapp.py

from paste.deploy.config import ConfigMiddleware
import selector

def make_app(
global_conf,
**kw):
app = selector.Selector()
conf = global_conf.copy()
conf.update(kw)
app = ConfigMiddleware(app, conf)
return app



Hello, worldを表示する。

wsgiwiki/wsgiapp.py

def index(environ, start_response):
start_response('200 OK', [('Content-type', 'text/html')])
return ['Hello, world.']
def make_app(....):
....
app.add('/', GET=index)



モデルを作る。
wsgiwiki/model.py

from elixir import *

class Page(Entity):
pagename = Field(Unicode, unique=True)
contents = Field(Unicode)

このモデルからテーブルを作る。(SQLiteを使う)

>>> from wsgiwiki.model import *
>>> metadata.bind = 'sqlite:///devdata.sqlite'
>>> metadata.bind.echo = True
>>> setup_all()
>>> create_all()

ついでに、初期データを投入

>>> page = Page(pagename='FrontPage', contents='This is FrontPage')
>>> page.flush()



コンフィグに接続文字列を設定する。
devel_config.ini

[app:devel]
dburi = sqlite:///%(here)s/devdata.sqlite



アプリケーション内でモデルを使う準備をする。
wsgiwiki/wsgiapp.py

from model import *

def make_app(...):
...
metadata.bind = kw['dburi']
setup_all()

Elixir0.5からは、setup_allを明示的に呼ぶようになりました。



ページ表示のwsgiアプリケーションを作成。
wsgiwiki/wsgiapp.py

def page(environ, start_response):
start_response('200 OK', [('Content-type', 'text/html')])

return ['Hello, world.']

マッピングする。
wsgiwiki/wsgiapp.py

def make_app(...):
...
app.add('/', GET=page)
app.add('/{pagename}', GET=page)

デフォルトの場合と、ページ名を指定した場合の2通りをマッピングする。



とりあえず、ページ名を表示させてみる。
wsgiwiki/wsgiapp.py

def page(environ, start_response):
start_response('200 OK', [('Content-type', 'text/html')])
pagename = environ.get('wsgiorg.routing_args')[1].get('pagename', 'FrontPage')
return [pagename]

モデルを取得して、そのページ名を表示させてみる。
wsgiwiki/wsgiapp.py

def page(environ, start_response):
start_response('200 OK', [('Content-type', 'text/html')])
pagename = environ.get('wsgiorg.routing_args')[1].get('pagename', 'FrontPage')
page = Page.get_by(pagename=pagename)
return [page.pagename]

tempitaで、テンプレートを使うようにする。
wsgiwiki/wsgiapp.py

def page(environ, start_response):
start_response('200 OK', [('Content-type', 'text/html')])
pagename = environ.get('wsgiorg.routing_args')[1].get('pagename', 'FrontPage')
page = Page.get_by(pagename=pagename)
template = tempita.HTMLTemplate.from_filename(os.path.join(os.path.dirname(__file__), 'page.html'))
return template.substitute(page=page)

テンプレート内で、とりあえずページ名を表示する。
wsgiwiki/page.html

{{page.pagename}}

ちゃんとしたxhtmlの体裁に整える。
wsgiwiki/page.html

<?xml version="1.0"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" value="text/html;charset=utf-8" />
<title>{{page.pagename}}</title>
</head>
<body>
<h1>{{page.pagename}}</h1>
<a href="{{page.pagename}};edit">Edit</a>
<div>
{{page.contents}}
</div>
</body>
</html>



ページ編集と入力チェック



ページ編集のwsgiアプリを作る。

def edit_page(environ, start_response):
start_response('200 OK', [('Content-type', 'text/html')])
pagename = environ.get('wsgiorg.routing_args')[1].get('pagename', 'FrontPage')
page = Page.get_by(pagename=pagename)
template = tempita.HTMLTemplate.from_filename(os.path.join(os.path.dirname(__file__), 'edit_page.html'))
return template.substitute(page=page)

マッピングを追加する。

app.add('/{pagename};edit', GET=edit_page) # /{pagename} の前に書くこと!



次は、保存用のアプリケーション。
POSTパラメータ解析のため、webobを使う。

import webob
....
def save_page(environ, start_response):
request = webob.Request(environ)
pagename = environ.get('wsgiorg.routing_args')[1].get('pagename', 'FrontPage')
p = Page.get_by(pagename=pagename)
p.contents = request.params['contents']
p.flush()
return page(environ, start_response)

そして、マッピングを追加。
既存のものと同じURLだが、メソッドが異なる。

app.add('/{pagename}', GET=page, POST=save_page)



フォーマット変換に、markdownを導入する。
モデルのプロパティとして、変換したコンテンツを取得できるようにする。

@property
def htmlContents(self):
return markdown.markdown(self.contents)

変換したコンテンツを表示させる。

<div>
{{page.htmlContents}}
</div>

デフォルトでは、HTMLタグがエスケープされるため、htmlフィルタを追加する。

<div>
{{page.htmlContents | html}}
</div>



WIKIネームをリンクに変換する。


pagePattern = re.compile(r'\b([A-Z][a-z]+[A-Z]\w*)\b')

@property
def htmlContents(self):
html = markdown.markdown(self.contents)
html = pagePattern.sub(r'<a href="\1">\1</a>', html)
return html



まだ登録されていないページにアクセスしたときは、edit_pageに処理を渡す。
また、このときに、start_responseが二度呼ばれてしまうことを回避するため、start_response呼び出しを、return 直前に移動する。

def page(environ, start_response):
pagename = environ.get('wsgiorg.routing_args')[1].get('pagename', 'FrontPage')
page = Page.get_by(pagename=pagename)
if page is None:
return edit_page(environ, start_response)
template = tempita.HTMLTemplate.from_filename(os.path.join(os.path.dirname(__file__), 'page.html'))
start_response('200 OK', [('Content-type', 'text/html')])
return template.substitute(page=page, pagename=pagename)

まだ存在しないページを編集することになるため、pagenameはリクエストの内容をテンプレートに渡す。
contentsもpageがある場合だけ表示するようにする。

def edit_page(environ, start_response):
....
return template.substitute(page=page, pagename=pagename)

<?xml version="1.0"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" value="text/html;charset=utf-8" />
<title>{{pagename}}</title>
</head>
<body>
<h1>{{pagename}}</h1>
<form action="{{pagename}}" method="post">
<div>
<textarea name="contents">{{page.contents if page else ''}}</textarea>
<button type="submit">Save</button>
</div>
</form>
</body>
</html>

保存時に、pageがない場合は新規作成する。

def save_page(environ, start_response):
...
p = Page.get_by(pagename=pagename)
if p is None:
p = Page(pagename=pagename)
p.contents = request.params['contents']

空の内容を登録可能なため、FormEncodeで入力チェックをする。
ページ用のスキーマを作る。
wsgiwiki/schema.py

import formencode
import formencode.validators as validators

class PageSchema(formencode.Schema):
contents = validators.UnicodeString(not_empty=True)

入力エラーの場合は、編集画面に戻るため、edit_pageの引数で、エラー辞書を受け取れるようにする。
また、エラー辞書はそのままテンプレートに渡す。
def edit_page(environ, start_response, errors={}):
...
return template.substitute(page=page, pagename=pagename, errors=errors)

page取得前に、schemaで入力チェックをする。
エラーの場合は、エラー辞書とともに、edit_pageに委譲。
def save_page(environ, start_response):
...
schema = PageSchema()
try:
params = schema.to_python(request.params)
except formencode.Invalid, e:
return edit_page(environ, start_response, e.error_dict)

edit_page.htmlテンプレート内で、エラーメッセージ表示をする。

<?xml version="1.0"?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" value="text/html;charset=utf-8" />
<title>{{pagename}}</title>
</head>
<body>
<h1>{{pagename}}</h1>
<form action="{{pagename}}" method="post">
<div>
<textarea name="contents">{{page.contents if page else ''}}</textarea>
{{if 'contents' in errors}}
<span class="error">{{errors['contents']}}</span>
{{endif}}
<button type="submit">Save</button>
</div>
</form>
</body>
</html>

That's all. Thanks!



Powered by ScribeFire.