Wiki

□未翻訳

□翻訳中

□翻訳完了(細田謙二)

■レビュー(中垣健志)

Wiki

このセクションでは、wikiをスクラッチから構築します。第13章で説明するplugin_wikiによって提供される拡張機能は使用しません。訪問者は、ページの作成、(タイトルによる)検索、編集を行うことができます。 訪問者は、コメントを(前回のアプリケーションと全く同様に)投稿することができ、また、(ページへ添付する形で)文章も投稿することができ、ページからその文章にリンクを張ることができるようになります。慣例として、ここではWiki構文のためにMarkmin構文を採用します。ここではまた、Ajaxを用いた検索ページ、そのページに対するRSSフィード、XML-RPCを介したページ検索用のハンドラ、を実装します。

In this section, we build a wiki, from scratch and without using the extended functionality provided by plugin_wiki which is described in chapter 13. The visitor will be able to create pages, search them (by title), and edit them. The visitor will also be able to post comments (exactly as in the previous applications), and also post documents (as attachments to the pages) and link them from the pages. As a convention, we adopt the Markmin syntax for our wiki syntax. We will also implement a search page with Ajax, an RSS feed for the pages, and a handler to search the pages via XML-RPC46 .

次の略図は、実装が必要なアクションと、それらの間で構築すべきリンクを列挙しています。

The following diagram lists the actions that we need to implement and the links we intend to build among them.

"mywiki"という名の新規の雛形アプリを作成して始めましょう。

Start by creating a new scaffolding app, naming it "mywiki".

モデルはページ(page)、コメント(comment)、文章(document)という3つのテーブルを持つ必要があります。commentとdocumentの両者はpageを参照します。それらはpageに属しているからです。documentは、前回の画像アプリケーションのように、upload型のファイル・フィールドを持ちます。

The model must contain three tables: page, comment, and document. Both comment and document reference page because they belong to page. A document contains a file field of type upload as in the previous images application.

以下にすべてのモデルを示します:

Here is the complete model:

1.

2.

3.

4.

5.

6.

7.

8.

9.

10.

11.

12.

13.

14.

15.

16.

17.

18.

19.

20.

21.

22.

23.

24.

25.

26.

27.

28.

29.

30.

31.

32.

33.

34.

35.

36.

37.

38.

39.

40.

41.

42.

db = DAL('sqlite://storage.sqlite')

from gluon.tools import *

auth = Auth(globals(),db)

auth.define_tables()

crud = Crud(globals(),db)

db.define_table('page',

Field('title'),

Field('body', 'text'),

Field('created_on', 'datetime', default=request.now),

Field('created_by', db.auth_user, default=auth.user_id),

format='%(title)s')

db.define_table('comment',

Field('page_id', db.page),

Field('body', 'text'),

Field('created_on', 'datetime', default=request.now),

Field('created_by', db.auth_user, default=auth.user_id))

db.define_table('document',

Field('page_id', db.page),

Field('name'),

Field('file', 'upload'),

Field('created_on', 'datetime', default=request.now),

Field('created_by', db.auth_user, default=auth.user_id),

format='%(name)s')

db.page.title.requires = IS_NOT_IN_DB(db, 'page.title')

db.page.body.requires = IS_NOT_EMPTY()

db.page.created_by.readable = db.page.created_by.writable = False

db.page.created_on.readable = db.page.created_on.writable = False

db.comment.body.requires = IS_NOT_EMPTY()

db.comment.page_id.readable = db.comment.page_id.writable = False

db.comment.created_by.readable = db.comment.created_by.writable = False

db.comment.created_on.readable = db.comment.created_on.writable = False

db.document.name.requires = IS_NOT_IN_DB(db, 'document.name')

db.document.page_id.readable = db.document.page_id.writable = False

db.document.created_by.readable = db.document.created_by.writable = False

db.document.created_on.readable = db.document.created_on.writable = False

"default.py"コントローラを編集し、以下のアクションを作成してください:

Edit the controller "default.py" and create the following actions:

  • index: すべてのwikiページを列挙する

    • list all wiki pages

  • create: 別のwikiページを投稿する

    • post another wiki page

  • show: wikiページとそのコメントを表示し、コメントを追加する

    • show a wiki page and its comments, and append comments

  • edit: 既存のページを編集する

    • edit an existing page

  • documents: ページに添付された文書を管理する

    • manage the documents attached to a page

  • download: (画像の例のように)文章をダウンロードする

    • download a document (as in the images example)

  • search: 検索用のボックスを表示し、Ajaxコールバックを介して、訪問者が入力したタイトルに該当するもの全てを返す

    • display a search box and, via an Ajax callback, return all matching titles as the visitor types

  • bg_find: Ajax用のコールバック関数。訪問者の入力に合わせて、検索ページに埋め込まれるHTMLを返す

  • the Ajax callback function. It returns the HTML that gets embedded in the search page while the visitor types.

ここに"default.py"コントローラを示します:

Here is the "default.py" controller:

1.

2.

3.

4.

5.

6.

7.

8.

9.

10.

11.

12.

13.

14.

15.

16.

17.

18.

19.

20.

21.

22.

23.

24.

25.

26.

27.

28.

29.

30.

31.

32.

33.

34.

35.

36.

37.

38.

39.

40.

41.

42.

43.

44.

45.

46.

47.

48.

49.

50.

51.

52.

53.

54.

55.

56.

57.

58.

59.

60.

61.

def index():

""" this controller returns a dictionary rendered by the view

it lists all wiki pages

>>> index().has_key('pages')

True

"""

pages = db().select(db.page.id,db.page.title,orderby=db.page.title)

return dict(pages=pages)

@auth.requires_login()

def create():

"creates a new empty wiki page"

form = crud.create(db.page, next = URL('index'))

return dict(form=form)

def show():

"shows a wiki page"

this_page = db.page(request.args(0)) or redirect(URL('index'))

db.comment.page_id.default = this_page.id

form = crud.create(db.comment) if auth.user else None

pagecomments = db(db.comment.page_id==this_page.id).select()

return dict(page=this_page, comments=pagecomments, form=form)

@auth.requires_login()

def edit():

"edit an existing wiki page"

this_page = db.page(request.args(0)) or redirect(URL('index'))

form = crud.update(db.page, this_page,

next = URL('show', args=request.args))

return dict(form=form)

@auth.requires_login()

def documents():

"lists all documents attached to a certain page"

this_page = db.page(request.args(0)) or redirect(URL('index'))

db.document.page_id.default = this_page.id

form = crud.create(db.document)

pagedocuments = db(db.document.page_id==this_page.id).select()

return dict(page=this_page, documents=pagedocuments, form=form)

def user():

return dict(form=auth())

def download():

"allows downloading of documents"

return response.download(request, db)

def search():

"an ajax wiki search page"

return dict(form=FORM(INPUT(_id='keyword',_name='keyword',

_onkeyup="ajax('bg_find', ['keyword'], 'target');")),

target_div=DIV(_id='target'))

def bg_find():

"an ajax callback that returns a <ul> of links to wiki pages"

pattern = '%' + request.vars.keyword.lower() + '%'

pages = db(db.page.title.lower().like(pattern))\

.select(orderby=db.page.title)

items = [A(row.title, _href=URL('show', args=row.id)) \

for row in pages]

return UL(*items).xml()

2~6行目は、indexアクションのコメントを提供します。コメント内にある4~5行目は、テストコード(doctest)としてpythonによって解釈されます。テストは管理インターフェイスから実行できます。この場合、テストはindexアクションがエラーなしで実行されることを検証します。

Lines 2-6 provide a comment for the index action. Lines 4-5 inside the comment are interpreted by python as test code (doctest). Tests can be run via the admin interface. In this case the tests verify that the index action runs without errors.

18、27、35行目は、request.args(0)のidを持つpageレコードを取り出そうと試みます。

Lines 18, 27, and 35 try to fetch a page record with the id in request.args(0).

13、20、37行目は、それぞれ新規ページ、新規コメント、新規文章のための、作成フォームを定義し処理します。

Lines 13, 20 and 37 define and process create forms, for a new page and a new comment and a new document respectively.

28行目はwikiページのための更新フォームを定義し処理します。

Line 28 defines and processes an update form for a wiki page.

51行目はいくつかの魔法が起こっています。"keyword"というINPUTタグのonkeyup属性が設定されます。訪問者がキーを放すたびに、onkeyup属性内におけるJavaScriptコードが、クライアント側で、実行されます。そのJavaScriptコードは次の通りです:

Some magic happens in line 51. The onkeyup attribute of the INPUT tag "keyword" is set. Every time the visitor releases a key, the JavaScript code inside the onkeyup attribute is executed, client-side. Here is the JavaScript code:

1.

ajax('bg_find', ['keyword'], 'target');

ajaxは、"web2py_ajax.html"ファイルに定義されたJavaScript関数です。このファイルはデフォルトの"layout.html"によって組み込まれます。これは3つのパラメタをとります:同期的コールバックを実行するアクションのURL("bg_find")、コールバックに送る変数のIDリスト(["keyword"])、レスポンスが挿入される場所のID("target")です。

ajax is a JavaScript function defined in the file "web2py_ajax.html" which is included by the default "layout.html". It takes three parameters: the URL of the action that performs the synchronous callback ("bg_find"), a list of the IDs of variables to be sent to the callback (["keyword"]), and the ID where the response has to be inserted ("target").

検索ボックスに何かを打ち込みキーを放すとすぐに、クライアントはサーバーを呼び出し、'keyword'フィールドの内容を送信します。そして、サーバーが応答したら、そのレスポンスは'target'タグのinnerHTMLとしてページ自身に埋め込まれます。

As soon as you type something in the search box and release a key, the client calls the server and sends the content of the 'keyword' field, and, when the sever responds, the response is embedded in the page itself as the innerHTML of the 'target' tag.

'target'タグは52行目で定義されるDIVです。これはビューにおいても定義することができます。

The 'target' tag is a DIV defined in line 52. It could have been defined in the view as well.

これは"default/create.html"ビューに対するコードです:

Here is the code for the view "default/create.html":

1.

2.

3.

{{extend 'layout.html'}}

<h1>Create new wiki page</h1>

{{=form}}

createページを訪れると、次のように表示されます:

If you visit the create page, you see the following:

"default/index.html"ビューに対するコードです:

Here is the code for the view "default/index.html":

1.

2.

3.

4.

5.

6.

7.

{{extend 'layout.html'}}

<h1>Available wiki pages</h1>

[ {{=A('search', _href=URL('search'))}} ]<br />

<ul>{{for page in pages:}}

{{=LI(A(page.title, _href=URL('show', args=page.id)))}}

{{pass}}</ul>

[ {{=A('create page', _href=URL('create'))}} ]

これは、次のページを生成します:

It generates the following page:

"default/show.html"ビューに対するコードです:

Here is the code for the view "default/show.html":

1.

2.

3.

4.

5.

6.

7.

8.

9.

10.

11.

12.

{{extend 'layout.html'}}

<h1>{{=page.title}}</h1>

[ {{=A('edit', _href=URL('edit', args=request.args))}}

| {{=A('documents', _href=URL('documents', args=request.args))}} ]<br />

{{=MARKMIN(page.body)}}

<h2>Comments</h2>

{{for comment in comments:}}

<p>{{=db.auth_user[comment.created_by].first_name}} on {{=comment.created_on}}

says <I>{{=comment.body}}</i></p>

{{pass}}

<h2>Post a comment</h2>

{{=form}}

markimin構文の代わりにmarkdown構文を使用する場合、次のようにします:

If you wish to use markdown syntax instead of markmin syntax:

1.

from gluon.contrib.markdown import WIKI

そして、MARKMINヘルパの代わりにWIKIを使用してください。また、markminの構文の代わりに生のHTMLを受け入れるように選択することができます。この場合、次のものを:

and use WIKI instead of the MARKMIN helper. Alternatively, you can choose to accept raw HTML instead of markmin syntax. In this case you would replace:

1.

{{=MARKMIN(page.body)}}

次のものに置き換えます:

with:

1.

{{=XML(page.body)}}

(デフォルトのweb2pyの挙動により、XMLはエスケープされません)

(so that the XML does not get escaped, as by default web2py behavior).

これは次のようにするのがより良いです:

This can be done better with:

1.

{{=XML(page.body, sanitize=True)}}

sanitize=Trueと設定すると、 "<script>"タグのような安全でないXMLタグをエスケープするようにし、XSSの脆弱性を防ぎます。

By setting sanitize=True, you tell web2py to escape unsafe XML tags such as "<script>", and thus prevent XSS vulnerabilities.

これで、indexページからページタイトルをクリックすると、作成したページを見ることができます:

Now if, from the index page, you click on a page title, you can see the page that you have created:

"default/edit.html"ビューに対するコードです:

Here is the code for the view "default/edit.html":

1.

2.

3.

4.

{{extend 'layout.html'}}

<h1>Edit wiki page</h1>

[ {{=A('show', _href=URL('show', args=request.args))}} ]<br />

{{=form}}

これは、作成ページとほぼ同じページを生成します。

It generates a page that looks almost identical to the create page.

"default/documents.html"ビューに対するコードです:

Here is the code for the view "default/documents.html":

1.

2.

3.

4.

5.

6.

7.

8.

9.

10.

{{extend 'layout.html'}}

<h1>Documents for page: {{=page.title}}</h1>

[ {{=A('show', _href=URL('show', args=request.args))}} ]<br />

<h2>Documents</h2>

{{for document in documents:}}

{{=A(document.name, _href=URL('download', args=document.file))}}

<br />

{{pass}}

<h2>Post a document</h2>

{{=form}}

"show"ページでdocumentsをクリックすると、ページに添付された文書を管理することができます。

If, from the "show" page, you click on documents, you can now manage the documents attached to the page.

最後は"default/search.html"ビューに対するコードです:

Finally here is the code for the view "default/search.html":

1.

2.

3.

4.

{{extend 'layout.html'}}

<h1>Search wiki pages</h1>

[ {{=A('listall', _href=URL('index'))}}]<br />

{{=form}}<br />{{=target_div}}

これは、次のようなAjax検索フォームを生成します:

which generates the following Ajax search form:

たとえば次のURLに訪れることで、コールバックのアクションを直接呼び出すことも可能です:

You can also try to call the callback action directly by visiting, for example, the following URL:

http://127.0.0.1:8000/mywiki/default/bg_find?keyword=wiki

このページのソースコードを見ると、コールバックによって返された次のようなHTMLを見ることができます:

If you look at the page source you see the HTML returned by the callback:

1.

<ul><li><a href="/mywiki/default/show/4">I made a Wiki</a></li></ul>

web2pyを用いて保存されたページからRSSフィードを生成することは簡単です。web2pyにはgluon.contrib.rss2があるからです。単に、次のアクションをdefaultコントローラに追加してください:

Generating an RSS feed from the stored pages using web2py is easy because web2py includesgluon.contrib.rss2. Just append the following action to the default controller:

1.

2.

3.

4.

5.

6.

7.

8.

9.

10.

11.

12.

13.

14.

def news():

"generates rss feed form the wiki pages"

pages = db().select(db.page.ALL, orderby=db.page.title)

return dict(

title = 'mywiki rss feed',

link = 'http://127.0.0.1:8000/mywiki/default/index',

description = 'mywiki news',

created_on = request.now,

items = [

dict(title = row.title,

link = URL('show', args=row.id),

description = MARKMIN(row.body).xml(),

created_on = row.created_on

) for row in pages])

そして、次のページに訪れると

and when you visit the page

http://127.0.0.1:8000/mywiki/default/news.rss

フィードが表示されます(フィードリーダによって見た目は異なります)。なお、URLの拡張子が.rssなので、dictは自動的にRSSに変換されています。

you see the feed (the exact output depends on the feed reader). Notice that the dict is automatically converted to RSS, thanks to the .rss extension in the URL.

web2pyはまた、サードパーティのフィードを読むためのfeedparserを同封しています。

web2py also includes feedparser to read third-party feeds.

最後に、プログラム的にwikiを検索可能にするXML-RPCハンドラを追加しましょう:

Finally, let's add an XML-RPC handler that allows searching the wiki programmatically:

1.

2.

3.

4.

5.

6.

7.

8.

9.

10.

11.

service=Service(globals())

@service.xmlrpc

def find_by(keyword):

"finds pages that contain keyword for XML-RPC"

return db(db.page.title.lower().like('%' + keyword + '%'))\

.select().as_list()

def call():

"exposes all registered services, including XML-RPC"

return service()

ハンドラのアクションは、このリストで指定された関数を(XML-RPCを介して)単純に公開しています。ここでは、find_byです。find_byはアクションではありません(引数があるためです)。この関数は、.select()でデータベースに問い合わせ、.responseでレコードをリストとして取り出し、そのリストを返します。

Here, the handler action simply publishes (via XML-RPC), the functions specified in the list. In this case, find_by. find_by is not an action (because it takes an argument). It queries the database with .select() and then extracts the records as a list with .response and returns the list.

ここに、どのように外部のPythonプラグラムからXML-RPCハンドラにアクセスするかの例を示します。

Here is an example of how to access the XML-RPC handler from an external Python program.

1.

2.

3.

4.

5.

>>> import xmlrpclib

>>> server = xmlrpclib.ServerProxy(

'http://127.0.0.1:8000/mywiki/default/call/xmlrpc')

>>> for item in server.find_by('wiki'):

print item.created_on, item.title

ハンドラは、XML-RPCを理解する多くのプログラミング言語(C、C++、C#、Javaなど)からアクセスすることができます。

The handler can be accessed from many other programming languages that understand XML-RPC, including C, C++, C# and Java.