まだやってました。でも大枠は完成って感じです。
- Redmineのプロジェクトの並びが気に入らないという話なのにHaskell書いてた - yunomuのブログ
- Redmineのプロジェクトの並びが気に入らないという話その2 Redmineプラグイン試作 - yunomuのブログ
ちょっと前ですが、redmine_projects_sorterをver.0.0.2にしました。0.1.0とかでもよかったかなー。
projects/indexだけじゃなくガントチャート他のプロジェクトの並び順制御にも対応しました。名前の辞書順だけですけど。
配布元:https://github.com/yunomu/redmine_projects_sorter
やったことなど
Redmineのバージョンは1.3.1です。
前回作ったプラグインは、projects/indexでのプロジェクトの並び順の制御はできていたけど、ガントチャートなどは従来のままでした。まあprojects_helperをいじっただけだからそりゃそうなんだけど。
で、やっぱりガントチャートもキレイに並び替えてくれないと使いものにならない。
ということで、ガントチャートの表示部分のデータがどこからどんな形できてるのかを追いかけてみました。
まずガントチャート画面のパスが"/projects/プロジェクト識別子/issues/gantt"なので、route.rbからそれっぽいところを探す。
config/route.rb
92 map.with_options :controller => 'gantts', :action => 'show' do |gantts_routes| 93 gantts_routes.connect '/projects/:project_id/issues/gantt' 94 gantts_routes.connect '/projects/:project_id/issues/gantt.:format' 95 gantts_routes.connect '/issues/gantt.:format' 96 end
これっぽい。Railsルールでは、これはparams[:project_id]にプロジェクト識別子を入れてgantts/showを呼んでいる。
gantts/showのviewを見る。
app/views/gantts/show.html.erb
74 <div class="gantt_subjects"> 75 <%= @gantt.subjects.html_safe %> 76 </div>
ここで表示してるっぽい。
じゃあ@gantt.subjectsはどこからきてるのか。とりあえずコントローラから辿る。
app/controllers/gantts_controller.rb
18 class GanttsController < ApplicationController (略) 33 def show 34 @gantt = Redmine::Helpers::Gantt.new(params) 35 @gantt.project = @project 36 retrieve_query 37 @query.group_by = nil 38 @gantt.query = @query if @query.valid? 39 40 basename = (@project ? "#{@project.identifier}-" : '') + 'gantt' 41 42 respond_to do |format| 43 format.html { render :action => "show", :layout => !request.xhr? } 44 format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image') 45 format.pdf { send_data(@gantt.to_pdf, :type => 'application/pdf', :filename => "#{basename}.pdf") } 46 end 47 end
34行目から、@ganttはそもそもRedmine::Helper::Ganttのインスタンスらしい。
このクラスはlib以下にあったので、subjectsメソッドを見てみる。
lib/redmine/helpers/gantt.rb
18 module Redmine 19 module Helpers 20 # Simple class to handle gantt chart data 21 class Gantt (略) 120 # Renders the subjects of the Gantt chart, the left side. 121 def subjects(options={}) 122 render(options.merge(:only => :subjects)) unless @subjects_rendered 123 @subjects 124 end
renderを呼んだ後に@subjectsを返してる。
@subjectsの出所を細かく追っていくと、とりあえずinitializeでは空文字が入れられているけど、実態はrenderで作ってるっぽい。
というのを追いかけてる途中で見つけた。
lib/redmine/helpers/gantt.rb
174 def render(options={}) (略) 182 Project.project_tree(projects) do |project, level| 183 options[:indent] = indent + level * options[:indent_increment] 184 render_project(project, options) 185 break if abort? 186 end
あれ、もしかしてsubjects関係なくここで書いてるんじゃない?
Project.project_treeでツリーを作って、levelでインデントを決めてrender_projectだから、たぶんこれ。読むべきはProject.project_treeだった。
app/models/project.rb
18 class Project < ActiveRecord::Base (略) 653 # Yields the given block for each project with its level in the tree 654 def self.project_tree(projects, &block) 655 ancestors = [] 656 projects.sort_by(&:lft).each do |project| 657 while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) 658 ancestors.pop 659 end 660 yield project, ancestors.size 661 ancestors << project 662 end 663 end
has_many多すぎだろ……というのはさておき、
ここでlftでソート(line:656)して、レベルを添えてブロックに渡してる(line:660)。
というわけでこのメソッドを前みたいに書き換えてやればよさそう。
っていうかprojects/indexでもこれ使えよ! まあそれはいいや。HTMLのリスト系タグの入れ子構造とインデントでのツリー表現の両立は意外に大変よね。
ということでちょっとここでまたアルゴリズムを考えるターン。
lftでソートされたリストを読んで、projectとlevelの組のリストを返す関数が欲しい。んだけど、ソート済みのツリー構造を作るところまでは既に書いているのでそれを流用することにして、ツリーをレベルと要素の組のリストに変換する関数を書けばよさそう。
試しにまたHaskellで書いたのがこれ。
flatten :: [Tree] -> [(Node, Int)] flatten t = f' t 0 where f' :: [Tree] -> Int -> [(Node, Int)] f' [] _ = [] f' (T (n, cs):ts) d = (n, d):f' cs (d+1) ++ f' ts d
ついでに、これをインデント付きで表示する関数がこれ。
out :: [(Node, Int)] -> IO () out [] = return () out ((node, level):ns) = do indent level printNodeLn node out ns where indent :: Int -> IO () indent 0 = return () indent l = do putChar ' ' indent (l-1)
あ、なんかすっきり? というかどう考えても前のよりこっちの方が簡単だぞ。
Rubyで書き直すとこんな感じ。
def flatten(ts, d = 0) return [] if ts.size == 0 n, cs = ts.shift [[n, d]] + flatten(cs, d+1) + flatten(ts, d) end
割とそのまま書けた。これを使って、Project.project_treeを上書きしてやればいい。特異メソッドだからちょっとめんどかったので無理やりな感じで実装してしまいましたが。
class << Project include Redmine::Plugins::ProjectsSorter::ProjectPatch::ClassMethods def project_tree(projects, &block) flatten(sortTree(construct(projects), :name)).each do |p, l| yield p, l end end end
これで、ガントチャートのviewでもプロジェクトが名前順に並びました。
ソートのカラム指定とかソート用カラムを足すとかは、まあ当分いいや。
続き:redmine_projects_sorter ver.1.0.0 - yunomuのブログ
前の:Redmineのプロジェクトの並びが気に入らないという話その2 Redmineプラグイン試作 - yunomuのブログ