##はじめに
Railsアプリは作ったことがあるけど、Redmine plugin開発に初めて取り組む際、
いろいろな疑問がわきました。
実際に開発していく中で疑問が解消されていったのでまとめておきます。
##前提
- 諸事情により今回はWindows版のRedmineを使用することを想定します。
- 必要な環境をまとめてインストールしてくれるBitNami Redmine Stackを使います。
- バージョンは少し古い2.3.2-1です。
- Cドライブ直下にインストールすることを想定します。
##疑問1 WindowsでどうやってRedmineを動かすの?
RubyからWebサーバーから必要なものを全部インストールしてくれるBitNami Redmine Stackを使います。
最新版は3.0.0ですが、2.3.2-1でインストールされたものは次の通りです。(気になったパッケージのみ)
- Ruby 1.9.3
- Rails 3.2.13
- Thin 1.3.1
- git 1.8.3
- Apache 2.4.6
- MySQL 5.5
- mingw32
mingw上で動くわけですね!
Apache - Thin - Redmine という構成です。
インストールすると自動的にサービスが起動しています。(productionモードで動作しています)
専用アプリで関連サービスの起動、停止、再起動を管理できます。
C:¥BitNami¥redmine-2.3.2-1¥manager-windows.exe
##疑問2 どうやってdevelopmentモードやtestモードで動かすの?
###1.コンソールを起動
まず、いろいろなPATHを通した専用コンソールを起動します。
C:¥BitNami¥redmine-2.3.2-1¥use_redmine.bat
このコンソールを使えばLinuxコマンド(mingw32)やrailsコマンド等が使えます。
###2.Redmineをdevelopmentモードで起動する
####2.1.起動中のサービスを停止
productionモードで動作中のサービスを停止します。
####2.2.相対パスの無効化
まず、アプリの相対パスを無効にします。
C:¥BitNami¥redmine-2.3.2-1¥apps¥redmine¥htdocs¥config¥additional_environment.rb
の中をコメントアウトします。(productionで動かす時は戻す必要があります)
#config.action_controller.relative_url_root='/redmine'
####2.3.development用、test用bundle設定有効化
development用、test用bundleが無効になっているので有効化します。
C:¥BitNami¥redmine-2.3.2-1¥apps¥redmine¥htdocs¥.bundle¥config
を変更します。
---
BUNDLE_BIN:bin
BUNDLE_WITHOUT: development:test:posgresql:sqlite3
---
BUNDLE_BIN:bin
BUNDLE_WITHOUT: posgresql:sqlite3
####2.4.bundle実行
C:¥BitNami¥redmine-2.3.2-1¥apps¥redmine¥htdocs
に移動します。
(ここがRedmineのRails.rootになります)
bundle
を実行します。
>bundle
####2.5.DB設定を確認
C:¥BitNami¥redmine-2.3.2-1¥apps¥redmine¥htdocs¥config¥database.yml
を確認します。
BitNamiのバージョンにもよりますが、productionとdevelopmentとtestが同じDBを指しているので、必要に応じて変更します。
また、DBのrootのパスワードはBitNamiインストール時のRedmine管理者アカウントのパスワードと同じものになっています。
####2.6.db:migrate実行
DBをproductionと分ける場合はDBを初期化します。
>bundle exec rake db:drop db:create db:migrate redmine:plugins:migrate redmine:load_default_data RAILS_ENV=development
####2.7.Redmineを起動
railsを起動します。
>bundle exec rails s
####2.8.Redmineを終了
Ctrl + C
で終了します。
##疑問3 pluginはどんな構成でどう配置するの?
Redmineのサイトにチュートリアルを見るのが一番わかりやすです。
一言で言うと、Redmineアプリの中に複数のRailsアプリ(plugin)が共存するイメージです。
-
ruby script/rails generate redmine_plugin <plugin_name>
コマンドで作成する -
C:¥BitNami¥redmine-2.3.2-1¥apps¥redmine¥htdocs¥plugins
以下に<plugin_name>
(RedmineのリソースにアクセスできるRailsアプリ)が配置される - plugin独自の設定(Redmine内の権限やメニュー設定など)はpluginの各アプリ直下の
init.rb
に記述する
##疑問4 pluginのControllerやModelの作成方法は?
plugin用のgenerateコマンドを使います。
Model:
ruby script/rails generate redmine_plugin_model <plugin_name> <model_name> [field[:type][:index] field[:type][:index] ...]
Controller:
ruby script/rails generate redmine_plugin_controller <plugin_name> <controller_name> [<actions>]
##疑問5 pluginのdb:migrateはどこで実行するの?
Redmineルート(C:¥BitNami¥redmine-2.3.2-1¥apps¥redmine¥htdocs)で実行します。
plugin用のmigrateはbundle exec rake redmine:plugins:migrate
です。
##疑問6 pluginのgemを追加する場合どこに書くの?
plugins/<plugin_name>/Gemfile
に書きます。
bundleコマンドはRedmineルート(C:¥BitNami¥redmine-2.3.2-1¥apps¥redmine¥htdocs)で実行します。
(全pluginのGemfileがマージされるようになっているので、pluginの方で実行しても意図したgemがロードされません)
##疑問7 pluginのroutesを追加する場合どこに書くの?
plugins/<plugin_name>/config/routes.rb
に書きます。
Redmineルート/
からのパスで記述します。(普通のRailsアプリと同じ)
##疑問8 pluginのjavascriptとcssを追加する場合どこに書くの?
MVC of Redmine Plugin in Bootstrapが参考になりました!
Viewに次の感じで組み込めます。
(<plugin_name>
部は自分のpluginで読み替えてください)
CSS:
<%= stylesheet_link_tag 'sample.css', :plugin => '<plugin_name>', :media => 'all' %>
JavaScript:
<%= javascript_include_tag 'sample.js', :plugin => '<plugin_name>' %>
JavaScriptで$('#xx').on("click", ...)
のようなDOM情報を使うもの含まれる場合は、コンテンツより後に記述しておく必要があります。
コンテンツより先に書いてしまうと、対象となるタグが存在しないのでイベントが追加されずはまってしまいました。
(普段のRailsアプリでは直接javascript_include_tagを書くことがないので気づくのに時間がかかりました)
置き場所は次の通りです。
CSS:
plugins/<plugin_name>/assets/stylesheets/sample.css
JavaScript:
plugins/<plugin_name>/assets/javascripts/sample.js
##疑問9 Bootstrapは使えるの?
疑問8のようにstylesheet_link_tagを使えば可能です。
MVC of Redmine Plugin in Bootstrapがとてもわかりやすいです。
ただ、bootstrap-sassのようにprecompileが必要なものはうまく動かせていません。
(asstes:precompileを実行してもcssが生成されない感じ)
このあたりのやり方を知っている人がいたらぜひ教えてほしいです!
##疑問10 CoffeeScriptは使えるの?
わかっていません。
いろいろ試してみたのですが、precompileが必要なものがうまく動かせていません。
こちらもやり方を知っている人がいたらぜひ教えてほしいです!
追記:
CoffeeScriptやSassなど、asset pipeline系は使えなさそうです。
public以下にはjavascriptsやstylesheetsが存在していますが、app以下にはassetsが存在しません。
config¥application.rbには
config.assets.enabled = false
が設定されており、そもそもasset piplineを使わない想定になっています。
どうしても使いたい場合は、手動コンパイルして、生成されたjsやcssをpluginのassets¥javascriptsやassets¥stylesheetsに置く必要がありそうです。
(起動時にpublic¥plugin_assets¥にコピーしてくれるので置いてjsやcssは普通に使えます)
##疑問11 pluginのlocaleはどこに書くの?
plugins/<plugin_name>/config/locales/
内のymlファイルに書きます。
##疑問12 カスタムフィールドにはどうアクセスするの?
Redmine.jpでは次のような方法で取得すると紹介されています。
issue = Issue.last
cf_name = "カスタムフィールド名"
cv = issue.custom_field_values.detect {|c| c.custom_field.name == cf_name}
cv.value if cv
確かにアクセスできることはできるのですが、カスタムフィールドの数が増えてくると遅いです。
-
Issue - CustomValue - IssueCustomField
の関係となっている - IssueCustomFieldはCustomFieldの単一テーブル継承(Single Table Inheritance)となっている
ということがわかったので、次のようにすれば快適に取得できます。
issue = Issue.last
cf_name = "カスタムフィールド名"
cv = CustomValue.where(customized_type: 'Issue').where(customized_id: issue.id).joins(:custom_field).where(custom_fields: {name: cf_name}).first
cv.try(:value)
動作が遅く感じる場合はこちらの方が早いかもしれません。
issue = Issue.last
cf_name = "カスタムフィールド名"
field_id = IssueCustomField.find_by_name(cf_name).try(:id)
cv = CustomValue.where(customized_type: 'Issue').where(customized_id: self.id).where(custom_field_id: field_id).first
cv.try(:value)
※該当するフィールドがない場合はfield_idがnilになるので注意。
Projectなど他のカスタムフィールドも同じ感じで取得できます。
組み込みたいクラスにメソッド化しておくと良いかと思います。
def cf_value_by_name(cf_name)
cv = CustomValue.where(customized_type: 'Issue').where(customized_id: self.id).joins(:custom_field).where(custom_fields: {name: cf_name}).first
cv.try(:value)
end
where句で取る方法がわかれば、検索にも使えて便利です。
##疑問13 Project等のRedmineのクラスにメソッドを追加するには?
オープンクラスを使って追加します。
例としてplugins/<plugin_name>/lib/customized_project.rb
を作成します。
module CustomizedProject
attr_accessor :xxx, :yyy, :zzz
def self.included base
base.extend ClassMethods
end
# クラスメソッド
module ClassMethods
def xxxxx
end
end
# インスタンスメソッド
def yyyyy
end
end
class Project
include CustomizedProject
end
##疑問14 pluginのTest(RSpec使用)の実行方法は?
RedmineのプラグインをRSpecでテストするが参考になりました!
###準備
plugins/<plugin_name>/Gemfile
にgemを追加
:
group :test do
gem 'rspec-rails', '~> 3.1.0'
gem 'factory_girl_rails', '~> 4.4.1'
gem 'timecop', '~> 0.7.1'
gem 'capybara', '~> 2.3.0'
gem 'poltergeist', '~> 1.5.0'
end
:
plugins/<plugin_name>/spec/spec_helper.rb
を追加
require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper')
require File.expand_path("../../../../config/environment", __FILE__)
require 'rspec/rails'
require 'capybara'
require 'capybara/rspec'
require 'capybara/poltergeist'
require File.expand_path(File.dirname(__FILE__) + '/support/shared_connection')
require File.expand_path(File.dirname(__FILE__) + '/support/wait_for_ajax')
Capybara.javascript_driver = :poltergeist
Capybara.current_driver = Capybara.javascript_driver
RSpec.configure do |config|
config.use_transactional_fixtures = true
config.infer_spec_type_from_file_location!
config.include FactoryGirl::Syntax::Methods
FactoryGirl.definition_file_paths = [File.expand_path("../factories", __FILE__)]
FactoryGirl.find_definitions
config.before(:all) do
FactoryGirl.reload
end
end
plugins/<plugin_name>/support/shared_connection.rb
を追加
参考:Three tips to improve the performance of your test suite
class ActiveRecord::Base
mattr_accessor :shared_connection
@@shared_connection = nil
def self.connection
@@shared_connection || retrieve_connection
end
end
# Forces all threads to share the same connection. This works on
# Capybara because it starts the web server in a thread.
ActiveRecord::Base.shared_connection = ActiveRecord::Base.connection
plugins/<plugin_name>/support/wait_for_ajax.rb
を追加
参考:Automatically wait for AJAX with Capybara
module WaitForAjax
def wait_for_ajax(timeout = Capybara.default_wait_time)
Timeout.timeout(timeout) do
loop until finished_all_ajax_requests?
end
end
def finished_all_ajax_requests?
page.evaluate_script('jQuery.active').zero?
end
end
RSpec.configure do |config|
config.include WaitForAjax, type: :feature
end
あとはfactoriesやmodels等を追加してspecを書きます。
###実行
Redmineルート(C:¥BitNami¥redmine-2.3.2-1¥apps¥redmine¥htdocs)にて、
bundle exec rpec plugins¥<plugin_name>¥spec
を実行します。
##疑問15 Windows用のGitツールは何を使う?
いくつか試しましたが、GitHub WindowsのGit Shell
が一番しっくりきました。
defaultではPowerShellで動きます。
##疑問16 エディタはどうしよう?
MacではVimを使っているのでWindows用Vimを試してみましたが、
use_redmine.bat
との連携がなかなか思ったようにいきませんでした。
なのでWindowsではAtomを使うことにしました。
##Tips1 コンソールに出力されるログが文字化け
use_redmine.bat
がUTF-8でないので日本語が文字化けします。
log/development.log
の方はUTF-8なので問題ありません。
##Tips2 コンソールが突然落ちる
これは原因がわかるまで悩みました。
(落ちるときと落ちない時があったので)
原因:
SQL文のlogで日本語が文字化けすることで、文字によってはコンソールが落ちてしまっていました。(チームメンバーが原因を特定してくれました)
対応:
logのlevelを変更して日本語logが出力しないようにしました。
C:¥BitNami¥redmine-2.3.2-1¥apps¥redmine¥htdocs¥config¥environments¥development.rb
に1行追加します。
RedmineApp::Application.configure do
:
config.log_level = :info
end
##Tips3 developmentモードとtestモードがproductionモードに比べ劇的に重いんですけど、、
developmentで動かすと正直使えないくらい動作が重くなりました。
(100件もないtestが10分以上かかるなど)
原因:
SQLのlogが多過ぎることで重くなっていました。
カスタムフィールドが多いと、アクセス方法によっては大量のSQLクエリーが発行されます。
対応:
こちらもlogのlevelを変更することで速度が劇的に改善されました。
C:¥BitNami¥redmine-2.3.2-1¥apps¥redmine¥htdocs¥config¥environments¥development.rb
に1行追加します。
RedmineApp::Application.configure do
:
config.log_level = :info
end
C:¥BitNami¥redmine-2.3.2-1¥apps¥redmine¥htdocs¥config¥environments¥test.rb
に1行追加します。
RedmineApp::Application.configure do
:
config.log_level = :info
end
##Tips4 Test時にPhantomJSが認識されない
poltergeistを使いたかったのでPhantomJSが必要です。
PhantomJSのサイトからWindows用をダウンロードして使いますが、いくつかはまった点があるので紹介しておきます。
- 最新の2.0.0ではpoltergeistとの連携がうまくいかなかった
- 1.8.2だとうまく連携できた
- BitNamiをCドライブ直下にインストールしないとRailsからphantomjsが認識できなかった(原因は不明)
##Tips5 Test時にCustomValueが空っぽになる
例えばProjectCustomFieldをcreateして、Projectをcreateすると、対応するCustomValueが自動的にcreateされます。
CustomValueを自前でcreateしたい場合は、Projectのcreateより先にcreateしておく必要があります。
詳しくはRedmine plugin開発で、テストコード作成時にCustomValueにテストデータを設定する時の注意点をご覧ください。
##Tips6 Projectメニューのメニュー名とコントローラー名が異なると、メニュータブをクリックしても選択状態にならない
init.rb
に記述するメニュー名とコントローラー名は同じ名前にしておきましょう。
:
menu :project_menu, :polls_dummy, { :controller => 'polls', :action => 'index' }, :caption => 'Polls', :after => :activity, :param => :project_id
:
:
menu :project_menu, :polls, { :controller => 'polls', :action => 'index' }, :caption => 'Polls', :after => :activity, :param => :project_id
: