本当はrails_adminをカスタマイズするより、自前で実装した方が最終的には幸せになれると思うんですが、そうもいかない事情もしばしばあったりして(主にスケジュール的な理由で)、後から泥臭い場当たり的な対処に奔走しなければならないこともあります。
そういう時のためのノウハウが貯まったので書いておきたいと思います。
deviseと連携させる
これは公式にもちゃんと書いてて、超簡単。
# 略
config.current_user_method { current_administrator }
# 略
テーブルごとにアクセス権限を限定する
cancanと連携できます。まだまだ楽勝。
# 略
config.authorize_with :cancan
# or
config.authorize_with :cancan, AdminAbility
# 略
class Ability
include CanCan::Ability
def initialize(model)
if model.super_admin?
can :manage, :all
else
can :manage, [NormalModel]
end
end
end
権限カラムを用意するなりRoleクラスを作って関連付けるなりして権限設定。
cssを上書きする
Rails Engineなので、探索パスの前の方にscssを置いてやればそっちを読みにいく。
一番簡単なのは、app/assets/stylesheets/rails_admin/custom/theming.css.scss
を作成して、そこに記述する。
既存の設定に影響を与えず、rails_adminの一番後ろに追加されるので上書きしやすい。
がっつり書き換えたい時は、公式のwiki見てテーマを作ることになる。
既存のdashboardを潰して、独自のトップ画面を表示する
同じく探索パスの前の方に配置すれば、テンプレートの差し替えが可能。
kaminariと同じ感じでカスタマイズが出来る。
app/views/rails_admin/dashboard.html.haml
を作成して、そこに記述する。
フィールドレベルでアクセス権限を限定する
この辺りから辛さが増してきますw
とりあえず、表示だけでも隠す方法
config.model 'SampleModel' do
field :super_admin_only_column do
visible do
if bindings[:controller].try(:current_administrator).try(:super_admin?)
true
else
false
end
end
end
end
同種の設定が増えてきたら、visible
のブロックを全部lambdaで囲んでしまって、フィールドの設定でinstance_eval
すれば多少すっきりします。
入力フォームの下に出てる「必須」とか「オプション」とかの注意書きを変更する
簡単だけど、何弄れば変わるのか調べづらい。
config.model 'SampleModel' do
field :name do
help "必須, 名前を入れてください"
end
end
サイドバーのナビゲーションメニューの並べ替え
weightというパラメーターの小さい順に上から並ぶ。
config.model 'SampleModelFirst' do
weight 1
end
config.model 'SampleModelSecond' do
weight 2
end
これもモデル数が増えてくると面倒なので、lambda
とinstance_eval
で工夫する。
カスタムアクションを定義する
公式ではgem作れ、みたいに書いてあるけど、別に普通にlib以下に書けば問題ありません。
そんな再利用性のあるカスタマイズしないしw
require "rails_admin/config/actions"
require "rails_admin/config/actions/base"
module RailsAdmin
module Config
module Actions
class CustomAction < RailsAdmin::Config::Actions::Base
# アクションの登録
RailsAdmin::Config::Actions.register(self)
# 編集とか削除とかのリンクと同じ箇所に追加する
register_instance_option :member? do
true
end
# 表示するかどうかの設定。特定のクラスだけ出すように設定する等。
register_instance_option :visible? do
if bindings[:abstract_model].model == TargetModel
true
else
false
end
end
# font_awesomeのアイコン名。ここで指定したアイコンが勝手に表示される。
register_instance_option :link_icon do
"icon-eye-open "
end
# pjaxを利用して画面遷移をするかどうか
register_instance_option :pjax? do
false
end
# controllerの実際の処理
# 実際には`(rails_adminのgemディレクトリ)/app/controllers/rails_admin/main_controller.rb`でclass_evalによって定義されたメソッドの中で、instance_evalされる事によって実行される
register_instance_option :controller do
Proc.new do
case @object
when Targetmodel
redirect_to main_app.root_path
else
raise "object is not a TargetModel"
end
end
end
end
end
end
end
rails_adminの設定ファイルにも追加が必要
require 'lib/custom_action'
# 略
config.actions do
# root actions
dashboard do
statistics false
end
# collection actions
index
new
export
bulk_delete
# member actions
show
edit
delete
show_in_app
custom_action # クラス名をアンダースコア形式で追加する
end
# 略
実際にコントローラー内でどういう値が利用できるか、利用できるオプション設定は、ソースを読んで確認した方が良い。
以下のコードが参考になる。
- rails_admin/app/controllers/rails_admin/main_controller.rb
- rails_admin/app/controllers/rails_admin/application_controller.rb
- rails_admin/lib/rails_admin/config/actions/base.rb
- rails_admin/lib/rails_admin/config/actions/show.rb
- rails_admin/app/views/rails_admin/main/show.html.haml
belongs_to関連のselectフォームのカスタマイズ (簡易版)
段々辛くなってきたぜ。
fieldの設定で、ある程度スコープを弄ることができます。
config.model 'SampleModel' do
field :parent_model do
associated_collection_cache_all false # 事前にモデルを読み込まなくなる
associated_collection_scope do
sample_model = bindings[:object]
Proc.new { |scope|
# scoping all ParentModel
scope = scope.limit(100) # 上限を増やす
}
end
end
end
実際、これは何をしているかというと、ParentModelに対するrails_adminのindexアクションを呼び出しています。
その結果を元にselectフィールドを更新しています。
indexアクションでは、main_controllerに定義されているlist_entries
を呼び出してモデルを取得しています。
list_entries
は基本的なスコープを構築します。
この時、さっき指定したassociated_collection_scopeを構築するためのProcがinstance_evalされてスコープが追加されます。
ただ、厄介な事にこの後に実際のデータを取得するget_collection
という処理が入ります。
ここで入力パラメーターを元にソートするスコープが追加されるので、上記の設定でorder指定しても上手く動作してくれなかったりします。
面倒だったので、私はこの問題はスルーしました。
ここでは以下のコードが参考になります。
- rails_admin/app/controllers/rails_admin/main_controller.rb
- rails_admin/app/controllers/rails_admin/application_controller.rb
- rails_admin/lib/rails_admin/config/actions/index.rb
- rails_admin/lib/rails_admin/config/fields/association.rb
- rails_admin/lib/rails_admin/config/fields/types/belongs_to_association.rb
この辺のコード読んでると、MVCとは何だったのか、みたいな気分になりますが仕方ないので諦めます。
belongs_to関連のselectフォームのカスタマイズ (カスタムフィールド版)
カスタムフィールドと自前のコントローラーを組み合わせれば結構自由にカスタマイズが聞きます。
例えば、二つのモデルをまとめて検索して一つのセレクトボックスで表示するとか。
belongs_toの検索はrails_admin/app/assets/javascripts/rails_admin/ra.filterling-select.js
によって実現されています。
実際の利用方法はrails_admin/app/views/rails_admin/main/_form_filtering_select.html.haml
が参考になります。
このテンプレートロジック入りまくってるんですが、仕方ないですよねー(棒
読んでると辛くなってきます。
どうもrails_adminを拡張するポイントとしては、viewから色々キックするのが楽っぽいです。非常に不幸になりそうなんですが…。
このfilterling-selectを丸々自前で書いても良いのですが、流石に面倒なのでこれを活用します。
大エリアと中エリアがあってそれをまとめて設定するみたいなフォームを作る例で説明していきます。
まずは、フォームのビューを書きます。
メイン部分はrails_admin/app/views/rails_admin/main/_form_filtering_select.html.haml
からパクってきます。
:ruby
selected_id = form.object.new_record? ? nil : form.object.area_ids
if selected_id
large_and_middle_area = LargeAndMiddleArea.find_by_ids(selected_id)
collection = large_and_middle_area ? [[large_and_middle_area.label, large_and_middle_area.id]] : []
else
collection = []
end
current_action = params[:action].in?(['create', 'new']) ? 'create' : 'update'
js_option = {
xhr: true,
remote_source: main_app.admin_large_and_middle_areas_path,
current_action: current_action,
}
= form.select field.method_name, collection, { selected: selected_id, include_blank: true }, field.html_attributes.reverse_merge({ data: { filteringselect: true, options: js_option.to_json }, placeholder: t('admin.misc.search') })
ビューから思いっきりモデルのfindとか走ってますがスルーしてください。
他に良いやり方が思いつかないので、泣きながらやるしかありませんでした。
rails_adminに限っては、MVCなんてものは存在しません。
js_optionのremote_sourceに自前で用意するコントローラーへのパスを書いておきます。
rails_adminから呼ばれるので、main_app
を忘れないように。
後は、rails_admin本体のコードを参考にフォームヘルパーを書けばオッケーです。
続いて、カスタムフィールドの定義が必要です。
ここのpartialオプションが非常に重要です。
module RailsAdmin
module Config
module Fields
class AreaSelect < Base
# フィールドタイプの登録
RailsAdmin::Config::Fields::Types::register(:area_select, self)
# モデルに登録されている値から、表示用に整形した値を返す
# 一覧画面に表示される
register_instance_option :pretty_value do
LargeAndMiddleArea.find_by_ids(value).try(:label)
end
# パラメーターによってソートできるかどうか
register_instance_option :sortable do
false
end
# テキスト入力によって関連モデルを検索できるかどうか
register_instance_option :searchable do
true
end
# このフィールドが表示するテンプレート
# さっき作成した自前のテンプレートを指定する
register_instance_option :partial do
:form_area_select
end
register_instance_option :inline_add do
false
end
register_instance_option :inline_edit do
false
end
# ビューから参照するために自分で定義したメソッド
def selected_id
bindings[:object].send(:area_ids)
end
# ビューから参照するために自分で定義したメソッド
def method_name
:area_ids
end
# ビューから参照するために自分で定義したメソッド
def multiple?
false
end
end
end
end
end
fieldのタイプが定義できたので、rails_adminの設定を書き換えます。
require 'lib/rails_admin_area_select'
# 略
config.model SampleModel do
field :area_ids, :area_select
end
# 略
最後に、jsonを返すコントローラーを書きます。
ra.filterling-select.jsは、idとlabelという属性を持ったjsonの配列を返す事で動作します。
class Admin::LargeAndMiddleAreasController < ApplicationController
def index
@large_and_middle_areas = LargeAndMiddleArea.find_all_by_name(params[:query])
end
end
json.array! @large_and_middle_areas do |large_and_middle_area|
json.extract! large_and_middle_area, :id, :label
end
これで、自前のコントローラーに対して検索処理をかけて関連モデルを選択し設定する事ができます。
今回の例に限らず、裏側でsolrで検索させる事も可能です。
かなり辛いので、こんなことする羽目になる前に、自前の管理画面をちゃんと実装した方が良いと思います。
今あるコードを捨てる勇気が欲しい…。
というわけで、ちょっとしたカスタマイズからカスタムアクション、カスタムフィールドの定義方法まで書きました。
なんかもう色々辛いし不毛なので必要にならない方が良いのですが…。