2010-10-09

GAE アプリでユーザごとの設定を可能にする #2

今回のお題は、GAE が提供する「データストア」の使い方を調査すること。ただし、Blogger Glass に追加しようとしているユーザごとの設定機能に必要なのは、User.user_id() をキーとしてデータ実体を作成・抽出・更新する方法だ。複雑なクエリに関しては今のところ不要。

とりあえず、サンプルを作ったので貼っておく。

ログインしていなければ「Login」というリンクだけの画面が表示され、リンクをたどりログインすればユーザごとに設定した「Blog ID」とその更新日時が表示される。また、同時に「Blog ID」を入力するフォームも表示されているので、ID を入力し「Save Settings」ボタンを押せば、ID の値が更新される。つまり、Blogger Glass に追加しようとしている「ユーザごとの設定」機能の核となる部分はここにふくまれているわけだ。

このコードで肝心なのは、Settings クラスの定義と、def_settings 関数の内部で、User.user_id() の値をデータストアのキーとしてデータの実体(entity)を作成、あるいは抽出しているところだ。

参考文献

Programming Google App Engine
Dan Sanderson
Oreilly & Associates Inc ( 2009-11-15 )
ISBN: 9780596522728

User.user_id() をキーにする方法は Example 2-5 を参考にした。

関連リンク

関連記事

2010-10-08

GAE アプリでユーザごとの設定を可能にする #1

2010-10-08 時点の Blogger Glass では、アプリ固有の設定(例: スタイルシートの URL やブログ ID 等)は config.yaml というファイルに記述することになっている。

この方法の利点は、何と言ってもその手軽さにある。YAML (のサブセット)で書かれたファイルを読み込むのは単純で、ハードコードしたくない情報を簡単にプログラムから切り離すことができる。一方、欠点はと言えば、設定の変更にはアプリの再配備が必要なこと。プロトタイプとしてはこれでも悪くないが、ある程度アプリの機能が成熟してきたら、設定自体をアプリの機能として提供すべきだ。

そこで、Blogger Glass へ次に 追加する機能として「設定(Settings)」を選んだ。ブログ ID を「設定」で変更できるようになれば、LOG+REPO 以外のブログ(ただし Blogger で作ったものに限られる)を見られるようになる。

原則として個人が一人で使う iPhone のようなデバイス上のアプリとは異なり、ウェブアプリの設定は、ユーザごとに設定・保存できるものでなければならない。みんなが同じ URL でアクセスする以上、アプリはすべてのユーザが共有する。設定をユーザごとのものにしなければ、全ユーザが共有することになってしまう。そして、ユーザごとの設定にはユーザの識別が必要になる。

「設定」機能のデザイン(意匠と設計)に入る前に、GAE におけるユーザの識別について調べてみよう。

ユーザの識別

GAE アプリで、アプリを使っているユーザの識別するには GAE が提供する「ユーザーサービス」を利用する。

(「ユーザー サービスの使用」より)
サービスの 1 つであるユーザー サービスは、アプリケーションと Google ユーザー アカウントを連携させるものです。ユーザー サービスでは、ユーザーはすでに持っている Google アカウントを通じて、アプリケーションにログインすることができます。

ログインとログアウト

アプリを利用するユーザにログインを促す方法の 1 つは、リクエストハンドラで users.get_current_user() を使い、ユーザがログイン中であることを確認することだ。そしてログイン中でなければログイン画面にリダイレクトする。

(ユーザーサービス利用例)
from google.appengine.api import users 
[...snip...]
class MainHandler(webapp.RequestHandler):
    def get(self):
        user = users.get_current_user()
        if user:
            [...snip...]
            self.response.out.write("<div><a href='"
                                    + users.create_logout_url(self.request.uri)
                                    + "'>logout</a></div>")
        else:
            self.redirect(users.create_login_url(self.request.uri))

ログアウトに関しては、アプリ画面のどこかにログアウト用のリンクを置けば良い。これには users.create_logout_url()を使う。↑の例では、ログイン中であれば(つまり users.get_current_user() が有効なオブジェクトを返せば)、レスポンスにログアウト用の URL を出力している(アプリ画面の一部になる)。この例の場合、ログアウト用のリンクをクリックすると、(a) ログアウトが実行され、(b) このハンドラを起動した URL にリダイレクトされ、(c) ログイン中ではないからログイン画面にリダイレクトされ、(d) ログイン画面が表示されることになる。

特定の URL に対してログインを要求する

app.yaml に記述することで、特定のリクエスト(URL のパターン)に対してログインを強制することもできる。

(app.yaml の handlers 区画の記述サンプル)
handlers:
- url: /hello/
  script: hello.py
  login: required

- url: .*
  script: main.py

この設定では、/hello/ を開こうとすると、ログイン画面にリダイレクトされ、ログインしない限り、hello.py ハンドラが実行されることはない。

ハンドラ内でログイン画面にリダイレクトする方法と、app.yaml でリクエスト自体にログイン要求を設定する方法とは適宜使い分けることになる。たとえば、ログインをしなくとも使える機能(のハンドラ)についてはハンドラ内でログイン状態を識別し、必要に応じてリダイレクトする。一方、ログインしている状態でのみ使える機能(のハンドラ)に対しては、app.yaml で制限をかける。

Blogger Glass の場合だと、一覧表示や内容表示はログインしていなくても使えるが、新しく追加しようとしている「設定」はログインが必須になる。だから、前者はハンドラ内で識別とリダイレクトを記述し、後者にはapp.yaml でログインを必須に設定することになる。

User オブジェクト

実際のユーザの識別には User オブジェクトを利用する。

(「Google アカウント Python API 概要」より)
User オブジェクトからは、ユーザー アカウントが存在する限り、メール アドレスが変更されても一定である一意なユーザー ID を取得できます。この値は、データストアのエンティティ キーまたはプロパティ値として使用できます。

User.user_id() メソッドはユーザの一意で永続的な ID (型は str) を返す。

開発環境(SDK)でのログイン

ユーザーサービスを使ったアプリを開発するために、SDK にはログイン(とログアウト)をシミュレートするダミーのログイン画面が用意されている(→)。

上述の例のように self.redirect(users.create_login_url(self.request.uri)) でログイン画面にリダイレクトするとこの画面が表示され、ログインボタンを押すとアプリ画面に戻る。

次回予告

ログイン要求つきの設定画面(ビュー)を用意してユーザごとの項目を入力できるようになっても、それが保存できなければ意味がない。正確には、後続のリクエストを処理する際に参照できなければ役に立たない。つまり、リクエストハンドラ間で情報を共有する仕組みが必要になる。

そのための一番確実な方法は「データストア」を利用することだろう。ユーザごとの設定情報を永続化してしまえば、どのリクエストハンドラからも参照できる。情報を保存するハンドラと、参照するハンドラが別のサーバで実行されていたとしても問題ない。

次回は、GAE の提供する「データストア」の使い方を調べることから始める。

関連リンク

関連記事

2010-10-07

Blogger で作ったブログの記事の内容を表示する #3

前回のリファクタリング構想にしたがい、実装を行う。

表示内容を保持するオブジェクト

アプリ固有の情報を AppInfo オブジェクト、ビュー(前回まではページと呼んでいた)固有の情報を ViewInfo オブジェクトに収めることにした。ただし、ViewInfo オブジェクトはビューの種類(一覧表示と内容表示)ごとにサブクラス化して使う。

ビュー内の表示領域の構成と表示内容オブジェクトの対応

これら表示内容オブジェクトは src/info.py で定義している。

AppInfo

アプリ固有の情報を保持するため、すべてのビューで使われる。このオブジェクトが保持するのは、基本的に config.yaml で設定する情報だ。

AppInfo のプロパティ一覧
プロパティ名 説明
lang 生成される HTML に対する言語指定。具体的には html 要素の lang 属性の値を指定する。
title アプリのタイトル。html/head/title 要素の内容になることを想定している。
stylesheet 生成される HTML で使用されるスタイルシート。
home_url アプリのトップ URL。
ListViewInfo

一覧表示ビューで表示する情報を保持する。

ListViewInfo のプロパティ一覧
プロパティ名 説明
page 表示するページ番号。
total_pages 全ページ数。
content フィードにふくまれる entry 要素のデータ。実際には Entry (後述) のリストになっている。

ListViewInfo.content は (main.py で定義している) Entry のリストであり、Entry は以下のプロパティを持つ。

Entry のプロパティ一覧
プロパティ名 説明
title 記事のタイトル。
feedlink 個別記事のフィードへの URL。
PostViewInfo

内容表示ビューで表示する情報を保持する。

PostViewInfo のプロパティ一覧
プロパティ名 説明
title 記事のタイトル。
permalink Blogger の個別記事への permalink。
content 記事の内容となるテキスト。実際には Unicode 文字列になっている。

リクエストハンドラの変更

MainHandler (main.py で定義、一覧表示リクエストを処理する)、PostViewHandler (postview.py で定義、内容表示リクエストを処理する)ともに、template.render にわたすディクショナリに直接値を詰めるのではなく、AppInfo と ViewInfo (のサブクラス) オブジェクトを用意するように変えた。

その他の修正

前回のリファクタリング・プランにはなかったが、feed/core.py で定義していた Entry クラスを削除した。このクラスは Atom フィードにふくまれる entry 要素のデータを保持するためのものだが、単純にディクショナリ型オブジェクトとして保持することに変えたため不要になった。

またこれにともない、AtomFeed と AtomPostFeed でも解析後にデータを保持する方法を修正している。

関連リンク

関連記事

2010-10-06

Blogger Glass をリファクタリングする

細かな機能の追加はまだ残っているが、Blgger Glass にとっての主機能となる 2 つの実装が完了した。すなわち、(a) 記事の一覧表示、(b) 個々の記事の内容表示、の 2 つだ。

細部の作り込みに移る前に全体の構造を見直しておきたい。つまり、今回のお題は、リファクタリングだ。

ウェブアプリの 2 層構造

「Python で書いたフィルタを GAE に載せる #3」の「ウェブアプリのテンプレート」では、GAE でテンプレートシステムとして使う Django の(HTML 以外の)記述は、あくまでも HTML 中に配置するオブジェクトを出力するための補助、なのだと書いた。そして、この「配置するオブジェクト」を定義することが、プレゼンテーション層とバックエンド層を分離することになる、と。これは言い換えると、ウェブアプリを 2 つの層からなる多層構造として作る、ということになる。

(「テンプレートシステム入門 (2) 基礎編」より)
何らかの表示を伴うアプリケーションは、大きく「ビジネス層」と「プレゼンテーション層」の 2 つに分けることができます。

ビジネス層
何 (What) を表示するか? を司る層であり、メインプログラムが担当します。
プレゼンテーション層
どう (How) 表示するか? を司る層であり、テンプレートシステムが担当します。

仕組みとしては、ビジネス層であるメインプログラムがデータを用意し、それをプレゼンテーション層であるテンプレートシステムに渡すことでデータが表示されます

この記事では、わたしがバックエンド層と呼んだものをビジネス層と名付けており、その役割は「何(What)を表示するか? を司る層」としている。一方、「プレゼンテーション層」は「どう(How)表示するか? を司る層」だ。ビジネス層という名前は気に入らないが、それぞれに与えられた役割は明確でわかりやすい。

以上を踏まえ、Blgger Glass のプレゼンテーション層のデザインを考え直してみよう。

Blogger Glass のデザイン(意匠と設計): プレゼンテーション層

すでに書いたように、Blgger Glass の論理的な画面構成は右のようになる。「論理的な」と書いたのは、スタイルの定義は後回しにしているからだ。たとえば、App Header が Page Header や Contents の上部に表示されるかどうかは、スタイルの定義次第になる。つまり、この図は Blogger Glass で「何を(What)」表示するかの抽象的な構造だと言える。

一般に、個々の画面に結びついた機能によって表示される内容は変化するが、中には変化しないものもある。たとえば、アプリの名称は一覧表示画面であれ、内容表示画面でれ、あるいはユーザ設定画面であれ変化することはない。このような情報をアプリ情報と呼ぶ。一方、機能ごとに変化する内容をページ情報と呼び、その中でもメインとなる内容を特に主コンテンツと呼ぶことにする。

以下に、Blogger Glass が持つ 2 つの画面における各情報の具体的内容を示す。

Blogger Glass のプレゼンテーション要素
ページの種類 アプリ情報 ページ情報 主コンテンツ
一覧表示 アプリ名
アプリの URL
ページ番号
ページ切り替え
記事一覧
内容表示 アプリ名
アプリの URL
記事の作成日時
記事タイトル
ラベル
記事内容

これを元に、実際のテンプレートごとに表示するオブジェクト(のプロパティ)を決める。

テンプレートごとのオブジェクトプロパティ
template AppInfo PageInfo (actual content)
listview.html title (text)
home_url (text)
page (number)
total_pages (number)
content (list of Entry object)
pager (list of Page object)
entry.title (text)
entry.feedlink(text)
postview.html title (text)
home_url (text)
title (text)
content (text)
(text)

この表は、たとえば listview.html には AppInfo クラスがあり、そのインスタンすは title と home_url というプロパティ(どちらも値はテキスト)を持っている、というように見る。また、listview.html の PageInfo インスタンスでは、content と pager というプロパティは他オブジェクトのリストを参照している。

細かいことだが、ページ(page)という言葉は「ウェブページ」からの連想で付けたもので、ウェブアプリのプレゼンテーション要素としては適切とは言えない。むしろ、ビュー(view)と呼ぶべきかも。

関連リンク

関連記事

2010-10-05

Blogger で作ったブログの記事の内容を表示する #2

前回ののデザイン(意匠と設計)に沿って実装を行った。

実装概要

app.yaml (修正)

記事内容を表示させるためのリクエスト形式(/post/?id=<id>) に対応させるため、handlers区画に以下の記述を追加した。

(app.yaml より)
- url: /post/.*
  script: postview.py
postview.py と postview.html (新規追加)

postview.py が内容表示リクエスト用のハンドラ、postview.html は同ハンドラが使用するテンプレートになる。

postview.py で定義されている PostViewHandler は一覧表示用の MainHandler を一部書き換えたものになっている。共通部分のくくり出し等のリファクタリングは行っていない。

構造も論理も MainHandler とほとんど変わっていない。目立つ差異はフィードを抽出するためのクラスに記事内容専用の AtomPostFeed を使っていることだ。

現状、テンプレートはフィードから抽出した記事の内容を流し込むだけのものになっている。タイトル以外のメタ情報(作成日付やタグ等)は一切表示していない(フィードから抽出もしていない)。

feed/parser.py (修正)

変更点は、記事の内容 (feed/entry/content) を抽出するようになったことと、ブログの permalink と個別フィードへの URL を抽出するようにしたこと。

main.py と feed/core.py (修正)

内容表示リクエストでは、フィードの取得時に必要なポスト ID をリクエストパラメータとして受け取る。一方、一覧表示リクエストでは、リクエストパラメータとしてページ番号を受け取る(フィードを取得する際に start-index を指定する値に変換される)。この違いを吸収するため、それぞれに専用のフィードオブジェクトを用意することにした。従来からある AtomFeed を一覧表示用に、新しく追加した AtomPostFeed を内容表示用として使う。リクエストパラメータは、それぞれをインスタンス化する際に引数として渡すことにした。このため、フィードオブジェクトのインスタンス化と get メソッドの呼び出し形式が変わることになり、main.py を修正した。

core.py では、この他にも、フィードデータを保持する Entry クラスを修正(抽出するデータの種類が増加したため)、さらに一覧表示で各項目に張るリンクを、これまでの記事への permalink から、内容表示リクエストの形式に変更してある。

feed/post.py (新規追加)

コードの中身はほぼ AtomFeed からの流用になっている。いずれリファクタリングし、共通部分を BloggerFeed に押し出すつもりだ。

この AtomPostFeed で少し特殊なのは、内容表示リクエストの形式で /post/ というパラメータなしのパターンをサポートしたこと。この場合、ポスト ID を指定するタイプの URL からフィードを取得することはできない。そこで、ポスト ID が指定されなかった場合に限って、通常のフィード取得用の URL (一覧表示リクエストで利用しているもの) に max-results=1 を指定して取得することで対応した。

ファイルのリネーム

テンプレートへの postview.html の追加にともない、page.html を listview.html とリネームした。今後、テンプレートを追加する際には、以下の名前規約にしたがう。

<表示するオブジェクト>view.html

今回、追加したテンプレートは記事("post")を表示するから postview.html、旧テンプレートは記事一覧("list")を表示するから listview.html となる。

その他の変更

今回から logging モジュールを使うことにした。sys.stderr に書き出していたエラーメッセージ(これもログに流れるようだが)を logging.error を使って書き換えている。

今後の展開

肝心の記事の内容そのものを表示することができるようになった。次は、記事のメタ情報を表示させることを考える。ここで言うメタ情報とは、作成日時、ラベルのことだ。

フィードには published に加えて updated という日時データもふくまれているので、更新日時も表示させても良い。

ラベルは複数指定可能なので、表示にも工夫の余地がある。まずは、辞書順にソートして並べて表示、で良いが。

関連リンク

関連記事