プロジェクト

全般

プロフィール

Redmineプラグイン開発

プラグイン開発の流れ

まず、自由にいじれる独自のRedmine実行環境を用意します。
Linuxマシンの自分のユーザーのHOMEディレクトリ下にRedmineを展開し、Railsに付属するWEBRickサーバー(Redmine 4(Rails 5)からはPumaサーバー)で実行し、DBにはSQLiteを使うのが開発用にはお手軽です。

Redmine開発環境の用意

Linuxコマンドライン環境(Windows 10で利用可能なWindows Subsystem for Linuxを含む)で簡単な開発環境を用意します。

プラグインの雛形生成

プラグイン名を指定して、以下のコマンドを実行します。

plugins$ bundle exec rails generate redmine_plugin redmine_hello
                                                   ^^^^^^^^^^^^^ 生成するプラグイン名

プラグインの雛形で生成されるディレクトリ構造

redmine_hello/
├─ app
│   ├─ controllers
│   ├─ helpers
│   ├─ models
│   └─ views
├─ assets
│   ├─ images
│   ├─ javascripts
│   └─ stylesheets
├─ config
│   └─ locales
├─ db
│   └─ migrate
├─ lib
│   └─ tasks
└─ test
     ├─ fixtures
     ├─ functional
     ├─ integration
     └─ unit

プラグインの雛形で生成されるファイル

redmine_hello/
├─ README.rdoc
├─ config
│   ├─ locales
│   │   └─ en.yml
│   └─ routes.rb
├─ init.rb
└─ test
     └─ test_helper.rb

helloプラグイン最低限の実装

init.rbの編集

プラグイン情報を記載します。記載内容が、Redmineの[管理] > [プラグイン]で確認できればOKです。

雛形で生成された内容 雛形を修正した内容(例)
Redmine::Plugin.register :redmine_hello do
  name 'Redmine Hello plugin'
  author 'Author name'
  description 'This is a plugin for Redmine'
  version '0.0.1'
  url 'http://example.com/path/to/plugin'
  author_url 'http://example.com/about'
end
Redmine::Plugin.register :redmine_hello do
  name 'Redmine Hello plugin'
  author 'TAKAHASHI,Toru'
  description 'This is a tiny sample plugin for Redmine'
  version '0.0.1'
  url 'http://www.torutk.com/projects/swe/wiki/Redmineプラグイン開発/'
  author_url 'http://www.torutk.com'
end

プラグイン作成のいろいろな方法

Redmineのプラグインには、いくつかの方法があります。現時点で認識しているプラグイン作成方法でも次のものがあります。

  1. MVCの各部品を作成するMVCプラグイン
  2. 既存の表示に追加するView hooksプラグイン
  3. 既存の表示(ビューテンプレート)を差し替えるプラグイン
  4. 既存の振る舞い(メソッド)を差し替えるパッチプラグイン
  5. マクロを提供するプラグイン

使い分けに関しては、データをデータベースに保存する場合はモデルの定義が必要なのでMVCプラグイン、既存の表示に付けたしをする程度であれば、適するフック箇所があればView hooksプラグイン、なければビューテンプレートを差し替えるプラグインかパッチプラグイン、Wikiの記載でちょっとした処理をするならマクロといったところでしょうか。

フック

Redmineには、プラグインで処理を差し込めるようにあらかじめフックが設けられています。
フックには、View、Controller、Model、HelperとそれぞれRailsの構成要素の種類に応じて用意されています。

フック一覧を調べる

Redmine本家サイトに掲載されています。
http://www.redmine.org/projects/redmine/wiki/Hooks_List

次の方法でも調べることができます(上述ページに記載の方法)。

redmine$ grep -r call_hook *

Viewフックのお試し

チケット一覧のViewに用意されている:view_issues_index_bottomにフックを差し込むサンプルを記述します。
フックの定義は上述の方法で調べました。

redmine$ grep -r call_hook *
  : 
app/views/issues/index.html.erb:<%= call_hook(:view_issues_index_bottom, { :issues => @issues, :project => @project, :query => @query }) %>

フックを実装するRubyのクラスを定義します。

  • redmine/plugins/redmine_mein/lib/redmine_mein/hooks.rb
    module RedmineMein
      class Hooks < Redmine::Hook::ViewListener
        # 実装
      end
    end
    

名前の衝突を避けるため、クラス名にプラグイン名を付けて長くなるのは避けたいので、moduleを使って名前空間を導入します。
ただし、moduleを使うとソースファイルを置く場所がモジュール名に対応するディレクトリの下にする必要があるようです。

モジュール名、クラス名は、Pascalケース(先頭が大文字で、続く単語の先頭が大文字のキャメルケース)で記述しますが、ディレクトリ名、ファイル名はスネークケースになるようです。

次に、このフックをプラグインで有効にするため、init.rbに依存を定義します。

require_dependency 'redmine_mein/hooks'

モジュールを導入しているのでモジュール名に対応するディレクトリとクラス名に対応するファイル名(拡張子.rbは不要)を記述します。

コントローラーとビュー

RailsのMVC構造におけるVCの部分を作成します。モデルは既存のものを利用します。
プラグインとしては、既にあるデータを活用して、新たなビューを作りたいときに該当します。

コントローラーの雛形作成

書式: ruby bin/rails generate redmine_plugin_controller <プラグイン名> <コントローラー名> <アクション>...
redmine$ ruby bin/rails generate redmine_plugin_controller redmine_mein mein index
create  plugins/redmine_mein/app/controllers/mein_controller.rb
      create  plugins/redmine_mein/app/helpers/mein_helper.rb
      create  plugins/redmine_mein/test/functional/mein_controller_test.rb
      create  plugins/redmine_mein/app/views/mein/index.html.erb

コントローラーには雛形生成時に指定したindexアクションに対応するindexメソッドが定義されます。
ビューには雛形生成時に指定したindexアクションに対応するindex.html.erbファイルが生成されます。

コントローラー名の指定について

IDで複数のモデル・インスタンスから1つを識別して制御するコントローラーについては、モデル名の複数形にControllerを付加した命名とします。

IDを指定する必要がないモデル・インスタンスを制御するコントローラーについては、モデル名(単数形)にControllerを付加した命名とします。
例えば、ログインユーザーに1つのインスタンスが紐付くモデルが該当します。

その他、モデルと対応しないコントローラー名は役割に応じた命名とします(複数形でなくてよい)。

  • 例1)モデルUserに対応するコントローラー名は、UsersControllerとする。
    • railsのgenerateコマンドではキャメルケースで指定した場合(この例ではUsers)、スネークケースのファイル名に変換されるはず(users_controller.rb)ですが、redmine_plugin_controllerを指定した場合、モデル名をキャメルケースで指定すると(Users)そのままファイル名に使用されてしまいます(Users_controller.rb)。小文字スネークケースで指定してください。
      https://www.redmine.org/issues/28668
    • redmine_plugin_modelを指定してモデルを作成する場合は大文字キャメルケースで指定したらファイル名は小文字スネークケースになります。

ルーティング設定

プラグインディレクトリ下のconfig/routes.rb にパスを定義します。

IDを指定するモデルのコントローラーについては、resourcesでルーティング設定をするのが望ましいです。

Rails.application.routes.draw do
  resources :mein
end

resources で指定したコントローラーは、URLのパスにコントローラー名でアクセスが可能となります。
この例では、http://<サーバー名>/mein でindexメソッドに処理が渡されます。

resources :mein の指定で、次のHTTPリクエストに対するコントローラーのメソッド呼び出しの対応が定義されます。

リクエストメソッド リクエストURI コントローラー メソッド
GET /mein mein index
POST /mein create
GET /mein/new new
GET /mein/:id/edit edit
GET /mein/:id show
PATCH, PUT /mein/:id update
DELETE /mein/:id destroy

IDを指定する必要のないモデルのコントローラーについては、resourceでルーティング設定をするのが望ましいです。

Rails.application.routes.draw do
  resource :besitz
end

resource :besitz の指定で、次のHTTPリクエストに対するコントローラーのメソッド呼び出しの対応が定義されます。

リクエストメソッド リクエストURI コントローラー メソッド
GET /mein mein index
POST /mein create
GET /mein/new new
GET /mein/:id/edit edit
GET /mein/:id show
PATCH, PUT /mein/:id update
DELETE /mein/:id destroy
ルーティング設定の確認方法

ルーティング設定を確認するのは、次のコマンドで可能です。

redmine$ bundle exec rails routes
indexアクションだけをルート定義する
resources :mein, only: :index
プロジェクトの下でコントローラーにアクセスする

T.B.D.

部分テンプレート(パーシャル)

HTMLテンプレートのある部分に別なファイルで記述した内容を差し込みます。

<%= render partial: 'show_mein' %>

renderメソッドでpartialオプションを指定し、名前を指定します。すると、_名前.html.erb ファイルがその箇所に挿入されます。この例の場合は、_show_mein.html.erbファイルが差し込まれます。

なお、他にオプション指定がない場合、partialキーの指定を省略し、<%= render 'show_mein' %>と記述することができます。

データを部分テンプレートへ渡す

部分テンプレートにデータを渡すには、objectオプション、collectionオプション、localsオプションの3種類があります。

  • objectオプション
    <%= render partial: 'show_mein', object: @mein %>
    

    partialオプションで指定した名前(show_mein)で参照される変数をobjectオプションで渡します。この例では、meinインスタンス変数が_show_mein.html.erb内では、@show_meinの名前で参照できます。(なんかとてもややこしい)
    • objectオプションの省略形
      <%= render @mein %>
      

      と記述すると、_mein.html.erbが部分テンプレートのファイル名となり、部分テンプレートファイル内では@meinインスタンス変数がmeinという名前のローカル変数として参照できます。
  • collectionオプション
    <%= render partial: 'show_mein', collection: @users %>
    

    collectionオプションでコレクションを指定すると、コレクションの要素ごとに部分テンプレートが展開され、部分テンプレート内では@usersインスタンス変数のコレクションの1つの要素がuserという名前のローカル変数として参照できます。
    • collectionオプションの省略形
      <%= render @users %>
      

      と記述すると、_user.html.erbが部分テンプレートのファイル名となり、部分テンプレートファイル内では@usersインスタンス変数(コレクション)の1つの要素がuserという名前のローカル変数で参照できます。
  • localsオプション
    <%= render partial: 'show_mein', locals: {mein: @mein, dein: euer[0]} %>
    

    localsオプションでハッシュを渡すと、部分テンプレートファイル内では、ハッシュ内のキーをローカル変数名として、その値で指定された変数を参照することができます。
    • localsオプションの省略形
      <%= render 'show_mein', {mein: @mein, dein: euer[0]} %>
      

      partialオプションとlocalsオプションのオプション名が省略可能です。ただし、partialオプション名を指定した場合は、localsオプション名も指定する必要があります(逆もまた然り)。

フォーム

ユーザーからの入力を受け付け、サーバーに入力内容を渡すビューです。いくつかの入力項目とサブミットボタンから構成されます。
Ruby on Railsでは、従来からのform_forおよびform_tag、そしてRuby on Rails 5.1からこの両者を統合するform_withが提供されています。また、Redmineではさらにform_forをラップして便利にしたlabelled_form_forが提供されています。

フォーム種類 内容
form_for モデルインスタンスを使用
form_tag カスタムURLの実現
form_with form_forform_tagを統合
labelled_form_for form_forをラップしたRedmineのAPI

Ruby on Rails 5.0までは、使い分けは次のとおりです。

  • モデルインスタンスがあり、その属性を入力・編集する場合はlabelled_form_for(中でform_forを呼び出し)を使います。
  • モデルインスタンスとは関係なくURLを指定する場合はform_tagを使います。

Ruby on Rails 5.1からは、form_withが推奨です。

form_withの使用例
  • モデルインスタンスがあり、その属性を入力・編集する例
    form_with(model: @post) do |form|
      form.text_field :title
      form.text_area :description
      from.submit
    end
    

model:指定だけで内部ではスコープをpostに、URLをurl_for(@post)に生成します。

  • モデルインスタンスを使用しない場合の例
    form_with(url: different_path, class: 'something', id: 'specific') do |form|
      form.text_filed :title, 'This is the value of title'
      form.text_area :description
      :
      form.sbumit
    end
    

モデル

モデルクラスは、データベースのテーブル構造に対応します。モデルクラスのオブジェクト1つは、テーブルのレコードのデータを保持します。そして、モデルクラスのオブジェクトが持つデータをテーブルに反映(新規のレコードとなるならインサート、既存のレコードの変更であればアップデート)します。また、逆にレコードのデータを持つモデルクラスのオブジェクトを取得します。

モデルクラスの生成

rails generateコマンドで、redmine_plugin_model を指定し、生成対象プラグインとモデルクラス名を指定します。
今回は属性(データベースのテーブルの列)を指定せず空のモデルを生成します(後から追加は可能です)。

redmine$ bundle exec rails generate redmine_plugin_model redmine_hello Hello
      create  plugins/redmine_hello/app/models/hello.rb
      create  plugins/redmine_hello/test/unit/hello_test.rb
      create  plugins/redmine_hello/db/migrate/001_create_hellos.rb

生成されたファイルの内容は次の通りです(Redmine 4.0/Rails 5.1で生成)。

  • hello.rb
    class Hello < ActiveRecord::Base
    end
    
  • 001_create_hellos.rb
    class CreateHellos < ActiveRecord::Migration[5.1]
      def change
        create_table :hellos do |t|
        end
      end
    end
    
    • マイグレーションのクラス CreateHellos は、ActiveRecord::Migration[5.1]を継承するとちょっと奇妙なコードになっています。Rails 5からは、MigrationはRailsのバージョンによって異なるクラスを継承できるように、Migrationクラスの[]メソッドを呼び引数のバージョンによって対応するクラスを返すようになっています。
      従来のように、ActiveRecord::Migration と記述したら次のエラーとなってしまいます。
      StandardError: Directly inheriting from ActiveRecord::Migration is not supported.
      Please specify the Rails release the migration was written for:
      
    • このマイグレーションを実行すると、SQLite3を使用した場合次のテーブルが作成されます。
      CREATE TABLE IF NOT EXISTS "hellos" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL);
      

      モデルクラスで指定しなくても、主キーで自動インクリメントされるidカラムが用意されます。
属性の指定

モデルを生成するrails generateコマンドで、オプションに属性名と型を指定します。

$ bundle exec rails generate redmine_plugin_model redmine_hello Hello message:string
  :

属性は、マイグレーション用のコードに記載され、モデルクラスには記述されません。

  • hello.rb
    class Hello < ActiveRecord::Base
    end
    
  • db/migrate/002_create_hellos.rb
    class CreateHellos < ActiveRecord::Migration[5.1]
      def change
        create_table :hellos do |t|
          t.string :message
        end
      end
    end
    
    • マイグレーションを実行すると、SQLite3を使用した場合次のスキーマが生成されます。
      CREATE TABLE IF NOT EXISTS "hellos" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "message" varchar);
      

属性の型とデータベース上の型

モデルの生成時に指定する属性の型と、データベース(MySQL)上のカラムの型の対応は次の通りです。

No モデルの属性型 MySQLデータベースの型 内容
1 String varchar(255) 255文字以下の文字列
2 Text text 無制限長の文字列
3 Integer int(11) 数値
4 Float float
5 Decimal decimal
6 Boolean tinyint(1)
7 Date date
8 Time time
9 DateTime datetime
10 Timestamp datetime

タイムスタンプに関する属性

モデルオブジェクト(データベースのテーブルのレコード)の生成日時および更新日時は、マイグレーション用のコードにtimestampsの指定をすると生成されます。
  • db/migrate/001_create_hellos.rb
    class CreateHellos < ActiveRecord::Migration[5.1]
      def change
        create_table :hellos do |t|
          t.string :message
    
          t.timestamps
        end
      end
    end
    
  • SQLite3の場合次のスキーマが生成されます。
    CREATE TABLE IF NOT EXISTS "hellos" (
        "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
         "message" varchar,
         "created_at" datetime NOT NULL,
         "updated_at" datetime NOT NULL);
    

マイグレーションスクリプトあれこれ

  • changeメソッドで記述するのは、add_column等それだけでマイグレーションの実行とそのロールバックが可能な場合
  • upメソッドとdownメソッドの双方を記述するのは、changeメソッドだけではロールバックができない場合

モデルオブジェクトのメソッド

  • インスタンスを生成 new
  • インスタンスの属性を更新 attributes
    引数に属性名をキー、値をバリューとするハッシュを渡して複数属性をまとめて設定
  • インスタンスの属性をデータベースに保存 save
  • インスタンスの生成とデータベース保存 create
    引数は属性名をキー、値をバリューとするハッシュを渡し、インスタンスの生成とデータベース保存を一気に実行
  • インスタンスの属性を更新しデータベース保存 update
  • インスタンスの属性を1つ更新しデータベース保存 update_attribute
  • すべてのインスタンスを取得 all
  • 主キーで検索 find
    引数には検索するキーを複数指定可。見つからないとActiveRecord::RecordNotFound例外発行。
  • 条件を指定し1件を検索 find_by
    LIMITで1個に絞っている模様。見つからないとnilを返す。
  • 条件を指定しN件を検索 where
  • 件数を調べる count
  • 並び順を指定 order
whereの指定方法
  • 文字列指定 モデル.where("category_id = '3'")
  • ハッシュ指定 モデル.where(category_id: 3)

モデル間の関係

モデル同士の関連の種類(抜粋)

  • belongs_to
  • has_one
  • has_many
belongs_to
class Alfa < ActiveRecord::Base
end

class Bravo < ActiveRecord::Base
  belongs_to :alfa
  • bravo.alfa と属性としてアクセス可能
  • bravosテーブルが alfa_idを外部キーとしてalfasテーブルを参照
class Charlie < ActiveRecord::Base
  belongs_to :author, class_name: 'User'

関連するクラス名(テーブル名)とは別の名前で関連付けする場合、class_nameで実のクラス名を指定します。
外部キーは、この場合、author_id の列名で生成されます。

has_many

belongs_to で関連付けられた側に指定します。belongs_toと対で使用しないと意味がなさそうです。一方、belongs_toを単独して使用することは意味があります。

class Alfa < ActiveRecord::Base
  has_many :bravos
end

class Bravo < ActiveRecord::Base
  belongs_to :alfa
  • alfa.bravos と属性としてアクセス可能(配列として取得)
    alfa.bravos.each do |b|

マクロ

マクロは、Wikiに埋め込むと実行時に展開されるブロックです。

テスト

プラグインのテストを記述および実行するにあたり、テスティングフレームワークが利用可能です。よく用いられているのは、Rails(Ruby)標準搭載のMinitestフレームワークと、外部ライブラリのRSpecです。

Minitestは、テストデータをfixtureと呼ぶYAML等のファイル形式で記述し、テストコードをRubyで記述し、テストを実行します。テストはassertで成否を判定しテスト実行結果として失敗したassertの件数が表示されます。テストの粒度はユニット、ファンクショナル、インテグレーションの3段階あります。

RSpecは、テストデータをFactoryBotライブラリを使って記述し、テストコードをRubyの内部DSLで記述し、テストを実行します。テストはmatcherを使って成否を判定します。テストの粒度はモデルに対するテスト、コントローラーに対するテスト、ヘルパーのテストがあります。

この他、エンドツーエンドテストでは外部ライブラリのCapybaraを用いるのが最近の流れです。Capybaraは、Seleniumを使ったテストを自動化するものです。

Minitestによるテスト

fixtureファイルの置き場所とテストでの利用方法

Minitestによるテストでは、デフォルトではRedmineインストールディレクトリ/test/fixtures/にあるfixtureファイルを使用します。プラグイン独自のfixtureファイルを使う場合は、テスト実行前にプラグイン独自のfixtureファイルをRedmineインストールディレクトリ/test/fixtures/へコピーします。ただし、これは手間がかかるのと、バージョン管理ツール上で作業ディレクトリのRedmineインストールディレクトリ/test/fixturesに変更が発生したと検知されてしまうのでちょっと面倒です。そこで、プラグインのtest_helper.rbに、プラグイン独自のディレクトリにあるfixtureファイルを読み込むコードを追加します。

  • プラグインディレクトリ/test/test_helper.rb
    # -*- coding: utf-8 -*-
    # Load the normal Rails helper
    require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper')
    
    module Redmine
      module PluginFixturesLoader
        def self.included(base)
          base.class_eval do
            def self.plugin_fixtures(*symbols)
              ActiveRecord::FixtureSet.create_fixtures(File.dirname(__FILE__) + '/fixtures/', symbols)
            end
          end
        end
      end
    end
    
    ## ファンクショナルテスト
    unless ActionController::TestCase.included_modules.include?(Redmine::PluginFixturesLoader)
      ActionController::TestCase.send :include, Redmine::PluginFixturesLoader
    end
    ## ユニットテスト
    unless ActiveSupport::TestCase.included_modules.include?(Redmine::PluginFixturesLoader)
      ActiveSupport::TestCase.send :include, Redmine::PluginFixturesLoader
    end
    ## インテグレーションテスト
    unless Redmine::IntegrationTest.included_modules.include?(Redmine::PluginFixturesLoader)
      Redmine::IntegrationTest.send :include, Redmine::PluginFixturesLoader
    end
    

テストコードからfixtureの読み込み使用する例を次に示します。

  • プラグインディレクトリ/test/units/term_category_test.rb
    require File.dirname(__FILE__) + '/../test_helper'
    
    class TermCategoryTest < ActiveSupport::TestCase
      plugin_fixtures :term_categories
    
      def test_load_fixture_one
        cat1 = TermCategory.find(1)
        assert_same 1, cat1.id
      end
    end
    

このコードでは、plugin_fixtures :term_categoriesと記述することで、プラグインディレクトリ/test/fixtures/term_categories.yml をテストデータとして読み込みます。読み込んだテストデータはモデルクラスのメソッドを通じて取得します。

Redmineインストールディレクトリ/test/fixtures/にあるfixtureファイルを読み込むときはテストコード側にはfixtures :usersのように記述します。fixturesメソッドで参照するときは、users(:user_a)のようにしてfixtureファイルに名前user_aとして定義したデータを取得できます。
plugin_fixturesで指定した場合は後者の呼び方はできません。プラグイン独自のfixtureファイルでfixtureの名前でデータを取得したいときは、次のように指定します。

  plugin_fixtures :term_categories
  fixtures :term_categories
    :
  def test_some
    cat_one = term_categories(:one)

モデルのテスト(unit test)

モデルのテストで実施すべき項目は次となります。

  • validationのテスト
  • データベースの制約(整合性)テスト
  • publicなメソッドのテスト

コントローラーのテスト(functional test)

コントローラーのテストで実施すべき項目は次となります。

  • アクションのテスト

統合テスト(integrated test)

システムテスト(end to end test)

便利集

Railsコンソール

Rails環境をコンソールで扱います。

redmine$ bundle exec rails console
irb(main):002:0> "man".pluralize
=> "men" 
irb(main):003:0>

参考資料

RAILS GUIDES日本語訳

Rails 4から5へのアップグレード対応

クリップボードから画像を追加 (サイズの上限: 1 GB)