Categories
open all | close allTags
Migration | パソコン | Aptana | CSRF | 認証 | RESTful | 国際化 | モデル | スキンエンジン | Subversion | rake | OpenID | ドキュメント | テスト | タグ | Flash | 名称 | フォーム | アクセス制御 | デュアル・コアSearch
サイドバーのウィジェッツ化実装の考察
管理画面のロジックとデザインをできるだけ切り離そうと作業中なのですが,案外難航しています。忙しくて一週間ほど全くプログラムできなかったのもあるのですが,もうちょっとかかりそうです。それはそれとして,アイディアレベルの話ですが,サイドバーをウィジェッツ化できるようにしたいと思ってます。実装としては,
①スキンの中に<%sidebar%>というスキン変数を置くことで,そこがウィジェッツ対応になる。ただし,これはインクルードファイルではなくスキンパーツ(レイアウト含む)の中にないといけない
②<%sidebar(left)%>みたいに名前も付けられる。複数置くときは名前が必須(一つは空でもいい)
③このスキン変数の自体はスペシャルスキンパーツで名前がsidebar(名前付きの場合はsidebar_leftのようにアンダースコアでつなげる)
④標準スキン変数でサイドバー対応のものはパーサのクラスで sidebar :categorylist のように宣言する
⑤これらのスキン変数のために call_categorylist のようにメソッドを用意(なくても動く)。このメソッドはサイドバーで使う名前を返す。メソッドがない場合はcategorylistを言語ファイルで変換
⑥プラグインの場合は event_sidebar を実装することでサイドバー対応になる
⑦event_sidebar はウィジェッツの名前を返す
⑧スキンパーツの編集画面からそのスキンパーツ内のサイドバーを編集する画面を呼び出す
⑨ドラグアンドドロップで編集できる
⑩ウィジェッツの実体は
<div class="sidebar"><h3><%text(プラグイン名)%></h3><div class="プラグイン名"><%プラグイン名%></div></div>
つまり,ウィジェッツ対応のスキンはCSSでクラスsidebarを実装する必要がある。
このほか,対話的にパラメータを変える仕組みを入れたいと思っていますが,そこはまだ詰めていません。
メタプログラミングで呼び出される側のプログラムをきれいにする
前のエントリーで書いた方法,確かに呼び出し側はきれいになるのですが,呼び出しを受ける側がちょっと気になってました。具体的に言うとCattreeというクラスを作って,そこでクロージャを保存するためのメソッド(すなわち呼び出しに使われるメソッド),保存するインスタンス変数,インスタンス変数を呼び出すためのattr_readerを書かなければいけません。カテゴリーツリーだけの話ならいいのですが,同じような形を利用できるものは現在用意しているだけでもタグクラウド,ページスイッチとあります。このとき,それぞれについて上記のCattreeのようなクラスを作るのが結構面倒です。同じようなことばかり何度も書くので全然DRY(Don't Repeat Yourself)になっていません。
そこで,この受け皿の部分をDRY化する方法を考えました。
まず,次のようなモジュールBlocksContainer(ファイル名はblocks_container.rb)を作ります。
module BlocksContainer
def method_missing(msg, *arg, &block)
varname = ('@'+msg.to_s).to_sym
if b = instance_variable_get(varname)
b.call(*arg)
elsif block
instance_variable_set(varname, block)
else
raise NameError
end
end
endここではmethod_missingというメソッドを作っていますが,これはメソッドが見つからなかったときに自動的に呼び出されるメソッドです。ここでは,そのメソッド名と同じ名前のインスタンス変数があったらそれをブロックとみなして実行し,インスタンス変数がなくて,ブロックが与えられていたらメソッド名と同じインスタンス変数を作ってブロックを保存します。そうでない場合はメソッドが見つからないものとしてNameErrorを起こします。
これを使う側は次のようになります。
def category_tree(&block)
tempbuffer = ""
level = 0
c = Object.new.extend BlocksContainer
block.call(c)
self.categories.inject(nil) do |last_right, cat|
# cat is under the last leaf
if !last_right || (last_right > cat.cleft)
level += 1
tempbuffer += c.level_up(level)
#
elsif (last_right + 2 <= cat.cleft)
tempbuffer += c.item_close
(last_right+2..cat.cleft).each do |i|
level -= 1
tempbuffer += c.level_down
end
else
tempbuffer += c.item_close
end
tempbuffer += c.item(cat)
last_right = cat.cright
end
if (level > 1)
tempbuffer += c.level_down
end
(level..1).each do |i|
tempbuffer += c.item_close
end
tempbuffer += c.level_down
return tempbuffer
end
前と違うのは
c = Object.new.extend BlocksContainer
として新しいオブジェクトに上記の機能を加えていること。
また,ブロックの呼び出しも,前は
down_block.call
といった形だったのが
level_down
だけで実行するようになります。これで,いちいち新しいクラスを作らなくても済むようになりました。
クロージャを使うメソッドをかっこよくする
ツリー型カテゴリーの表示など,ロジックが必要な表示ルーチンで,ロジックと表示そのものの部分を分けるためにクロージャを使う話は以前に書きました。そのときのロジック側(Blogモデル)はこうなってました。
def category_tree
tempbuffer = ""
level = 0
self.categories.inject(nil) do |last_right, cat|
# cat is under the last leaf
if !last_right || (last_right > cat.cleft)
level += 1
tempbuffer += yield(:level_up, level)
#
elsif (last_right + 2 <= cat.cleft)
tempbuffer += yield(:item_close, level)
(last_right+2..cat.cleft).each do |i|
level -= 1
tempbuffer += yield(:level_down, level)
end
else
tempbuffer += yield(:item_close, level)
end
tempbuffer += yield(:item, level, cat)
last_right = cat.cright
end
if (level > 1)
tempbuffer += yield(:level_down, level)
end
(level..1).each do |i|
tempbuffer += yield(:item_close, level)
end
tempbuffer += yield(:level_down, level)
return tempbuffer
end
一方,呼び出し側(パーサ内のparse_categorylist)は
def parse_categorylist params
blog = params[2] ? Blog.find_by_bname(params[2]) : @controller.blog
template = _template(params[1])
bd = blog.basic_data
data = Hash.new
blog.category_tree do |type, level, cat|
data['level'] = level.to_s
case type
when :level_up
fill(template['CATLIST_HEADER'], bd.merge(data))
when :level_down
fill(template['CATLIST_FOOTER'], bd.merge(data))
when :item
data['catlink'] = url_for(:controller=>'categories', :catid=>cat.catid, :action=>'show')
fill(template['CATLIST_LISTITEM'], data.merge(cat.attrs))
when :item_close
fill(template['CATLIST_LISTITEM_END'], bd.merge(data))
end
end
end
これで実用上は不自由ないのですが,どうも呼び出し側があまりかっこよくない。case文で場合分けというのが「ださい」感じがします。yieldで呼び出されるたびにこのcase文を通るのが効率的にももう一つ。ここの部分を
blog.category_tree do |cat|
cat.level_up { |level|
data['level'] = level.to_s
fill(template['CATLIST_HEADER'], bd.merge(data))
}
cat.level_down { fill(template['CATLIST_FOOTER'], bd.merge(data)) }
cat.item { |cat|
data['catlink'] = url_for(:controller=>'categories', :catid=>cat.catid, :action=>'show')
fill(template['CATLIST_LISTITEM'], data.merge(cat.attrs))
}
cat.item_close { fill(template['CATLIST_LISTITEM_END'], bd.merge(data)) }
end
と書けたら,かなり「Railsっぽい」感じになります。で,やってみました。
» Read more
国際化の方法についてのアイディア
Nucleusと同じように定数に言語別の文字列を割り当てて,と考えていたのですが,別の方法を考え中。gettextみたいに,デフォルトの文字列を直接書き入れていき,それに言語ファイルで言語ごとの翻訳を割り当てるといった形。具体的には,これまでの方法だと
「_ADD_NEW_ITEM」
に対して英語だったら
「Add New Item」
日本語だったら
「新規アイテム追加」
といった形で言語ファイルを作っていったわけですが,これだと言語ファイルがなかったり,あっても該当する定数やシンボルに対応する翻訳がないときに,かっこ悪いことになってしまいます。
これを
"Add New Item"
という文字列に対して
英語だったらそのまま,
日本語だったら
「新規アイテム追加」
にするといった形で言語ファイルを作っていく方法が,今考えているものです。これだと,①言語ファイルを作らなくてもデフォルトの文字列でスキンや管理画面を作っていけます。特にスキンやプラグインであれば,デフォルトの文字列を日本語にしておき,それを言語ファイルで英語などに置き換えるといった作り方も許されるでしょう。
Nucleusでは言語ファイル作るのが面倒なのでプラグインのメッセージを英語で作っていたこともよくあったのですが,この方法なら,言語ファイル対応にするためのしきいが低いのではないかと思います。
管理画面作るときも英語のまま作っておいて後から言語ファイルを作るといった作業手順にできるので,これまでのように言語ファイルと平行して作業するといった手間が省略できます。
ただし,難点は二つあって,一つは「シンボル」から変換する形式を取るRailsの標準の国際化機能と相性が悪いこと。もう一つはデフォルト文字列を変えてしまうと,翻訳した文字列を呼べなくなってしまうこと。
前者については,シンプルで実用的でさえあればかならずしもRailsの機能を使わなくてもいいかなと思ってます。言語ファイルは管理画面やスキン,プラグインなどがそれぞれ持つので,サイズもそれほど大きくなく,この設計でも読み込み負荷はあまりないだろうと予想しています。
後者については,デフォルトの文字列を変えたいときはデフォルト文字列の言語用に言語ファイルを用意して,そこで変換させてしまうというのが一つの逃げ道になります。例えば,先ほどの例で「Add New Item」を「Add a Item」に変えたいとしたら,管理画面のテンプレートに書く文字列を変えるのではなく,英語の言語ファイルに
"Add New Item": "Add a Item"
といった形で変換語の文字列を入れる方法です。後は,元の文字列が変わったのだからと割り切って言語ファイルも全部修正する方法。数が少なければ前者もあるでしょうけど,あまりこれをやると訳が分からなくなってしまうので,言語ファイルを修正するのが面倒でも正解かもしれません。
管理画面の効率的な作り方はないものか?
管理画面作成の方にようやく戻ってきましたが,どうも作業効率がよくありません。もっと自動的に作る方法はないものか,いいアイディアが浮かびません。例えば,routes.rbに登録されている管理画面のリソースから自動的に管理画面をある程度構成してくれるとか,メニューに入れる項目をもっと宣言的にかつ,ダイナミックrに変えられるようにするとか。
描画とロジックの分離ももっと進めたいところ。例えばブログ表示ではカテゴリーリストの描画とか,タグ・クラウドの描画と言ったロジックに関係するところは,ブログ・モデルに記述し,スキン・エンジンはそこからコールバックのような形で呼び出すことで,スキン・エンジン側にはロジックをおかず,モデル側では描画をしないといった要件を満たしています。
管理画面でも同じようなことをしておかないと,後でデザインを変えたくなったときに多大な作業が発生します。
なぜか管理画面関係では考えが浮かびません。困ったものです。
後,つまらないことですけどAptana RadRailsで開いているファイルの名前しか表示されないのはちょっと困ったところ。リソース・ベースで画面作っていたらindex.html.erbだれけで,どこのindex.html.erbなのかわかりゃしません。
インライン・テンプレート
インライン・テンプレートの機能,動くようになったのですが,今の「ネストなしパーサー」だとインラインでテンプレート書くときに<%%>が使えないので<::>に書きなおす必要があります。やはりこれだと面倒なので,ネスト付きパーサーに切り替えることを画策中です。
ただ,単純に切り替えただけではさすがにエラーが…。ちゃんとテストしていないメソッドなので当然ですが。しばらくバグ取りに時間がかかりそうです。
それと,このネスト付きパーサーを考えたときは,ネストの内側も全部解釈実行することを前提にしていたのですが,考えてみたら内側は別のパーサー(例えばスキン変数の内側はテンプレートのパーサーが扱うべき)で実行するのが普通なので,そのあたりの処理も変えないといけません。例えばifの解釈もネストできるのですが,そこはオーバースペックでしょう。
Foodynの各機能(仕様)への評価
先日のNucleusの合宿で行ったプレゼンについて,参加者にアンケートを取りました。全部紹介すると大変なので,各機能に付いての評価をまとめたものを図二つにしました。

おおむね好評だったといっていいと思うのですが,Ruby on Railsを使っていることについてはやはり賛否両論分かれました。実績が少ないことでの不安に加え,現在のところホスティングで使えるところが限られてしまうのが理由だと思います。
ただ,PHPやMySQLも5,6年前には安くて使えるところはほとんどありませんでした。Nucleusの最初のバージョンがでてきたころはPHPが広がった時期とほぼ同じではないかと思います。
Railsも後1年くらいでそれくらいのところまで来るのではないかというのが僕の予想です。
ほかにはHTTPの認証について,やはり意見が分かれました。ここも仕方ないところだと思いますが,Foodynではプラグインで別の認証が使えるようにしておく(はず)なので,大きな問題にはならないのではないかと思っています。
限られた時間でのプレゼンだったので,RESTにまつわるところなど説明が足りていない部分もかなりあると思いますが,それでもこれだけ理解してもらったことはありがたいことです。
OpenIDとブログ
FoodynではOpenID対応はプラグインで実現することになると思いますが(~~認証も),この手のログイン用にデフォルトで用意しているユーザーを増やさないといけないなあと思っています。すなわち,これまでだったらログインしている人はすべてメンバーだったわけですが,OpenIDなどが入ると,ログインしているけどゲストって状態があるわけで,ログインしていないゲストと別のロールにしておかないといけません。昨日書いたsafeスキンのフィールドも含めてちょっとテーブルをいじらないと。
テーブルいじること自体はMigration書くだけなので簡単なのですが,Migrationがあんまり多いと配布するときは格好悪いので,どこかでマージして数を減らす必要があります。そうすると既にインストールしている人にテーブルをいじってもらわないといけなくなるなど,また別の面倒が出てくるのが鬱陶しいところ。
複数スキン・エンジンのサンプル
先日のプレゼンについてのアンケート結果は近日まとめるつもりですが,なかで「複数表示エンジン」のところが具体例がないとちょっと分かりにくいかなという話があり。そこで考えてみたのですが,一番簡単で,かつ意味があるサンプルとして,次のようなエンジンを考えてみました。先日のプレゼンでは時間の関係でさらっとしか話しませんでしたが,Foodynではスキン・エンジンに「safe」あるいは「unsafe」の属性が付きます。デフォルトのNucleus互換エンジンは「unsafe」なので,「safe」なスキンしか使えないユーザーはアクセスできません。そこで,Nucleus互換のエンジンを使った「safe」なエンジンが必要になります。
このエンジンでは,ユーザーはスキンの選択はできますが編集できません。indexページに表示するアイテムの数だけが設定できます。
この程度のエンジンだったら簡単に作れますし,イメージつきやすいかなあと。
インライン・テンプレートとヘッダー変数を実装
仕様として書いたからには実装しないと,ということでNucleus互換表示エンジンにヘッダー・スキン変数の機能とインライン・テンプレートの機能を実装しました。ヘッダーの方はごくごく簡単で,コントローラにヘッダを保持するハッシュを用意しておき,プラグインから読み書きできるようにして,スキン変数で書き出すだけです。文字列でなくハッシュにしたのは,同じものが既にないかどうか調べられるようにするため。インライン・テンプレートはスキン変数のパラメータに{}で挟まれた部分があると,そこをJSON(実際にはYAMLのライブラリを使っていますが)として解釈するもの。まだちゃんとデバッグはしていませんが,たぶんそんなに問題はないでしょう。