機能 #75
未完了Redmine Glossary プラグインを一から作成する
50%
説明
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プラグイン再構築
ファイル
高橋 徹 さんが7年以上前に更新
- 説明 を更新 (差分)
- ステータス を 新規 から 進行中 に変更
- 進捗率 を 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です。
高橋 徹 さんが7年以上前に更新
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を使用する必要があります。
高橋 徹 さんが7年以上前に更新
コントローラーの作成
まだ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 |
高橋 徹 さんがほぼ7年前に更新
#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
高橋 徹 さんがほぼ7年前に更新
ゼロから始める用語集プラグイン作成の初期リポジトリを作成した。
手順は次に。
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
となっている。
高橋 徹 さんがほぼ7年前に更新
ルーティングエラーの調査
生成されたコントローラーのファイル名が
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の内容が表示された。
高橋 徹 さんがほぼ7年前に更新
- ファイル picture720-1.png picture720-1.png を追加
一覧表示を実装する。
先程までで、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ブラウザからアクセスをした
高橋 徹 さんがほぼ7年前に更新
- ファイル picture497-1.png picture497-1.png を追加
- ファイル picture497-2.png picture497-2.png を追加
すこし見栄えを設定
表の表示があまりに寂しいので、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>
結果は次の画面となった。
見出し行の見栄え、データ要素がセンタリングされているのを左寄せにしたい、といった微調整をあまり深入りしない範囲で記述していく。
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>
この設定で表示した画面は次のとおり。
高橋 徹 さんがほぼ7年前に更新
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記法をテキスト化する表示が可能
高橋 徹 さんがほぼ7年前に更新
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
高橋 徹 さんがほぼ7年前に更新
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) %>
高橋 徹 さんがほぼ7年前に更新
プロジェクトの配下に用語を置こうとした。
ルーティング設定で、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にプロジェクトが含まれないので、プロジェクトの外に出た状態で詳細表示される(プロジェクトメニューが消えている)。
高橋 徹 さんがほぼ7年前に更新
権限がある場合にのみ新規作成アイコン(リンク)を表示させたい。
<%= 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 %>
後者はネストを伴うロジックが毎度入るので微妙、、、
高橋 徹 さんが6年以上前に更新
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フォルダ下に並べて置いていたため
高橋 徹 さんが6年以上前に更新
用語をカテゴリ毎に分類して表示する方法の模索(続)
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を取り出し直しているのが今三つな実装。
高橋 徹 さんが6年以上前に更新
用語をカテゴリ毎に分類して表示する方法の模索(続々)
カテゴリ一覧を取り出し、カテゴリがhas_manyで用語と関連しているのでカテゴリに属する用語をそこから取り出すという方法が思いつく。ただし、
- 未分類(カテゴリがnil)の用語が拾えない
コード断片は次
<% categories = GlossaryCategory.where(project_id: @project.id).sort %>
<% categories.each do |category| %>
<h3><%= category.name %></h3>
:
<% category.terms.each do |term| %>
高橋 徹 さんが6年以上前に更新
ファイル添付の機能追加で添付したファイルのダウンロードがエラー¶
一通り実装後、次の問題が発生している。
- 編集でファイルを選択して添付すると、
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>
高橋 徹 さんが6年以上前に更新
添付ファイル機能追加でコントローラーの実装について¶
本を参考に次の記述をしたが添付されない模様
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
これはカレントユーザーをチェックしている模様。
高橋 徹 さんが6年以上前に更新
@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
- 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 |
高橋 徹 さんが6年以上前に更新
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 %>
高橋 徹 さんが6年以上前に更新
テストの実行で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してから再度テストモードを実行するとテストが実行可能となった。
高橋 徹 さんが6年以上前に更新
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/
高橋 徹 さんが6年以上前に更新
機能テストで、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パスを名前付きルーティングで生成しているコードを見て回ると、ほぼ引数にプロジェクトを指定している。
高橋 徹 さんが5年以上前に更新
フェーズ21 検索機能の追加 で、プロジェクト指定の検索対象のチェックボックスに用語集が表示されない。¶
- プロジェクト参加ユーザーでログインし、プロジェクトを開く
- 右上検索欄に任意の単語を入れて[Enter]キーを押す
検索語入力欄の右横のプロジェクト欄に、開いているプロジェクトが表示されている - 検索画面(結果画面)が表示されるが、検索対象のチェックボックスには用語集がひょうじされない。チケット、ニュース、文書、等は表示される
なお、検索対象プロジェクトを「全プロジェクト」にすると、検索対象のチェックボックスに用語集が表示されるので、権限周りの問題と推測される。
高橋 徹 さんが5年以上前に更新
検索(結果)画面をブラウザ上で表示し、開発者ツールを表示(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_モデル名 のシンボルで権限を定義しなくてはならない。
高橋 徹 さんが5年以上前に更新
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で定義される(はず)
- link_toの引数で指定した識別子
ルーティング設定は次
- 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メソッドが実行される。
- asで指定した
- 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 + " »".html_safe, :name => nil %></p> <% end %>
- ファイル選択のフォームが表示される。
form_tag
の第1引数imports_path
は、routes.rb
の中に次の定義があるpost '/imports', :to => 'imports#create', :as => 'imports'
高橋 徹 さんが約5年前に更新
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|
高橋 徹 さんが約5年前に更新
- ファイル alt_csv_import_view-1.png alt_csv_import_view-1.png を追加
- ファイル alt_csv_import_view-2.png alt_csv_import_view-2.png を追加
CVSファイル選択のビューをどう設けるか¶
オリジナルの用語集プラグインの画面構成¶
オリジナルの用語集一覧画面(index)のサイドバーには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)を実行し、ファイル選択とカラム対応付け設定フォーム画面が表示されます。
フォームがサブミットされると、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>