プロジェクト

全般

プロフィール

機能 #75

未完了

Redmine Glossary プラグインを一から作成する

高橋 徹 さんが約4年前に追加. ほぼ2年前に更新.

ステータス:
進行中
優先度:
通常
担当者:
カテゴリ:
Redmine
開始日:
2019/10/19
期日:
進捗率:

50%

予定工数:
(合計: 0.00時間)

説明

Redmine は、次のメジャーバージョンである 4.0.0 で、Rails 5.0 5.2対応となる予定です。
Railsのバージョンが大きく更新されることで、プラグインの追従が厳しくなると予想されます。

Redmine Glossary Pluginは、オリジナルのRedmine 1.2 対応が公開されています。
http://www.redmine.org/plugins/glossary

Redmine 1.4以降では動作しませんが、オリジナルに手を入れてGithubで公開しているリポジトリがいくつか存在します。その中から、2.3対応している次のリポジトリをベースとして、
https://github.com/chiastolite/redmine_glossary

これを自分のGithubにフォークしてRedmine 3.x対応させることとしました。
https://github.com/torutk/redmine_glossary

ただし、Redmineプラグイン開発の知識と経験が不十分なため、骨格となる機能が3.xでエラーがなく動けばいいという程度で、エラーとなった箇所を場当たり的に修正したものです。

Redmine Glossary Plugin の内部構造を理解していないので、バグ対応や今後のRedmineバージョンアップ対応が困難です。

そこで、バグ対応、新しいRedmineバージョンへの追従をできるようにするため、Redmine Glossary Plugin の作り方を理解することを目標に、今までのようなエラーになったところを泥縄式に対処するやり方ではなく、ゼロからGlossary Pluginを作ってみることにしました。

Glossary Pluginゼロから作成するリポジトリ(ブランチ)は次です。
https://github.com/torutk/redmine_glossary/tree/reconstruct

Glossary Plugin 再構築(Redmine 4.x)をステップ・バイ・ステップで記述したWikiは次です。
Redmine Glossaryプラグイン再構築


ファイル

picture720-1.png (13 KB) picture720-1.png 高橋 徹, 2018/05/02 00:40
picture497-1.png (13 KB) picture497-1.png 高橋 徹, 2018/05/02 09:36
picture497-2.png (12.9 KB) picture497-2.png 高橋 徹, 2018/05/02 09:36
create_error_forbidden-attributes-1.png (49.1 KB) create_error_forbidden-attributes-1.png サブミット時にForbiddenAttributesエラー 高橋 徹, 2018/05/05 12:24
alt_csv_import_view-1.png (38.5 KB) alt_csv_import_view-1.png オリジナルのCSVインポートビュー(サイドバー上のリンク) 高橋 徹, 2019/12/01 08:03
alt_csv_import_view-2.png (24.2 KB) alt_csv_import_view-2.png オリジナルのCSVインポートビュー(ファイル選択、カラム設定フォーム) 高橋 徹, 2019/12/01 08:03

子チケット 1 (0件未完了1件完了)

機能 #96: Redmine Glossary プラグインのスキーマの変更終了高橋 徹2019/10/19

操作

関連するチケット

関連している 機能 #40: Redmine 3.0でglossary pluginを動くようにする終了高橋 徹2015/03/21

操作
関連している サポート #39: Redmineにglossary pluginを入れる終了高橋 徹2014/12/12

操作
関連している 機能 #81: Redmine 4.0でglossary pluginを動くようにする進行中2018/04/22

操作
関連している バグ #82: Redmineで同じ添付ファイルを指すthumbnailマクロを2つ記述すると2つ目でnot foundエラーとなる終了2018/06/17

操作
関連している 機能 #83: Redmine Glossaryプラグインをリファクタリングする却下高橋 徹2018/07/16

操作
関連している 機能 #95: Redmine 4.xに対応するRedmine Glossaryプラグインを、旧バージョンのデータベースを引き継ぎ、且つコードをきれいに再構築する却下高橋 徹2019/10/19

操作

高橋 徹 さんが約4年前に更新

  • 関連している 機能 #40: Redmine 3.0でglossary pluginを動くようにする を追加

高橋 徹 さんが約4年前に更新

  • 関連している サポート #39: Redmineにglossary pluginを入れる を追加

高橋 徹 さんが約4年前に更新

  • 説明 を更新 (差分)
  • ステータス新規 から 進行中 に変更
  • 進捗率0 から 50 に変更

Fedora 26(Windows 10のHyper-V上で稼働)のホームディレクトリ下にRedmine 3.4をチェックアウトし、そこにプラグインの作成をしていきます。

3.4-stable$ cd plugins
plugins$ 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
plugins$

雛形が生成した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 termrs in a project.'
  version '0.0.1'
  requires_redmine :version_or_higher => '3.0.0'
  url 'https://github.com/torutk/redmine_glossary'
  author_url 'http://www.torutk.com'
end

この時点でいったんRedmineを起動、管理者(admin)でログインし、[管理]メニュー > [プラグイン]を見て、redmine_glossaryが一覧に表示されていれば第1段階はOKです。

高橋 徹 さんが約4年前に更新

Glossary pluginには、次の3つのモデルがあります。

  • Term, TermCategory, GlossaryStyle

MySQL(MariaDB)のテーブルスキーマを調べると次のようになっています。

  • terms テーブル
カラム名 制約 備考
id int(11) NOT NULL, AUTO_INCREMENT, PRIMARY_KEY
project_id int(11) NOT NULL, DEFAULT=0
category_id int(11) DEFAULT=NULL
author_id int(11) NOT NULL, DEFAULT=0
updater_id int(11) DEFAULT=NULL
name varchar(255) NOT NULL, DEFAULT=''
name_en varchar(255) DEFAULT='' 英語名
datatype varchar(255) DEFAULT='' データ型
codename varchar(255) DEFAULT='' コーディング用名称例
description text 説明
created_on datetime DEFAULT=NULL
updated_on datetime DEFAULT=NULL
rubi varchar(255) DEFAULT='' ふりがな
abbr_whole varchar(255) DEFAULT='' 略語の展開名称
tech_en varchar(255) DEFAULT=''
name_cn varchar(255) DEFAULT=''
name_fr varchar(255) DEFAULT=''

用語を格納するTermと、用語のカテゴリーを定義するTermCategoryをまず作成してみます。GlossaryStyleの用途は追って調べることとします。

まず、属性を指定せずにモデルを生成してみます。

plugins$ bundle exec rails generate redmine_plugin_model redmine_glossary term
      create  plugins/redmine_glossary/app/models/term.rb
      create  plugins/redmine_glossary/test/unit/term_test.rb
      create  plugins/redmine_glossary/db/migrate/001_create_terms.rb
plugins$

term.rbの中は次のようになってます。

class Term < ActiveRecord::Base
  unloadable
end
  • Redmine 4.0ではunloadableが削除

001_create_terms.rbの中は次のようになってます。

class CreateTerms < ActiveRecord::Migration
  def change
    create_table :terms do |t|
    end
  end
end

空っぽのモデルクラスとデータベースのテーブルが生成されます(データベースのテーブルは実際にpluginのマイグレーションを実行すると生成されます)。

plugins$ bundle exec rake redmine:plugins:migrate
(in /home/torutk/Documents/redmine/3.4-stable)
Migrating redmine_glossary (Redmine Glossary plugin)...
== 1 CreateTerms: migrating ===================================================
-- create_table(:terms)
   -> 0.0009s
== 1 CreateTerms: migrated (0.0011s) ==========================================

plugins$

データベースの中を見てみます。プラグイン開発環境では、sqliteをDBに使用しています。

plugins$ sqlite3 ../db/redmine.sqlite3
SQLite version 3.20.1 2017-08-24 16:21:36
Enter ".help" for usage hints.
sqlite> .table
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                            terms
email_addresses                      time_entries
enabled_modules                      tokens
enumerations                         trackers
groups_users                         user_preferences
import_items                         users
imports                              versions
issue_categories                     watchers
issue_relations                      wiki_content_versions
issue_statuses                       wiki_contents
issues                               wiki_pages
journal_details                      wiki_redirects
journals                             wikis
member_roles                         workflows
sqlite> .schema terms
CREATE TABLE IF NOT EXISTS "terms" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL);
sqlite>

ということで、空っぽのモデルクラスを生成し、マイグレーションすると、カラムidを持つテーブルが生成されます。つまり、モデルクラスの作成にidの属性指定は不要です。

マイグレーションしたプラグインの削除は次です。

plugins$ bundle exec rake redmine:plugins:migrate NAME=redmine_glossary VERSION=0
(in /home/toru/Documents/redmine/3.4-stable)
Migrating redmine_glossary (Redmine Glossary plugin)...
== 1 CreateTerms: reverting ===================================================
-- drop_table(:terms)
   -> 0.0007s
== 1 CreateTerms: reverted (0.0037s) ==========================================

さて、モデルの削除は次のとおりです。

plugins$ bundle exec rails destroy redmine_plugin_model redmine_glossary term
      remove  plugins/redmine_glossary/app/models/term.rb
      remove  plugins/redmine_glossary/test/unit/term_test.rb
      remove  plugins/redmine_glossary/db/migrate/002_create_terms.rb
plugins$

ちょっと問題! 001_create_terms.rb が生成したファイルですが、destroyでは002_create_terms.rbを削除しようとしています(存在しないファイル)。そのため、001_create_terms.rbが残ったままとなっています。とりあえず手動で削除しました。

次に、属性を指定してモデルを作成します。

plugins$ bundle exec rails generate redmine_plugin_model redmine_glossary term name:string name_en:string datatype:string codename:string description:text rubi:string abbr_whole:string created_on:datetime updated_on:datetime
      create  plugins/redmine_glossary/app/models/term.rb
      create  plugins/redmine_glossary/test/unit/term_test.rb
      create  plugins/redmine_glossary/db/migrate/001_create_terms.rb
plugins$

term.rb は次のとおり(コマンドラインで指定したフィールドに関するコードは一切入っていない)

class Term < ActiveRecord::Base
  unloadable
end

001_create_terms.rb は次の通り(コマンドラインで指定したフィールドがテーブルのカラムに追加)

class CreateTerms < ActiveRecord::Migration
  def change
    create_table :terms do |t|
      t.string :name
      t.string :name_en
      t.string :datatype
      t.string :codename
      t.text :description
      t.string :rubi
      t.string :abbr_whole
      t.datetime :created_on
      t.datetime :updated_on
    end
  end
end

カラムの制約(NOT NULLとかデフォルト値とか)は、この生成されたマイグレーションファイルに追記します。

      t.string :name, default:'', null:false

他にも、インデックスを張るindex、外部キーを指定するforeign_key、文字列型でのlimit(文字数の上限)、decimal型でのprecisionとscaleが指定できます。

外部キーの指定例の一つは次です。

      t.references :project, foreign_key: true, default: 0, null:false

外部キーはreferences型を指定(大概のRDBMSでは整数型に結び付けられる)、foreign_keyをtrueに指定します。これから生成されるカラム名は、外部キーの参照先モデル名projectに_idを付けたproject_idとなります。

別な名前を付けたいときは、add_foreign_keyという記述で指定します。

class CreateTerms < ActiveRecord::Migration
  def change
    create_table :terms do |t|
      t.references :project, foreign_key: true, index: true
      t.references :category
      t.references :author
      t.references :updater
      t.string :name, index: true
      t.string :name_en
      t.string :datatype
      t.string :codename
      t.text :description
      t.string :rubi
      t.string :abbr_whole
      t.datetime :created_on
      t.datetime :updated_on
    end
    add_foreign_key :terms, :term_categories, column: :category_id
    add_foreign_key :terms, :users, column: :author_id
    add_foreign_key :terms, :users, column: :updater_id
  end
end

このMigrationを実施すると、次のスキーマが作成されます(sqlite3の場合)。

sqlite> .schema terms
CREATE TABLE IF NOT EXISTS "terms" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "project_id" integer, "category_id" integer, "author_id" integer, "updater_id" integer, "name" varchar, "name_en" varchar, "datatype" varchar, "codename" varchar, "description" text, "rubi" varchar, "abbr_whole" varchar, "created_on" datetime, "updated_on" datetime);
CREATE INDEX "index_terms_on_project_id" ON "terms" ("project_id");
CREATE INDEX "index_terms_on_name" ON "terms" ("name");

SQLite3では外部キーが機能として用意されていないので、確認には別なRDBMSを使用する必要があります。

高橋 徹 さんが約4年前に更新

コントローラーの作成

まだTermモデルしか作成していませんが、コントローラを作っていきます。
Glossaryプラグインには次の3つのコントローラがあります。

  • GlossaryController
  • TermCategoriesController
  • GlossaryStylesController

ここでは、GlossaryControllerを作ります。

$ bundle exec rails generate redmine_plugin_controller redmine_glossary glossary index
      create  plugins/redmine_glossary/app/controllers/glossary_controller.rb
      create  plugins/redmine_glossary/app/helpers/glossary_helper.rb
      create  plugins/redmine_glossary/test/functional/glossary_controller_test.rb
      create  plugins/redmine_glossary/app/views/glossary/index.html.erb
$

glossary_controller.rb は次の内容です。

class GlossaryController < ApplicationController
  unloadable

  def index
  end
end

index.html.erb は次の内容です。

<h2>GlossaryController#index</h2>

コントローラ(glossary)にindexが定義されており、コントローラのindexに対応するビュー(views/glossary/index.html.erb)が定義されているので、ブラウザからglossaryに
アクセスすればビューが表示されるはずですが、ルート定義をしないとアクセスできません。

Routing Error
No route matches [GET] "/glossary" 

Rails.root: /home/torutk/redmine/3.4-stable

ルート定義をconfig/routes.rbに記述します。

# Plugin's routes
# See: http://guides.rubyonrails.org/routing.html
Rails.application.routes.draw do
  resources :glossary
end

resources でコントローラ名を定義すると、次のリクエストURLに対してコントローラーのメソッドが呼び出されます。

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

高橋 徹 さんがほぼ4年前に更新

  • 関連している 機能 #81: Redmine 4.0でglossary pluginを動くようにする を追加

高橋 徹 さんがほぼ4年前に更新

#75-4 では、モデルTermの属性にcreated_onとupdated_onを指定していました。
Rails 5.1のチュートリアル 6.1.1の説明では、モデルのクラスの属性にはcreated_onとupdated_onを指定せず、マイグレーションのコードの中でt.timestampsを記述するとcreated_atとupdated_atというマジックカラムがデータベースに生成されるとあります。

Ruby on Rails 5.1のチュートリアル(日本語訳)6章
https://railstutorial.jp/chapters/modeling_users?version=5.1#cha-modeling_users

また、同チュートリアルでは、モデルクラスは従来のActiveRecord::BaseではなくApplicationRecordを継承するようになっています(なおApplicationRecordはActiveRecord::Baseを継承)。

Redmine 4.0開発版のlib/generators/redmine_plugin_model/templates/model.rb.erb は次のようになっています。

class <%= @model_class %> < ActiveRecord::Base
end

なので、Rails 5の推奨にはなっていない模様。

続く調査で、ApplicationRecordは、Railsのライブラリ中には含まれておらず、モデル生成時にモデルと同じディレクトリ(app/models)に生成されるものと判明。内容は、Ruby on Rails 5のチュートリアルに記載あり。

class ApplicationRecord < ActiveRecord::Base
  self.abstract_class = true
end

高橋 徹 さんがほぼ4年前に更新

ゼロから始める用語集プラグイン作成の初期リポジトリを作成した。
手順は次に。
http://d.hatena.ne.jp/torutk/20180430

Githubの次に再構築用ブランチ(reconstruct)を作成した。
https://github.com/torutk/redmine_glossary/tree/reconstruct

まずは雛形の生成したinit.rbの説明を修正しコミット。

フェーズ1の目標は、用語集の一覧表示とし、表示内容は用語と説明の2つとする。
モデルクラスGlossaryTerm、コントローラGlossaryTermsController、そしてindexのビューまで。

テスト用スクリプトでモデル生成できればそうするし、難しければコマンドラインからモデルを生成する。

再構築前のGlossaryプラグインではモデルクラス名Termであったが、データベースのテーブルはRedmine本体と各種プラグインで共通(フラット)のため、命名で衝突回避をする。Termは衝突可能性の高い単語なのでGlossaryを接頭辞として入れたGlossaryTermにした。

モデルクラスGlossaryTermの生成

redmine_glossary$ 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_glossary$

モデルクラスGlossaryTermに作成日時、更新日時を追加するため、001_create_glossary_terms.rbに追記、またnameをnon nullにする記述を追記。

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

コントローラークラスGlossaryTermsControllerの生成

redmine_glossary$ bundle exec rails generate redmine_plugin_controller redmine_glossary GlossaryTerms index
      create  plugins/redmine_glossary/app/controllers/GlossaryTerms_controller.rb
      create  plugins/redmine_glossary/app/helpers/GlossaryTerms_helper.rb
      create  plugins/redmine_glossary/test/functional/GlossaryTerms_controller_test.rb
      create  plugins/redmine_glossary/app/views/GlossaryTerms/index.html.erb
redmine_glossary$

アクションにindexのみを指定したので、index関係のコントローラとビューが生成された。

routesの設定

URLへのアクセスからコントローラへの対応付けが必要なので記述する。

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

実行し、Webブラウザからアクセスするとエラーに

http://サーバー名:3000/glossary_terms

にアクセスするとエラーページが表示された。
Routing Error
uninitialized constant GlossaryTermsController

同ページのルート設定からGlossaryTermsControllerのルーティング設定を見ると

glossary_terms_path     GET     /glossary_terms(.:format)     

glossary_terms#index

となっている。

高橋 徹 さんが3年以上前に更新

ルーティングエラーの調査

生成されたコントローラーのファイル名が

GlossaryTerms_controller.rb

となっていたのがRailsの規約違反で、ただしくは小文字スネークケースの
glossary_terms_controller.rb

でないとクラスがロードされない。rails generateコマンドで、名前をGlossaryTermとキャメルケースで指定したのがいけなかった模様。

いったんコントローラーを削除し、再度生成する。

redmine_glossary$ bundle exec rails destroy redmine_plugin_controller redmine_glossary GlossaryTerms
      remove  plugins/redmine_glossary/app/controllers/GlossaryTerms_controller.rb
      remove  plugins/redmine_glossary/app/helpers/GlossaryTerms_helper.rb
      remove  plugins/redmine_glossary/test/functional/GlossaryTerms_controller_test.rb
redmine_glossary$

名前の指定を小文字スネークケースで再度生成する。

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
redmine_glossary$

サーバーを再起動し、

http://サーバー名:3000/glossary_terms

にアクセスすると、無事index.html.erbの内容が表示された。

高橋 徹 さんが3年以上前に更新

一覧表示を実装する。

先程までで、Webブラウザからのリクエストでコントローラーのindexが呼び出され、デフォルトの内容が表示された。次はモデルの一覧を表示する実装に入る。

まず、コントローラーのindexメソッドに用語一覧を取得しインスタンス変数に保持する実装を記述。

class GlossaryTermsController < ApplicationController

  def index
    @glossary_terms = GlossaryTerm.all
  end
end

  • モデルクラスのallを呼ぶと、データベースに存在するレコードすべてがモデルクラスのインスタンスとして取得される
  • @で始まる変数はインスタンス変数、コントローラーから呼ばれるビューで参照可能

次にindexアクションで描画されるビューに、最低限の一覧表示をする実装を記述(見栄えは気にしない)。

<h2>GlossaryTermsController#index</h2>

<table>
  <thead>
    <tr>
      <td>name</td>
      <td>description</td>
    </tr>
  </thead>
  <tbody>
    <% @glossary_terms.each do |term| %>
    <tr>
      <td>
        <%= term.name %>
      </td>
      <td>
        <%= term.description %>
      </td>
    </tr>
    <% end %>
  </tbody>
</table>
  • コントローラーのindexメソッドで用意されたインスタンス変数@glossary_termsを参照し、名前と説明をHTMLのテーブルで表示
  • CSSに関する実装は後回し

最初は、モデルクラスGlossaryTermに対応するデータベースのテーブルglossary_termsはレコードが0件なので何も表示されない。そこで、コマンドで初期データを作成する。

redmine_glossary$ bundle exec rails c
Loading development environment (Rails 5.1.6)
irb(main):001:0> term1 = GlossaryTerm.new(name: 'alfa', description: 'phonetic code of A')
irb(main):002:0> term1.save
   (0.1ms)  begin transaction
  SQL (18.1ms)  INSERT INTO "glossary_terms" ("name", "description", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "alfa"], ["description", "phonetic code of A"], ["created_at", "2018-05-01 21:14:32.617351"], ["updated_at", "2018-05-01 21:14:32.617351"]]
   (4.4ms)  commit transaction
=> true
irb(main):003:0> term2 = GlossaryTerm.new(name: 'bravo', description: 'phonetic code of B')
irb(main):004:0> term2.save
   (0.2ms)  begin transaction
  SQL (3.1ms)  INSERT INTO "glossary_terms" ("name", "description", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "bravo"], ["description", "phonetic code of B"], ["created_at", "2018-05-01 21:15:05.353711"], ["updated_at", "2018-05-01 21:15:05.353711"]]
   (3.8ms)  commit transaction
=> true
irb(main):005:0> term3 = GlossaryTerm.new(name: 'charlie', description: 'phonetic code of C')
irb(main):006:0> term3.save
   (0.2ms)  begin transaction
  SQL (2.2ms)  INSERT INTO "glossary_terms" ("name", "description", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "charlie"], ["description", "phonetic code of C"], ["created_at", "2018-05-01 21:16:24.505457"], ["updated_at", "2018-05-01 21:16:24.505457"]]
   (3.6ms)  commit transaction
=> true

3件のデータを入れた後にWebブラウザからアクセスをした

picture720-1.png

高橋 徹 さんが3年以上前に更新

すこし見栄えを設定

表の表示があまりに寂しいので、CSSの設定を調べてみた。
まずredmine本体のpublic/stylesheets/application.cssに見栄えの設定が記述されている。表関係は同ファイルの219行目から定義がある。最初の3行を抜粋すると、

/***** Tables *****/
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;}

なので、index.html.erb のテーブルタグにクラスlistを指定すると最初のCSS定義に合致するので罫線がひかれる。

--- a/app/views/glossary_terms/index.html.erb
+++ b/app/views/glossary_terms/index.html.erb
@@ -1,6 +1,6 @@
 <h2>GlossaryTermsController#index</h2>

-<table>
+<table class="list">
   <thead>

結果は次の画面となった。

picture497-1.png

見出し行の見栄え、データ要素がセンタリングされているのを左寄せにしたい、といった微調整をあまり深入りしない範囲で記述していく。

application.cssにはtable.list thのセレクタで見出し行っぽい見栄えにする定義があるので、見出しは、HTMLの要素<TH>に変更する。

   <thead>
     <tr>
-      <td>name</td>
-      <td>description</td>
+      <th>name</th>
+      <th>description</th>
     </tr>
   </thead>

application.cssにはtable.list tdのセレクタではテキストがセンタリングされるが、table.list td.name, table.list td.description, ...のセレクタでテキストが左寄せされる定義がある。これが適用されるようクラス定義を追加する。

-      <td>
+      <td class="name">
        <%= term.name %>
       </td>
-      <td>
+      <td class="description">
        <%= term.description %>
       </td>

この設定で表示した画面は次のとおり。

picture497-2.png

高橋 徹 さんが3年以上前に更新

showアクションで表示する用語の詳細表示のビュー

<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>

とすると、説明はべたなテキストとなる。Wiki記法を有効とするなら、

<h2><%=t :label_glossary_term %> #<%= @term.id %></h2>

<h3><%= @term.name %></h3>

<p><%=t :field_description %></p>
<div class="wiki">
  <%= textilizable @term, :description %>
</div>

と、Wiki記法をテキスト化する表示が可能

高橋 徹 さんが3年以上前に更新

showアクションでリクエストパラメーターのidからモデルのインスタンスを取得する処理の記述例を探した。

redmine本体の app/controllers/issues_controller.rb

  before_action :find_issue, :only => [:show, :edit, :update]

find_issueメソッドはこのファイルにはなく、検索したところ次にあり。
redmine本体の app/controllers/application_controller.rb

  # Find the issue whose id is the :id parameter
  # Raises a Unauthorized exception if the issue is not visible
  def find_issue
    # Issue.visible.find(...) can not be used to redirect user to the login form
    # if the issue actually exists but requires authentication
    @issue = Issue.find(params[:id])
    raise Unauthorized unless @issue.visible?
    @project = @issue.project
  rescue ActiveRecord::RecordNotFound
    render_404
  end

高橋 徹 さんが3年以上前に更新

newアクションで表示した新規フォームからサブミットすると
ActiveModel::ForbiddenAttributesError が発生する。

create_error_forbidden-attributes-1.png

ストロングパラメータを使うべし。

高橋 徹 さんが3年以上前に更新

2つのモデルを1対多の関係で関連付け。

  • [GlossaryCategory] has many [GlossaryTerm]
  • [GlossaryTerm] belongs to [GlossaryCategory]

GlossaryTermの一覧表示において、GlossaryTermを表示したい。

    <% @glossary_terms.each do |term| %>
      <tr>
         :(中略)
    <td class="roles">
      <%= term.category.name %>
    </td>
      </tr>
    <% end %>

term.category がnilだとエラー

undefined method `name' for nil:NilClass

term.categoryがnilでないときにのみnameを呼び、nilのときはnilを返すと空の表示となる。Railsの機能でtry!を使う。

-      <%= term.category.name %>
+      <%= term.category.try!(&:name) %>

高橋 徹 さんが3年以上前に更新

用語の新規作成/編集のフォームで属性カテゴリの選択をcollection_selectで行うと、左側のラベルが表示されない現象あり。selectで代替する。

高橋 徹 さんが3年以上前に更新

プロジェクトの配下に用語を置こうとした。
ルーティング設定で、projectのネストとしてglossary_termを指定

Rails.application.routes.draw do
  resources :projects do
      resources :glossary_terms
  end

すると、すべてのアクションのURLが、/projects/プロジェクト識別子/glossary_term/14のようにプロジェクトの下になってしまう。

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
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

チケットのように、idを指定するものはプロジェクト識別子をURLに含めると長いのでネストしない場合の/glossary_term/14のようにしたい。
調べたところ、router.rbにshallowを指定するとよい模様

Rails.application.routes.draw do
  resources :projects, shallow: true do
      resources :glossary_terms
  end
end

書き直したルーティング設定で生成されるのは次です。(glossary_termに関する箇所のみ抜粋)

   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

一覧表示、新規作成に際しては(用語のidがないので)、プロジェクトがURLに含まれる。詳細表示、編集、削除についてはIDで一意に指定されるのでプロジェクトがURLに含まれる必要はなく、shallowを指定することで含まないルーティング設定で生成される。

この変更で生じた問題点

  • 用語を新規作成または編集で作成・変更した際、リンク先のURLにプロジェクトが含まれないので、プロジェクトの外に出た状態で詳細表示される(プロジェクトメニューが消えている)。

高橋 徹 さんが3年以上前に更新

権限がある場合にのみ新規作成アイコン(リンク)を表示させたい。

<%= link_to_if_authorized l(:label_glossary_term_new), new_project_glossary_term_path, class: 'icon icon-add' %>

と記述するとエラーになってしまう。
no implicit conversion of Symbol into Integer

Redmineのlink_to_if_authorizedの定義箇所(app/helpers/application_helper.rb)を調べると、

def link_to_if_authorized(name, options = {}, html_options = nil, *parameters_for_method_reference)
  link_to(name, options, html_options, *parameters_for_method_reference)
      if authorize_for(options[:controller] || params[:controller], options[:action])
end

となっている。ここで、link_to_if_authorizedの第2引数にpathを指定すると、ハッシュではなく
options=/projects/glossary-test/glossary_terms/new のようなパス文字列が渡っている模様。そのため、options[:controller]がエラーとなる。
  • 対策(1)
    パスではなく、コントローラー、アクションのハッシュを渡す
  • 対策(2)
    link_to_if_authorizedではなく、次のチェックで判別
    <% if User.current.allowed_to?(:manage_glossary, @project) %>
      <%= link_to ... %>
    <% end %>
    

後者はネストを伴うロジックが毎度入るので微妙、、、

高橋 徹 さんが3年以上前に更新

bundle exec rails generate migrationの実行や、bundle exec rails redmine:plugins:migrateの実行でエラーが発生した。

rails aborted!                                                                                          
Child already added                                                                                     
/path/to/trunk_redmine/lib/redmine/menu_manager.rb:370:in `add_at'                                                                                         
/path/to/trunk_redmine/lib/redmine/menu_manager.rb:387:in `add'                                                                                              :

原因は、プラグインのディレクトリをコピーして別名にしてpluginsフォルダ下に並べて置いていたため

高橋 徹 さんが3年以上前に更新

権限制御の確認で問題発生

class GlossaryTermsController < ApplicationController
  before_action :authorize

ロール報告者に割り当てたユーザー、ロール開発者に割り当てたユーザーのどちらも、プロジェクトメニューの「用語集」をクリックすると403エラーページとなってしまう。
adminでも同じ・・・

原因と対処

authorizeは@projectが定義されていることが前提、よって、before_actionでprojectを設定した後にauthorizeを記述する。

高橋 徹 さんが3年以上前に更新

用語をカテゴリ毎に分類して表示する方法の模索

<% @glossary_terms.group_by(&:category).sort.each do |category, terms| %>
  :

カテゴリにnilが入っているとエラーになる。メッセージは比較に失敗したという内容(nilとは直接示されない)

comparison of Array with Array failed

高橋 徹 さんが3年以上前に更新

用語をカテゴリ毎に分類して表示する方法の模索(続)

nilがコレクションに含まれるときsortがエラーとなるので、比較方法を指定できるsort_byを使って回避するというのがググって見かけた対処。試行錯誤してちょっと汚い記述になるが一つ実現できたコードが次。

<% @glossary_terms.group_by(&:category_id).sort_by { |k,v| k.to_i }.each do |category_id, terms| %>
  <h3><%= category_id.nil? ? l(:label_not_categorized) : GlossaryCategory.find(category_id).name %></h3>

nilに対してto_iメソッドを呼ぶと0が返される(nil.to_i => 0)ので、group_byでcategory_idを指定、ハッシュのキーをcategory_id(整数)とする。sort_byで比較方法をk.to_iとする。これでnilがキーに含まれても0と評価されるのでエラーを回避できる。

ただし、category_idではカテゴリ名称が表示できないので、GlossaryCategory.find(category_id).nameとnameを取り出し直しているのが今三つな実装。

高橋 徹 さんが3年以上前に更新

用語をカテゴリ毎に分類して表示する方法の模索(続々)

カテゴリ一覧を取り出し、カテゴリがhas_manyで用語と関連しているのでカテゴリに属する用語をそこから取り出すという方法が思いつく。ただし、

  • 未分類(カテゴリがnil)の用語が拾えない

コード断片は次

<% categories = GlossaryCategory.where(project_id: @project.id).sort %>
<% categories.each do |category| %>
  <h3><%= category.name %></h3>
  :
      <% category.terms.each do |term| %>

高橋 徹 さんが3年以上前に更新

ファイル添付の機能追加で添付したファイルのダウンロードがエラー

一通り実装後、次の問題が発生している。

  • 編集でファイルを選択して添付すると、render_attachment_warning_if_needed がエラー表示、ただし添付ファイルは付加されている。
    「1個の添付ファイルが保存できませんでした。」
  • 詳細で添付ファイルをクリックすると403エラー

後者について、PUMAのログでチケット機能の添付ファイルのダウンロード(成功)と用語の添付ファイルのダウンロード(403エラー)とを比べると、

   (0.5ms)  SELECT "enabled_modules"."name" FROM "enabled_modules" WHERE "enabled_modules"."project_id" = ?  [["project_id", 1]]

このログの次が異なっている。チケット機能の方は、Member Loadのログが表示、用語集の方は、Rendering common/error.html.erb within layouts/baseのログが表示。

どうやら、enable_modulesのチェックで失敗している模様。

ログに出ているSELECT文をsqlite3コマンドから実行してみると、

sqlite> select * from enabled_modules where project_id=1;
1|1|issue_tracking                                       
6|1|wiki                                                 
11|1|glossary                                            
sqlite>                                                  

高橋 徹 さんが3年以上前に更新

添付ファイル機能追加でコントローラーの実装について

本を参考に次の記述をしたが添付されない模様

  def update
    @term.attributes = glossary_term_params
    if @term.save
      @term.save_attachments(params[:attachments])
      render_attachment_warning_if_needed(@term)
      redirect_to [@project, @term], notice: l(:notice_successful_update)
    end

順番を次に変えると添付はされる( #75-24 の問題はあるが)。

  def update
    @term.attributes = glossary_term_params
    @term.save_attachments(params[:attachments])
    render_attachment_warning_if_needed(@term)
    if @term.save
      redirect_to [@project, @term], notice: l(:notice_successful_update)
    end

ダウンロードのURL(/attachments/download/:id/:filename)は、
AttachmentsController(app/controllers/attachments_controller.rb)のdownloadメソッドを呼ぶ。メソッド内でモジュールの有効性をチェックしているロジックはぱっと見なさそうなので、before_actionで事前チェックしているどこかにあるのではと推測。

file_readable, read_authorize がそれっぽい。

  • read_authorize
      def read_authorize
        @attachment.visible? ? true : deny_access
      end
    

これはカレントユーザーをチェックしている模様。

高橋 徹 さんが3年以上前に更新

@attachment.visible? は、<redmineルート>/app/models/attachment.rb に定義されている。

  def visible?(user=User.current)
    if container_id
      container && container.attachments_visible?(user)
    else
      author == user
    end
  end

ここで、containerは何か探して路頭に迷ったが、putsでcontainerのクラス名を調べるとモデルクラス(GlossaryTerm)であった。そして、acts_as_attachable呼び出しモデルにはattachments_visible?メソッドが追加されるので、それを調べてみた。

  • <redmineルート>/lib/plugins/acts_as_attachable/lib/acts_as_attachable.rb
            def attachments_visible?(user=User.current)
              (respond_to?(:visible?) ? visible?(user) : true) &&
                user.allowed_to?(self.class.attachable_options[:view_permission], self.project)
            end
    
ここで、attachable_options[:view_permission]の値を調べると、view_glossary_termとなっていた。これは、acts_as_attachableメソッドの中で、モデルクラス名(GlossaryTerm)をキャメルケースからスネークケースに変換し、先頭にview_を付けているためである。
  • acts_as_attachableメソッドは引数で明示的にオプション指定が可能

user.allowed_to? の中では、第2引数にprojectを渡した場合、projectのallows_to?を第1引数を渡して呼び出している。
その中で、allowed_permissions.include? を呼びだしている。

user.allowed_to?(:view_glossary_term, my_project)
  ↓
my_project.allows_to?(:view_glossary_term)
  ↓
allowed_permissions.include? :view_glossary_term
  ↓
Redmine::AccessControl.modules_permissions(「my_projectのenabled_modulesに含まれる名前リスト」)

と流れていくが、ここで問題なのは、用語集のモジュール名は用語集プラグインのinit.rbで閲覧権限名を"view_glossary"と指定しているが、acts_as_attachableメソッド呼び出しの流れではモデルクラス名にview_を接頭辞とした"view_glossary_term"となっているので一致しないというのが問題。

acts_as_attachbleメソッドは引数にハッシュを取り、このハッシュが空でない場合は権限名として使われる模様。よって、権限名がモデル名と異なる場合は引数で権限名を明示的に渡す必要がある。用語集プラグイン(再構築)では次を指定する。

キー
view_permission view_glossary
edit_permission manage_glossary
delete_permission

高橋 徹 さんが3年以上前に更新

render_attachment_warning_if_needed が常にメッセージを吐く件

ApplicationControllerにメソッドが定義されている。

  # Renders a warning flash if obj has unsaved attachments
  def render_attachment_warning_if_needed(obj)
    flash[:warning] = l(:warning_attachments_not_saved, obj.unsaved_attachments.size) if obj.unsaved_attachments.present?
  end

コントローラーでrender_attachment_warning_if_neededを呼ぶ時点でunsaved_attachmentsの内容は次のようになっていた。

obj.unsaved_attachments = [#<Attachment id: nil, container_id: nil, container_type: nil, filename: "", disk_filename: "", filesize: 0, content_type: "", digest: "", downloads: 0, author_id: 18, created_on: nil, description: "", disk_directory: nil>]

これは、size=1、present?=trueとなる内容である。よって、添付ファイルが保存されたとしても、メッセージとして未保存1件とされてしまう。

用語の編集フォームで添付ファイルを付けてサブミットした時のパラメータは次

  Parameters: {"utf8"=>"✓", "authenticity_token"=>"dOSuRYmnc6j4iwXqNa4Sp6r8BrtGl7lTIxHC0/chlVTbZfncvMCDd2HpYYIKggUuySo5Fn2r8wnqK4WmimDTNQ==", "glossary_term"=>{"name"=>"朝日のア", "name_en"=>"", "rubi"=>"あさひのあ", "abbr_whole"=>"", "datatype"=>"a", "codename"=>"", "category_id"=>"4", "description"=>"日本語のフォネティックコード「あ」"}, "attachments"=>{"1"=>{"filename"=>"f022.png", "description"=>"aaa", "token"=>"38.8e4d1dd26bde5a3b4c0f79c186713c1f6922ce57a1d42dca1e9dcbaab5604f84"}, "dummy"=>{"file"=>""}}, "commit"=>"Edit", "project_id"=>"glossary-test", "id"=>"15"}

attachmentsキーの値であるハッシュは

"attachments"=>{
  "1"=>{"filename"=>"f022.png", "description"=>"aaa", "token"=>"38.8e4d1dd26bde5a3b4c0f79c186713c1f6922ce57a1d42dca1e9dcbaab5604f84"},
  "dummy"=>{"file"=>""}
}

と、dummyが付いてきている。このdummyが未保存としてカウントされているのではと推察。

ここでググってみたときに、次のブログ発見
【Redmine拡張】Redmineの添付ファイル機能を使う

フォームはmultipartをONにしておく必要あり。

の記述が・・・、

app/views/glossary_term/edit.html.erbを以下のように指定したところrender_attachment_warning_if_needed が出なくなった。

<%= labelled_form_for @term, url: project_glossary_term_path, html: {multipart: true} do |f| %>
  <%= render partial: 'glossary_terms/form', locals: {form: f} %>
  <%= f.submit l(:button_edit) %>
<% end %>

高橋 徹 さんが3年以上前に更新

  • 関連している バグ #82: Redmineで同じ添付ファイルを指すthumbnailマクロを2つ記述すると2つ目でnot foundエラーとなる を追加

高橋 徹 さんが3年以上前に更新

テストの実行でnilエラー

まず、プラグイン雛形を生成した環境でテスト用の構築はしていない状況でテストを実行するとエラーとなった。

$ bundle exec rails redmine:plugins:test
rails aborted!
NoMethodError: undefined method `[]' for nil:NilClass
/home/toru/work/redmine/trunk_glossary_dev/vendor/bundler/ruby/2.4.0/gems/activerecord-5.1.6/lib/active_record/tasks/database_tasks.rb:198:in `purge'
  :

testモードで実行していない(developmentモードかな?)ために発生している模様

RAILS_ENV=testを指定しtestモードでテストを実行

$ bundle exec rails redmine:plugins:test RAILS_ENV=test
rails aborted!
ActiveRecord::AdapterNotSpecified: 'test' database is not configured. Available: ["production", "development"]
/home/toru/work/redmine/trunk_glossary_dev/vendor/bundler/ruby/2.4.0/gems/activerecord-5.1.6/lib/active_record/connection_adapters/connection_specification.rb:246:in `resolve_symbol_connection'
  :

データベースのtestモードが構成されていないのでエラーとなっている。
config/database.ymlにtestモードのデータベース設定を追記する必要あり。

testモードのデータベースを作成し、db:migrateしてから再度テストモードを実行するとテストが実行可能となった。

高橋 徹 さんが3年以上前に更新

functionalテストでplugin_fixturesがエラー

書籍のとおり、プラグインのtest_helper.rb にRedmine::PluginFixturesLoader::included に、ActiveRecord::Fixturesの記述がある場所で
NameError: uninitialized constant ActiveRecord::Fixtures
のエラー発生。

ActiveRecord::Fixtures は、Ruby on Rails 3.2.13を最後にdeprecatedとなって削除された?

Rails 5.2 のAPIドキュメントには、ActiveRecord::FixtureSetはあるが、ActiveRecord::Fixturesは見当たらない。

次のブログでは、ActiveRecord::Fixtures ではなく、ActiveRecord::FixtureSet を使っている。
https://blog.scimpr.com/2015/05/09/redmine3%E3%81%A7%E3%81%AE%E3%83%86%E3%82%B9%E3%83%88%E3%81%82%E3%82%8C%E3%81%93%E3%82%8C%E3%80%9Cminitest/

高橋 徹 さんが3年以上前に更新

  • 関連している 機能 #83: Redmine Glossaryプラグインをリファクタリングする を追加

高橋 徹 さんが3年以上前に更新

機能テストで、assert_redirect_to の記述について

GlossaryCategoriesControllerTestにおいて、destroyアクションのテストを記述・実行していた際

   assert_redirected_to project_glossary_categories_path(@project)

と記述したところ、結果が不一致となった。
Expected response to be a redirect to <http://test.host/projects/ecookbook/glossary_categories> 
but was a redirect to <http://test.host/projects/1/glossary_categories>.

GlossaryCategoriesControllerのdestroyメソッドの記述は次

  def destroy
    @category.destroy
    redirect_to project_glossary_categories_path
  end

コントローラーではインスタンス変数@projectが定義されているので、project_glossary_categories_pathにプロジェクトを指定しなくてもURLパスが生成できるが、その際パスのプロジェクトIDは数値となっている。
一方、テストコードでproject_glossary_categories_path(@project)と引数にプロジェクトを指定すると、URLパスのプロジェクトIDは数値ではなく識別子になっている。

ここは、実コードを修正すべきか、テストコードを修正すべきか?

Redmine本体のコントローラーでredirect_toでプロジェクト下のURLパスを名前付きルーティングで生成しているコードを見て回ると、ほぼ引数にプロジェクトを指定している。

高橋 徹 さんが3年以上前に更新

高橋 徹 さんが3年以上前に更新

高橋 徹 さんが2年以上前に更新

  • カテゴリRedmine にセット

高橋 徹 さんが2年以上前に更新

  • 関連している 機能 #95: Redmine 4.xに対応するRedmine Glossaryプラグインを、旧バージョンのデータベースを引き継ぎ、且つコードをきれいに再構築する を追加

高橋 徹 さんが2年以上前に更新

フェーズ21 検索機能の追加 で、プロジェクト指定の検索対象のチェックボックスに用語集が表示されない。
  • プロジェクト参加ユーザーでログインし、プロジェクトを開く
  • 右上検索欄に任意の単語を入れて[Enter]キーを押す
    検索語入力欄の右横のプロジェクト欄に、開いているプロジェクトが表示されている
  • 検索画面(結果画面)が表示されるが、検索対象のチェックボックスには用語集がひょうじされない。チケット、ニュース、文書、等は表示される

なお、検索対象プロジェクトを「全プロジェクト」にすると、検索対象のチェックボックスに用語集が表示されるので、権限周りの問題と推測される。

高橋 徹 さんが2年以上前に更新

検索(結果)画面をブラウザ上で表示し、開発者ツールを表示(Edgeの場合F12)
検索対象のチェックボックスを表示しているHTML部位を絞り込んだところ、
<p id="search-types">...</p>
であった。

Redmineのソースコード app/views/search/index.html.erb で該当部位を探すと次のコードが見つかった。

  <p id="search-types">
  <% @object_types.each do |t| %>
  <label><%= check_box_tag t, 1, @scope.include?(t) %> <%= link_to type_label(t), "#" %></label>
  <% end %>

ここで、object_typesに用語集が含まれていないのが表面的な原因と考える。object_typesはインスタンス変数なので、コントローラー(search_controller.rb)を調べる。次のコードで定義されていた。

    @object_types = Redmine::Search.available_search_types.dup
    if projects_to_search.is_a? Project
      # don't search projects
      @object_types.delete('projects')
      # only show what the user is allowed to view
      @object_types = @object_types.select {|o| User.current.allowed_to?("view_#{o}".to_sym, projects_to_search)}
    end

RubyMineでRedmineをデバッグ実行し、上述コードにブレークポイントを置いて検索を実行したところ、下から2行目の User.current.allowed_to? で用語集が削られていることが分かった。
ここで、view_#{o}は用語集の場合view_glossary_termに展開されていた。

Userのallowed_to?メソッドを確認したところ、第1引数は権限シンボルとなる。現時点の用語集プラグインでは、権限として、view_glossary、manage_glossaryの2つを定義している。したがって、view_glossary_termsは存在しない。

ここまでの調査結果次のことが判明した

acts_as_searchable を使用したRedmineの検索機能にプラグイン側でモデルを検索対象に追加する場合、プラグイン側でview_モデル名 のシンボルで権限を定義しなくてはならない。

高橋 徹 さんが約2年前に更新

Redmine標準のチケットCSV入出力の実装を探る

チケット一覧からインポートでチケット一覧をCSVファイルから入力する実装を探る

チケット一覧画面(app/views/issues/index.html.erb)の右上にインポートのリンクがある。
http://localhost:3000/issues/imports/new

このリンクを生成するビューのコードは次。

  • app/views/issues/index.html.erb
    <%= link_to l(:button_import), new_issues_import_path %>
    
    • link_toの引数で指定した識別子new_issues_import_pathは、routes.rbで定義される(はず)

ルーティング設定は次

  • config/routes.rb
    get   '/issues/imports/new', :to => 'imports#new', :defaults => { :type => 'IssueImport' }, :as => 'new_issues_import'
    
    • asで指定したnew_issues_import から機械的に生成されるnew_issues_import_pathを呼び出すと、/issues/imports/newが実行され、Importコントローラのnewメソッドが実行される。
  • app/views/imports/new.html.erb
    <%= form_tag(imports_path, :multipart => true) do %>
      <%= hidden_field_tag 'type', @import.type %>
      <fieldset class="box">
        <legend><%= l(:label_select_file_to_import) %> (CSV)</legend>
        <p>
          <%= file_field_tag 'file' %>
        </p>
      </fieldset>
      <p><%= submit_tag l(:label_next).html_safe + " &#187;".html_safe, :name => nil %></p>
    <% end %>
    
    • ファイル選択のフォームが表示される。
    • form_tagの第1引数imports_pathは、routes.rbの中に次の定義がある
      post  '/imports', :to => 'imports#create', :as => 'imports'

高橋 徹 さんが約2年前に更新

csvファイルの文字コード対応

CSVファイルの文字コードはデフォルトUTF-8となる模様。
旧用語集プラグインからCSVエクスポートしたファイルの文字コードは、Windows上ではWindows-31J(IANA登録名、Shift JISに独自拡張を追加したCP932)であるため、読み込み時にエラーになることがあります(ASCIIコード以外の文字があると発生し得る)。

対策は

-    CSV.foreach(file.path, headers: true) do |row|
+    CSV.foreach(file.path, headers: true, encoding: "CP932:UTF-8") do |row|

高橋 徹 さんが約2年前に更新

CVSファイル選択のビューをどう設けるか

オリジナルの用語集プラグインの画面構成

オリジナルの用語集一覧画面(index)のサイドバーにはCSVからインポートのリンクがあります。

オリジナルのCSVインポートビュー(サイドバー上のリンク)

  • _sidebar.html.erb
    <%= link_to(l(:label_glossary_import_csv), {:controller => 'glossary', :action => 'import_csv', :project_id => @project}) %>
    

リンクは、glossaryコントローラーのimport_csvアクションを呼び出します。import_csvメソッドは空で、import_csvビュー(import_csv.html.erb)を実行し、ファイル選択とカラム対応付け設定フォーム画面が表示されます。

オリジナルのCSVインポートビュー(ファイル選択、カラム設定フォーム)

フォームがサブミットされると、glossaryコントローラーのimport_csv_execアクションを呼び出します。

  • glossary_controller.rb
      def import_csv_exec
        @import_info = CsvGlossaryImportInfo.new(params)
        glossary_from_csv(@import_info, @project.id)
        : 
      end
    

glossaryコントローラーのimport_csv_execメソッドでは、CSVファイルを読み込みデータベースへ登録する処理を行います。import_csv_execビュー(import_csv_exec.html.erb)では、インポート結果(読み込んだ用語の数、カテゴリの数)を表示します。

再構築画面

再構築では、CSVファイルのインポート操作として、ファイルを選択してimportするだけとします。

Web画面の遷移が発生すると、ブラウザーとサーバー間で通信が発生し、画面の再描画が走るので待たされ感、操作の断続感が生まれます。実装としても、アクションの定義とビューの定義が増えていきます。

Redmineでは、チケットの一覧画面でのフィルター条件、オプションの指定が折り畳み式となっています。これを真似て、サイドバーのCSVインポートを別アクションへのリンクではなく、折り畳みでファイル選択フォームを持つようにします。これで、画面遷移、アクションとビューを1段階減らすことができます。

  • _sidebar.html.erb
      <fieldset class="collapsible collapsed">
        <legend onclick="toggleFieldset(this);" class="icon icon-collapsed">Import CSV</legend>
        <div style="display: none;">
          <%= form_with url: import_project_glossary_terms_path, method: :post, local: true do |form| %>
            <%= form.file_field :file %>
            <%= form.submit "Import" %>
          <% end %>
        </div>
      </fieldset>
    

高橋 徹 さんがほぼ2年前に更新

  • 対象バージョンコロナウィルス休業期間中 にセット

他の形式にエクスポート: Atom PDF