プロジェクト

全般

プロフィール

Redmine Glossaryプラグイン再構築

はじめに

Glossasry(用語集)プラグインは、Redmineのプラグインとしてはかなり初期の頃(Redmine 1.0.x、2010年)から公開されている、用語集(データ辞書)を作成・管理するプラグインです。用語集の作成は、Redmine標準のWiki機能を使って頑張ればプラグインを導入しなくてもできなくはないですが、このプラグインは、日本語・英語・略語展開・ルビ・説明・コーディング時名称といった枠で統一して管理でき、インデックスが自動生成され、用語の一覧詳細管理が用意にできるので、大変便利です。詳しい機能は次のプラグイン説明ページに記載されています。

Glossaryプラグインの説明

Glossaryプラグインのメインテナンスはオリジナルの作者によってRedmine 1.2.x まで行われていました。それ以降のRedmineバージョンアップに際しては、オリジナルの作者によるメインテナンスは停止しているので、GlossaryプラグインをフォークしてRedmine 2.0や3.0に対応したものが登場しています。筆者も次のリポジトリにフォークしたRedmine 3.x対応版を作ってします。

https://github.com/torutk/redmine_glossary

このRedmine Glossaryプラグイン・フォーク版は、現在Redmine 3.4で何とか動いています(細部機能では動かないものがちらほら)。しかし、Redmineの次のメジャーバージョンアップであるRedmine 4.0ではベースとなるRuby on Railsのバージョンが4.2から5.1に更新され、これまでのRedmineプラグインが利用してきた機能(API)が使えなくなる等、相当に手を入れないと動かない状況が見えています。

redmine trunkを落としてglossary pluginが動くか試してみる
redmine trunkを落としてglossary pluginが動くか試してみる(続)
redmine trunkを落としてglossary pluginが動くか試してみる(続々)
redmine trunkを落としてglossary pluginが動くか試してみる(続々々)

目的

Glossaryプラグインを、Redmine 4.0で利用できるようにすることを目的とします。

目標

これまでのRedmineバージョンアップにGlossaryプラグインを対応させる作業では、Redmineのバージョンアップ版にGlossaryプラグインを入れて、Glossaryプラグインがエラーとなる箇所を修正するアプローチで何とか対応してきました。しかし、来たるRedmine 4.0ではかなり手を入れる必要があることが試し打ちで判明しています(というか挫折してしまいました)。今回手を入れるためには、ある程度Redmineプラグインの作成方法、すなわちRuby on Railsの開発に習熟が必要です。そこで、Redmineプラグイン/Ruby on Railsの開発へ習熟することと、GlossaryプラグインのRedmine 4.0対応を進めることを兼ねて、Glossaryプラグインをゼロから再構築してみることにしました。

再構築の計画

Glossaryプラグインの再構築を行うにあたり、プラグイン作成技術をステップ・バイ・ステップで段階的に獲得しながら、機能の実装を進める計画とします。

  1. 用語モデルと一覧表示(プロジェクトの縛りなし)
  2. 国際化対応
  3. 一覧表示→詳細表示
  4. 用語の作成・変更
  5. カテゴリの導入
  6. プロジェクトの縛りを入れる
  7. 右サイドバー表示(設置、索引、新規作成リンクなど)
  8. セキュリティ、権限の導入
  9. 用語の属性を充実
  10. 用語をグループごとに分類表示
  11. 用語一覧の表示項目を設定可能にする
  12. マクロ
  13. 機能向上いろいろ
  14. バリデーション
  15. セキュリティ、権限の厳格化

プラグイン開発環境の準備は、Redmineプラグイン開発環境 を参照ください。

再構築の実施

再構築で使用するリポジトリ(Github)は次です。
https://github.com/torutk/redmine_glossary/tree/reconstruct


フェーズ1)最小限の用語モデルと一覧表示

オリジナルのGlossaryプラグインでは、モデルクラスとしてTerm、Category、GlossaryStyleの3つを使用します。フェーズ1では最小限のMVC構造を持つプラグインとして、1つのモデルクラス、1つのコントローラークラス、一覧表示のビューだけを作成します。

フェーズ1の概略

フェーズ1として次を作成します。

No クラス(役割) ファイル 内容
1 プラグイン情報 init.rb プラグイン情報・初期設定を記述
2 GlossaryTerm glossary_term.rb 用語モデルクラス
3 マイグレーション 001_create_glossary_terms.rb 用語モデルのテーブル作成
4 GlossaryTermsController glossary_terms_controller.rb 用語コントローラークラス
5 用語一覧のビュー index.html.erb 用語一覧を表示する埋め込みruby
6 ルーティング設定 routes.rb HTTPリクエストのURLに対応するコントローラー/メソッドの割当て
  • プラグイン名は、redmine_glossaryです(オリジナルと一緒)。
  • モデルクラス名は、オリジナルのクラス名はTermです。モデルクラス名とそれにちなむデータベースのテーブル名は、Redmine本体および全プラグインでフラットな空間に置かれるので、単純な名前では衝突可能性が高くなります。また、モデルクラス名およびテーブル名を見たときにどのプラグインのものか分かりにくいです。そこで、モデルクラス名をGlossaryTermとしました。
  • フェーズ1ではGlossaryTermモデルの持つ属性は最低限とし、用語名(name)、説明(description)、作成日時(created_at)、更新日時(updated_at)を持たせます。
  • コントローラークラス名は、モデル名の複数形にちなんでGlossaryTermsControllerとします。
  • フェーズ1ではGlossaryTermsControllerコントローラーの持つアクションは最低限とし、indexを持たせます。
  • i18n対応はフェーズ2とします。

データを作る機能はフェーズ1では用意しないので、動作確認時は、Railsのコンソール上でrubyのコードを入力実行してデータを作成します。


プラグイン雛形の生成

Redmineプラグインの雛形を生成するコマンドを実行し、redmine_glossaryプラグインの雛形を生成します。

redmine$ bundle exec rails generate redmine_plugin redmine_glossary
      create  plugins/redmine_glossary/app
      create  plugins/redmine_glossary/app/controllers
      create  plugins/redmine_glossary/app/helpers
      create  plugins/redmine_glossary/app/models
      create  plugins/redmine_glossary/app/views
      create  plugins/redmine_glossary/db/migrate
      create  plugins/redmine_glossary/lib/tasks
      create  plugins/redmine_glossary/assets/images
      create  plugins/redmine_glossary/assets/javascripts
      create  plugins/redmine_glossary/assets/stylesheets
      create  plugins/redmine_glossary/config/locales
      create  plugins/redmine_glossary/test
      create  plugins/redmine_glossary/test/fixtures
      create  plugins/redmine_glossary/test/unit
      create  plugins/redmine_glossary/test/functional
      create  plugins/redmine_glossary/test/integration
      create  plugins/redmine_glossary/README.rdoc
      create  plugins/redmine_glossary/init.rb
      create  plugins/redmine_glossary/config/routes.rb
      create  plugins/redmine_glossary/config/locales/en.yml
      create  plugins/redmine_glossary/test/test_helper.rb
redmine$

生成された雛形に含まれるinit.rbの記述を修正します。

Redmine::Plugin.register :redmine_glossary do
  name 'Redmine Glossary plugin'
  author 'Toru Takahashi'
  description 'This is a plugin for Redmine to create a glossary that is a list of terms in a project.'
  version '1.0.1'
  url 'https://github.com/torutk/redmine_glossary'
  author_url 'http://www.torutk.com'
end

Redmineを起動し[管理]メニュー > [プラグイン]をクリックし、上述init.rbの内容が表示されていることを確認します。

administration_plugin-1.png (管理メニュープラグインの表示)

rubyの書き方でびっくりしないために

このrubyのコードは、なかなかに面喰う書き方です。モジュールRedmineの中のクラスPluginのクラスメソッドregisterを呼んでいます。registerの書式は、

register(id, &block) => Object

です。rubyではメソッド呼び出しの引数を囲む丸括弧を省略できるので、上述コードには丸括弧がありません。
registerの第1引数は:redmine_glossaryです。
第2引数はブロックで、do~endでブロックを定義し引数として渡しています。ブロックの中は、プラグインオブジェクトのフィールド(属性)をセットするメソッド呼び出しとなっていると思われます。これも引数の括弧が省略されています。

文字列は、シングルクォート・ダブルクォートどちらでも囲めます。ダブルクォートで囲うと、#{..}の中の式が展開されます。シングルクォートの場合は展開されません。


モデルクラスの作成

モデルクラスとマイグレーションファイルを生成します。モデルクラスはデータベースに情報を永続化するので、モデルクラスを作成するときはその対となるデータベースのテーブルを作成するマイグレーションファイルを作成します。

まず、Redmineプラグインのモデル雛形を生成するコマンドを実行し、redmine_glossaryプラグインの中にモデルクラスGlossaryTermの雛形を生成します。

redmine$ bundle exec rails generate redmine_plugin_model redmine_glossary GlossaryTerm name:string description:text                                                                               
      create  plugins/redmine_glossary/app/models/glossary_term.rb                                      
      create  plugins/redmine_glossary/test/unit/glossary_term_test.rb                                  
      create  plugins/redmine_glossary/db/migrate/001_create_glossary_terms.rb                          
redmine$

Ruby on Railsでは、モデルクラスはデータベースのテーブルに対応し、属性がテーブルのカラムに対応します。上述の生成コマンドでは属性名と型(カラム名と型に対応)を指定します。string型はおよそ255文字(バイト?)以下の文字列、text型は不定長の文字列に対応します。
カラムは後から変更もできるので、あまり気にせず必要そうなものを生成しておきます。

生成された雛形の中身は次です。

  • app/models/glossary_term.rb
    class GlossaryTerm < ActiveRecord::Base                                          
    end
    
  • db/migrate/001_create_glossary_terms.rb
    class CreateGlossaryTerms < ActiveRecord::Migration[5.1]
      def change
        create_table :glossary_terms do |t|
          t.string :name
          t.text :description
        end
      end
    end
    

モデルクラスは空ですが、対応するデータベースのカラムを属性として持ちます。
データベースに作成されるテーブル名(glossary_terms)は、モデルクラス名(GlossaryTerm)をスネークケースにし複数形にしたものとなります。

フェーズ1では、雛形に次を追加します。

  • カラムに作成日時・更新日時を追加
  • nameカラムにNOT NULL制約を追加
  • 作成日時・更新日時にNOT NULL制約を追加

001_create_glossary_terms.rb を修正します。

  class CreateGlossaryTerms < ActiveRecord::Migration[5.1]
    def change
      create_table :glossary_terms do |t|
-       t.string :name
+       t.string :name, null: false
        t.text :description
+       t.timestamps null: false
      end
    end
  end                                                                         

マイグレーションファイルで、t.timestampsと記述すると、カラムcreated_atupdated_atが生成されます。
NOT NULL制約は、null: falseを追記します。

マイグレーションを実行し、データベースにテーブルを作成します。

redmine_glossary$ bundle exec rails redmine:plugins:migrate                                     
Migrating redmine_glossary (Redmine Glossary plugin)...                                                 
== 1 CreateGlossaryTerms: migrating ===========================================                         
-- create_table(:glossary_terms)                                                                        
   -> 0.0091s                                                                                           
== 1 CreateGlossaryTerms: migrated (0.0121s) ==================================                         

redmine_glossary$

データベースのテーブルを確認します。まず、Redmineのデータベース上に存在するテーブルの一覧を表示させます。この中に、今回マイグレーションを実行して生成されるテーブル、glossary_termsがあることを確認します。

  • Redmineのデータベースのテーブル一覧を表示
    redmine_glossary$ sqlite3 ../../db/redmine.sqlite3
    SQLite version 3.8.10.2 2015-05-20 18:17:19
    Enter ".help" for usage hints.
    sqlite> .table
    ar_internal_metadata                 member_roles
    attachments                          members
    auth_sources                         messages
    boards                               news
    changes                              open_id_authentication_associations
    changeset_parents                    open_id_authentication_nonces
    changesets                           projects
    changesets_issues                    projects_trackers
    comments                             queries
    custom_field_enumerations            queries_roles
    custom_fields                        repositories
    custom_fields_projects               roles
    custom_fields_roles                  roles_managed_roles
    custom_fields_trackers               schema_migrations
    custom_values                        settings
    documents                            time_entries
    email_addresses                      tokens
    enabled_modules                      trackers
    enumerations                         user_preferences
    glossary_terms                       users
    groups_users                         versions
    import_items                         watchers
    imports                              wiki_content_versions
    issue_categories                     wiki_contents
    issue_relations                      wiki_pages
    issue_statuses                       wiki_redirects
    issues                               wikis
    journal_details                      workflows
    journals
    
  • glossary_termsテーブルのスキーマを確認
    sqlite> .schema glossary_terms
    CREATE TABLE "glossary_terms" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL, "description" text, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL);
    sqlite>
    

テーブルglossary_termsのスキーマには、モデル作成時にコマンドラインで指定したname、description、マイグレーションファイルに追記したt.timestampsによって生成されたcreated_at、updated_at、およびデフォルトで生成されるidが見られます。
NOT NULL制約を指定したnameとtimestamps(で生成されるcreated_atとupdated_at)には、NOT NULLが付いています。

railsのコマンドラインを起動し、GlossaryTermモデルのインスタンスを生成してみましょう。

redmine_glossary$ bundle exec rails console
irb(main):001:0> GlossaryTerm.new
=> #<GlossaryTerm id: nil, name: nil, description: nil, created_at: nil, updated_at: nil>

irb(main):002:0> term1 = GlossaryTerm.new(name: 'alfa', description: 'Phonetic code of "A".')
=> #<GlossaryTerm id: nil, name: "alfa", description: "Phonetic code of \"A\".", created_at: nil, updated_at: nil>
irb(main):003:0> term1.save
   (0.1ms)  begin transaction
  SQL (6.3ms)  INSERT INTO "glossary_terms" ("name", "description", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "alfa"], ["description", "Phonetic code of \"A\"."], ["created_at", "2018-05-03 14:04:16.394566"], ["updated_at", "2018-05-03 14:04:16.394566"]]
   (30.8ms)  commit transaction
=> true

モデルクラスGlossaryTermにnewメソッドを送ってインスタンスを生成します。属性の指定はnewの引数で渡せます。
生成したインスタンスはsaveメソッドを送ってデータベースのテーブルに追加できます。

GlossaryTermクラスのインスタンスをもう一つ作成します。

irb(main):004:0> term2 = GlossaryTerm.new(name: 'bravo', description: 'Phonetic code of "B".')
=> #<GlossaryTerm id: nil, name: "bravo", description: "Phonetic code of \"B\".", created_at: nil, updated_at: nil>
irb(main):005:0> term2.save
   (0.4ms)  begin transaction
  SQL (6.4ms)  INSERT INTO "glossary_terms" ("name", "description", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "bravo"], ["description", "Phonetic code of \"B\"."], ["created_at", "2018-05-03 14:16:35.539337"], ["updated_at", "2018-05-03 14:16:35.539337"]]
   (29.5ms)  commit transaction
=> true

created_atとupdated_atの日時はどのタイミングで付くのか確認をするため、newとsaveの間に時間を置いてみました。その結果、日時はsaveメソッド実行時点になっていることが確認できました。

SQLite3にSQLのSELECT文を使ってテーブルの内容を確認します。

sqlite> select * from glossary_terms;
1|alfa|Phonetic code of "A".|2018-05-03 14:04:16.394566|2018-05-03 14:04:16.394566
2|bravo|Phonetic code of "B".|2018-05-03 14:16:35.539337|2018-05-03 14:16:35.539337

と、2件のレコードが格納されていることが分かります。


コントローラークラスの作成

コントローラークラスを生成します。Redmineプラグインのコントローラー雛形を生成するコマンドを実行し、redmine_glossaryプラグインの中にコントローラークラスGlossaryTermsControllerの雛形を生成します。

redmine_glossary$ bundle exec rails generate redmine_plugin_controller redmine_glossary glossary_terms index
      create  plugins/redmine_glossary/app/controllers/glossary_terms_controller.rb
      create  plugins/redmine_glossary/app/helpers/glossary_terms_helper.rb
      create  plugins/redmine_glossary/test/functional/glossary_terms_controller_test.rb
      create  plugins/redmine_glossary/app/views/glossary_terms/index.html.erb

モデルに対応するコントローラーを生成する場合は、モデルクラスの複数形を名前に指定します。
対応するアクションを指定します。今回は一覧表示だけ実装するのでindexだけを指定しています。これも後から追加できるので必要なものだけでも指定しておきます。

コントローラークラスと指定したアクションに対応するビューが生成されました。生成された雛形の中身は次です。

  • app/controllers/glossary_terms_controller.rb
    class GlossaryTermsController < ApplicationController
    
      def index
      end
    end
    
  • app/views/glossary_terms/index.html.erb
    <h2>GlossaryTermsController#index</h2>
    
  • app/helpers/glossary_terms_helper.rb
    module GlossaryTermsHelper
    end
    

コントローラーのindexが呼ばれると、index.html.erbが表示されます。


ルーティング設定

プラグインの雛形生成時に作られたroutes.rbは次の内容です。

# Plugin's routes
# See: http://guides.rubyonrails.org/routing.html

コメントのみで内容は空です。
ルーティング設定はrailsのコマンドラインで次のコマンドで確認できます。

redmine_glossary$ bundle exec rails routes
    Prefix Verb             URI Pattern           Controller#Action
    home GET                /                     welcome#index
    signin GET|POST         /login(.:format)      account#login
    :(以下略)

HTTPリクエストのURL、コマンドと対応するコントローラー名・アクション名の対応が表示されます。
ここに、今作成したコントローラーとアクションを定義します。

追加したいルーティング設定は次です。

HTTPメソッド URLパス コントローラー#アクション
GET /glossary_terms glossary_terms#index

routes.rbに次の記述をします。

Rails.application.routes.draw do
  resources :glossary_terms, only: [:index]
end

resources で、リソース名(モデルクラスの複数形:小文字スネークケース)を指定すると、リソース名へのURLアクセスを対応するコントローラーのアクションにルーティングする設定が生成されます。特定のアクションに限定する場合、only でアクションを指定します。

上述ルーティング設定を記述後、ルーティング設定の内容を確認すると次が追加されていました。

redmine_glossary$ bundle exec rails routes
    :(中略)
    glossary_terms GET      /glossary_terms(.:format)     glossary_terms#index

サーバーを起動し、Webブラウザから <サーバー名>:3000/glossary_terms へアクセスします。

glossary_terms_index-1.png (雛形のindexビューが表示)


用語の一覧表示を追加

モデルクラスから用語一覧を取り出し、ビューにそれを表示させる実装を書いていきます。

まず、GlossaryTermsControllerのindexメソッドが呼ばれたら、モデルから用語一覧を取り出してコントローラーのインスタンス変数にセットします。

  • app/controllers/glossary_terms_controller.rb
      def index
        @terms = GlossaryTerm.all
      end
    
  • index.html.erbに用語を表形式(<table>)で表示するHTMLと埋め込みRubyスクリプトを記述します。
    <table>
      <thead>
        <tr>
          <th>name</th>
          <th>description</th>
        </tr>
      </thead>
      <tbody>
        <% @terms.each do |term| %>
        <tr>
          <td>
            <%= term.name %>
          </td>
          <td>
            <%= term.description %>
          </td>
        </tr>
        <% end %>
      </tbody>
    </table>
    

再度Webブラウザからglossary_termsへアクセスすると、データベースに格納された用語の内容が一覧表示されます。

glossary_terms_index-2.png (一覧表示(CSS未使用、HTMLと埋め込みrubyのみ))

罫線がない、色が付いていない、など見栄えは悪いですがデータ内容は表示できています。


ちょっとだけ見栄えをよく

Redmineの他の画面での一覧表示に見栄えを合わせたいので、Redmineのカスケード・スタイルシート(CSS)の設定に合わせて、先ほど作成したindex.html.erbのHTML記述にCSSのセレクタに引っ掛けられるようクラスを追記します。

RedmineのCSSは、Redmineルートディレクトリから、public/stylesheets/application.cssに記載されています。
このCSSの中から一覧表示(HTMLのtable)に関わるCSSを調べます。すると、

table.list, .table-list { border: 1px solid #e4e4e4;  border-collapse: collapse; width: 100%; margin-bottom: 4px; }
table.list th, .table-list-header { background-color:#EEEEEE; padding: 4px; white-space:nowrap; font-weight:bold; }
table.list td {text-align:center; vertical-align:middle; padding-right:10px;}
table.list td.id { width: 2%; text-align: center;}
table.list td.name, table.list td.description, table.list td.subject, table.list td.comments, table.list td.roles, table.list td.attachments {text-align: left;}
  :

のように記載があります。tableタグにクラスlistを指定すれば、Redmineのapplication.cssで用意されたCSSが適用されそうです。

  • index.html.rb の修正
    <h2>GlossaryTermsController#index</h2>
    
    <table class="list">
      <thead>
        <tr>
          <th>name</th>
          <th>description</th>
        </tr>
      </thead>
      <tbody>
        <% @terms.each do |term| %>
        <tr>
          <td class="name">
            <%= term.name %>
          </td>
          <td class="description">
            <%= term.description %>
          </td>
        </tr>
        <% end %>
      </tbody>
    </table>
    

画面表示は次のようになりました。

glossary_terms_index-3.png (一覧表示(CSS適用))


フェーズ1の実装完了とフェーズ2へ

フェーズ1として、最小限のMVC構造を持つ用語一覧表示プラグインを実装しました。フェーズ1で実施したことを簡潔にまとめます。

  • プラグインの雛形の生成(rails generate redmine_pluginコマンド)
  • プラグイン情報の記述(init.rbの記述)
  • モデルの生成(rails generate redmine_plugin_modelコマンド)
  • マイグレーションの追記と実行(001_create_glossary_terms.rbの記述)
  • コントローラーの生成(rails generate redmine_plugin_controllerコマンド)
  • 一覧表示の実装(glossary_terms_controller.rbindex.html.erbの記述)
  • ルーティング設定の記述(routes.rbの記述)

国際化対応をやり残したので、フェーズ2を国際化対応とします。


フェーズ2)国際化対応

フェーズ1では、ビューのテキストがそのまま表示される、べた書きで記述していました。一方で、Redmineは画面の各所が国際化対応により各国語に翻訳されて表示されます。そこで、ビューを多数作りこむ前に先に国際化対応をしておきます。以後、ビューを追加するときは極力国際化対応する方針とします。

フェーズ2の概略

Redmine(Rails)では、画面に表示するテキストを国際化対応する仕組みが用意されています。
プラグインの雛形を生成したときに、config/localesディレクトリが作られ、その中にデフォルトでen.ymlが置かれ、ハッシュ構造でキーとテキストが定義されています。そして、翻訳したいロケール毎にja.ymlのようにファイルを作り、キーはen.ymlと同じくして、テキスト部分をそのロケールの言葉に書き換えます。

フェーズ2として、次を作成または修正します。

No 役割 ファイル 内容
1 翻訳ファイル(英語) en.yml 英語ロケール用のキーとテキストの定義
2 翻訳ファイル(日本語) ya.yml 日本語ロケール用のキーとテキストの定義
3 用語一覧のビュー index.html.erb 用語一覧のビューを国際化

フェーズ2では、フェーズ1の一覧表示画面から、表のタイトル、表の列名のテキストを国際化対応させ、英語および日本語のリソースを用意します。ただし、表の列名(nameとdescription)はRedmine本体にあるものを利用します。

国際化対応対象テキスト キー名 定義場所
一覧表のページタイトル label_glossary glossary plugin
表の名前列 field_name redmine本体
表の説明列 field_description redmine本体

国際化対応の仕組み

config/localesディレクトリ下にある拡張子.ymlおよび.rbファイルが読み込まれ、ロケール毎にキーと値で管理されます。
例を次に示します。

en:
  general_text_No: 'No'
  general_text_Yes: 'Yes'

ロケールen: において、キーgeneral_text_Noは、テキストNoに対応します。
Rubyのコードにおいて、I18n.translate(:general_text_No)と参照すると、ロケールenでは、Noのテキストに置き換わります。
また、日時等をロケールに応じた表記とするI18n.localizeメソッドもあります。

Railsの国際化対応の仕組み

Railsでは、ビューのヘルパーメソッドとして、I18n.translateI18n.localizeの省略表記tおよびlが提供されています。よって、次のように記述することができます。

<%=t :general_text_No %>
Redmineの国際化対応の仕組み

Redmineでは、lib/redmine/i18n.rb が用意され、Railsの国際化対応をラップした構造となっています。Railsのヘルパーメソッドの1つ"l"と重なった命名ですが、lメソッドが用意され、引数の数によって次のような仕組みになっています。

    def l(*args)
      case args.size
      when 1
        ::I18n.t(*args)
      when 2
        if args.last.is_a?(Hash)
          ::I18n.t(*args)
        elsif args.last.is_a?(String)
          ::I18n.t(args.first, :value => args.last)
        else
          ::I18n.t(args.first, :count => args.last)
        end
      else
        raise "Translation string with multiple values: #{args.first}" 
      end
    end

簡単にまとめると、次となります。

  • <%=l :general_text_No %>
    引数が1つのときは、そのままRailsのtメソッドに引数を渡す
  • <%=l :hello_message_with_name, name: "Smith" %>
    引数が2つ以上で2つ目以降がハッシュのときは、そのままRailsのtメソッドに引数を渡す
  • <%=l :hello_with_value, "Three" %>
    引数が2つで2つ目が文字列のときは、Railsのtメソッドに1つ目の引数はそのまま、2つ目の引数はキーvalueの値として渡す
  • <%=l :hello_with_count, 31 %>
    引数が2つで2つ目が数値のときは、Railsのtメソッドに1つ目の引数はそのまま、2つ目の引数はキーcountの値として渡す。
    countは、例えば英語で1と2以上で後ろの単語が単数形、複数形と変わるような場合に対応する。

このほか、redmine/i18n.rb には、format_dateformat_time、他日付・時刻関係のローカライズメソッドがいろいろ揃っています。


index.html.erb での国際化対応(1)

ハードコードされているテキストを、I18nヘルパーメソッドを使って国際化対応に置き換えます。
まずは、Redmine本体で定義されているキーfield_namefield_descriptionを指定し、ロケールに応じたテキストに置き換えます。

     <tr>
-      <th>name</th>
-      <th>description</th>
+      <th><%=l :field_name %></th>
+      <th><%=l :field_description %></th>
     </tr>

日本語環境で実行した結果が次の画面です。

index_i18n-1.png


index.html.erb での国際化対応(2)

次は、プラグインのlocalesに独自のキーとテキストを指定します。

  • config/locales/en.yml
    en:
      label_glossary_terms: "Glossary terms" 
    
  • config/locales/ja.yml
    ja:
      label_glossary_terms: "用語集" 
    

ビューを修正します。

  • index.html.erb
    -<h2>GlossaryTermsController#index</h2> 
    +<h2><%=l :label_glossary_terms %></h2> 
    

日本語ロケールで実行すると次の画面となります。

index_i18n-2.png


フェーズ2の実装完了とフェーズ3へ

フェーズ2では、国際化対応として表示画面中の表示文字列をロケールによって切り替える対応をしました。フェーズ2で実施したことを次に簡単にまとめます。

  • ビューの中で表示する文字列の部分を、埋め込みruby記述を使い、lメソッドを使ってシンボルを指定する呼び出しに置き換え
  • そのシンボルに対応する文字列をロケール別に(今回は英語と日本語の2つのロケール)定義
    ロケールの定義ファイルは、英語はen.yml、日本語はja.yml

フェーズ3では、一覧表示から用語をクリックするとその用語の詳細表示を行うようにします。


フェーズ3)一覧表示→詳細表示

一覧表示の名称列を、詳細表示へのリンクとし、詳細表示画面を作成します。
詳細表示は、コントローラー(GlossaryTermsController)のshowアクションとします。

フェーズ3の概略

フェーズ3として、次を作成または修正します。

No クラス(役割) ファイル 内容
1 ルーティング設定 routes.rb showアクションのルーティング追加
2 GlossaryTremsController glossary_terms_controller.rb showアクションの追加
3 用語詳細のビュー show.html.erb 用語詳細を表示する埋め込みruby
4 用語一覧のビュー index.html.erb 詳細へのリンクを追加
5 翻訳ファイル(英語) en.yml 詳細表示で使うテキストを追加
6 翻訳ファイル(日本語) ja.yml 詳細表示で使うテキストを追加

ルーティング設定の追加

routes.rbに、showアクションを追記します。

 Rails.application.routes.draw do
-  resources :glossary_terms, only: [:index]
+  resources :glossary_terms, only: [:index, :show]
 end

上述の追記によって、次のルーティングが追加されます。

redmine$ bundle exec rails routes
    :
    glossary_term GET     /glossary_terms/:id(.:format)      glossary_terms#show

Webブラウザから、<サーバー名>:3000/glossary_terms/24 のようにidを指定してGETリクエストをすると、GlossaryTermsControllershowメソッドにルーティングされます。


showアクションの追加

GlossaryTermsControllershowメソッドを追加し、処理を記述します。まずはメソッドを用意します。

  • app/controllers/glossary_terms_controller.rb
     class GlossaryTermsController < ApplicationController
       def index
         @glossary_terms = GlossaryTerm.all
       end
    +
    +  def show
    +  end
     end
    

showアクションでは、HTTPリクエストのURLパスにリソース名(ここではglossary_terms)とidが指定されます。このidを取り出し、データベース内のリソースに対応するテーブルから、指定されたidを持つレコードを取り出し、そのレコードからGlossaryTermインスタンスを作成してビューに渡すことがコントローラーの処理となります。ビューに渡すデータはインスタンス変数に格納します。そして、渡されたGlossaryTermインスタンスから表示を作るのがshowアクション時に実行されるビューshow.html.erbとなります。

[Webブラウザ]
    ↓  GETリクエスト(glossary_terms/24)
[GlossaryTermsController#show]
    ↓  id=24のGlossaryTermインスタンスを生成
[show.html.erb]
    ↓  id=24のGlossaryTermからHTMLを生成
[Webブラウザ]

コントローラーのアクションに対応するメソッドが呼ばれるとき、idを始め幾種類かのリクエストパラメーターがハッシュparamsに格納されています。idを取り出すには、params[:id]となります。

そこで、showメソッドの処理は次のように記述したくなります。(エラー処理は省略)

def show
  @term = GlossaryTerm.find(params[:id])
end

ところで、コントローラーにおいて指定されたidからモデルのインスタンスを取得する処理はshowアクションだけでなく、他のアクション(editなど)でも必要です。よいコードは重複を排除するので、idからモデルのインスタンスを取得する処理を別メソッドに切り出し、そのメソッドをshowアクション時に実行するようにします。Railsではベストプラクティスとして、before_actionを用いて実装します。

class GlossaryTermsController < ApplicationController

  before_action :find_term_from_id, only: [:show]
    :(中略)
  def find_term_from_id
    @term = GlossaryTerm.find(params[:id])
  end
end

before_actionで、アクションが実行される前(before)に実行するメソッド(ここではfind_term_from_id)を指定します。アクションは、onlyをキーに指定します。複数指定できるように配列でアクションを列挙します。

次にビューの実装をします。showアクションが実行されるときは、ビューはshow.html.erbが使用されるので新たにこのファイルを作成します。

  • app/views/glossary_terms/show.html.erb
    <h2><%=t :label_glossary_term %> #<%= @term.id %></h2>
    
    <h3><%= @term.name %></h3>
    
    <table>
      <tr>
        <th><%=t :field_description %></th>
        <td><%= @term.description %></td>
      </tr>
    </table>
    
  • 題名として、国際化対応テキストを使用します。キーをlabel_glossary_termとします(単数形に注意)。新たなキーを使うので、ロケールファイル(en.ymlとja.yml)にキーと翻訳テキストを追記します。
    • en.yml
       en:                                     
         label_glossary_terms: "Glossary terms"                                   
      +  label_glossary_term: "Glossary term"  
      
    • ja.yml
       ja:
         label_glossary_terms: "用語集" 
      +  label_glossary_term: "用語" 
      

これで詳細表示が実装できました。Webブラウザからidを指定したURLリクエストを行います。あらかじめデータベースに作成している用語のidをどれか指定します。
サーバー名:3000/glossary_terms/2

glossary_terms_show-1.png (詳細表示)

ここで、idに存在しない番号を指定すると、次のようなエラー画面となってしまいます。

glossary_terms_show_error-1.png (詳細表示で存在しないidを指定しRailsの開発者向けエラー画面表示)

そこで、idからモデルのインスタンスを取得する処理においてidに対応するレコードが存在しないときの例外を拾って404エラー画面を表示する処理を追加します。

   def find_term_from_id
     @term = GlossaryTerm.find(params[:id])
+  rescue ActiveRecord::RecordNotFound
+    render_404
   end

このエラー画面は次のようになります。

glossary_terms_show_error-2.png (詳細表示で存在しないidを指定し一般利用者向けエラー画面表示)


一覧表示から詳細表示へのリンク

いよいよ、一覧表示の対象件名をクリックすると詳細表示を開くリンクを追加します。
リンクの追加には、link_toメソッドを利用します。このlink_toメソッドは、オプションの指定方法が極めて多彩なので、使い方を把握するのが大変ですがビューでは非常によく使うメソッドです。

凡その使い方は、

link_to リンク文字列, リンク先

となります。

今回は、リンク先がリソース(モデル)となっており、ルーティング設定済みで、showアクション(GET)を呼び出す場合なので、最も簡単な指定例(リンク先をリソースのオブジェクトで示す)が適用できます。

  • app/views/glossary_terms/index.html.erb
         <% @glossary_terms.each do |term| %>
         <tr>
           <td class="name">
    -       <%= term.name %>
    +       <%= link_to term.name, term %>
           </td>
    

リンク文字列には、用語(GlossaryTermオブジェクト)のname属性を指定し、リンク先には用語(GlossaryTermオブジェクト)自身を指定します。
表示される一覧画面は次のようになります。

glossary_terms_index-4.png (詳細画面へのリンクを追加した一覧画面)

リンクのURLは、例えば次のようになります。

http://localhost:3000/glossary_terms/2


link_toの補足

ルーティング設定(routes.rb)にリソース指定(resources)をした場合、ルーティング設定にそのリソースに関する定義が追加されます。

Prefix           Verb      URI Pattern                       Controller#Action
glossary_terms   GET       /glossary_terms(.:format)         glossary_terms#index
glossary_term    GET       /glossary_terms/:id(.:format)     glossary_terms#show

ここで、Prefixの列に表示される名前がlink_toで使用できます。

link_to "一覧表示へ", glossary_terms_path

と記述すると、glossary_terms_pathから末尾の_pathを取り除いた名前glossary_termsをPrefixに持つURIパターンがリンクとして生成されます。

ここで、Prefixにglossary_termとあるURIパターンのリンクを生成するには、glossary_term_pathと指定したいところですが、このURLパターンにはidが含まれるため、実際のリンクを生成するときは実際のidの値が必要です。このidの値を指定する方法の一つに、リソースであるGlossaryTermインスタンス自体を指定するものがあります。

link_to "とある詳細表示へ", glossary_term_path(@term)

これをさらに省略して表記したのが

link_to "とある詳細表示へ", @term

となります。

routes.rbにリソース指定をしていない場合、この書き方はできません。初期のRedmineやプラグインに多いlink_toの記述には、次のようにリンク先のコントローラー名、アクション名を指定し、idが必要な場合はidを渡しているものをよく見かけます。

<% link_to "○○はこちら", { controller: :glossary_terms, action: :show, id: @term.id } %>

フェーズ3の実装完了とフェーズ4へ

フェーズ3では、一覧表示の名称列を、詳細表示へのリンクとし、詳細表示画面を作成しました。フェーズ3で実施したことは次です。

  • routes.rbに詳細表示のshowアクションを追記
  • GlossaryTermコントローラーにshowアクションのメソッドを記述
  • 用語詳細表示のビューshow.html.erb実装
  • 用語一覧ビューindex.html.erbに詳細表示へのリンクを追加
  • 表示文字列を国際化対応

フェーズ3までで最低限の表示ができるようになりました。フェーズ4では、新規作成および編集を実現します。


フェーズ4) 用語の作成・変更

新規の用語作成、既存の用語の変更ができる編集ページを作ります。

フェーズ4の概略

新規に用語を作成する画面は、Webアプリケーションではフォームと呼ばれ、典型的には入力項目とサブミットボタンから構成されます。また、既存の用語を変更する画面も同じくフォームとなりますが、新規の場合は入力項目が空であるのに対して既存の変更の場合は入力項目に値(文字)が入って編集可能な状態になっています。アクションとしては新規フォームの表示がnewアクション、既存フォームの表示がeditアクション、新規入力フォームをサブミットするcreateアクション、既存の変更フォームをサブミットするupdateアクションで構成されます。

フェーズ4として、次を作成または修正します。

No 役割 ファイル 内容
1 ルーティング設定 routes.rb new/create/edit/updateアクションのルーティング追加
2 GlossaryTermsController glossary_terms_controller.rb new/create/edit/updateアクションの追加
3 用語新規作成のビュー new.html.erb 用語新規作成の埋め込みruby
4 用語フォームの部分ビュー _form.html.erb フォーム部分だけ切り出した埋め込みruby
5 用語編集のビュー edit.html.erb 用語編集の埋め込みruby
6 翻訳ファイル(英語) en.yml 新規作成で使うテキストを追加
7 翻訳ファイル(日本語) ja.yml 新規作成で使うテキストを追加

用語の新規作成画面

newアクションで用語の新規作成画面(空のフォーム画面)を表示します。

  • ルーティング設定にnewアクションを追加
     Rails.application.routes.draw do
    -  resources :glossary_terms, only: [:index, :show]
    +  resources :glossary_terms, only: [:index, :show, :new, :create]
     end
    

glossary_termsに関するルーティング設定は次のようになります。

Prefix             Verb      URI Pattern                       Controller#Action
glossary_terms     GET       /glossary_terms(.:format)         glossary_terms#index
                   POST      /glossary_terms(.:format)         glossary_terms#create
new_glossary_term  GET       /glossary_terms/new(.:format)     glossary_terms#new
glossary_term      GET       /glossary_terms/:id(.:format)     glossary_terms#show

コントローラーにnewアクションのメソッドを追加します。空の用語インスタンスを生成しビューで属性をセットできるようインスタンス変数に定義します。
2行目はPrefixが一見空ですが、直前の行と同じ(glossary_terms)となります。

 def new
    @term = GlossaryTerm.new
  end

新規作成画面を作成します。新規作成の画面はnew.html.erbに記述しますが、フォーム部分(属性の入力とサブミットボタン)は後で作成する編集画面と共通するので、フォーム部分を独立したファイル_form.html.erbに記述し、新規作成画面から呼び出します。

  • new.html.erb
    <h2><%=l :label_glossary_term_new %></h2>
    
    <%= labelled_form_for :glossary_term, @term,
        url: glossary_terms_path do |f| %>
      <%= render partial: 'glossary_terms/form', locals: {form: f} %>
      <%= f.submit l(:button_create) %>
    <% end %>
    
  • ページ名を表示するH2タグに、国際化対応テキストのキーを指定しています。
  • HTMLの入力フォームの作成には、labelled_form_forを使用しています。
    • フォームの内容は、第1引数で指定したキーでparamsハッシュに格納されます。取り出す際はparams[:glossary_term]と記述します。
    • フォームの各入力フィールドには第2引数で指定したインスタンスの属性の値が入った状態で表示されます。新規作成時は引数なしのnewで生成した空のインスタンスを指定しているので、属性はnilとなっており、各フィールドは空欄となります。
    • 第3引数にはサブミット時のパスを指定します。フォームのサブミットはPOSTとなります。ルーティング設定でcreateアクションに対応するPrefixがglossary_termsなので、このPrefixに_pathを追加した文字列glossary_terms_pathを:urlをキーとしたハッシュで指定します。
  • 共通のフォーム部分(_form.html.erb)を呼び出す render partialを記述します。リソース名/form と指定すると、app/views/リソース名/_form.html.erb ファイルを呼び出します。locals: は、部分描画のビューに渡す変数のハッシュです。ここでは、ローカル変数fを渡しています。渡した変数は、部分描画のビューではfではなくformの名前で参照できます。
  • _form.html.erb
    <div class="box tabular">
      <p><%= form.text_field :name, size: 80, required: true %></p>
      <p><%= form.text_area :description, size: "80x10", required: false %></p>
    </div>
    
  • 背景を灰色とするため CSSセレクタのbox、入力フィールドのラベルと入力欄が複数並ぶときに各入力フィールドの開始位置が左右にずれないよう揃えるため CSSセレクタのtabularを指定しています。
  • 1行入力フィールドtext_fieldを用いて名称を入れます。属性名nameを指定、桁数を80文字分、必須入力記号の表示を設定しています。
    入力フィールドのラベル指定を省略しているので、field_属性名の国際化対応のキー(この例ではfield_name)が指定され対応するテキストがラベル表示されます。
  • 複数行入力フィールドtext_areaを用いて説明を入れます。属性名descriptionを指定、桁数を80文字×10行分、必須入力記号はなしを設定しています。
    これも入力フィールドのラベル指定を省略しているので、field_属性名の国際化対応のキー(この例ではfield_description)が指定され対応するテキストがラベル表示されます。

ここで作成した画面は次となります。

glossary_terms_new-1.png (新規画面(空のフォーム))

参考資料

新規作成のコントローラー処理

新規画面からサブミットされると、コントローラーのcreateメソッドが呼ばれます。フォームの内容は、labelled_form_for の引数で指定したキーを使用して、params[:glossary_term]で取り出します。createメソッドでは、フォームの内容を属性として保持するGlossaryTermインスタンスを作り、データベースに永続化する処理を記述します。

 class GlossaryTermsController < ApplicationController
    :(中略)
+  def create
+    term = GlossaryTerm.new(glossary_term_params)
+    if term.save
+      flash[:notice] = l(:notice_successful_create)
+      redirect_to glossary_term_path(term.id)
+    end
+  end
    :(中略)
+  private                                 
+                                          
+  def glossary_term_params                
+    params.require(:glossary_term).permit(
+      :name, :description                 
+    )                                     
+  end                                     

フォームから渡された各フィールドの値を一括でモデルのインスタンス生成に渡す次のコードは、従来のRailsコード/Redmineコードではよく見かけますが、「マスアサインメント」と呼ばれるセキュリティ上好ましくない振る舞いを招きます。

term = GlossaryTerm.new(params[:glossary_term])

そこで、ストロングパラメーターと呼ばれる仕組みを使ってフォームから渡されたパラメータ設定可能なフィールドを許可します。
コントローラーのprivateメソッド(ここでは、glossary_term_params)で、値を設定してもよい属性名を明示的に指定します。
params.require(:glossary_term)で、フォームから渡されたパラメータに、キー:glossary_termが含まれることを確認し、このパラメータで更新してもよい属性をpermit(:name, :description)で許可しています。この許可が与えられたパラメータでGlossaryTerm.newを実行しています。

フォームの値で作成されたGlossaryTermインスタンスをデータベースに保存(saveメソッドで)し、成功すればflashメッセージを表示させ、作成した用語の詳細表示画面へ遷移します。flashメッセージの表示例は次です。

flash[:notice] = "こんにちは、flashメッセージです。"

ここまでの範囲で動作確認をしてみます。
Webブラウザから、<サーバー名>:3000/glossary_terms/new とURLを指定して開くと、用語の作成画面が表示されます。名称と説明に入力し作成ボタンを押すと、コントローラーのcreateメソッドが呼ばれ、データベースにデータが保存されます。

glossary_terms_new-2.png (新規画面のフォームに入力)

データベースへの保存後、保存した用語の詳細表示画面へ遷移し、flashメッセージが表示されます。

glossary_term_new-3.png (新規作成完了後の詳細表示)

参考資料

Railsガイド 4.5 Strong Parameters


一覧表示画面に新規作成アイコンを追加

用語集の一覧表示画面の右上に、[新しい用語]のボタンを追加し、用語の新規作成画面に遷移する機能を追加します。

  • index.html.erb
     <h2><%=l :label_glossary_terms %></h2>                                                       
    
    +<div class="contextual">                                                                     
    +  <%= link_to l(:label_glossary_term_new), new_glossary_term_path, class: 'icon icon-add' %> 
    +</div>                                                                                       
    +                                                                                             
     <table class="list">                                                                         
    
  • div でクラスcontextualを指定し、そこに追加・削除等の操作を置きます。
  • link_to でリンク名に用語の作成のテキスト、リンク先にルーティング設定に基づくパス、classキーで追加アイコンを指定しています。

画面は次のようになります。

glossary_term_index-4.png (一覧表示に追加アイコン)


詳細表示に編集アイコンを追加

まずルーティング設定に、編集(edit)と更新(update)を追加します。今まではリソースに対して使用するアクションを列挙していましたが、今回の追加でdestroy以外のアクションを使用することになり、destroyもすぐに追加することになります。そこで、onlyの記述を削除し、全てのアクションを有効にします。

  • routes.rb
     Rails.application.routes.draw do
    -  resources :glossary_terms, only: [:index, :show, :new, :create]
    +  resources :glossary_terms
     end
    

リソースglossary_termsに対するルーティング設定は次のようになります。

Prefix               Verb      URI Pattern                           Controller#Action
glossary_terms       GET       /glossary_terms(.:format)             glossary_terms#index
                     POST      /glossary_terms(.:format)             glossary_terms#create
new_glossary_term    GET       /glossary_terms/new(.:format)         glossary_terms#new
edit_glossary_term   GET       /glossary_terms/:id/edit(.:format)    glossary_terms#edit
glossary_term        GET       /glossary_terms/:id(.:format)         glossary_terms#show
                     PATCH     /glossary_terms/:id(.:format)         glossary_terms#update
                     PUT       /glossary_terms/:id(.:format)         glossary_terms#update
                     DELETE    /glossary_terms/:id(.:format)         glossary_terms#destroy

用語集の詳細表示画面の右上に、[編集]アイコンを追加し、用語の編集を可能にします。

  • show.html.erb
    +<div class="contextual">                                                          
    +  <%= link_to l(:button_edit), edit_glossary_term_path, class: 'icon icon-edit' %>
    +</div>                                                                            
    +                                                                                  
     <h2><%=l :label_glossary_term %> #<%= @term.id %></h2>                                                                                                               
    

編集アイコンを追加した画面は次になります。

glossary_terms_show-2.png (詳細表示に編集アイコン追加)


編集のコントローラーへの実装

コントローラーにeditアクションとupdateアクションに対応するメソッドを追加します。

 class GlossaryTermsController < ApplicationController             

-  before_action :find_term_from_id, only: [:show]                 
+  before_action :find_term_from_id, only: [:show, :edit, :update] 

+  def edit
+  end
+
+  def update
+    @term.attributes = glossary_term_params
+    if @term.save
+      flash[:notice] = l(:notice_successful_update)
+      redirect_to @term
+    end
+  rescue ActiveRecord::StaleObjectError
+    flash.now[:error] = l(:notice_locking_conflict)
+  end

edit および update アクションでは既存の用語を扱うので、showアクションの時と同様before_actionで対象用語のidから用語オブジェクトをインスタンス変数に取得しておきます。
editメソッドはbefore_actionでの処理以外に足すものがないので中身が空として定義します。
updateメソッドはフォームで変更された内容を受け取るときに呼ばれます。フォームから受け取ったパラメータのうち更新可能なフィールドを変更許可したものをモデルオブジェクトのattributesに代入します。saveメソッドでデータベース保存が成功すればflashメッセージで成功を表示し、詳細表示画面へリダイレクトします。redirect_toでは、ルーティング設定でリソース定義したモデルについては、モデルオブジェクトをURL替わりに指定可能です。

そこで、ストロングパラメーターと呼ばれる仕組みを使ってフォームから渡されたパラメータ設定可能なフィールドを許可します。


用語の編集画面

  • edit.html.erb
    <h2><%=l :label_glossary_term %> #<%= @term.id %></h2>
    
    <%= labelled_form_for :glossary_term, @term, url: glossary_term_path do |f| %>
      <%= render partial: 'glossary_terms/form', locals: {form: f} %>
      <%= f.submit l(:button_edit) %>
    <% end %>
    

既存の変更でフォームをサブミットするURLは次です。

glossary_term        PUT       /glossary_terms/:id(.:format)         glossary_terms#update

labelled_form_for でURLに指定するのは、このglossary_termに_pathを付加したglossary_term_pathとなります。
単数形・複数形が紛らわしいので注意してください。

formの部分描画のビュー(_form.html.erb)は新規作成で作ったものと同じものを使用します。

サブミットするボタンのラベルは、新規ではなく修正なのでnew.html.erbのものとは指定を変えています。


削除機能の追加

用語の詳細表示画面の右上、編集アイコンの右隣に削除アイコンを追加します。

  • show.html.erb
     <div class="contextual">
       <%= link_to l(:button_edit), edit_glossary_term_path, class: 'icon icon-edit' %>
    +  <%= link_to l(:button_delete), glossary_term_path, method: :delete,
    +  data: {confirm: l(:text_are_you_sure)}, class: 'icon icon-del' %>
     </div>
    

削除のルーティング設定は次なので、URLはglossary_term_path、HTTPメソッドはDELETEなので、method: deleteを指定します。

glossary_term    DELETE    /glossary_terms/:id(.:format)     glossary_terms#destroy

link_to で削除前に確認ダイアログを表示する場合、次の記述がよく行われていましたが、これはRails 4.1以降で削除されています。

<%= link_to l(:button_delete), glossary_term_path,
  confirm: l(:text_are_you_sure), method: :delete, class: 'icon icon-del' %>

用語のコントローラーにdestroyメソッドを追加します。

  • glossary_terms_controller
     class GlossaryTermsController < ApplicationController                      
    
    -  before_action :find_term_from_id, only: [:show, :edit, :update]          
    +  before_action :find_term_from_id, only: [:show, :edit, :update, :destroy]
      :(中略)
    +
    +  def destroy
    +    @term.destroy
    +    redirect_to glossary_terms_path
    +  end
    

削除対象のオブジェクトを取得するため、before_actionにdestroyを追記します。
モデルオブジェクトのdestroyメソッドを呼びデータベースから用語を削除したあとは、用語の一覧表示にリダイレクトします。


フェーズ4の実装完了とフェーズ5へ

フェーズ4では、用語の新規作成と既存の用語の編集をできるようにしました。フェーズ4で実施したことを次に簡単にまとめます。

  • ルーティング設定routes.rbに、新規作成に関するアクション(newとcreate)を追加
  • 用語コントローラーGlossaryTermsControllerに、newアクションに対応するnewメソッドを追加
  • 新規作成画面new.html.erbを作成
  • 新規作成と編集とで共通するフォーム画面を部分ビュー_form.html.erbに作成
  • 用語コントローラーGlossaryTermsControllerに、createアクションに対応するcreateメソッドを追加
  • 一覧画面index.html.erbの右上に新規作成のアイコンを追加
  • 詳細画面show.html.erbに編集アイコンを追加
  • 用語コントローラーGlossaryTermsControllerに、editアクションに対応するeditメソッドを追加
  • 編集画面edit.html.erbを作成
  • 詳細画面に削除アイコンを追加
  • 用語コントローラーGlossaryTermsControllerに、destroyアクションに対応するdestoryメソッドを追加

これで、用語リソース(GlossaryTerm)のCRUDを扱う機能が最低限実装出来ました。フェーズ5では、用語を分類するカテゴリを導入します。


フェーズ5) カテゴリの導入

用語をグルーピングする概念としてカテゴリGlossaryCategoryを導入します。

フェーズ5の概略

No クラス(役割) ファイル 内容
1 GlossaryCategory glossary_category.rb カテゴリのモデルクラス
2 GlossaryTerm glossary_term.rb GlossaryCategoryへの関連を追加
3 ルーティング設定 routes.rb GlossaryCategoryのルーティング設定を追記
4 GlossaryCategoriesController glossary_categories_controller.rb
5 カテゴリ一覧のビュー app/views/glossary_categories/index.html.erb 新規作成
6 カテゴリの新規作成ビュー app/views/glossary_categories/new.html.erb 新規作成
7 カテゴリの編集ビュー app/views/glossary_categories/edit.html.erb 新規作成
8 用語一覧のビュー app/views/glossary_terms/index.html.erb カテゴリ毎にグルーピングして一覧表示
9 用語詳細のビュー app/views/glossary_terms/show.html.erb カテゴリ追加
10 用語作成編集のフォーム app/views/glossary_terms/_form.html.erb カテゴリ追加
11 GlossaryTermsController glossary_terms_controller.rb 属性カテゴリの設定許可(ストロングパラメーター)

モデルの作成・修正

モデルの関係をクラス図で表現しました。

model_diagram-phase5.png (モデルのクラス図)

用語はカテゴリに属し、用語とカテゴリの関係は、1つのカテゴリが多数の用語と関連する1:多の関係となります。なお、用語が属するカテゴリは付け替えができるようライフサイクルを同一にはしない関係とします。使用する機能は、has_many と belongs_to です。

  • GlossaryCategory(カテゴリ)クラスを生成し、has_many でGlossaryTerm(用語)クラスと関連付け
    • 外部キーがデフォルトだとglossary_category_idとなるが長いのでcategory_idとする
  • 用語クラスを修正し、belongs_to でカテゴリと関連付け、およびカテゴリへの外部キーを追加するマイグレーションを生成
GlossaryCategoryモデルの生成

railsの生成コマンドで種類をredmine_plugin_modelとして属性にnameを持つモデルクラスを生成します。

redmine$ bundle exec rails generate redmine_plugin_model redmine_glossary GlossaryCategory name:string
      create  plugins/redmine_glossary/app/models/glossary_category.rb
      create  plugins/redmine_glossary/test/unit/glossary_category_test.rb
      create  plugins/redmine_glossary/db/migrate/002_create_glossary_categories.rb
redmine$
  • 注1) カラムの型は小文字で指定します(先頭大文字 Stringと指定するとマイグレーションの実行でエラー発生)

生成されたモデルクラスのコードと、マイグレーションファイルは次です。

  • glossary_category.rb
    class GlossaryCategory < ActiveRecord::Base
    end
    
  • 002_create_glossary_categories.rb
    class CreateGlossaryCategories < ActiveRecord::Migration[5.1]
      def change
        create_table :glossary_categories do |t|
          t.String :name
        end
      end
    end
    

生成されたGlossaryCategory にGlossaryTermとの関連付けを追記します。has_manyの指定は特にテーブルへのカラム追加は必要ないので、GlossaryCategoryのマイグレーションファイルは修正不要です。

  • glossary_category.rb
    class GlossaryCategory < ActiveRecord::Base
      has_many :terms, class_name: 'GlossaryTerm', foreign_key: 'category_id'
    end
    
  • カテゴリから用語にアクセスする属性名を簡潔にtermとするため、has_manyのオプションでクラス名を指定
  • 用語からカテゴリを参照する外部キーを簡潔にcategory_idとするため、has_manyのオプションで参照先のGlossaryTermで使用する外部キーを指定
  • TODO: has_manyのオプション dependent: :nullify 指定が必須か否か調べ、必須なら追記する
GlossaryTermモデルの修正

GlossaryTermに、GlossaryCategoryへの関連付けを追記します。belongs_toの指定はテーブルへの外部キーのカラムを追加する必要があるので、新たにマイグレーションファイルを作成し、カラムを追加します。

  • glossary_term.rb
     class GlossaryTerm < ActiveRecord::Base
    +  belongs_to :category, class_name: 'GlossaryCategory', foreign_key: 'category_id'
     end
    

マイグレーションファイルの名前は、変更内容が分かるように具体的に付けます。Redmineのプラグイン用のマイグレーションファイルでは次のようにファイル名を命名します。

  • カラムの追加・削除
    3桁連番_(add|remove)_カラム名_to_テーブル名.rb
    
  • テーブルの追加
    3桁連番_create_テーブル名.rb
    
  • 変更
    3桁連番_change_変更対象.rb
    

Redmineにはプラグイン用のマイグレーション生成コマンドが用意されていないので、新規に手で作るか、Railsのマイグレーション生成コマンドでいったん作成し、そのファイルの名前を変えて場所をプラグインの下に移動します。

マイグレーション生成コマンドで生成する場合は、カラムを追加する場合は、"Addカラム名Toテーブル名"と指定します。

redmine$ bundle exec rails generate migration AddCategoryToGlossaryTerms category:belongs_to
      invoke  active_record
      create    db/migrate/20180506043152_add_category_to_glossary_terms.rb
redmine$

  • 外部キーを指定するときは、型名をreferenceまはたbelongs_toを指定します。その際、外部キーが参照する先のモデルクラス名(category)を指定します。マイグレーションを実行するとカラム名に_idが付いた名前が設けられます。

Redmine本体のdb/migrate/ディレクトリ下に、ファイル名先頭が3桁連番ではなく年月日時分秒で生成されるので、次のように名前を場所を変更します。

現在プラグインのdb/migrateディレクトリ下にあるマイグレーションファイルの連番を確認します。以下の例では、001と002のマイグレーションファイルがあるので、次に作るマイグレーションファイルは003となります。

redmine$ ls plugins/redmine_glossary/db/migrate/
001_create_glossary_terms.rb  002_create_glossary_categories.rb

redmine$ mv db/migrate/20180506043152_add_category_to_glossary_terms.rb plugins/redmine_glossary/db/migrate/003_add_category_to_glossary_terms.rb
redmine$

  • 003_add_category_to_glossary_terms.rb
    class AddCategoryToGlossaryTerms < ActiveRecord::Migration[5.1]
      def change
        add_reference :glossary_terms, :category, foreign_key: true
      end
    end
    
マイグレーションの実行

プラグイン用のマイグレーションを実行します。

redmine$ bundle exec rails redmine:plugins:migrate
Migrating redmine_glossary (Redmine Glossary plugin)...
== 2 CreateGlossaryCategories: migrating ======================================
-- create_table(:glossary_categories)
   -> 0.0149s
== 2 CreateGlossaryCategories: migrated (0.0157s) =============================

== 3 AddCategoryToGlossaryTerms: migrating ====================================
-- add_reference(:glossary_terms, :category, {:foreign_key=>true})
   -> 0.0189s
== 3 AddCategoryToGlossaryTerms: migrated (0.0203s) ===========================

redmine$

2つのテーブルのスキーマを確認します。

sqlite> .schema glossary_categories
CREATE TABLE "glossary_categories" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar);
sqlite> .schema glossary_terms
CREATE TABLE "glossary_terms" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL, "description" text, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL, "category_id" integer);
CREATE INDEX "index_glossary_terms_on_category_id" ON "glossary_terms" ("category_id");

軽い動作確認を実施(本来はユニットテスト等を記述すべきですが)します。
  • GlossaryCategoryの作成とデータベース保存
    irb(main):002:0> cat1 = GlossaryCategory.new(name: 'Redmine')
    => #<GlossaryCategory id: nil, name: "Redmine">
    irb(main):003:0> cat1.save
       (0.2ms)  begin transaction
      SQL (13.6ms)  INSERT INTO "glossary_categories" ("name") VALUES (?)  [["name", "Redmine"]]
       (124.8ms)  commit transaction
    => true
    irb(main):004:0> cat1
    => #<GlossaryCategory id: 1, name: "Redmine">
    
  • GlossaryTermの作成とデータベース保存
    irb(main):005:0> term1 = GlossaryTerm.new(name: 'issue', category: cat1)
    => #<GlossaryTerm id: nil, name: "issue", description: nil, created_at: nil, updated_at: nil, category_id: 1>
    irb(main):006:0> term1.save
       (0.2ms)  begin transaction
      SQL (12.7ms)  INSERT INTO "glossary_terms" ("name", "created_at", "updated_at", "category_id") VALUES (?, ?, ?, ?)  [["name", "issue"], ["created_at", "2018-05-06 15:59:28.072455"], ["updated_at", "2018-05-06 15:59:28.072455"], ["category_id", 1]]
       (121.1ms)  commit transaction
    => true
    irb(main):007:0> term1
    => #<GlossaryTerm id: 13, name: "issue", description: nil, created_at: "2018-05-06 06:59:28", updated_at: "2018-05-06 06:59:28", category_id: 1>
    

ルーティング設定

新規に作成したモデル(GlossaryCategory)のリソースを追記します。

  • routes.rb
     Rails.application.routes.draw do
       resources :glossary_terms     
    +  resources :glossary_categories
     end                             
    

ルーティング設定は次のようになります。

trunk_redmine(master)$ bundle exec rails routes | grep glossary
Prefix                  Verb      URI Pattern                               Controller#Action
glossary_terms          GET       /glossary_terms(.:format)                 glossary_terms#index
                        POST      /glossary_terms(.:format)                 glossary_terms#create
new_glossary_term       GET       /glossary_terms/new(.:format)             glossary_terms#new
edit_glossary_term      GET       /glossary_terms/:id/edit(.:format)        glossary_terms#edit
glossary_term           GET       /glossary_terms/:id(.:format)             glossary_terms#show
                        PATCH     /glossary_terms/:id(.:format)             glossary_terms#update
                        PUT       /glossary_terms/:id(.:format)             glossary_terms#update
                        DELETE    /glossary_terms/:id(.:format)             glossary_terms#destroy
glossary_categories     GET       /glossary_categories(.:format)            glossary_categories#index
                        POST      /glossary_categories(.:format)            glossary_categories#create
new_glossary_category   GET       /glossary_categories/new(.:format)        glossary_categories#new
edit_glossary_category  GET       /glossary_categories/:id/edit(.:format)   glossary_categories#edit
glossary_category       GET       /glossary_categories/:id(.:format)        glossary_categories#show
                        PATCH     /glossary_categories/:id(.:format)        glossary_categories#update
                        PUT       /glossary_categories/:id(.:format)        glossary_categories#update
                        DELETE    /glossary_categories/:id(.:format)        glossary_categories#destroy

カテゴリーのコントローラーとビューの作成

やることは用語のコントローラーとビューのときとほぼ一緒です。

ビューを必要とするアクションは、index、show、new、editの4つなので、この4つのアクションを指定してコントローラーを生成します。

redmine$ bundle exec rails generate redmine_plugin_controller redmine_glossary glossary_categories index show new edit
      create  plugins/redmine_glossary/app/controllers/glossary_categories_controller.rb
      create  plugins/redmine_glossary/app/helpers/glossary_categories_helper.rb
      create  plugins/redmine_glossary/test/functional/glossary_categories_controller_test.rb
      create  plugins/redmine_glossary/app/views/glossary_categories/index.html.erb
      create  plugins/redmine_glossary/app/views/glossary_categories/show.html.erb
      create  plugins/redmine_glossary/app/views/glossary_categories/new.html.erb
      create  plugins/redmine_glossary/app/views/glossary_categories/edit.html.erb
redmine$

生成された雛形のコントローラーに、用語コントローラーとほぼ同じ実装を記述します。

  • glossary_categories_controller.rb
    class GlossaryCategoriesController < ApplicationController
    
      before_action :find_category_from_id, only: [:show, :edit, :update, :destroy]
    
      def index
        @categories = GlossaryCategory.all
      end
    
      def show
      end
    
      def new
        @category = GlossaryCategory.new
      end
    
      def edit
      end
    
      def create
        category = GlossaryCategory.new(glossary_category_params)
        if category.save
          flash[:notice] = l(:notice_successful_create)
          redirect_to category
        end
      end
    
      def update
        @category.attributes = glossary_category_params
        if @category.save
          flash[:notice] = l(:notice_successful_update)
          redirect_to @category
        end
      rescue ActiveRecord::StaleObjectError
        flash.now[:error] = l(:notice_locking_conflict)
      end
    
      def destroy
        @category.destroy
        redirect_to glossary_categories_path
      end
    
      # Find the category whose id is the :id parameter
      def find_category_from_id
        @category = GlossaryCategory.find(params[:id])
      rescue ActiveRecord::RecordNotFound
        render_404
      end
    
      private
    
      def glossary_category_params
        params.require(:glossary_category).permit(
          :name
        )
      end
    end
    

カテゴリの各ビュー(index, show, new, edit)にも用語のビューとほぼ同じ実装を記述します。

  • index.html.erb
    <h2><%=l :label_glossary_categories %></h2>
    
    <div class="contextual">
      <%= link_to l(:label_glossary_category_new), new_glossary_category_path, class: 'icon icon-add' %>
    </div>
    
    <table class="list">
      <thead>
        <tr>
          <th>#</th>
          <th><%=l :field_name %></th>
        </tr>
      </thead>
      <tbody>
        <% @categories.each do |category| %>
          <tr>
        <td class="id"><%= category.id %></td>
        <td class="name"><%= link_to category.name, category %></td>
          </tr>
        <% end %>
      </tbody>
    </table>
    
  • show.html.erb
    <div class="contextual">
      <%= link_to l(:button_edit), edit_glossary_category_path, class: 'icon icon-edit' %>
      <%= link_to l(:button_delete), glossary_category_path, method: :delete,
      data: {confirm: l(:text_are_you_sure)}, class: 'icon icon-del' %>
    </div>
    
    <h2><%=l :label_glossary_category %> #<%=@category.id %></h2>
    
    <h3><%= @category.name %></h3>
    
  • new.html.erb
    <h2><%=l :label_glossary_category_new %></h2>
    
    <%= labelled_form_for :glossary_category, @category,
    url: glossary_categories_path do |f| %>
      <%= render partial: 'glossary_categories/form', locals: {form: f} %>
      <%= f.submit l(:button_create) %>
    <% end %>
    
  • _form.html.erb
    <div class="box">
      <p><%= form.text_field :name, size: 60, required: true %></p>
    </div>
    
  • edit.html.erb
    <h2><%=l :label_glossary_category %> $<%= @category.id %></h2>
    
    <%= labelled_form_for :glossary_category, @category,
    url: glossary_category_path do |f| %>
      <%= render partial: 'glossary_categories/form', locals: {form: f} %>
      <%= f.submit l(:button_edit) %>
    <% end %>
    
国際化リソースに、ビューで使用したリソースを追加します。
  • en.yml
    -  label_glossary_term_new: "New glossary term" 
    +  label_glossary_term_new: "New glossary term" 
    +  label_glossary_categories: "Glossary categories" 
    +  label_glossary_category: "Glossary category" 
    +  label_glossary_category_new: "New glossary category" 
    

用語のコントローラーとビューの修正

用語をカテゴリと関連付けしたことにより、用語の属性にカテゴリが指定できるようになるので、その処理を追加します。

用語の一覧表示を修正

まず、一覧表示の列にカテゴリを追加します。

  • app/views/glossary_terms/index.html.erb(抜粋)
    +       <td class="roles">
    +         <%= term.category.try!(:name) %>
    +       </td>
    

用語にカテゴリ指定は必須ではないので、上述コードではcategoryがnilのことがあります。nilのときに.nameを呼ぶと実行時エラーundefined method `name' for nil:NilClassが出ます。
そこで、Railsの機能にあるtryを使って、nilでないときにメソッドを呼び、nilのときはメソッドを呼ばずにnilを返すようにします。

  • term.category.try!(:name)
    categoryがnilでなければ、nameメソッドを呼びます。categoryがnilならnameメソッドは呼び出さずにnilを返します。
  • tryの後ろに!を付けないと、nilのときだけでなく、nil以外のオブジェクトでnameメソッドがないときにエラーにならずnilを返してしまいます。

一覧表示に、id表示列も追加し、少し改善します。差分は次となります。

 <table class="list">
   <thead>
     <tr>
+      <th>#</th>
       <th><%=l :field_name %></th>
+      <th><%=l :field_category %></th>
       <th><%=l :field_description %></th>
     </tr>
   </thead>
   <tbody>
     <% @glossary_terms.each do |term| %>
       <tr>
+       <td class="id">
+         <%= term.id %>
+       </td>
        <td class="name">
          <%= link_to term.name, term %>
        </td>
+       <td class="roles">
+         <%= term.category.try!(:name) %>
+       </td>
        <td class="description">
          <%= term.description %>
        </td>

  • カテゴリ列の列名表示では、Redmine本体の標準機能でチケットの属性にカテゴリがあるのでそのキーを使っています。
  • カテゴリ列の値を表示する<td>タグでCSS用のクラスはずばりcategoryはなかったので似たようなものを探し、roleを使っています。

修正後の用語一覧表示画面を次に示します。

glossary_term_index-5.png (用語の一覧表示にカテゴリ列を追加)

用語の詳細表示を修正

用語の詳細表示にカテゴリを追加します。

  • app/views/glossary_terms/show.html.erb
     <table>
       <tr>
    +    <th><%=l :field_category %></th>
    +    <td><%= @term.category.try!(:name) %>
    

修正後の用語詳細表示の画面を次に示します。

glossary_terms_show-3.png (用語の詳細表示にカテゴリ欄を追加)

用語の新規・編集画面のフォームを修正
  • app/views/glossary_terms/_form.html.erb
     <div class="box tabular">
       <p><%= form.text_field :name, size: 80, required: true %></p>
    +  <p><%= form.select :category_id, GlossaryCategory.pluck(:name, :id), include_blank: true %>
       <p><%= form.text_area :description, size: "80x10", required: false %></p>
    

データベースのテーブルから選択肢を拾って選択する入力(選択)フィールドを、select で出します。
最初の引数にカラム名、第2引数に選択肢となる配列またはハッシュ、第3引数はオプション指定で、include_blankは、選択肢の先頭を空白(データはnil)にするかどうかの指定です。
第2引数の選択肢は、配列の場合、[表示名, カラムに格納する値]のペアの配列となります。モデルクラスからplunkで選択肢の表示に使う属性:nameとカラムに格納する値:idを取り出し配列にします。

フォームで作成・変更する属性にカテゴリ(category_id)が増えたので、コントローラーのストロングパラメーターを更新します。

  • app/controllers/glossary_terms_controller.rb
       def glossary_term_params
         params.require(:glossary_term).permit(
    -      :name, :description
    +      :name, :description, :category_id
         )
    

新規編集画面は次となります。

glossary_term_new-5.png (用語の新規作成にカテゴリ欄を追加(ラベル表示修正後))

既存の用語の編集画面は次となります。

glossary_term_edit-2.png (用語の編集にカテゴリ欄を追加(ラベル表示修正後))

  • 外部キーのテーブルを選択肢にする入力フォームにはselectとは別にcollection_selectがあります。
    <p><%= form.collection_select :category_id, GlossaryCategory.all, :id, :name, include_blank: true %>

    しかし、Redmineのlabelled_form_forでcollection_selectを使うと、入力欄の左側にラベルが表示されない問題が生じています。

glossary_term_new-4.png (用語の新規作成にカテゴリ欄を追加)

glossary_term_edit-1.png (用語の編集にカテゴリ欄を追加)


小さな修正・改善

flashメッセージをリダイレクト後表示するなら一行で記述できる
     if category.save
-      flash[:notice] = l(:notice_successful_create)
-      redirect_to category
+      redirect_to category, notice: l(:notice_successful_create)
     end

redirect_toのオプションを指定時、ハッシュでnotice:をキーにメッセージを入れて渡すと、リダイレクト後にflashメッセージを表示します。
上のコードの修正のように書き換えが可能です。


フェーズ5の実装完了とフェーズ6へ

フェーズ5では、用語をグルーピングするカテゴリモデルを導入し、カテゴリのCRUD操作をできるようにしました。フェーズ5で実施したことを次に簡単にまとめます。

  • カテゴリモデル(GlossaryCategory)を新規作成し、用語モデル(GlossaryTerm)と1:多の関係(belongs_toとhas_many)となるよう関係を構築
  • 用語モデルの変更に対応するデータベースのスキーマ変更スクリプト作成(003_add_category_to_glossary_terms.rb)
  • ルーティング設定(routes.rb)にカテゴリを追加
  • カテゴリのコントローラー(glossary_categories_controller.rb)を作成
  • カテゴリーのビューを作成(index.html.erb, show.html.erb, new.html.erb, edit.html.erb, _form.html.erb)
  • 国際化対応に追加
  • 用語の一覧表示、詳細表示にカテゴリの列表示を追加
  • 用語の編集フォームにカテゴリの選択リストを追加

これで、2つのモデル(用語、カテゴリ)からなる用語集の最低限のCRUDができるようになりました。フェーズ6では、用語集をプロジェクト単位で扱えるようにします。


フェーズ6)プロジェクトごとに用語集を分ける

これまでの実装では、Redmineインスタンスの全体で一つの用語集を管理していました。チケットの様にプロジェクト毎に分けて別々に管理できるようにします。

フェーズ6の概略

用語およびカテゴリをプロジェクトに紐づけます。用語およびカテゴリのモデルにはプロジェクトへの関連(belongs_to)を追加します。一覧表示ではプロジェクトに紐づいた用語とカテゴリを対象にします。また、プロジェクトのメニューに用語集を追加します。プロジェクトに紐づけることでURLパスが変わるので、ルーティング設定を変更します。

No クラス(役割) ファイル 内容
1 GlossaryTerm glossary_term.rb プロジェクトに関連付け
2 GlossaryCategory glossary_category.rb
3 マイグレーション 004_add_project_to_terms_and_categories.rb テーブルにprojectへの外部キーカラムを追加
4 GlossaryTermsController glossary_terms_controller.rb projectの扱いを追加
5 GlossaryCategoriesController glossary_categories_controller.rb
6 ルーティング設定 routes.rb URLパスにプロジェクトを追加
7 プラグイン設定 init.rb プロジェクトメニューの設定追加
  • プロジェクトのモデルにhas_manyで用語やカテゴリへの関連も追加したいところですが、Redmine本体に手を入れるのは避けたいところです。また、プラグイン側にパッチを用意しRedmine本体のプロジェクトモデルにhas_manyや関連するメソッドを外部から追加する方法もありますが、それは今後必要に迫られたときに考えることにします。

モデルクラスにプロジェクトへの関連を追加

モデルのGlossaryTermGlossaryCategoryクラスにProjectクラスへの関連belongs_toを追加します。

  • glossary_term.rb
     class GlossaryTerm < ActiveRecord::Base
       belongs_to :category, class_name: 'GlossaryCategory', foreign_key: 'category_id'
    +  belongs_to :project
     end
    
  • glossary_categories.rb
     class GlossaryCategory < ActiveRecord::Base
       has_many :terms, class_name: 'GlossaryTerm', foreign_key: 'category_id'
    +  belongs_to :project
     end
    

次に、各モデルに対応するデータベースのテーブルにbelongs_toで指定したモデルのテーブルの外部キー用カラムを追加するマイグレーションスクリプトを作成します。Railsのマイグレーション生成コマンドで、マイグレーション名、カラムと型を指定します。外部キーの場合、カラム名にはモデル名(小文字)と型にbelongs_toを指定します。

redmine$ bundle exec rails generate migration AddProjectToTermsAndCategories project:belongs_to
      invoke  active_record
      create    db/migrate/20180511125114_add_project_to_terms_and_categories.rb
redmine$

生成される場所はRedmine全体のdb/migrate下なので、これをプラグイン下のdb/migrateに移動します。その際、ファイル名先頭の年月日時分秒をそのプラグインのdb/migrate下のマイグレーションファイルの連番の一番大きなものに1を足した3桁ゼロサプレスなしの名前に変更します。

redmine$ mv db/migrate/20180511125114_add_project_to_terms_and_categories.rb plugin/redmine_glossary/db/migrate/004_add_project_to_terms_and_categories.rb

生成されたマイグレーションファイルの内容は次の通り。

  • 004_add_project_to_terms_and_categories.rb
    
    class AddProjectToTermsAndCategories < ActiveRecord::Migration[5.1]
      def change
        add_reference :terms_and_categories, :project, foreign_key: true
      end
    end
    

なんかちょっと違うコードになってしまいましたので、手で修正します。

class AddProjectToTermsAndCategories < ActiveRecord::Migration[5.1]
  def change
    add_reference :glossary_terms, :project, foreign_key: true
    add_reference :glossary_categories, :project, foreign_key: true
  end
end

プラグインのマイグレーションを実行します。

$ bundle exec rails redmine:plugins:migrate
Migrating redmine_glossary (Redmine Glossary plugin)...
== 4 AddProjectToTermsAndCategories: migrating ================================
-- add_reference(:glossary_terms, :project, {:foreign_key=>true})
   -> 0.0113s
-- add_reference(:glossary_categories, :project, {:foreign_key=>true})
   -> 0.0015s
== 4 AddProjectToTermsAndCategories: migrated (0.0147s) =======================
$

マイグレーション実行後のデータベースのスキーマを確認します。
まず、glossary_termsテーブルから見ます。

$ sqlite3 db/redmine.sqlite3
SQLite version 3.8.10.2 2015-05-20 18:17:19
Enter ".help" for usage hints.
sqlite> .schema glossary_terms
CREATE TABLE "glossary_terms" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL, "description" text, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL, "category_id" integer, "project_id" integer);
CREATE INDEX "index_glossary_terms_on_category_id" ON "glossary_terms" ("category_id");
CREATE INDEX "index_glossary_terms_on_project_id" ON "glossary_terms" ("project_id");

  • project_idのカラムが生成されています。

続いて、glossary_categoriesテーブルを見ます。

sqlite> .schema glossary_terms
CREATE TABLE "glossary_terms" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL, "description" text, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL, "category_id" integer, "project_id" integer);
CREATE INDEX "index_glossary_terms_on_category_id" ON "glossary_terms" ("category_id");
CREATE INDEX "index_glossary_terms_on_project_id" ON "glossary_terms" ("project_id");
sqlite> .schema glossary_categories
CREATE TABLE "glossary_categories" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar, "project_id" integer);
CREATE INDEX "index_glossary_categories_on_project_id" ON "glossary_categories" ("project_id");

  • project_idのカラムが生成されています。

GlossaryTermControllerクラスにプロジェクトの制御を追加

Redmineのプラグインでは、プロジェクトに紐づく機能を扱う場合、コントローラーでインスタンス変数@projectに現在のプロジェクトを詰めておき、ビューで@projectを参照するのが定番なようです。そこで、before_actionでインスタンス変数@projectにプロジェクトを詰める処理を記述します。

  • glossary_terms_controller.rb
    +  before_action :find_project_from_id, only: [:index, :create, :destroy]
       :(中略)
    +  # Find the project whose id is the :project_id parameter
    +  def find_project_from_id
    +    @project = Project.find(params[:project_id])
    +  rescue ActiveRecord::RecordNotFound
    +    render_404
    +  end
    

アクションの中でプロジェクトを必要とするのは次の場合です。そこで、before_actionの対象をonlyで次の3つに絞っています。

  • 一覧表示(index)
    プロジェクトに属する用語だけ一覧するために使用
  • 新規作成(create)
    属性projectにプロジェクトを入れるために使用
  • 削除(destroy)
    削除後、一覧表示(index)に遷移するために使用

用語集をプロジェクト単位に設けるようにするので、一覧表示(indexアクション)でこれまですべての用語を返却していた部分を現在のプロジェクトに属する用語を返すように修正する必要があります。

今回は、Redmine本体のProjectモデル(クラス)に、GlossaryTermモデル(クラス)への関連has manyを付けていないので、project.glossary_termsのようには取り出せません。そこで、GlossaryTermからproject_idが現在のプロジェクトに一致するものだけを検索して取り出すことにします。Ruby on Rails 4からは、whereを使うのが推奨です。(findで:allと:conditionsを指定するのはRails 4で廃止)

  • glossary_terms_controller.rb
       def index
    -    @glossary_terms = GlossaryTerm.all
    +    @glossary_terms = GlossaryTerm.where(project_id: @project.id)
       end
    

また、新規に用語を作成するときに、フォーム上ではプロジェクトを選択しないので、コントローラーで現在のプロジェクトを用語インスタンスの属性としてセットする必要があります。

  • glossary_terms_controller.rb
       def create
         term = GlossaryTerm.new(glossary_term_params)
    +    term.project_id = @project.id
         if term.save
           redirect_to term, notice: l(:notice_successful_create)
         end
    

パラメーターのハッシュをセットする場合と違って、コントローラー内で属性をセットするときはストロングパラメータの記述は不要でした。

@projectにプロジェクトを詰める処理はRedmineで提供あり

インスタンス変数projectにプロジェクトを詰める処理は、コントローラーの基底クラス@ApplicationControllerでメソッドが定義済みです。

  • find_project_by_project_id

なので、before_actionでこのfind_project_by_project_idを呼べばOKで、各コントローラーで実装しなくて済みます。
なお、find_projectという似た名前のメソッドがありますが、これはパラメーターのキーがproject_idではなくidの場合に使用します。


ルーティング設定を変更

プロジェクトに紐づける前の用語集(GlossaryTerm)のURLパスは、/glossary_termsでした。プロジェクトに紐づけるとURLパスは、/projects/someproj/glossary_terms(someprojはプロジェクト識別子)となります。

そこで、ルーティング設定を変更します。

  • routes.rb
     Rails.application.routes.draw do
    -  resources :glossary_terms
    +  resources :projects, shallow: true do
    +      resources :glossary_terms
    +  end
       resources :glossary_categories
     end
    
  • shallowにtrueを指定すると、URIパターンにネストを要求するアクションが最小限で済みます。

この変更後、用語集プラグインに関するルーティング設定は次のようになります。

Prefix Verb                       URI Pattern                                            Controller#Action
   project_glossary_terms GET     /projects/:project_id/glossary_terms(.:format)         glossary_terms#index
                          POST    /projects/:project_id/glossary_terms(.:format)         glossary_terms#create
new_project_glossary_term GET     /projects/:project_id/glossary_terms/new(.:format)     glossary_terms#new
       edit_glossary_term GET     /glossary_terms/:id/edit(.:format)                     glossary_terms#edit
            glossary_term GET     /glossary_terms/:id(.:format)                          glossary_terms#show
                          PATCH   /glossary_terms/:id(.:format)                          glossary_terms#update
                          PUT     /glossary_terms/:id(.:format)                          glossary_terms#update
                          DELETE  /glossary_terms/:id(.:format)                          glossary_terms#destroy
   glossary_categories GET     /glossary_categories(.:format)                         glossary_categories#index
                          POST    /glossary_categories(.:format)                         glossary_categories#create
    new_glossary_category GET     /glossary_categories/new(.:format)                     glossary_categories#new
   edit_glossary_category GET     /glossary_categories/:id/edit(.:format)                glossary_categories#edit
        glossary_category GET     /glossary_categories/:id(.:format)                     glossary_categories#show
                          PATCH   /glossary_categories/:id(.:format)                     glossary_categories#update
                          PUT     /glossary_categories/:id(.:format)                     glossary_categories#update
                          DELETE  /glossary_categories/:id(.:format)                     glossary_categories#destroy

まず、routes.rbファイルで変更したglossary_termに関しては、PrefixとURIパターンが共に変更となっています。
次に、一覧表示(indexアクション)を要求する場合のルーティング設定がどのように変更されたのかを示します。

項目 変更前 変更後
Prefix glossary_terms project_glossary_terms
Verb GET
URI Pattern /glossary_terms(.:format) /projects/:project_id/glossary_terms(.:format)
Controller#Action glossary_terms#index
  • URIパターンを見ると、/projects/プロジェクトID/glossary_terms となっています。
ビューの変更
ビューの中で、ルーティング設定で生成されるリンク名を使用している個所があります。例えば、次です。
  • app/views/glossary_terms/index.html.erb
      <%= link_to l(:label_glossary_term_new), new_glossary_term_path, class: 'icon icon-add' %>
    

一覧表示の右上に[新しい用語]のリンクがあります。リンク先に指定しているのはルーティング設定で生成されるリンク名(Prefixに_pathを付加)のnew_glossary_term_pathです。しかしプロジェクトに紐づけるためにルーティング設定を変更した結果、リンク名もnew_project_glossary_pathに変更となっています。そこで、ビューも合わせて修正していきます。

  • app/views/glossary_terms/index.html.erb
     <div class="contextual">
    -  <%= link_to l(:label_glossary_term_new), new_glossary_term_path, class: 'icon icon-add' %>
    +  <%= link_to l(:label_glossary_term_new), new_project_glossary_term_path, class: 'icon icon-add' %>
    </div>
    
コントローラーの変更
次のように、モデルクラスのインスタンスを指定してredirect_toを記述している場合、Railsが内部でプロジェクトに紐づかないリンク名ができるらしく、エラーとなってしまました。
  • glossary_terms_controller.rb
        if term.save
          redirect_to term, notice: l(:notice_successful_create)
        end
    

そこで、リダイレクトにはリンク名を指定することにします。


プロジェクトメニューへの追加

都度URLを入力するのも大変なので、プロジェクトに紐づけたこのフェーズで、プロジェクトメニューに用語集を追加します。
プロジェクトメニューに用語集を追加するには、次の2つのステップが必要です。

  • プラグインを「プロジェクトモジュール」として登録
  • プロジェクトメニューに追加
プロジェクトモジュールとして登録

プロジェクトモジュールとして登録するには、init.rbに権限の設定を記述します。

  • init.rb
    Redmine::Plugin.register :redmine_glossary do
      :(中略)
    
      project_module :glossary do
        permission :all_glossary, glossary_terms: [:index]
      end
    end
    

プロジェクトモジュールに登録する名前は、通常プラグイン名と一緒にしますが、ここではプラグイン名の先頭の"redmine_"が冗長なのと実験を兼ねてプラグイン名とは別な名前の"glossary"で登録します。

project_module :glossary do

プロジェクトモジュールとしてプラグインを登録する際、1つpermission設定がないと認識されない模様です。そこで、permission定義を1つ記述します。本格的なアクセス制御は後のフェーズで行うので、ここでは最低限の記述をします。
パーミッション名、権限対象画面を指定するコントローラーとアクションのハッシュ、オプション指定を記述します。パーミッション名は、Redmineの[管理]メニュー > [ロールと権限]で登場する名前となります。権限対象画面の指定は、次のメニュー登録で登録したプロジェクトメニューをクリックしたときに表示する画面のコントローラーとアクションを記述します。

プロジェクトメニューの登録

プロジェクトのメニューにプラグインのメニューを追加するには、init.rbにメニュー登録を記述します。

  • init.rb
    Redmine::Plugin.register :redmine_glossary do
      :(中略)
      menu :project_menu, :glossary,
           { controller: 'glossary_terms', action: 'index' },
           caption: :glossary_title,
           param: :project_id
    

記載するブロックは、Redmine::Plugin.register のブロック直下か、さらにその中のproject_moduleのブロックとなります。
今回は、前者に置きました。

  • メニュー種類は、プロジェクトのメニューに置きたいので:project_menuを指定します。
  • メニュー名は、今回はモジュール名(:glossary)に合わせましたが、違っていてもよいようです。
  • 次のURL指定は、メニューが選択されたときに飛ぶパスとなり、Railsのurl_forメソッドの指定と同じ仕様とあります。コントローラーとアクションのハッシュを渡す事例がほとんどです。ルーティング設定で生成されるリンクパス(project_glossary_terms_path)を指定してもエラーとなりました。
  • オプションでは、プロジェクトメニューの場合、param: :project_idを指定します。また、メニューに表示される名称を指定するcaptionもあります。captionにシンボルを指定すると、国際化対応キーとしてロケールに応じた文字列に変換されます。
en.ymlとja.ymlに、キーglossary_titleと対応する文字列の定義を追加します。
  • en.yml
    +  glossary_title: Glossary
    
  • ya.yml
    +  glossary_title: 用語集
    

これで、プロジェクトメニューに"Glossary"が表示されます。

なお、権限設定はデフォルトでは全ロールでOFFなので、adminを除くログインユーザーでプラグインメニューを表示するには管理メニューから権限を追加する必要があります。


ルーティング設定を再変更

先のルーティング設定変更では、IDを持つリソースへのアクセスはプロジェクトをパスに含まないようにしていました。すると、作業中のプロジェクトで用語集一覧を表示し、その中の1つの用語の詳細を表示すると、詳細表示画面はプロジェクトの外に出てしまい、メニューがプロジェクトではなくなってしまいます。

Redmine本体のチケットの機能では、プロジェクトのチケット一覧から、どれか一つのチケットの詳細を表示しても、URLにはプロジェクトは含まれませんが、画面はプロジェクトメニューが表示されたままとなっています。
このやり方が解析できれば取り入れたいのですが、簡単には解析できなかったので、IDを持つリソースへのアクセスもすべてプロジェクトをパスに含むようにします。

  • routes.rb
     Rails.application.routes.draw do
    -  resources :projects, shallow: true do
    +  resources :projects do
           resources :glossary_terms
       end
    

変更後のルーティング設定(glossary_terms)は次のようになります。

Prefix Verb                         URI Pattern                                               Controller#Action
project_glossary_terms      GET     /projects/:project_id/glossary_terms(.:format)            glossary_terms#index
                            POST    /projects/:project_id/glossary_terms(.:format)            glossary_terms#create
new_project_glossary_term   GET     /projects/:project_id/glossary_terms/new(.:format)        glossary_terms#new
edit_project_glossary_term  GET     /projects/:project_id/glossary_terms/:id/edit(.:format)   glossary_terms#edit
project_glossary_term       GET     /projects/:project_id/glossary_terms/:id(.:format)        glossary_terms#show
                            PATCH   /projects/:project_id/glossary_terms/:id(.:format)        glossary_terms#update
                            PUT     /projects/:project_id/glossary_terms/:id(.:format)        glossary_terms#update
                            DELETE  /projects/:project_id/glossary_terms/:id(.:format)        glossary_terms#destroy

ルーティング設定の再変更に伴い、リンク先の指定の変更が発生します。また、GlossaryTermsControllerにおいて、IDを伴うアクションであってもプロジェクト配下となるので、before_action の find_project_from_id対象となるよう修正します。

  • glossary_terms_controller.rb(before_actionの修正)
    -  before_action :find_project_from_id, only: [:index, :create]
    +  before_action :find_project_from_id
    
  • glossary_terms_controller.rb(リンク先指定の修正)
       def index
         :(中略)
         if term.save
    -      redirect_to term, notice: l(:notice_successful_create)
    +      redirect_to [@project, term], notice: l(:notice_successful_create)
         end
       end
    
       def update
         :(中略)
         if @term.save
    -      redirect_to @term, notice: l(:notice_successful_update)
    +      redirect_to [@project, @term], notice: l(:notice_successful_update)
         end
       end
    
       def destroy
    -    project = @term.project
         @term.destroy
    -    redirect_to project.nil? ? home_path : project_glossary_terms_path(project)
    +    redirect_to project_glossary_terms_path
       end
    
  • app/views/glossary_terms/index.html.erb
            <td class="name">
    -         <%= link_to term.name, term %>
    +         <%= link_to term.name, [@project, term] %>
            </td>
    
  • app/views/glossary_terms/show.html.erb
     <div class="contextual">
    -  <%= link_to l(:button_edit), edit_glossary_term_path, class: 'icon icon-edit' %>
    -  <%= link_to l(:button_delete), glossary_term_path, method: :delete,
    +  <%= link_to l(:button_edit), edit_project_glossary_term_path, class: 'icon icon-edit' %>
    +  <%= link_to l(:button_delete), project_glossary_term_path, method: :delete,
       data: {confirm: l(:text_are_you_sure)}, class: 'icon icon-del' %>
     </div>
    
  • app/views/glossary_terms/edit.html.erb
    -<%= labelled_form_for :glossary_term, @term, url: glossary_term_path do |f| %>
    +<%= labelled_form_for :glossary_term, @term, url: project_glossary_term_path do |f| %>
    

フェーズ6の実装完了とフェーズ7へ

フェーズ6では、用語とカテゴリをプロジェクトに紐づけました。プロジェクトのメニューに用語集を追加し、プロジェクトに紐づいた用語の一覧表示をし、その先ではプロジェクトに紐づいた用語を扱います。フェーズ6で実施したことを次に簡単にまとめます。

  • モデルクラス(GlossaryTerm、GlossaryCategory)にプロジェクトへの関連(belongs_to)を追加
  • モデルの変更に伴うデータベースのスキーマの変更をするマイグレーションスクリプト(004_add_project_to_terms_and_categories.rb)を作成し実行
  • コントローラークラス(GlossaryTermsController)にプロジェクトを扱う実装を追加
  • ルーティング設定(routes.rb)を、用語がプロジェクトの配下になるようネストする記述
  • ビューとコントローラーにおいてルーティング設定を変更したことによるリンク先の変更
  • プロジェクトメニューに用語集を追加

フェーズ7では、右側にサイドバーを表示し、そこに設定、作成リンク、用語のインデックスなどを並べます。


フェーズ6a) カテゴリのプロジェクト紐づけ

フェーズ6では、用語(GlossaryTerm)をプロジェクトに紐づけました。一方、カテゴリ(GlossaryCategory)は、モデル部分はプロジェクトに紐づけるべくカラム"project_id"の追加、belongs_toでプロジェクトに紐づけはしたものの、ルーティング設定を変更しておらず、カテゴリのコントローラーとビューのプロジェクト紐づけ対応がなされていません。

そこで、カテゴリについてルーティング設定を変更し、コントローラーとビューの対応を行います。

フェーズ6aの概略

No 役割(クラス) ファイル名 内容
1 ルーティング設定 routes.rb カテゴリのリソースをプロジェクトのネストに置く
2 GlossaryTermCategory glossary_categories_controller.rb インスタンス変数プロジェクトの設定、リンク先修正
3 一覧表示 index.html.erb リンク先修正
4 詳細表示 show.html.erb
5 新規作成 new.html.erb
6 編集 edit.html.erb

ルーティング設定変更

カテゴリのリソースをプロジェクトのネストにします。

 Rails.application.routes.draw do
   resources :projects do
-      resources :glossary_terms
+    resources :glossary_terms
+    resources :glossary_categories
   end
-  resources :glossary_categories
 end

この修正でカテゴリのルーティング設定は次の通りです。

Prefix                          Verb      URI Pattern                                                    Controller#Action
project_glossary_categories     GET       /projects/:project_id/glossary_categories(.:format)            glossary_categories#index
                                POST      /projects/:project_id/glossary_categories(.:format)            glossary_categories#create
new_project_glossary_category   GET       /projects/:project_id/glossary_categories/new(.:format)        glossary_categories#new
edit_project_glossary_category  GET       /projects/:project_id/glossary_categories/:id/edit(.:format)   glossary_categories#edit
project_glossary_category       GET       /projects/:project_id/glossary_categories/:id(.:format)        glossary_categories#show
                                PATCH     /projects/:project_id/glossary_categories/:id(.:format)        glossary_categories#update
                                PUT       /projects/:project_id/glossary_categories/:id(.:format)        glossary_categories#update
                                DELETE    /projects/:project_id/glossary_categories/:id(.:format)        glossary_categories#destroy

コントローラーの修正

GlossaryCategoriesController のプロジェクト紐づけ対応を実施します。
まずは、プロジェクトIDからプロジェクト取得を行うメソッドを追加、各アクションの前に実行するようbefore_actionで設定します。

 class GlossaryCategoriesController < ApplicationController

+  before_action :find_project_from_id

+  # Find the project whose id is the :project_id parameter
+  def find_project_from_id
+    @project = Project.find(params[:project_id])
+  rescue ActiveRecord::RecordNotFound
+    render_404
+  end

一覧表示はプロジェクトに属する用語の一覧を取得する用に変更します。

   def index
-    @categories = GlossaryCategory.all
+    @categories = GlossaryCategory.where(project_id: @project_id)
   end

カテゴリの新規作成時、プロジェクトをモデルインスタンスに代入します。

   def create
     category = GlossaryCategory.new(glossary_category_params)
+    category.project = @project
     if category.save

ルーティング設定の変更に伴うリンク先パスの変更をします。

   def create
     :(中略)
-      redirect_to @category, notice: l(:notice_successful_update)
+      redirect_to [@project, @category], notice: l(:notice_successful_update)

   def update
     :(中略)
-      redirect_to @category, notice: l(:notice_successful_update)
+      redirect_to [@project, @category], notice: l(:notice_successful_update)

   def destroy
     @category.destroy
-    redirect_to glossary_categories_path
+    redirect_to project_glossary_categories_path


ビューの修正

ルーティング設定の変更に応じて、リンク先パスの修正をします。

  • app/views/glossary_categories/index.html.erb
     <div class="contextual">
    -  <%= link_to l(:label_glossary_category_new), new_glossary_category_path, class: 'icon icon-add' %>
    +  <%= link_to l(:label_glossary_category_new), new_project_glossary_category_path, class: 'icon icon-add' %>
     </div>
      :(中略)
           <tr>
            <td class="id"><%= category.id %></td>
    -       <td class="name"><%= link_to category.name, category %></td>
    +       <td class="name"><%= link_to category.name, [@project, category] %></td>
           </tr>
    
  • app/views/glossary_categories/show.html.erb
     <div class="contextual">
    -  <%= link_to l(:button_edit), edit_glossary_category_path, class: 'icon icon-edit' %>
    -  <%= link_to l(:button_delete), glossary_category_path, method: :delete,
    +  <%= link_to l(:button_edit), edit_project_glossary_category_path, class: 'icon icon-edit' %>
    +  <%= link_to l(:button_delete), project_glossary_category_path, method: :delete,
       data: {confirm: l(:text_are_you_sure)}, class: 'icon icon-del' %>
     </div>
    
  • app/views/glossary_categories/new.html.erb
     <%= labelled_form_for :glossary_category, @category,
    -url: glossary_categories_path do |f| %>
    +url: project_glossary_categories_path do |f| %>
    
  • app/views/glossary_categories/edit.html.erb
     <%= labelled_form_for :glossary_category, @category,
    -url: glossary_category_path do |f| %>
    +url: project_glossary_category_path do |f| %>
    

フェーズ7) 右サイドバー表示

用語集画面において、右側にサイドバーを表示し、そこに表示制御、用語の作成やカテゴリの作成へのリンク、索引などを載せます。

フェーズ7の概略

サイドバーを表示するには、ビューにおいて、content_forで:sidebarを指定して呼びます。

<% content_for :sidebar do %>
  <div></div>
<% end %>

サイドバーには、最低限の機能として、次のリンクを持たせます。

  • 新しい用語の作成
  • 新しいカテゴリの作成
  • カテゴリ一覧の表示
  • 索引(A, B, C, ... で始まる用語一覧へのリンク)

これを、各画面共通で呼び出せるようにします。

索引で指定した先頭文字を持つ用語の一覧を検索して表示します。

No 役割(クラス) ファイル名 内容
1 サイドバービュー _sidebar.html.erb サイドバーに表示する
2 用語一覧のビュー app/views/glossary_terms/index.html.erb サイドバービューを呼び出すよう修正
3 用語詳細のビュー app/views/glossary_terms/show.html.erb
4 カテゴリ一覧のビュー app/views/glossary_categories/index.html.erb
5 カテゴリ詳細のビュー app/views/glossary_categories/show.html.erb
6 GlossaryTerm glossary_term.rb 先頭文字の用語の検索(scope)追加
7 GlossaryTermsController glossary_terms_controller.rb 索引で指定した先頭文字の用語一覧を表示する追加

サイドバービューの作成(見出し)

サイドバーは、一覧表示、詳細表示、といった各画面で同じものを表示します。そこで、共通で利用できるよう部分ビューとして作成します。
まずは、見出しを並べます。<h3>がよいようです。

<% content_for :sidebar do %>
  <h3><%=l :label_view %></h3>

  <h3><%=l :label_glossary_term %></h3>

  <h3><%=l :label_glossary_category %></h3>

  <h3><%=l :label_glossary_index %></h3>
<% end %>

content_forで:sidebarを指定すると、サイドバー表示ブロックが構成できます。この中に、サイドバーに表示するパーツを配置します。
まず、用語の一覧表示(app/views/glossary_terms/index.html.erb)に、サイドバーを表示するための呼び出しを追加します。

 <div class="contextual">
   <%= link_to l(:label_glossary_term_new), new_project_glossary_term_path, class: 'icon icon-add' %>
 </div>

+<%= render partial: 'sidebar' %>
+
 <table class="list">

このサイドバー表示は次のようになります。

sidebar-2.png (サイドバー表示(見出しのみ))

サイドバービューの作成(新規・一覧リンク追加)

次のリンクを追加します。

  • 新しい用語の作成
  • 新しいカテゴリの作成
  • カテゴリ一覧の表示
   <h3><%=l :label_glossary_term %></h3>
+  <p><%= link_to l(:label_glossary_term_new), new_project_glossary_term_path,
+     class: 'icon icon-add' %></p>

   <h3><%=l :label_glossary_category %></h3>
+  <p><%= link_to l(:label_glossary_category_new),
+     new_project_glossary_category_path, class: 'icon icon-add' %></p>
+  <p><%= link_to l(:label_glossary_categories),
+     project_glossary_categories_path %></p>

既に新しい用語の作成はフェーズ4で用語一覧表示の右上に配置しているので、それと同じ記述をしています。

新しいカテゴリの作成は、表示文字列とリンク先が異なる以外は新しい用語と一緒です。表示文字列は国際化対応のキーで指定、対応する文字列をen.ymlやja.ymlに記述しておきます。リンク先は、ルーティング設定のnew_project_glossary_categoryに_pathを付けて指定します。

カテゴリ一覧へのリンクは、ルーティング設定のproject_glossary_categoriesに_pathを付けて指定します。

ここまでの記述で表示されるサイドバー画面は次です。

sidebar-3.png (サイドバー表示(見出しと作成・一覧リンク))


サイドバービューの作成(索引追加)

続いて、索引を作ります。AからZまでを並べ、A、B、・・・ をぞれぞれAで始まる用語の一覧表示、Bで始まる用語の一覧表示、とリンクを作っていきます。
リンクは、http://localhost/projects/my-proj/glossary_terms?index=AのようにURLのパスではなくパラメーターとして渡します。

ビューでは次の様にlink_toのオプション指定します。

<%= link_to ch, project_glossary_terms_path(index: ch) %>

AからZまでの並びは、国際化ロケールのen.ymlに記述しておきます。

  index_en: |
      A B C D E F
      G H I J K L
      M N O P Q R
      S T U V W X
      Y Z

改行を含めるため、キー(index_en)の次にパイプ記号(|)を指定します。
このキーで改行込みの文字列('A'~'Z')を読み込み、各文字に検索リンクを付けて表示します。

   <h3><%=l :label_glossary_index %></h3>
+  <table>
+    <% l(:index_en).each_line do |line| %>
+      <tr>
+       <% line.split(" ").each do |ch| %>
+         <td><%= link_to ch, project_glossary_terms_path(index: ch) %></td>
+       <% end %>
+      </tr>
+  <% end %>
+  </table>

sidebar-4.png (サイドバー表示(英語索引追加))


モデルの修正(曖昧検索LIKE)

指定した先頭文字から始まる用語の一覧を検索する機能を用語モデル(GlossaryTerm)に持たせます。

SQL文では、LIKE句を使った検索で実現できますが、RailsのActiveRecordには検索条件にlikeを直接使用するメソッドが提供されていません。そこで、whereメソッドの条件でLIKEを使います。

その際、クライアントからのパラメーターをそのままSQLに放り込むのは危険なので(SQLインジェクション)、プレースホルダーを使って指定します。さらに、パラメーターの文字列の中に'%'や'_'および'\'といった文字がある場合にエラーとならないようエスケープが必要です。Rails 4.2では、モデルクラスでsanitize_sql_likeが提供されました。ただし、protectedなのでモデルクラス内でしか使えないので、先頭文字を指定しての検索はモデルクラス側に実装します。

モデルクラスにはメソッドではなく、scopeで検索を実装します。scopeは、モデルクラスで共通するクエリをメソッドのように呼び出す仕組みです。

  • glossary_term.rb
     class GlossaryTerm < ActiveRecord::Base
       belongs_to :category, class_name: 'GlossaryCategory', foreign_key: 'category_id'
       belongs_to :project
    +
    +  scope :search_by_name, -> (keyword) {
    +    where 'name like ?', "#{sanitize_sql_like(keyword)}%" 
    +  }
    +
     end
    

scopeの実装は、ラムダ式で定義し渡します。


コントローラーの修正(索引からのリンク対応)

まずは、一覧表示(indexアクション)にパラメーター付きで索引からのリンクが張られます。
パラメーターなしでindexアクションが呼ばれたときは従来通り、プロジェクトに属する用語の一覧を取得します。
パラメーターありでindexアクションが呼ばれたときは、先にモデルに追加したscope(@search_by_name)を呼び、パラメーターで指定された文字で始まる用語の一覧を取得します。

  • app/controllers/glossary_terms_controller.rb
       def index
         @glossary_terms = GlossaryTerm.where(project_id: @project.id)
    +    @glossary_terms = @glossary_terms.search_by_name(params[:index]) unless params[:index].nil?
       end
    

いったん、プロジェクトで絞り込んだあと、indexアクションに索引のパラメーターが指定されていた場合に、さらに用語の名前で絞り込みます。
whereはActiveRecord::Relationを返し、メソッドチェーンで複数の条件を絞り込んでいくことができます。SQLの実行は結果を取り出すまで保留され、最後にSQLに組み立てられます。

別な記述方法

べたにif式で書くと次のコードになります。

  def index
    if params[:index].nil?
      @glossary_terms = GlossaryTerm.where(project_id: @project.id)
    else
      @glossary_terms = GlossaryTerm.where(project_id: @project.id).search_by_name(params[:index])
    end
  end

サイドバー表示の追加

用語の詳細画面、カテゴリの一覧画面、カテゴリの詳細画面にサイドバー表示を追加します。

  • app/views/glossary_terms/show.html.erb
     </div>
    
    +<%= render partial: 'sidebar' %>
    +
    
  • app/views/glossary_categories/index.html.erb
     </div>
    
    +<%= render partial: 'glossary_terms/sidebar' %>
    +
     <table class="list">
    

カテゴリのビューは、用語のビューとは別ディレクトリにあるので、用語のビューの共通画面(_sidebar.html.erb)を参照するときはディレクトリ込みで指定しています。

  • app/views/glossary_categories/show.html.erb
     </div>
    
    +<%= render partial: 'glossary_terms/sidebar' %>
    +
     <h2><%=l :label_glossary_category %> #<%=@category.id %></h2>
    

フェーズ7の実装完了とフェーズ8へ

フェーズ7では、サイドバーの表示と、サイドバーに索引(先頭文字指定)を用意し、指定した索引を検索表示する処理を追加しました。
なお、索引については'A'~'Z'のアルファベットのみであり、日本語の「あ」~「ん」、また国際化対応上各国語の索引も用意したいところですが、それは今後の対応とします。

フェーズ8では、セキュリティ・権限の導入をします。


フェーズ8) セキュリティと権限

フェーズ8では、プラグインの権限設定をします。
用語集を参照する権限、用語集を変更する権限と2種類の権限を設け、それぞれに異なる役割(ロール)を設定できるようにします。
そして、アクションの発動、新規作成、編集などのリンクは権限がある場合に制限できるようにします。

フェーズ8の概略

No 役割(クラス) ファイル 内容
1 プラグイン情報 init.rb 権限設定
2 翻訳ファイル(英) en.yml 英語ロケール用キーと文字列
3 翻訳ファイル(日) ja.yml 日本語ロケール用キーと文字列
4 用語一覧のビュー app/views/glossary_terms/index.html.erb 権限がある場合に新規作成のリンク表示
5 用語詳細のビュー app/views/glossary_terms/show.html.erb 権限がある場合に編集・削除のリンク表示
6 サイドバービュー app/views/glossary_terms/_sidebar.html.erb 権限がある場合に新規作成のリンク表示
7 カテゴリ一覧のビュー app/views/glossary_categories/index.html.erb 権限がある場合に新規作成のリンク表示
8 カテゴリ詳細のビュー app/views/glossary_categories/show.html.erb 権限がある場合に編集・削除のリンク表示
9 GlossaryTermsController glossary_terms_controller.rb アクションを実行する前に権限チェック
10 GlossaryCategoriesController glossary_categories_controller.rb

参照する権限、変更する権限の定義

設定ファイルのproject_moduleに、参照・変更それぞれpermissionを定義します。

  • init.rb
       project_module :glossary do
    -    permission :all_glossary, glossary_terms: :index
    +    permission :view_glossary, {
    +                 glossary_terms: [:index, :show],
    +                 glossary_categories: [:index, :show]
    +               }
    +    permission :manage_glossary, {
    +                 glossary_terms: [:new, :create, :edit, :update, :destroy],
    +                 glossary_categories: [:new, :create, :edit, :update, :destroy],
    +               },
    +               require: :member
    +
       end
    

権限は、コントローラーとアクションの組み合わせで指定します。全アクションがどちらかに含まれるように記述します。用語集ではコントローラーが2つあるので、上述では2つ指定しています。
require: :memberを指定すると、プロジェクトに属するメンバーが前提となります。

権限を設定する管理者メニュー上で、プラグイン名、権限名を国際化対応するために、en.ymlとja.ymlに追記します。

  • プラグイン名
    project_moduleで使用したモジュール名(ここではglossary)の接頭辞にproject_module_を付与します。
    en.yml ja.yml
    project_module_glossary: Glossary project_module_glossary: 用語集
  • 権限名
    permissionの第1引数に指定した権限名(ここではview_glossarymanage_glossary)の接頭辞にpermission_を付与します。
    en.yml ja.yml
    permission_view_glossary: View glossary permission_view_glossary: 用語集の閲覧
    permission_manage_glossary: Manage glossary permission_manage_glossary: 用語集の管理

ここまでの設定で権限の設定画面は次の様になります。
[管理]メニュー > [ロールと権限]でロール一覧が表示されるので、一覧から[Manager]をクリックすると各プラグインの権限設定画面が表示されます。

rolesetting_glossary-1.png (権限設定で用語集プラグインの設定)


用語集の変更に関わるリンクは管理権限のあるユーザーのみ

いままでは、どのロールのユーザーであっても、用語の追加・編集・削除およびカテゴリの追加・編集・削除ができてしまいました。そこで、権限がある場合(用語集の管理権限)にのみ追加・編集・削除のリンクが表示されるように制御します。

用語一覧画面右上の新規作成リンク

用語集の管理権限を持っているユーザーのみ新規作成リンクが表示されるようにします。

  • app/views/glossary_terms/index.html.erb
     <div class="contextual">
    -  <%= link_to l(:label_glossary_term_new), new_project_glossary_term_path, class: 'icon icon-add' %>
    +  <%= link_to_if_authorized l(:label_glossary_term_new),
    +  { controller: :glossary_terms, action: :new, project_id: @project},
    +  class: 'icon icon-add' %>
     </div>
    

Redmineのヘルパー関数link_to_if_authorizedを使って、権限のあるときにのみリンクが表示されるようにします。権限のある/なしは、link_to_if_authorizedのオプションで指定するコントローラーおよびアクションとログインしているユーザーとから判断されるので、link_to_if_authorizedのオプションにはRESTfulスタイルのルーティング名に基づく文字列ではなく、ハッシュでコントローラーとアクションを渡す必要があります。この部分の変更を次に示します。

- new_project_glossary_term_path
+ { controller: :glossary_terms, action: :new, project_id: @project }

また、Redmineでは、プロジェクトに紐づけられるリソースではアクションを呼び出すHTTPリクエストでオプションパラメーターとしてproject_idをキーにプロジェクトのIDを指定するのが規約です。

リンク元(このビューを表示しているコントローラー)とリンク先のコントローラーが同一の場合、コントローラーの指定を省略しても動くようです。Redmine本体にlink_to_if_authorizedを使い、コントローラーの指定を省略しているものが2つほど存在していました。また、実験した限りではコントローラーを省略しても動作しました。

サイドバー表示の用語・カテゴリの新規作成リンク

用語集の作成、編集権限を持っているユーザーのみ用語の新規作成、カテゴリの新規作成リンクが表示されるようにします。

  • app/views/glossary_terms/_sidebar.html.erb
       <h3><%=l :label_glossary_term %></h3>
    -  <p><%= link_to l(:label_glossary_term_new), new_project_glossary_term_path,
    +  <p><%= link_to_if_authorized l(:label_glossary_term_new),
    +     { controller: :glossary_terms, action: :new, project_id: @project },
          class: 'icon icon-add' %></p>
    
       <h3><%=l :label_glossary_category %></h3>
    -  <p><%= link_to l(:label_glossary_category_new),
    -     new_project_glossary_category_path, class: 'icon icon-add' %></p>
    +  <p><%= link_to_if_authorized l(:label_glossary_category_new),
    +     { controller: :glossary_categories, action: :new, project_id: @project},
    +     class: 'icon icon-add' %></p>
    
用語詳細表示の右上にある編集、削除リンク

用語集の作成、編集権限を持っているユーザーのみ、用語の編集・削除リンクが表示されるようにします。

  • app/views/glossary_terms/show.html.erb
     <div class="contextual">
    -  <%= link_to l(:button_edit), edit_project_glossary_term_path, class: 'icon icon-edit' %>
    -  <%= link_to l(:button_delete), project_glossary_term_path, method: :delete,
    -  data: {confirm: l(:text_are_you_sure)}, class: 'icon icon-del' %>
    +  <%= link_to_if_authorized l(:button_edit),
    +  { controller: :glossary_terms, action: :edit, project_id: @project },
    +  class: 'icon icon-edit' %>
    +  <%= link_to_if_authorized l(:button_delete),
    +  { controller: :glossary_terms, action: :destroy,
    +    id: @term, project_id: @project },
    +    method: :delete, data: {confirm: l(:text_are_you_sure)}, class: 'icon icon-del' %>
     </div>
    

編集の場合は、idを省略しても今表示している用語の編集画面に遷移しましたが、削除の場合はidを指定しないと削除されませんでした。
そこで、削除の場合はオプション指定のハッシュにキーidで用語を指定しています。

カテゴリ一覧画面右上の新規作成リンク

用語一覧とほぼ一緒です。

  • app/views/glossary_categories/index.html.erb
     <div class="contextual">
    -  <%= link_to l(:label_glossary_category_new), new_project_glossary_category_path, class: 'icon icon-add' %>
    +  <%= link_to_if_authorized l(:label_glossary_category_new),
    +  { controller: :glossary_categories, action: :new, project_id: @project },
    +  class: 'icon icon-add' %>
     </div>
    
カテゴリ詳細画面右上の編集・削除リンク

用語詳細とほぼ一緒です。

  • app/views/glossary_categories/show.html.erb
     <div class="contextual">
    -  <%= link_to l(:button_edit), edit_project_glossary_category_path, class: 'icon icon-edit' %>
    -  <%= link_to l(:button_delete), project_glossary_category_path, method: :delete,
    -  data: {confirm: l(:text_are_you_sure)}, class: 'icon icon-del' %>
    +  <%= link_to_if_authorized l(:button_edit),
    +  { controller: :glossary_categories, action: :edit, project_id: @project },
    +  class: 'icon icon-edit' %>
    +  <%= link_to_if_authorized l(:button_delete),
    +  { controller: :glossary_categories, action: :destroy,
    +    id: @category, project_id: @project },
    +    method: :delete, data: {confirm: l(:text_are_you_sure)}, class: 'icon icon-del' %>
     </div>
    

アクションの実行は権限のあるユーザーのみ

リンクの表示を権限のあるユーザーだけに限定しただけでは、直接URLを入力してアクセスされた場合に権限のないユーザーからのアクセスを防ぐことができません。そこでコントローラーにおいてアクションを実行する前に権限があるかのチェックをして、権限のないアクセスを防ぐ処理を入れます。

  • glossary_terms_controller.rb
     class GlossaryTermsController < ApplicationController
    
    -  before_action :find_project_from_id
    +  before_action :find_project_from_id, :authorize 
    

アクションが実行される前に、ApplicationControllerのauthorizeメソッドを呼び権限チェックを実行します。
authorizeメソッドは、インスタンス変数@projectが定義されていることが前提のため、先に@projectを設定するメソッドを呼びます。

権限のないユーザーがURLを手入力する等でアクセスした場合、権限チェックによって権限がないと次の画面が表示されます。

authorize-1.png (権限のないユーザーがURL手入力アクセスした場合)

未ログイン状態でURLを手入力する等でアクセスした場合は、ログイン画面に飛ばされます。

カテゴリのコントローラーにも権限チェックを入れます。

  • glossary_categories_controller.rb
     class GlossaryCategoriesController < ApplicationController
    
       before_action :find_category_from_id, only: [:show, :edit, :update, :destroy]
    -  before_action :find_project_from_id
    +  before_action :find_project_from_id, :authorize
    

フェーズ8の実装完了とフェーズ9へ

参照のみと変更可と2種類の権限をinit.rbに用意し、ユーザーのロールに応じて権限を設定できるようにしました。

まず、各ビューにおいて、変更権限のないユーザーには新規作成、編集、削除のリンクを非表示にするという方法を、link_to_if_authorizedを用いて権限の制御をしました。

ただし、この方法では権限のないユーザーが直接URLを入力して新規作成や編集をすることができてしまいます。そこで、コントローラーがアクションを実行する前に権限チェックをする方法をbefore_actionでauthorizeを呼び出すことで追加しました。

フェーズ9では、用語の属性に、振り仮名、英語名、略語展開などいろいろ追加します。


フェーズ9) 用語の属性を拡充

オリジナルの用語集プラグインでは、用語の属性として、用語、英語名、ふりがな、略語の展開名称、データ型、コーディング用名称例、カテゴリ、説明があります。フェーズ8までは、そのうち用語(名前)、説明、カテゴリの3つだけを扱ってきました。
フェーズ9では、用語の属性をオリジナルに近づけます。

フェーズ9の概略

No 役割(クラス名) ファイル名 内容
1 GlossaryTerm glossary_term.rb 属性の追加
2 マイグレーション 005_add_columns_to_glossary_terms.rb 属性追加に伴うスキーマ変更
3 用語作成・編集 _form.html.erb 属性の追加
4 翻訳ファイル(英) en.yml 属性の表示名追加
5 翻訳ファイル(日) ja.yml 属性の表示名追加
6 GlossaryTermsController glossary_terms_controller.rb ストロングパラメーターに属性追加
7 用語詳細表示 show.html.erb 属性の追加

データベースのスキーマの変更

glossary_termsテーブルに次のカラムを追加します。

属性の名称 カラム名 カラムの型
英語名 name_en string
ふりがな rubi string
略語の展開名称 abbr_whole string
データ型 datatype string
コーディング用名称例 codename string

マイグレーションスクリプトは現在004まで作っているので、005で開始します。

redmine_glossary$ ls db/migrate/
001_create_glossary_terms.rb           003_add_category_to_glossary_terms.rb
002_create_glossary_categories.rb      004_add_project_to_terms_and_categories.rb

  • 005_add_columns_to_glossary_terms.rb
    class AddColumnsToGlossaryTerms < ActiveRecord::Migration[5.1]
      def change
        add_column :glossary_terms, :name_en, :string, default: ''
        add_column :glossary_terms, :rubi, :string, default: ''
        add_column :glossary_terms, :abbr_whole, :string, default: ''
        add_column :glossary_terms, :datatype, :string, default: ''
        add_column :glossary_terms, :codename, :string, default: ''
      end
    end
    

マイグレーションを実行します。

redmine$ bundle exec rails redmine:plugins:migrate
Migrating redmine_glossary (Redmine Glossary plugin)...
== 5 AddColumnsToGlossaryTerms: migrating =====================================
-- add_column(:glossary_terms, :name_en, :string, {:default=>""})
   -> 0.0048s
-- add_column(:glossary_terms, :rubi, :string, {:default=>""})
   -> 0.0007s
-- add_column(:glossary_terms, :abbr_whole, :string, {:default=>""})
   -> 0.0010s
-- add_column(:glossary_terms, :datatype, :string, {:default=>""})
   -> 0.0007s
-- add_column(:glossary_terms, :codename, :string, {:default=>""})
   -> 0.0010s
== 5 AddColumnsToGlossaryTerms: migrated (0.0118s) ============================

データベースのテーブルglossary_termsのスキーマを確認します。

sqlite> .schema glossary_terms
CREATE TABLE "glossary_terms" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar NOT NULL, "description" text, "created_at" datetime NOT NULL, "updated_at" datetime NOT NULL, "category_id" integer, "project_id" integer, "name_en" varchar DEFAULT '', "rubi" varchar DEFAULT '', "abbr_whole" varchar DEFAULT '', "datatype" varchar DEFAULT '', "codename" varchar DEFAULT '');
CREATE INDEX "index_glossary_terms_on_category_id" ON "glossary_terms" ("category_id");
CREATE INDEX "index_glossary_terms_on_project_id" ON "glossary_terms" ("project_id");
sqlite>

追加したカラムが存在することが確認できました。


用語の新規作成・編集フォームに属性追加

用語モデルに追加した属性について、用語の新規作成および編集フォームの入力・編集フィールドを追加します。

  • app/views/glossary_terms/_form.html.erb
     <div class="box tabular">
       <p><%= form.text_field :name, size: 80, required: true %></p>
    +  <p><%= form.text_field :name_en, size: 80 %></p>
    +  <p><%= form.text_field :rubi, size: 80 %></p>
    +  <p><%= form.text_field :abbr_whole, size: 80 %></p>
    +  <p><%= form.text_field :datatype, size: 80 %></p>
    +  <p><%= form.text_field :codename, size: 80 %></p>
       <p><%= form.select :category_id, GlossaryCategory.pluck(:name, :id), include_blank: true %></p>
       <p><%= form.text_area :description, size: "80x10", required: false %></p>
     </div>
    

入力・編集のテキストフィールドには、カラム名と同じ名称を指定しています。
この名称の接頭辞にfield_を付けたもの(例:name_enであれば、field_name_en)が国際化対応テキストのキーとなります。

  • en.yml
    +  field_name_en: English
    +  field_abbr_whole: Whole word for Abbreviation
    +  field_datatype: Data type for coding
    +  field_codename: Abbreviation for coding
    +  field_rubi: Ruby
    
  • ja.yml
    +  field_name_en: 英語名
    +  field_abbr_whole: 略語の展開名称
    +  field_datatype: データ型
    +  field_codename: コーディング用名称例
    +  field_rubi: ふりがな
    

属性を追加し、編集用フォームに属性の入力フィールドを追加した用語の作成画面を次に示します。

ph9_form-1.png (用語属性追加対応フォーム)


コントローラーの修正

新規作成、編集アクションで追加した属性をモデルに保存するため、GlossaryTermsControllerのストロングパラメーターにクライアントから渡されモデルに格納する属性名の記述を追加します。

  • glossary_terms_controller.rb
       def glossary_term_params
         params.require(:glossary_term).permit(               
    -      :name, :description, :category_id                  
    +      :name, :description, :category_id,                 
    +      :name_en, :rubi, :abbr_whole, :datatype, :codename 
         )                                                    
       end                                                    
    

用語詳細表示の修正

追加した属性の表示をする修正を入れます。

  • app/views/glossary_terms/show.html.erb
     <table>
    +  <tr>
    +    <th><%=l :field_name_en %></th>
    +    <td><%= @term.name_en %></td>
    +  </tr>                                  
    +  <tr>                                   
    +    <th><%=l :field_rubi %></th>         
    +    <td><%= @term.rubi %></td>           
    +  </tr>                                  
    +  <tr>                                   
    +    <th><%=l :field_abbr_whole %></th>   
    +    <td><%= @term.abbr_whole %></td>     
    +  </tr>                                  
    +  <tr>                                   
    +    <th><%=l :field_datatype %></th>     
    +    <td><%= @term.datatype %></td>       
    +  </tr>                                  
    +  <tr>                                   
    +    <th><%=l :field_codename %></th>     
    +    <td><%= @term.codename %></td>       
    +  </tr>                                  
       <tr>                                   
         <th><%=l :field_category %></th>     
         <td><%= @term.category.try!(:name) %>
       </tr>                                  
    

似たような記述の繰り返しで少し面倒です。オリジナルの用語集プラグインでは、属性名を引数にラベル文字列を返すヘルパーメソッドと、属性名を引数に属性の値を返すヘルパーメソッドを用意し、html.erbの中ではループで表の各行を展開していました。ヘルパーメソッドはまだ習得していないので、後のフェーズで取り組む予定です。再構築の過程であるここでは、パッと見では分かりやすいHTMLと埋め込みRubyでべた書きしています。

属性を追加した用語詳細画面表示の例を次に示します。

glossary_term_show_9-1.png (属性を追加した詳細表示画面)

erbかヘルパーメソッドか

オリジナルの用語集プラグインでは、用語の一覧表示で用語のふりがなをHTMLのruby要素(プログラミング言語のrubyではなく)を使って表示させています。簡単にHTML(erb)で記述できるようならこのフェーズで追加したかったのですが、用語の属性ふりがなが空でなければruby要素を展開し、属性ふりがなが空ならruby要素を展開しない、といった制御構造が入り、さらにHTMLのタグがネスト(ruby要素の子要素にrp要素、rt要素を持つ)するのでHTML(erb)が見苦しくなってしまいました。

このことから、HTML(erb)では、変数の値、あるいはメソッドを呼んだ戻り値の文字列をHTMLに埋め込むこと、制御構造は集合のイテレーション程度にとどめ、それより複雑なことを実装するならヘルパーメソッドを別途作成するというのがよいと考えます。


フェーズ9の実装完了とフェーズ10へ

フェーズ9では次の実装を行いました。

  • 用語モデルに次の属性を追加
    英語名、ふりがな、略語の展開名称、データ型、コーディング用名称例
  • データベースの用語テーブルに属性に対応するカラムを追加するマイグレーションファイル作成
  • 用語の新規作成画面、編集画面へモデルに追加した属性のフィールドを追加
  • 上述で追加したフィールドのラベル名を翻訳ファイルに追加
  • 用語コントローラーへ、上述で追加したフィールドの値でモデルを更新できるようストロングパラメーターに追加
  • 用語の詳細表示へ、モデルに追加した属性を追加

フェーズ10では、一覧表示においてグループ毎に分類して表示する機能を追加します。



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