Categories
open all | close allTags
スキンエンジン | OpenID | RESTful | タグ | 名称 | ドキュメント | テスト | フォーム | モデル | Aptana | Subversion | Migration | デュアル・コア | 認証 | Flash | rake | パソコン | 国際化 | CSRF | アクセス制御Search
ちょっと付け焼刃ですが一応TagCloud完成
スキンからは<%TagCloud(default/index,10)%>みたいな感じで呼び出します。パーサーでは
def parse_tagcloud params
blog = params[3] ? Blog.find_by_bname(params[3]) : @controller.blog
templatename = params[1]
template = Template.find_by_tdname(templatename)
bd = blog.basic_data
data = Hash.new
tempbuffer = fill(template['TAGLIST_HEADER'], bd)
tags = Category.tags(blog.id, params[2].to_i)
max = tags.first.count.to_i
min = tags.last.count.to_i
maxlevel = (params[4] || 4).to_i
if max == min
factor = 0.0
else
factor = (maxlevel - 1).to_f / (max - min)
end
tags.each do |tag|
data['level'] = Math.sqrt((tag.count.to_f - min) * factor).to_i + 1;
data['taglink'] = url_for(:controller=>'categories', :catid=>tag.catid, :action=>'show')
data['tagname'] = tag.cname
data['count'] = tag.count
tempbuffer += fill(template['TAGTLIST_LISTITEM'], bd.merge(data))
end
tempbuffer += fill(template['TAGLIST_FOOTER'], bd)
return tempbuffer
end
ここでCategory.tagsで指定した数だけタグをカウント順に取ってきます。この最大値と最小値から,比率を求めて大きさ(テンプレート中で<%level%>)を決めます。
Category.tagsの方が,苦労したところ。
def self.tags blogid, limit = nil
if limit
find(:all, :joins=>('LEFT JOIN '+TagCategory.table_name+' on catid=category_id'), \
:conditions=>['cblog = ? and caliasid is not null', blogid], \
:group=>'catid', :select=>'*, count(*) as count', :order=>'count DESC', \
:limit=>limit.to_i)
else
find(:all, :joins=>('LEFT JOIN '+TagCategory.table_name+' on catid=category_id'), \
:conditions=>['cblog = ? and caliasid is not null', blogid], \
:group=>'catid', :select=>'*, count(*) as count', :order=>'count DESC')
end
end数を限定する場合としない場合で分けてあります。これでSQL文一回呼び出すだけでカウントが取れます。
今の仕様だと,カウントが多い順にしか出力されないところがいまいちです。リストを取ってきた後ランダムに並べ替えるようなことができるといいのですが。
表示されていないだけで
countはちゃんと取れていました。ただし型がStringになる点に注意が必要です。Integerにする方法はないのかな?
:joinsに変えたら
SQLは正しく発行できているのを確認しました。でもオブジェクトとしてcountが取れていないみたい。どうしてかちゃんと調べようとしたところで昨晩はダウン。今週は仕事が忙しいのでさすがに疲れ気味です。
:includeと:selectは共存できない
まだTagCloudではまっているのですが,何かというとタグクラウドを作るためにはタグごとに使われているアイテム数を調べる必要があるのです。タグごとに調べるのであればActiveRecord使って簡単に書けますが,どうみても効率が悪そう。SQL文一発で実行したいのです。group byしてcount取れば行けるのはSQLレベルでは確認したのですが,なかなかRailsでうまく行きません。生成したSQLを見るとselectにcountが入っていません。
さらに理由を調べると,どうやら:includeオプションを使うと:selectは使われないそうな。:includeは結構膨大なテーブル検索をかけるのでこれも効率悪そうなので,:joins使って実現することにします。テーブル名を直接指定しないといけないのでちょっと面倒ですが。
TagCloudがめんどくさい
探せばタグクラウド作るプラグインくらい,あるんでしょうけど,下手な作り方をすると結構効率が落ちそうな気がするのでなるべく自前でやろうとしています。アルゴリズムが難しいわけではないのですが,クエリーが結構複雑なので,なかなか手が進みません。がんばれ>自分
OpenIDの使い方
最近,実験的なものが中心だとは思いますがOpenIDで認証可能なサービスがいくつか出てきました。今のところFoodynは自前の認証しかしていませんが,プラグインで拡張可能な形にはする予定です。そんなに難しくなくて,自前の認証の前にプラグイン用のイベントを用意しておき,そこで認証が行われたときは自前の認証はしない,というだけで済むはず。とはいってもまだOpenID自体生半可にしか理解していません。例えばYahoo!ジャパンのOpenIDにだけ対応して,そのサイトにユーザーがアクセスしにくると,Yahoo!でログインしているかどうかを調べて,自動的に自分のところもログインするなんてことはできるのでしょうか。
なんでそんなことを考えているのかというと,今プライベートでYahoo!グループ(昔のeGroups)を使っているのですが,これだけだと機能が低いので,自サイトのサービスを使って拡張できないか,ただそのときに新たに認証登録などはしないで済ませられないかと思っているのです。コンピュータ・リテラシがあまり高くない人も結構いるので,今のOpenIDのサイトみたいにギークでないと使えないようなものは無理。なるべく自動的にしてあげないといけないのです。
テンプレート表示
前回書いたようなモデル化までが済んでいれば,テンプレートでマルチカテゴリをサポートするのは簡単です。とりあえずテンプレートにと書いたときに展開するというだけのものをあげておきます。これをテンプレートのパースのクラスに置くだけでOKです。
def parse_multiplecategories params
temp = ""
separator = nil
item.categories.each do |cat|
temp += separator.to_s
separator ||= ','
temp += link_to( cat.cname, {:controller=>'categories', :catid=>cat.id.to_s, :action=>'show'})
end
return temp
end
カテゴリとタグのリレーション設定
Foodynではタグとカテゴリは1テーブルで管理し,どのアイテムがどのカテゴリ,あるいはタグに属しているかを中間テーブルを使って記す方法を取っています。Ruby on Railsではこれはhabtn(has and belongs to many)あるいはhas_many throughという方法で取り扱います。ここでは中間テーブルも明示的にいじれるように後者の実装を採用します。中間テーブルのTagCategoryでは次のように取り扱います。
class TagCategory < ActiveRecord::Base
set_base_name 'tag_category'
belongs_to :categories, :class_name=>'Category', :foreign_key=>'category_id'
belongs_to :items, :class_name=>'Item', :foreign_key=>'item_id'
endItem側ではitemオブジェクトに対してitem.categoriesとすればカテゴリ一覧を,item.tagsとすればタグ一覧を表示するために条件を加えてやります。
has_many :categories, :through => :tag_category, :conditions=>"cleft is not null"
has_many :tags, :through => :tag_category, :source=>:categories, :conditions=>"caliasid is not null"
has_many :tag_category
2行目で:source=>:categoriesとしているのはTagCategoryの belongs_to:categoiesを参照しなさいという意味。
これで :conditionsのところの条件でカテゴリとタグを分けます。
マルチカテゴリのテンプレート指定
マルチカテゴリを標準機能として採用すると,Nucleusのテンプレート・モデルと合わない部分が出てきます。具体的には<%catname%>や<%catlink%>といったテンプレート変数は意味がなくなります。例えばdefault/indexのテンプレート中における<a href="<%categorylink%>" title="Category: <%category%>"><%category%></a>
という部分です。どうしても上位互換にするなら,これらのテンプレート変数が来たときは,検索してトップに来たカテゴリーを代表とするとかってことにする手もありますが,どうですかねえ(こうすればシングル・カテゴリーで使っている人の互換性は一応保たれます)。昔考えたときはitemテーブル内のicatをデフォルト・カテゴリという扱いにする,といった実装も考慮していたと思いますが,あまり前向きでないので,それは避けたいところ。
とりあえず,MultipleCategories使っている人はこの部分をそのままにしておくはずはないだろうから,MultipleCategoriesというテンプレート変数をそのまま使ってしまって,というのが当面の実装になりそうです。
で,肝心なことは,複数カテゴリが標準のときのテンプレートにおけるカテゴリー指定はどう書くのがいいか,ということ。<%categories%>とかってテンプレート変数を作って,それが呼ばれたときにテンプレート内の専用の項目を見に行くとかっていうのがNucleusの今の仕様に一番近い形ですが,セパレータの指定をどう書くかなど,ちょっと悩ましい部分があります。
あとは,これまで使用を封印してきたネスト付きパーサで,テンプレート内テンプレートをやらせてしまう,というのもあるけど,やっぱり今のモデルと極端に違うものを加えるのは良くないだろうなあ。
設計とメソッド名と内部構造
どうも頭がまだ手続き型言語から抜けていないようで,メソッド名を付けるときに,ついつい「get~~」みたいに動詞を入れてしまいがちです。ということに気付いたのは,あるブログに属するカテゴリー一覧を取得するとき。実はこれまでCategoryクラスの方でget_listというメソッドを作ってブログオブジェクトをパラメータに取得するようにしていました。こうしておくと,ブログが指定されないときの一覧やブログが複数あるときの一覧とかも同じメソッドで取れるというのがメリットなのですが,見直してみるとCategory.get_list ... と入るのはあまりきれいじゃないです。やはり blog.categories と書いた方がスマート。
これだと順序付けができないのかと思って,この記述を使っていなかったというのもあるのですが,APIを見たら has_many のリレーション指定の中で :order を使って指定できることが分かったので,今回はそちらに切り替えました。
ただ,しっくりこない面がないわけではありません。これだとBlogクラスの中でCategoryのテーブルの内部情報を使って :order のところを書くことになるため,Categoryの実装に完全に依存してしまいます。Categoryの内部構造が変わったらBlogクラスも変えなければいけないのでメンテナンス性が悪くなります。
どうするのがベストなんでしょう。