Ruby Freaks Lounge

第34回Redmineプラグイン開発(1)

はじめに

RedmineはRuby on Railsで実装されたプロジェクト管理ツールです。競合のツールとしてTracが有名ですが、Tracと比較して開発速度が早く、ここ数年で急激にユーザを増やしています。Tracには標準で用意されていない機能として、ガントチャートの表示や複数プロジェクトの管理、チケットの種別ごとのワークフローのカスタマイズ機能に加え、最新の0.9系ではTracに比べて唯一貧弱だったチケットのレポーティング機能がほぼ同等レベルまで強化されました。

これにより今年から本格的にRedmineへの移行が始まっていくと思われます。既に2009年の9月にはオープンソースのSNSエンジンであるOpenPNEの開発チームがTracからRedmineに移行したことや、Googleトレンドの検索数でRedmineがTracを追い抜いたというニュースもあります。

今回は、Redmineのプラグインの開発について解説していきます。Redmineは記事執筆時点での最新安定版であるバージョン0.9.3を使っています。

Redmineのインストール

Redmineのインストール方法は公式サイトの記事を始め、沢山のサイトで解説されています。Passengerというソフトウェアを利用すればApache上でRedmineを稼働させることも可能ですが、ここでは最低限の手順でインストールを進めます。詳しく知りたい方は検索してみてください。

1. Redmineのダウンロード、展開
$ wget http://rubyforge.org/frs/download.php/69449/redmine-0.9.3.tar.gz
$ tar zxf redmine-0.9.3.tar.gz; cd redmine-0.9.3
2. データベースの設定
$ mysql -u root -p -e 'create database redmine character set utf8'
$ cp config/database.yml.example config/database.yml
$ vi config/database.yml
※ お使いのDBMSに従ってデータベースのアカウント情報を入力して下さい。
$ RAILS_ENV=production rake db:migrate
3. migrationの実行、初期設定
$ RAILS_ENV=production rake db:migrate
$ RAILS_ENV=production rake redmine:load_default_data
※ 言語選択のプロンプトが表示されるので、「ja」と入力します。
4. アプリケーションサーバの起動
$ sudo script/server -p 80 -e production

作ってみよう

さて、それでは早速プラグインを作っていきましょう。

プラグインの開発は、以下のような流れになります。

  1. 雛形の作成
  2. プラグインの初期設定
  3. モデルの実装
  4. マイグレーションの実施
  5. コントローラ・ビューの実装

今回作成するプラグインの仕様

今回はServerStatusというプラグインを作ります。稼動しているサーバのステータスを一覧で確認することで、稼働/停止を切り替えられるようにします。また、ステータスの変更履歴を残すようにもしてみましょう。

雛形の作成

Redmineではプラグインの実装用に3つのジェネレータが用意されています。

  1. redmine_plugin
  2. redmine_plugin_controller
  3. redmine_plugin_model

雛形の作成にはredmine_pluginジェネレータを使用します。

$ ruby script/generate redmine_plugin ServerStatus

REDMINE_ROOT/vendor/plugins/redmine_server_status ディレクトリ以下に沢山のファイルが作成されるはずです。これがプラグインのルートディレクトリになります。以降はPLUGIN_ROOTと表記します。

プラグインの初期設定

PLUGIN_ROOT/init.rbを編集し、初期設定を記述します。初期設定で最も重要なのは表示メニューへの追加です。追加可能な表示箇所は以下の4つに分類されます。

  1. トップメニュー(top_menu)
  2. アカウントメニュー(account_menu)
  3. アプリケーションメニュー(application_menu)
  4. プロジェクトメニュー(project_menu)

それぞれのメニューの表示箇所を下図に表示します。なお、プロジェクトメニューはプロジェクト選択時に、アプリケーションメニューはプロジェクト非選択時に表示されます。

図1 トップメニュー、アカウントメニュー、プロジェクトメニュー
図1 トップメニュー、アカウントメニュー、プロジェクトメニュー
図2 アプリケーションメニュー
図2 アプリケーションメニュー

今回はトップメニューに追加します。メニューにはラベルとURLを指定する必要があります。URLはHashでも文字列でも構いませんが(外部サイトも指定可能です⁠⁠、今回はserversコントローラのindexアクションを呼び出すようにします。

init.rb
require 'redmine'

Redmine::Plugin.register :redmine_server_status do
  name 'Redmine Server Status plugin'
  author 'Takayuki Kyowa'
  description 'Redmine Server Status plugin'
  version '0.0.1'

  menu :top_menu, :server_list, { :controller => 'servers', :action => 'index' }, :caption => "サーバ一覧", :last => true
end

最後のmenuメソッドでメニューへの追加先を指定しています。引数は以下のとおりです。

menu
  :top_menu, # 追加先メニュー名
  :server_list,      # メニュー項目の識別子。一意でなければならない。
  { :controller => 'contributions', :action => 'index' },  # URLのハッシュ、または文字列。
  :caption => "サーバ一覧", # メニューの項目名
  :last => true # メニューの追加先。標準メニューの間や先頭に挿入することも可能です。

余談ですが、実はRedmineのメニューはMenuManagerと言うモジュールによって、標準の項目もプラグインと同様に扱われています。標準メニューの定義はREDMINE_ROOT/lib/redmine.rb の119行目の辺りから実装されています。トップメニューを定義している箇所を抜粋してみましょう。

Redmine::MenuManager.map :top_menu do |menu|
  menu.push :home, :home_path
  menu.push :my_page, { :controller => 'my', :action => 'page' }, :if => Proc.new { User.current.logged? }
  menu.push :projects, { :controller => 'projects', :action => 'index' }, :caption => :label_project_plural
  menu.push :administration, { :controller => 'admin', :action => 'index' }, :if => Proc.new { User.current.admin? }, :last => true
  menu.push :help, Redmine::Info.help_url, :last => true
end

Redmine::Plugin.registerブロックのmenuメソッドはMenuManagerを呼び出すラッパーになっています。Redmineではこのような実装に共通性があり、他にも様々な機能がモジュール化されています。非常に参考になりますので、興味のある方は是非一度lib/redmine.rbを読んでみてください。

モデルの実装

次にモデルを作成します。今回必要になるのはサーバとサーバのステータス変更履歴を表現する二つのモデルです。

通常はそれぞれでテーブルを作成しますが、今回は履歴用のモデルにはRedmineで使用しているcommentsテーブルを使ってみましょう。commentsテーブルはRailsの Polymorphic Associationを使って実装されており、様々なモデルに対するコメントを集約しているテーブルです。現在はニュース機能へのコメントのみを扱っていますが、将来的に機能が追加された際はこのテーブルが使われることでしょう。

redmine_plugin_modelジェネレータでServerモデルを作成します。

$ ruby script/generate redmine_plugin_model ServerStatus Server name:string, status:integer, comment_counts:integer

redmine_plugin_modelの引数は以下の通りです。初めにプラグイン名を指定する以外はRailsと同じですね。

redmine_plugin_model <plugin_name> <model_name> [ <column_name:column_type>, ...]

なお、comment_countsというカラムにはそのサーバのコメント(=履歴)数が格納されます。counter cacheというRailsの機能で実現されており、実装者が意識する必要はありません。

モデルにリレーションや基本的なロジックを記述していきます。

app/models/server.rb
class Server < ActiveRecord::Base

  has_many :comments, :as => :commented, :dependent => :delete_all, :order => "created_on DESC" 

  module Status
    WORKING = 1
    STOPPED = 2
  end

  def status_name
    case self.status
    when Status::WORKING
      "稼働中" 
    when Status::STOPPED
      "停止" 
    end
  end
end

マイグレーション

次にマイグレーションと初期データを投入します。

モデルを作成した時点でPLUGIN_ROOT/db/migrateディレクトリ以下にマイグレーションファイルが作成されています。ただし、Redmineのプラグインはタイムスタンプ式のマイグレーションに対応しておらず、連番のファイル名に変更する必要があります。

mv ${PLUGIN_ROOT}/db/migrate/20100301102344_create_servers.rb ${PLUGIN_ROOT}/db/migrate/001_create_servers.rb

また、Rails 2.3.4から初期データはdb/seeds.rbに書くことが推奨されていますが、残念ながらvendor/plugin以下にあるseeds.rbは読み込んでくれません。今回はmigrationファイルに直接記述してしまいましょう。

db/migrate/001_create_servers.rb
class CreateServers < ActiveRecord::Migration
  def self.up
    create_table :servers do |t|
      t.column :name, :string
      t.column :status, :integer
      t.column :comments_count, :integer, :default => 0
    end

    (1..8).each{|i| Server.create!(:name => sprintf("server%02d", i), :status => Server::Status::WORKING)}
  end

  def self.down
    drop_table :servers
  end
end

一通りの修正が完了したらマイグレーションを実施します。

$ rake db:migrate_plugins

コントローラ・ビューの実装

さて、それではいよいよコントローラの実装に入ります。

まずはredmine_plugin_controllerジェネレータでファイルを作成します。

$ ruby script/generate redmine_plugin_controller ServerList servers index

PLUGIN_ROOT/app/controllers 以下にservers_controller.rbが、 PLUGIN_ROOT/app/views/servers/以下にindex.html.erbファイルができました。

次にコントローラの実装です。Serverモデルからすべてのレコードを名前順に取得します。なお、実装についてですが、Redmineと関連しない箇所に関しては通常のRailsアプリと同じ感覚でコードを記述しても問題ありません。

app/controllers/servers_controller.rb
class ServersController < ApplicationController

  def index
    @servers = Server.find(:all, :order => "name ASC")
  end

end

次にビューです。@servers変数を展開してサーバ名とステータスを書き出しています。

app/views/servers/index.html.erb
<style type="text/css">
table.servers {
width: 60%;
}
</style>
<h2>サーバ一覧</h2>

<table class="list servers"> <!-- ① -->
<thead><tr>
<th>サーバ名</th>
<th>状態</th>
</tr></thead>
<tbody>
<%- @servers.each do |server| -%>
<tr class="<%= cycle("odd", "even") %> server">
<td><%=h server.name %></td>
<td><%=h server.status_name %></td>
</tr>
<% end %>
</tbody>
</table>

①ではテーブルのclass属性にlistを指定しています。このクラスはチケットの一覧画面など、表形式のビューで共通して使われており、指定することでレイアウトやマウスオーバー時のハイライトなどがおこなわれます。Redmine本体のバージョンアップに伴いレイアウトが崩れる可能性がありますが、デザインの手間を省くことができ、UIが統一されますのでメリットの方が大きいと個人的には考えています。

これで準備完了です。アプリケーションサーバを再起動してブラウザをリロードしてみましょう。トップメニューに「サーバ一覧」追加されていて、そのリンクにアクセスすると以下のような画面が表示されるはずです。

図3 サーバ一覧画面
図3 サーバ一覧画面

ステータス変更機能の実装

では今度はステータスの変更機能を実装していきます。アクション名はtoggleとし、ステータスのリンクをクリックしたら稼働中/停止のキャプションがAJAXで変わるようにします。

まずはコントローラのコードです。

app/controllers/servers_controller.rb(追加部分のみ)
def toggle
  if request.xhr? && request.post?
    server = Server.find(params[:id])
    server.update_attributes!(:status => server.status ^ 3)

    user = User.current # ①
    comment = Comment.new(:comments => "#{user.login}さんが#{server.status_name}に変更しました。", :author => user)
    server.comments << comment

    render :update do |page|
      page.replace_html "status_#{server.id}", server.status_name
      page.visual_effect :shake, "status_#{server.id}" 
    end
  end
end

①のUser.currentメソッドでログインユーザのインスタンスを取得しています。その他は特にRedmineとは関連しないため、解説は割愛します。

次にビューのコードです。AJAXに対応するため、ステータスの表示部分を書き換えています。

app/views/servers/index.html.erb(変更部分のみ)
<%- @servers.each do |server| -%>
<tr class="<%= cycle("odd", "even") %> server">
<td><%=h server.name %></td>
<td><%= link_to_remote content_tag(:span, h(server.status_name), :id => "status_#{server.id}"), :url => { :action => "toggle", :id => server.id }, :method => "post" %></td>
</tr>
<%- end -%>

履歴の確認機能の実装

最後に履歴の確認画面を作ります。アクション名はshowとします。これも特にRedmineとは関連しないため、解説は割愛します。

app/controllers/servers_controller.rb(変更部分のみ)

def show
  @server = Server.find(params[:id], :include => [:comments])
end
app/views/servers/index.html.erb(変更部分のみ)
<%- @servers.each do |server| -%>
<tr class="<%= cycle("odd", "even") %> server">
<td><%= link_to h(server.name), :action => 'show', :id => server.id %></td>
<td><%= link_to_remote content_tag(:span, h(server.status_name), :id => "status_#{server.id}"), :url => { :action => "toggle", :id => server.id }, :method => "post" %></td>
</tr>
<%- end -%>
app/views/servers/show.html.erb
<h2><%=h @server.name %>のステータス変更履歴</h2>

<%- @server.comments.each do |comment| -%>
  <p><%=h comment.created_on.strftime("%F %T") %> : <%=h comment.comments %></p>
<%- end -%>

これで一通りの実装が完了しました。こんな感じの画面になります。

図4 サーバ一覧画面
図4 サーバ一覧画面
図5 ステータス変更履歴画面
図5 ステータス変更履歴画面

まとめ

今回は基本的なプラグインの開発方法を解説しました。ここまで見ていただいたように、Redmineのプラグイン開発は非常に自由度が高く、チケットやWikiなどのビルトインされている機能とまったく関連のないものでも作ることができます。また、通常のRailsアプリを作るような感覚で実装できるため、Railsに慣れている方であればほとんど学習コストはかからないでしょう。今回作成したServerStatusプラグインにサーバの追加や削除機能を追加するのも簡単です。

RedmineやTracなどのプロジェクト管理ツールはプロジェクトのポータルページを兼ねることが多いと思います。そこに手軽に自作のシステムを組み込むことができる、と言うのは非常に魅力的ではないでしょうか。

次回はワークフローとデプロイタスクを統合するContinuousDeploymentというプラグインを作成します。チケットやタイムラインとの連動についても解説していきますので、どうぞお楽しみに。

おすすめ記事

記事・ニュース一覧