読者です 読者をやめる 読者になる 読者になる

yunomuのブログ

酒とゲームと上から目線

redmine_projects_sorter ver.1.0.0

githubでwatcherが増えてたので、いい加減バージョンを1以上にしよう。

しました。

yunomu/redmine_projects_sorter at v1.0.0 · GitHub

今回はDBを変更するのでmigrateが必要です。

% git clone git://github.com/yunomu/redmine_projects_sorter.git vender/plugins/redmine_projects_sorter
% rake db:migrate_plugins

なにこれ

Redmineのプロジェクト一覧とかガントチャートの画面で、同じ階層にあるプロジェクトの並び順がバラバラだったので、そのへんを制御できるようにしようというプラグインです。

プロジェクト設定に"order"って要素を追加して、同じ階層のプロジェクトをorder順に並び替えます。

やったこと

DBや画面の変更を伴うRedmineプラグインの書き方。

DBの変更

migrationを書く。この辺は普通のRailsと同じ。
こんな感じ。
vendor/plugins/redmine_projects_sorter/db/migrate/001_add_order_column.rb

  1 class AddOrderToProjects < ActiveRecord::Migration
  2   def self.up
  3     add_column :projects, :ord, :integer,
  4                :null => false, :default => 100
  5   end
  6
  7   def self.down
  8     remove_column :projects, :ord
  9   end
 10 end

ファイル名はRails2.x仕様じゃないとRedmine的にダメらしいです。試してませんが。
で、ordってカラムを追加しました。カラム名はorderだとSQL的にややこしいし、lft,rgtに並ぶからこれでいいかって感じです。デフォはなんとなく100で。最終的にこの値の小さい順に並べます。

ロジック変更

プラグイン内で、今まではソートキーをプロジェクト名にしてたのを、ordにした。
ソースは略。

画面の変更

projectsの設定画面を変更する。そもそもまずRedmineのprojects設定画面のソースがどこにあるのかというと、
config/route.rb

184   map.with_options :controller => 'projects' do |project_mapper|
185     project_mapper.with_options :conditions => {:method => :get} do |project_views|
186       project_views.connect 'projects/:id/settings/:tab', :controller => 'projects', :action => 'settings'
187       project_views.connect 'projects/:project_id/issues/:copy_from/copy', :controller => 'issues', :action => 'new'
188     end
189   end

このあたり。projects#settingsって感じ。
app/views/projects/settings.html.erb

  1 <h2><%=l(:label_settings)%></h2>
  2  
  3 <%= render_tabs project_settings_tabs %>
  4    
  5 <% html_title(l(:label_settings)) -%>

これだけ?
まあ、render_tabsとproject_settings_tabsを見ればいいんだろう。
render_tabsはApplicationHelperに、project_setting_tabsはProjectsHelperにあった。
app/helpers/application_helper.rb

  23 module ApplicationHelper
(略)
 219   # Renders tabs and their content
 220   def render_tabs(tabs)
 221     if tabs.any?
 222       render :partial => 'common/tabs', :locals => {:tabs => tabs}
 223     else
 224       content_tag 'p', l(:label_no_data), :class => "nodata"
 225     end
 226   end

app/helpers/projects_helper.rb

 20 module ProjectsHelper
(略)
 26   def project_settings_tabs
 27     tabs = [{:name => 'info', :action => :edit_project, :partial => 'projects/edit', :label => :label_information_plural},
 28             {:name => 'modules', :action => :select_project_modules, :partial => 'projects/settings/modules', :label => :label_module_plural},
 29             {:name => 'members', :action => :manage_members, :partial => 'projects/settings/members', :label => :label_member_plural},
 30             {:name => 'versions', :action => :manage_versions, :partial => 'projects/settings/versions', :label => :label_version_plural},
 31             {:name => 'categories', :action => :manage_categories, :partial => 'projects/settings/issue_categories', :label => :label_issue_category_plural}    ,   
 32             {:name => 'wiki', :action => :manage_wiki, :partial => 'projects/settings/wiki', :label => :label_wiki},
 33             {:name => 'repository', :action => :manage_repository, :partial => 'projects/settings/repository', :label => :label_repository},
 34             {:name => 'boards', :action => :manage_boards, :partial => 'projects/settings/boards', :label => :label_board_plural},
 35             {:name => 'activities', :action => :manage_project_activities, :partial => 'projects/settings/activities', :label => :enumeration_activities}
 36             ]
 37     tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
 38   end 

Redmineの設定画面はタブがたくさんあっていろんな設定を変更できるようになっているので、たぶんこれはそのタブのディスパッチャ的なもののようです。
見たいのはinfoタブなのでinfoを見ると、":partial => 'projects/edit'"って、まあそのまんまだった。
app/views/projects/_edit.html.erb

  1 <% labelled_tabular_form_for @project do |f| %>
  2 <%= render :partial => 'form', :locals => { :f => f } %>
  3 <%= submit_tag l(:button_save) %>
  4 <% end %>

"form"を見ろ。たらい回し感。
app/views/projects/_form.html.erb

 16 <p><%= f.text_field :homepage, :size => 60 %></p>
 17 <p><%= f.check_box :is_public %></p>
 18 <%= wikitoolbar_for 'project_description' %>
 19    
 20 <% @project.custom_field_values.each do |value| %>
 21   <p><%= custom_field_tag_with_label :project, value %></p>
 22 <% end %>
 23 <%= call_hook(:view_projects_form, :project => @project, :form => f) %>
 24 </div>

ここらへん、17行目が公開のチェックボックスなので、このあたりに挿し込みたいんだけど……と思ってたらちょうど23行目にhookがあった。

Redmineにはpluginで拡張するためにいろんな所にhookを書けるようになっているらしい。
Hooks - Redmine
このあたりにHookの使い方が書いてある。
model、view、controller、helperのそれぞれにhookがあって、その一覧がこちら。
Hooks List - Redmine
件の"view_projects_form"なんかもありますね。今回はこれを利用します。

これの見方は、

def view_projects_form(context) # context #=> {:project => @project, :form => f}
  ...
end

という感じのメソッドを定義してくれたらcontextにコメントみたいなハッシュを送り込みますよということらしい。パラメータの意味はviewやhelperのソースを参照するとして、全体としてはこう。

  1 class ProjectsSorter < Redmine::Hook::ViewListener
  2   def view_projects_form(context)
  3     project = context[:project]
  4     f = context[:form]
  5
  6     html = "<p>"
  7     html << f.text_field(:ord, :value => project.ord, :type => :number)
  8     html << "</p>"
  9     html
 10   end
 11 end

ViewListenrというのを継承してやる必要があるらしい。あとはRedmineフレームワークに則ってhtmlを返すコードを書いておく。
このListenerは、init.rbあたりでロードしておくだけでいいらしい。
vendor/plugins/redmine_projects_sorter/init.rb

(略)
require "redmine_projects_sorter_listener"
(略)

あとは、i18n。項目のラベルはそれなりのところに書いておかなきゃいけないそうです。
今回は"ja, form_ord"というところに書かなきゃいけないんですが、他の場合にどこに書かなきゃいけないかは、実際に動かしてみてエラーメッセージを見ながら探せばいいんじゃないでしょうか。
今回は具体的にはここ。面倒なので英語と日本語だけ。
vendor/plugins/redmine_projects_sorter/config/locales/en.yml

en:
  field_ord:
    Order

vendor/plugins/redmine_projects_sorter/config/locales/ja.yml

ja:
  field_ord:
    表示順

これで一見完成なんだけど、まだ値の更新ができない。何故か。

じゃあどこで止まってるのかと思って、だいぶ上の方で出てきたproject_settings_tabsを見てみると、":action => :edit_project"となってるけどProjectsControllerにそんなメソッドは無い。
でもどうせProjectsController#updateとかなんでしょ? と思ってparamsをログに吐くようにしてみると、まあここまでは正常に渡ってきてるっぽい。ちゃんとparams[:project][:ord]に値が入ってる。
それを保存しているようにも見える。(189行目)
app/controllers/projects_controller.rb

188   def update
189     @project.safe_attributes = params[:project]
190     if validate_parent_id && @project.save

と思ったけど、この"safe_attributes"って何だ。

app/models/projects.rb

569   safe_attributes 'name',
570     'description',
571     'homepage',
572     'is_public',
573     'identifier',
574     'custom_field_values',
575     'custom_fields',
576     'tracker_ids',
577     'issue_custom_field_ids'
578
579   safe_attributes 'enabled_module_names',
580     :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }

なんかチェックっぽい?
色々試してみたところ、要はこのsafe_attributesの中にordも追加してあげると、とりあえず値の更新ができるという事はわかった。
でもこれが結構難しかったので、
vendor/plugins/redmine_projects_sorter/init.rb

(略)
Dispatcher.to_prepare :redmine_projects_sorter do
  require_dependency 'projects_helper'
  ProjectsHelper.send(:include, Redmine::Plugins::ProjectsSorter::ProjectsHelperPatch)
  Project.safe_attributes 'ord'
end
(略)

Dispatcherの中で書き換える。
sqlite3でしかチェックしてないからmysqlでやるとバグるかもしれませんが。

前の:Redmineのプロジェクトの並びが気に入らない その3 - yunomuのブログ