torutkのブログ

ソフトウェア・エンジニアのブログ

Redmineのプラグイン構造を探求する(モデル編の1)

はじめに

プロジェクトで用語集を定義するのはBest practiceの一つです。10数年前になりますが、その頃はExcelで用語集を作っているプロジェクトが多かったように思います。しかし、Excelはいろいろきついので、当初はWikiで用語集を作成していました。

しかし、用語が増えるにつれて、Wikiでは長くなり過ぎた表のメンテナンスが苦痛になってきました。その頃、Redmineに用語集プラグインRedmine Glossary Plugin)があるのを知り、プロジェクトのRedmineに追加してみました(Redmine 1.0前後)。
https://www.r-labs.org/projects/rp-glossary/wiki/Glossary

なかなか素晴らしいプラグインですが、Redmineのバージョンアップに追従されなくなっていました(2.xあたり)。その頃、Glossary PluginをフォークしてRedmine 2.xで動かしているリポジトリを見つけ、しばらくそちらを利用していました。
https://github.com/chiastolite/redmine_glossary

Redmine 3.0が出たあと、そのリポジトリのものは3.0には対応する気配はありませんでした。そこで、ものは試しとそのリポジトリをフォークしてやみくもにいじって3.0で一見動くようにしてみました。
用語集プラグイン(glossary)をRedmine 3.0で動くようにしてみる - torutkの日記

Githubに置いているので、ちらほら利用されているようです。たまに(年1件程度)issueが書き込まれるのですが、Glossary Pluginの内部を理解していないので、対応することが難しく、結果的に放置となっています。

もう一度Redmineプラグインの作り方をマスターしよう

そこで、再度腰を据えて、このGlossary Pluginの内部を理解し、ひいてはRedmineプラグイン製作ができるようになることを目指すことにしました。

まずはモデルからかな?

Redmineプラグインは、Ruby on Railsのアプリケーションと同様のMVC構造となっています。
そこで、まずは依存関係の一番下にあるM:Modelから理解を始めようとしました。

Glossary Pluginは、3つのモデルクラスが存在しています。
Term、TermCategory、GlossaryStyle の3つのクラスです。

まずはソースコードを読むところから始めよう、と紐解いてみます。
redmine_glossary/term.rb at master · torutk/redmine_glossary · GitHub

term.rbを読んで
class Term < ActiveRecord::Base
  unloadable

モデルクラス名はTermで、RailsActiveRecord::Baseクラスを継承しています。
次に、unloadableが登場します。これは、Redmineでモデルの雛形コードを生成すると書かれているものですが、Rails 4(Redmine 3.x)では不要との情報をいただきました。

続いて

  belongs_to :category, :class_name => 'TermCategory', :foreign_key => 'category_id'
  belongs_to :project
  belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'

別テーブルと関連する記述です。同じGlossaryプラグインのモデルTermCategory、Redmine標準のモデルProject、Userへの関連を記述しています。belongs_toは別テーブルと関連付ける方法の1つです。term.category のように属性としてアクセスできるようになります。

通常は、Projectへの関連のようにシンプルに書けます。が、クラス名とは異なる属性名を使用する場合は、class_nameハッシュでクラス名を指定します。foreign_keyの定義は、上の例では省略可能です。

  validates_presence_of :name, :project
  validates_length_of :name, :maximum => 255

これはフィールド(属性)の妥当性検査用コードで、nameフィールド(データベースのtermsテーブルのnameカラムに紐づく)は存在することと長さが最大255であることが条件となっています。

acts_as_attachable

だんだん読むのが苦しくなってきました。これは、Redmineでこのモデルにファイルの添付を可能にする記述です。acts_as_* は、Redmineの内部プラグインとして用意されているものの1つです。クラスレベルメソッド(Redmine::Acts::Attachable::ClassMethods)だそうです。
これだけで添付ファイル機能が利用できる???

acts_as_searchable :columns => ["#{table_name}.name", "#{table_name}.description"],
:project_key => [:project]

これは、Redmineでモデルを検索可能にするもののようです。ここからコードの解釈が困難になってきます。:columnsには検索するカラム(カラムの配列)を指定、:project_keyにはProjectの外部キーを指定します。

acts_as_event :title => Proc.new {|o| "#{l(:glossary_title)} ##{o.id}: #{o.name}" },
                  :description => Proc.new {|o| "#{o.description}"},
                  :datetime => :created_on,
                  :type => 'terms',
:url => Proc.new {|o| {:controller => 'glossary', :action => 'show', :id => o.project, :term_id => o.id} }

検索可能なモデルには、acts_as_eventを定義します。細部定義とどのような使い方をするのかは現時点では分かっていないので、今後調べることとします。

  attr_accessible :project_id, :category_id, :author, :name, :name_en, :datatype, :codename, :description,
:rubi, :abbr_whole

attr_accessibleを検索すると、Rails 4では非推奨という記述を見かけますが、Redmineではこれを指定しないと変更(データベースの変更)ができないようです。

  def author
    author_id ? User.find_by_id(author_id) : nil
  end

  def updater
    updater_id ? User.find_by_id(updater_id) : nil
  end
  
  def project
    Project.find_by_id(project_id)
  end

外部キーで関連付けしたモデルのインスタンスを取り出すメソッドが続きます。
Railsは、モデルにbelongs_toで別モデルと関連づけると、フィールドと同様にterm.authorでインスタンスが取得できるとあるので、これらのメソッドが必要なのかはよくわかりません。

  def datetime
    (self[:created_on] != self[:updated_on]) ? self[:updated_on] : self[:created_on]
  end

作成日時(created_on)と更新日時(updated_on)の2つの日時を持つので、datetimeメソッドでは新しい方の日時を返却するようにしているようです。

  def value(prmname)
    case prmname
    when 'project'
      (project) ? project.name : ""
    when 'category'
      (category) ? category : ""
    when 'datetime'
      datetime
    else
      self[prmname]
    end
  end

属性名を文字列で指定するとその値を返すメソッドのようです。

  def param_to_s(prmname)
    if (prmname == 'created_on' or prmname == 'updated_on')
      format_time(self[prmname])
    else
      value(prmname).to_s
    end
end

指定した属性名の値を文字列化するメソッドです。

 def <=>(term)
    id <=> term.id
end

謎のメソッドです。<=>がメソッド名? 

まだまだメソッド定義が続きますが、力尽きたのでこの辺で・・・

書籍

acts_as_attachable、acts_as_searchable, acts_as_eventについては、洋書ですが次の書籍に詳しく書かれていました。

Redmine Plugin Extension and Development (English Edition)

Redmine Plugin Extension and Development (English Edition)

4章 Attaching Files to Models に、acts_as_attachableの解説、5章 Making Models Searchable に、acts_as_searchableとacts_as_eventの解説があります。