interprism's blog

インタープリズム株式会社の開発者ブログです。

Redmineのプラグインを開発してみて(備忘録)

この投稿は インタープリズムの面々が、普段の業務に役立つ記事を丹精込めて書き上げる! Advent Calendar 2016 - Qiitaの9日目 の記事です。

こんにちは、最近JavaScriptが好きになってきたこばやしです。

業務でRedmineプラグインを開発中な私ですが、開発をするにあたってハマったこと、気になったことなどをつらつら書いておいて同じ道にハマった場合にすぐに抜け出せるようにしようと思います。

OSがUbuntuでrootになりたい時はsudo su -

Ubuntuには初期設定ではrootがいないので何をするにもsudoが必要で煩わしいです。

そんな場合には

sudo su -

でrootになれます。(CentOSは初期でrootがいるのでそんな煩わしさはありませんでした)

新しい環境にプラグインを導入する場合に

インストールディレクトリ/plugins に配置したら

bundle install
bundle exec rake db:migrate

大体これでうまくいく

Developmentモードでの起動(デバッグモード)

Redmineのインストールディレクトリ(大体 /var/lib/redmine/ )で

ruby bin/rails server webrick

でDevelopmentモードで起動することができます。(ポートは3000番)

この状態だとlocalhostからしかアクセスを受け付けません。(GUIでないとデバッグが面倒です)

他の端末からアクセスするには

ruby bin/rails server -b 0.0.0.0

でアクセスすることが可能です。(0.0.0.0はネットワーク内の誰でもってことで、ここに192.168.0.2とか割り当てればその端末しかアクセスできなくなります)

プラグイン名とディレクトリ名は揃える

Plugin名というのは init.rbRedmine::Plugin.register で指定している部分です。

ディレクトリ名というのはプラグインinit.rb があるディレクトリの名前です。

基本的に ruby script/rails generate redmine_plugin <plugin_name> のようにプラグインを作成する場合はプラグイン名とディレクトリ名が一緒になっていますが、配布する際や導入する際にちょっとディレクトリ名変えてみようかなって変えちゃうとエラーが出る場合があります。

MySQLPostgreSQLで返り値が変わる

SQLを直接実行したい場合に ActiveRecord::Base.connection.execute を使用していましたが、実行した際の返り値が異なっていました。

User.connection.execute('SELECT id, login FROM users').each do |row|
  p row
end

とかすると、MySQLの場合は

[1, "admin"]
[2, ""]
[3, ""]

のように返ってきます。
アクセスする場合は row[0] とかすると値が取れます。

PostgreSQLの場合は

{"id"=>1, "login"=>"admin"}
{"id"=>2, "login"=>""}
{"id"=>3, "login"=>""}

となります。
アクセスする場合は row['id'] のようになります。

アクセス方法が異なるため同じプラグインを導入してもMySQLPostgreSQLかでRubyの実行時エラーになります。

そこでSELECT文を発行する場合は ActiveRecord::Base.find_by_sql がおすすめです。
返り値はモデルの配列が返ってきます。

例えば

User.find_by_sql("SELECT * FROM users").each do |row|
  puts row.class.name
end

とかやると

User
User
User

みたいに返ってきます。
各カラムには row.id のようにドットでアクセスできます。

モデルで返ってきますが、モデルに含まれないSELECT文で指定したカラムも取得しているようです。

User.find_by_sql("SELECT id, (SELECT name FROM projects LIMIT 1) hoge FROM users").each do |row|
  p row.hoge
end

とかやると

Project1
Project1
Project1

のようになります。

view hook を開発する場合

lib 配下に Redmine::ViewListener を継承したクラスを作成する

そのファイルを init.rb でインクルードする

具体的な手順は下記

lib/hoge.rb 下記のように作成する

class HogeHook < Redmine::Hook::ViewListener
  def view_issues_new_top(context)
    p 'foo'
  end
end

init.rb で下記をインクルード

require 'hoge'

これで view_issues_new_top というフックを開発できます。

Redmineで使用できるフックの一覧は下記を参照してください

Hooks List - Redmine

alias_method_chain を使う

init.rb に下記のように追加

ActionDispatch::Callbacks.to_prepare do
  IssuesHelper.send(:include, Redmine::Plugins::IssuesHelperCustomizer)
end

lib/hoge.rb 下記のように作成

module Redmine
  module Plugins
    module IssuesHelperCustomizer
      def self.included(base)
        base.send(:include, InstanceMethods)
        base.class_eval do
          alias_method_chain :render_descendants_tree, :boo
        end
      end
      module InstanceMethods
        def render_descendants_tree_with_boo(issue)
          issue = Issue.first
          render_descendants_tree_without_boo(issue)
        end
      end
    end
  end
end

上記の例は IssuesHelper.render_descendants_tree のメソッドを render_descendants_tree_with_boo に置き換えています。

オリジナルのメソッドを呼び出す場合は render_descendants_tree_without_boo のように with でなくて without にする。

既存クラスのメソッドを置き換える

init.rb に下記のように追加

ActionDispatch::Callbacks.to_prepare do
  IssuesHelper.send(:include, Redmine::Plugins::IssuesHelperCustomizer)
end

lib/hoge.rb 下記のように作成

module Redmine
  module Plugins
    module IssuesHelperCustomizer
      def self.included(base)
        base.class_eval do
          def render_descendants_tree(issue)
            return ''
          end
        end
      end
    end
  end
end

前項の alias_method_chain は、元のメソッドを残したまま新たにメソッドを差し込むような動きをしますが、この方法は元のメソッド自体を置き換えます。

最後に

ここまで読んで頂いてありがとうございます。

探していた内容が見つかっていれば幸いです。

インタープリズムの面々が、普段の業務に役立つ記事を丹精込めて書き上げる! Advent Calendar 2016 - Qiita10日目の記事

PAGE TOP