rails_adminをカスタマイズする方法まとめ

本当はrails_adminをカスタマイズするより、自前で実装した方が最終的には幸せになれると思うんですが、そうもいかない事情もしばしばあったりして(主にスケジュール的な理由で)、後から泥臭い場当たり的な対処に奔走しなければならないこともあります。

そういう時のためのノウハウが貯まったので書いておきたいと思います。

## deviseと連携させる
これは公式にもちゃんと書いてて、超簡単。

```rb:config/initializers/rails_admin.rb
# 略

config.current_user_method { current_administrator }

# 略
```

## テーブルごとにアクセス権限を限定する
[cancan](https://github.com/ryanb/cancan "ryanb/cancan")と連携できます。まだまだ楽勝。


```rb:config/initializers/rails_admin.rb
# 略

config.authorize_with :cancan

# or

config.authorize_with :cancan, AdminAbility

# 略
```

```rb:app/models/ability.rb
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
とりあえず、表示だけでも隠す方法

```rb:config/initializers/rails_admin.rb
  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`すれば多少すっきりします。

## 入力フォームの下に出てる「必須」とか「オプション」とかの注意書きを変更する
簡単だけど、何弄れば変わるのか調べづらい。

```rb:config/initializers/rails_admin.rb
  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

```lib/custom_action.rb
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の設定ファイルにも追加が必要

```rb:config/initializers/rails_admin.rb
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の設定で、ある程度スコープを弄ることができます。

```rb:config/initializers/rails_admin.rb
  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`からパクってきます。


```rb:app/views/rails_admin/main/_form_area_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オプションが非常に重要です。

```rb:lib/rails_admin_area_select.rb
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の設定を書き換えます。

```rb:config/initializers/rails_admin.rb
require 'lib/rails_admin_area_select'

# 略

  config.model SampleModel do
    field :area_ids, :area_select
  end

# 略
```

最後に、jsonを返すコントローラーを書きます。
ra.filterling-select.jsは、idとlabelという属性を持ったjsonの配列を返す事で動作します。

```rb:app/controllers/admin/large_and_middle_areas_controller.rb
class Admin::LargeAndMiddleAreasController < ApplicationController
  def index
    @large_and_middle_areas = LargeAndMiddleArea.find_all_by_name(params[:query])
  end
end
```

```rb:app/views/admin/large_and_middle_areas/index.json.jbuilder
json.array! @large_and_middle_areas do |large_and_middle_area|
  json.extract! large_and_middle_area, :id, :label
end
```

これで、自前のコントローラーに対して検索処理をかけて関連モデルを選択し設定する事ができます。
今回の例に限らず、裏側でsolrで検索させる事も可能です。
かなり辛いので、こんなことする羽目になる前に、自前の管理画面をちゃんと実装した方が良いと思います。
今あるコードを捨てる勇気が欲しい…。


というわけで、ちょっとしたカスタマイズからカスタムアクション、カスタムフィールドの定義方法まで書きました。
なんかもう色々辛いし不毛なので必要にならない方が良いのですが…。