はじめに
先日、半年ぐらいプログラミングの勉強をしている友人に、「何か機能を追加したくて、実装方法がいくつかあるとき、どういうことを考えて選択するの?」という質問を受けましたが、結構壮大なテーマなんで、漠然とありきたりなことしか言わなかったことをちょっと後悔しているので、本来はこういう小さな機能でここまできっちり検討しないですが、具体例を提示するという意味で、思考を明文化する形でくどく書いていきます。
Administrateとは
Railsにおけるの管理画面のCRUDや検索機能を自動で導入してくれるGem。rails管理画面系gem比較してみたのように、Railsでは管理画面をGemでさっと作りたいとき、いくつか選択肢があるが、Administrateは比較的新興のGemで、以下のような利点がある。
- カスタマイズすることを前提としている
- でも色々よしなにやってくれて余計な操作が要らない
- ドキュメントがある
- コードの見通しが良い
- 認証系のGemに依存していない
- 普通のRubyのコードなので、学習コストが低い
- Thoughtbotが開発しているので、今後の開発やメンテナンスの継続性についても個人で開発しているGemより期待できる
環境や前提
- Rails 5.2.1
- Ruby 2.5.1
class Post < ActiveRecord::Base
enum status: { submitted: 0, approved: 1, rejected: 2 }
end
ja:
activerecord:
models:
user: ユーザー
admin_user: 管理者
post: ポスト
attributes:
post:
date: 日付
post/status:
submitted: "承認待ち"
approved: "承認済み"
rejected: "取り消し"
実装に必要な要素は以下の3点で、
a. enumの値一覧の取得する
b. I18nを利用してenumの値一覧を元にロカライズされたテキスト一覧を取得する(form用)
c. enumの値からロカライズされたテキストを取得する(index/show用)
これらをどこで行うかによって異なる。
方法0: プラグインのGemを使う
- Administrate標準で提供されている機能で十分
- 実装に何分もかかるものではない
- このGemが継続的にメンテされる保証はない。
よってこの方法は採用しない。
方法1: 既存の機能のみで実装する
作戦
-
a
はPostDashboard
で実行 -
b
は行わない -
c
は行わない
メリット
ファイルやコードを追加する必要がない。既存の機能のみで実装可能。
デメリット
テキストのカスタマイズができない。ロカライズできない。
collection
オプションにネストされていない配列を渡すことでドロップダウンを作ることできるので、Enumの値を直接ドロップダウンの選択肢に出してよいのであれば、Selectフィールドを使うだけでできる。
ATTRIBUTE_TYPES = {
status: Field::Select.with_options(collection: Post.statuses.keys)
}
ドロップダウンに現れる選択肢は"submitted", "approved", "rejected"になる。
<select name="post[status]" id="post_status">
<option selected="selected" value="submitted">submitted</option>
<option value="approved">approved</option>
<option value="rejected">rejected</option>
</select>
<td class="cell-data cell-data--select">
<a href="/admin/posts/202" class="action-show">
submitted
</a>
</td>
<dd class="attribute-data attribute-data--select">submitted
</dd>
しかし、RailsのFormヘルパーのように、選択肢のテキストと値のペアの配列(e.g. [['Submitted', 'submitted'], ['Approved', 'approved'], ['Rejected', 'rejected']])を渡すことはできないので、テキストをいじることはできない。
要件次第で採用する。
方法2: カスタムビューを作る
作戦
-
a
はPostで実行 -
b
はPostでメソッド定義してPostDashboardで実行 -
c
は_index/_showで実行
メリット
カスタムフィールドを作る必要がない。データを処理するコードがモデルとビューだけに置かれるので流れが明確。collection
オプションに通常の配列も配列の配列も渡せるので、RailsのFormヘルパーのような使い心地になる。
デメリット
既存のフィールドのカスタマイズを行うので、デフォルトの挙動と異なることになる。チームで開発する場合は、他の誰かがハマる可能性があり、コメントやドキュメントを残すなどコミュニケーション手段が必要。
実装
rails g administrate:views:field select
def self.localized_statuses
I18n.t('activerecord.attributes.post/status')
end
# あるいは
def self.localized_statuses
Post.statuses.keys.map {|k| [k, Post.human_attribute_name("post.status.#{k}")] }
end
status: Field::Select.with_options(
collection: Post.localized_statuses.invert
)
<div class="field-unit__label">
<%= f.label field.attribute %>
</div>
<div class="field-unit__field">
<%= f.select(
field.attribute,
field.collection,
{ selected: field.data.presence }
) %>
</div>
<%= field.resource.class.human_attribute_name("#{field.resource.class.name.underscore}.#{field.attribute}.#{field.data}") %>
<%= field.resource.class.human_attribute_name("#{field.resource.class.name.underscore}.#{field.attribute}.#{field.data}") %>
ちょっともう書くのが面倒くさくなって実装がテキトーになったが、上のような感じ。
方法3: カスタムフィールドを作る
すでに記事がいくつかある。
Administrate で enumフィールドを多言語化して表示する
-
a
はEnumで実行。 -
b
はEnumでメソッド定義してビューで実行。 -
c
については言及がない。 - コードの見通しや再利用性から言ってベストかどうかわからない
administrate でenum 専用のfield を作ったよ
-
a
はPostで実行。 -
b
はEnumでメソッド定義してEnumの_formで実行。 -
c
は_index/_showで実行。 - Enumクラスを作る意味が薄い。
- 関係ないことだが、localeのYAMLファイルでのenumの書き方がRailsの慣習に従っていなくて、あとでカオスになる可能性あり。
作戦
前述の通り、方法はいくつかあるが、せっかくEnumクラスを作ったとしたら、全部Enumクラスで処理したい。
-
a
はEnumで実行。 -
b
はEnumで定義して_formで実行 -
c
はEnumで定義して_index/_showで実行
メリット
Field::Enum
と書ける自己満足感に浸れる。処理のコードが一つにまとまる。デフォルトでEnumクラスがないので、「カスタムフィールドですよ」と一目でわかる。
デメリット
手数が増える。管理するファイル・コードが増える。コードの再利用性が高くない(I18n関連のコードはアプリの他の部分でも使うと思うので、モデルにあったほうが良いと思うという意味で)。
実装
bundle exec rails g administrate:field enum
require "administrate/field/select"
class Enum < Administrate::Field::Select
def self.searchable?
true
end
def selectable_options
collection
end
def label_data
translate data
end
private
def collection
@collection ||= options.fetch(:collection, default_collection)
end
def i18n_path
options.fetch(:i18n_path, nil)
end
def pluralized_attribute
options.fetch(:method, default_pluralized_attribute)
end
def translate(key)
return I18n.t("#{i18n_path}.#{key}", default: key) if i18n_path
default_translation
end
def default_collection
resource.class.send(pluralized_attribute).map { |k, _| [translate(k), k] }
end
def default_pluralized_attribute
attribute.to_s.downcase.pluralize
end
def default_translation
resource.class.human_attribute_name("#{attribute}.#{key}")
end
end
<div class="field-unit__label">
<%= f.label field.attribute %>
</div>
<div class="field-unit__field">
<%= f.collection_select(
field.attribute,
field.selectable_options,
:last,
:first,
{ value: field.data.presence }
) %>
</div>
<%= field.label_data %>
<%= field.label_data %>
使用例
ATTRIBUTE_TYPES = {
status: Field::Enum
# status: Field::Enum(
# collection: [["custom", 1], ["collection", 2]],
# i18n_path: "some.custom.path",
# method: "custom_enum_methods"
# ) というような使い方もできるようになります。
}
方法4: PRを送った
そもそも、AdministrateのSelectフィールドのコードを修正すれば、開発者が各々でカスタムフィールドを作るような無駄な作業を繰り返す必要がないので、PRを送った。マージされるかわからないが、やり方はこんな感じになる。
基本的な使い方
status: Field::Select.with_options(collection: [ ['Pending',1], ['Approved',2], ['Rejected',3] ])
Enumを扱う場合
status: Field::Select.with_options(collection: Post.localized_statuses.invert)
class Post < ApplicationRecord
enum status: { submitted: 0, approved: 1, rejected: 2 }
def self.localized_statuses
I18n.t 'activerecord.attributes.post/status'
end
# あるいは
# def self.localized_statuses
# Post.statuses.keys.map {|k| [k, Post.human_attribute_name("post.status.#{k}")] }
# end
end
activerecord:
attributes:
post/status:
submitted: "承認待ち"
approved: "承認済み"
rejected: "取り消し"
いまのところ、Administrateはフィールドごとに検索機能の挙動をカスタマイズすることを想定しておらず、そこが機能拡張のネックになっている部分もあるので、修正されたら良いなと思う。時間があれば修正したい。
おわりに
スピードが優先なら方法1、普通にやるなら方法2、チームでやるなら方法2か3、もしPRがマージされれば方法4。Administrateは便利ですが、まだ色々整っていない部分もあります。PR送ってみるといいかもしれませんよ。