Rails に ActiveAdmin で管理画面を追加してみた

# はじめに
Rails の管理画面作成において、便利な Gem ActiveAdmin で簡単に管理画面が実装できるということで、調べながら実装した際のメモ  

今回は、既存の User モデルのユーザーを使用して管理画面へのログインをし、
User の新規作成、一覧、詳細、更新、削除のページを用意
デフォルトでは、route は `/admin` で作成されるが、それを `/manage` に変えて作成  

# 準備
Gemfile に下記を追加して、`bundle install`

```rb:Gemfile
# for manage page
gem 'activeadmin', github: 'activeadmin'
```

今回は、既存の User モデルを使用するので、AdminUser モデルを作成しないように `--skip-users` オプションを追加して install

```
$ bin/rails g active_admin:install --skip-users
      create  config/initializers/active_admin.rb
      create  app/admin                             # ActiveAdmin で編集させたいリソース毎にファイルを設置するフォルダ
      create  app/admin/dashboard.rb                # ログイン後のダッシュボードのページ
       route  ActiveAdmin.routes(self)
    generate  active_admin:assets
      create  app/assets/javascripts/active_admin.js.coffee
      create  app/assets/stylesheets/active_admin.css.scss
      create  db/migrate/20150218033243_create_active_admin_comments.rb
```

model が追加されたので、migrate する
active_admin_comments というテーブルが作成される

```
$ bin/rake db:migrate
```

ActiveAdmin の設定を編集する

```rb:config/initializers/active_admin.rb
  config.site_title = 'hoge'                               # サイトのタイトル
  config.site_title_link = '/manage'                       # ロゴのリンク設定
  config.site_title_image = '/assets/hoge_logo.png'        # ロゴの画像を用意
  config.default_namespace = :manage                       # route がデフォルトの /admin ではなく、/manage になる
  config.namespace :manage do |manage|
    manage.site_title = 'hoge 管理ページ'
  end
  config.favicon = '/assets/favicon.ico'                   # favicon を用意できたら
  config.authentication_method = :authenticate_admin_user! # 管理画面にアクセスする際の認証メソッド ( concerns に追加するメソッド )
  config.current_user_method = :current_admin_user         # ログインユーザー情報を取得するメソッド ( concerns に追加するメソッド )
  config.logout_link_path = :destroy_admin_session_path    # ログアウト時のパス 新たに定義する routes
  config.logout_link_method = :delete                      # ログアウト時の METHOD を DELETE へ

```

上記設定をすると、下記 URL でアクセスできる

`http://アプリケーションのドメイン/manage`

環境によっては、`undefined method ``environment' for nil:NilClass` が発生するかも

調べてみたら、`sass-rails` と `sprocket` のバージョンの問題みたい
http://stackoverflow.com/questions/22392862/undefined-method-environment-for-nilnilclass-when-importing-bootstrap

Gemfile を修正して、sass-rails を `bundle update` したらアクセスできた

# ActiveAdmin の routes
ActiveAdnin の initializer で自動的に命名規則にのっとった routes が追加される
下記の manage_root 以外は、追加した resouce 毎に追加されるが、resource ファイル内で利用する action を設定すると記載された actionに対応する routes のみが追加される

|Prefix|Verb|URI Pattern|Controller#Action|
|:----|:----:|----|----|
|manage_root|GET|/manage(.:format)|manage/dashboard#index|
|batch_action_manage_resources|POST|/manage/resources/batch_action(.:format)|manage/resources#batch_action|
|manage_resources|GET|/manage/resources(.:format)|manage/resources#index|
||POST|/manage/resources(.:format)|manage/resources#create|
|new_manage_resource|GET|/manage/resources/new(.:format)|manage/resources#new|
|edit_manage_resource|GET|/manage/resources/:id/edit(.:format)|manage/resources#edit|
|manage_resource|GET|/manage/resources/:id(.:format)|manage/resources#show|
||PATCH|/manage/resources/:id(.:format)|manage/resources#update|
||PUT|/manage/resources/:id(.:format)|manage/resources#update|

※ `default_namespace` を変更しない場合は、`manage` が `admin` になる


# ログイン処理までの各種実装
管理画面用のログイン処理系は、controller の concern に追加した。

ログイン画面、ログアウトの routes を追加

ログイン画面の URL を `http://www.example/manage/login` みたいにしたかったので、scope を切った

```rb:config/routes.rb
  ActiveAdmin.routes(self) # active_admin:install に自動的に追加されてる
  scope :manage do
    get :login, to: 'users#new_admin_session', as: :new_admin_session
    post :login, to: 'users#create_admin_session', as: :create_admin_session
    delete :logout,to: 'users#destroy_admin_session', as: :destroy_admin_session
  end
```

ログイン処理、ログアウト処理を concers に追加
下記は、UsersController で include する

```rb:app/controllers/concerns/admin_user_authentication_action.rb
module AdminUserAuthenticationAction
  extend ActiveSupport::Concern

#  include する UsersController とかに before_action が入っていた場合は、下記の様な感じで skip するようにする
#  self.included do
#    skip_before_action :authenticate_by_token, only: [:new_admin_session, :create_admin_session, :destroy_admin_session]
#  end

  def new_admin_session
    redirect_to manage_root_path if current_admin_user
    render template: 'users/admin_session/new' # view は users に別途フォルダをわけて作成した
  end

  # session 情報に、ログイン者の情報を追加
  def create_admin_session
    begin
      @current_admin_user = User.admin_authenticate_by_email(params[:user][:email], params[:user][:password])
      session[:admin_user_id] = @current_admin_user.id
      session[:admin_session_expires_at] = 60.minutes.from_now # セッション保持期間は 1 時間
      redirect_to manage_root_path
    rescue User::LoginFailed
      redirect_to new_admin_session_path
    end
  end

  # ログアウト処理
  def destroy_admin_session
    if current_admin_user
      session.clear
    end
    redirect_to new_admin_session_path
  end
end
```

下記は、ApplicationController で include する

```rb:app/controllers/concerns/admin_user_authentication_filter.rb
module AdminUserAuthenticationFilter
  extend ActiveSupport::Concern
  class AdminUnauthenticated < StandardError; end

  self.included do
    rescue_from AdminUnauthenticated do
      redirect_to manage_root_path
    end
  end

  def authenticate_admin_user!
    redirect_to new_admin_session_path unless current_admin_user
  end

  # active admin から呼ばれるログインしたユーザーを取得するメソッド
  def current_admin_user
    if session[:admin_user_id] && session[:admin_session_expires_at].try(:>, Time.zone.now)
      session[:admin_session_expires_at] = 60.minutes.from_now
      begin
        # Enumerize で role が admin のユーザーを検索
        @current_admin_user = User.with_role(:admin).find_by!(id: session[:admin_user_id])
        session[:admin_user_id] = @current_admin_user.id
      rescue ActiveRecord::RecordNotFound
        raise AdminUnauthenticated
      end
    else
      @current_admin_user = nil
    end
    @current_admin_user
  end
end
```

それぞれ、作った concern を include

```rb:app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include AdminUserAuthenticationFilter # include しておく

  # 色々な処理
  # 色々な処理
end
```
```rb:app/controllers/users_controller.rb
class UsersController < ApplicationController
  include AdminUserAuthenticationAction # include しておく

  # 色々な処理
  # 色々な処理
end
```

管理者ユーザーのログインメソッドを追加
`role` については、[Enumerize](https://github.com/brainspec/enumerize) を使ってます。

```rb:app/models/user.rb
class User < ActiveRecord::Base
  class LoginFailed < StandardError; end

  extend Enumerize
  enumerize :role, in: { user: 1, admin: 2 }, default: :user, predicates: true, scope: true

  belongs_to :prefecture # 都道府県

  validates :email, :name, :prefecture, :address, presence: true

  scope :active, -> { where(deleted_at: nil) }

  def self.admin_authenticate_by_email(email, password)
    user = self.with_role(:admin).find_by!(email: email)
    user.authenticate(password) or raise LoginFailed # Devise は使ってないので、標準の authenticate を使用
  rescue ActiveRecord::RecordNotFound
    raise LoginFailed
  end
end
```

ログイン画面の view は下記のような感じ

```haml:app/views/user/admin_session/new.html.haml
:sass
  #login_form
    width: 40%
    margin: 30px auto
    label
      margin-bottom: 5px
  #user_email, #user_password
    width: 100%
    margin-bottom: 5px
  input
    box-sizing: border-box
    &[type="submit"]
      width: 30%
  .actions
    text-align: center

= stylesheet_link_tag "active_admin"
= form_for :user, url: create_admin_session_path, :html=> {:id => 'login_form'}, method: :post do |f|
  .field
    = f.label :email
    %br
    = f.email_field :email, autofocus: true
  .field
    = f.label :password
    %br
    = f.password_field :password, autocomplete: "off"
  .actions
    = f.submit "ログイン"
```

画面デザインとか考えるの面倒臭かったので、Devise の session の erb をコピーして haml に書き直して、
ActiveAdmin で用意されてる css を `= stylesheet_link_tag "active_admin"` で読み込んで、
足りない分だけ実装した。

ちゃんとやるんだったら、sass をわけなきゃだけど。。。とりあえずです。

# ログイン後の各画面の追加
上記までで、ログインから、ダッシュボード表示まで出来ているので、後は個別のモデルを表示する画面を実装
また、新規作成、更新のフォーム、検索条件について、string や、integer の型により自動的に表示される

例：
name が string の場合、filter では、あいまい検索、前方一致、後方一致、完全一致が自動で用意され、input では、`type="text"` で表示される
prefecture が belongs_to の場合、filter 、input 共に select タグが出力される

### リソースの追加方法
下記を実行すると、`/app/admin/user.rb` が作成される

```
$ bin/rails g active_admin:resource User
```

このままでも既に動くようになっているが、DSL を書けば個別にカスタマイズもできる
よく使うものを下記

|パラメータ|概要|
|:----|:----|----|
|filter|一覧ページの右の検索に表示する項目を追加するパラメータ|
|remove_filter|一覧ページの右の検索に表示する項目を削除するパラメータ（複数可）|
|index|一覧ページを定義するパラメータ|
|column|一覧ページに表示する項目を定義するパラメータ|
|actions|一覧ページ、編集ページに各アクションボタンを表示するパラメータ|
|show|詳細ページを定義するパラメータ|
|attributes_table|詳細ページで表示する項目を定義するパラメータ|
|panel|表示する項目の領域を分ける際に使用するパラメータ|
|form|編集ページを定義するパラメータ|
|input|編集ページの編集可能な項目を定義するパラメータ|
|permit_params|編集ページで実際に編集可能にするパラメータ(StrongParameters と同じ働きをする)|

下記 User のページを作った際のコード

```rb:/app/admin/user.rb
ActiveAdmin.register User do
  config.per_page = 15 # 一覧ページのページングの件数

  # 一覧ページの検索条件
  filter :id
  filter :role
  filter :email
  filter :name
  filter :prefecture # 自動で Prefecture#name の select タグが表示される
  filter :address
  filter :deleted_at
  # 上記検索条件に表示する項目を remove_filter で書くと下記のようになる
  # remove_filter :crated_at, :updated_at

  # 一覧ページ
  index do
    column :id
    column :role
    column :email
    column :name
    column :prefecture # 勝手に prefecture.name を表示
    column :address
    column :deleted_at

    actions 
  end

  # 詳細ページ
  show do
    attributes_table do
      row :id
      row :role
      row :email
      row :name
      row :prefecture # 勝手に prefecture.name を表示
      row :address
      row :deleted_at
    end
  end

  # 新規作成/編集ページ
  form do |f|
    inputs  do
      input :role
      input :email
      input :name
      input :prefecture
      input :address
    end

    actions
  end

  permit_params :role, :email, :name, :prefecture, :address # 更新可能な attribute を記載
end
```

# Tips
## ログインユーザーの表示名を変更する
ログイン後の右上部のログインユーザーの情報は変更できる
email を表示する場合は、config に下記を追加

```rb:config/initializers/active_admin.rb
config.display_name_methods = [:email]
```

default は `[ :display_name, :full_name, :name, :username, :login, :title, :email, :to_s ]` の順に評価され、存在するメソッドの結果を表示されるみたい ([source](https://github.com/activeadmin/activeadmin/blob/master/lib/active_admin/application.rb#L119))

## ページタイトルを変える
show や index に対して title を渡すと表示される
下記は、詳細ページのタイトルに name を表示

```rb
show title: :name do
  row: :id
  row :name
end
```

## 実行できる Action を制限する
新規作成は ActiveAdmin からできるようにしたいけど、更新はしたくないといった場合に対応できる

- index だけ可能にする場合

```rb
ActiveAdmin.register User do
  actions :index # index だけ可能

  ....
end
```

- 更新はだけ不可にする場合

```rb
ActiveAdmin.register User do
  actions :all, except: [:edit, :update]

  .... 
end
```

指定できるアクションは、`:index`, `:show`, `:new`, `:create`, `:edit`, `:update`, `:destroy`

## 一覧にフィルターボタンを追加
よく使うフィルターの条件とかを、一覧画面の上部に追加できる
元ある scope を利用することもできるし、その場で条件を記載することも出来る
下記は、ユーザーのみ表示する

```rb
ActiveAdmin.register User do
  scope 'Active', :active # User に定義された scope 
  scope('ユーザーのみ') { |scope| scope.with_role(:user) } # with_role は Enumerize で追加された scope
  .... 
end
```


## 一覧に表示させる record を制限する
scope に、 `default: true` と記載すると、その scope に一致するものしか表示できなくなる

```rb
ActiveAdmin.register User do
  scope 'Active', :active, default: true

  .... 
end

```

## フォームのボタンを日本語化

I18n で日本語用の辞書があれば、勝手に日本語化してくれるが、
作成画面とか、更新画面のボタンの日本語は、下記のように改めて辞書の用意が必要

これが無いと、ボタンに `Update ユーザー` のように表示される

```yaml:config/locales/active_admin/ja.yml
ja:
  formtastic:
    :yes: 'はい'
    :no: 'いいえ'
    :create: '%{model}を作成'
    :update: '%{model}を更新'
    :submit: '%{model}を投稿'
    :cancel: '%{model}を中止'
    :reset: '%{model}を初期化'
    :required: '必須'
```
## カスタマイズした項目を表示する
上記の User#role が Enumerize を使用しているので、一覧、詳細で表示した場合に、user とか、 admin とかで表示されてしまうので、
それを I18n で定義した辞書を使用して表示する場合

```rb
index do
  column :role do |user|
    user.role_text # or user.role.text
  end
end

show do
  column :role do |user|
    coupon.role_text # or user.role.text
  end
end
```

## filter や form の input をカスタマイズする
上記のように belong_to 等について自動で select タグを出力してくれるが、select の表示したい名称を変えた場合や、
Enumerize を select タグで表示したい時に、カスタマイズが必要になる

```rb
filter :role, as: :select,
  collection: User.role.values.map { |value| [User.role.find_value(value).text, User.role.find_value(value).value] }
```
```rb
input :role, as: :select,
  collection: User.role.values.map { |value| [User.role.find_value(value).text, User.role.find_value(value).value] }
```

collection に表示名、値の順の配列を渡すことで可能になる

ちなみに、filter で `as: :check_boxes` にするとチェックボックスに変わり、複数選択で検索できる  

## form の input に、attribute を追加する
フォームのファイルアップロードのファイル選択画面で画像だけを選択させるよう、
`input` タグに `accept` の attribute を追加したかった際に、ソース調べて試したら下記のやり方で出来た

```rb
form do |f|
  inputs  do
    input :image, as: :file, input_html: { accept: 'image/*' }
  end
end
```

`input_html` に追加したい attribute を Hash で渡すと変換して表示してくる  


[input要素のaccept属性のブラウザ対応状況](http://w0s.jp/diary/254)
[input_html の該当部分のソース](http://www.rubydoc.info/github/justinfrench/formtastic/Formtastic/Inputs/Base/Options#input_options-instance_method)

## form の date カラムの日付をカレンダーから選択
ActiveAdmin のデータ作成画面は、標準では `date` カラムの入力フィールドは年月日が全て `select` タグになっており入力しづらい。
それを下記のように書くと `Formtastic` の `datetime picke`rが使えるようになる

```rb
form do |f|
  inputs do
    input :start_date, as: :date_picker
  end

  actions
end
```

datetime 型の場合は、[just-datetime-picker](https://github.com/mspanc/just-datetime-picker) を使うとカレンダーから選択できるようになる

## index ページの actions を自前で書いてみる
一覧画面で、特定条件に一致するユーザーのみ削除ボタンを表示させる見たいのなことを実装するため、デフォルトの actions を false にして自前で実装してみた （詳細は[こちら](http://qiita.com/yutackall/items/95b79e4fbed3e47d886e)）

item の後に表示名、path で作成できるが、デフォルトで作成されるタグには、`view_link` と `member_link` の class が付いているので、それも追加
表示名については、`I18n` で取得
使用できる辞書ファイルは[こちら](https://github.com/activeadmin/activeadmin/tree/master/config/locales)

```rb
ActiveAdmin.register User do
  index do
    column :id
    column :name

    actions defaults: false do |user|
      item I18n.t('active_admin.view'), manage_user_path(user), class: 'view_link member_link'
      item I18n.t('active_admin.edit'), edit_manage_user_path(user), class: 'edit_link member_link'
      item I18n.t('active_admin.delete'), manage_user_path(user), class: 'delete_link member_link'
    end
  end
end
```

## カスタムなアクションを定義する
ユーザーの削除を、削除日を入れて更新するのではなく、標準の削除ボタンの見た目を装い、実はカスタムアクションを実行

```rb
ActiveAdmin.register Campaign do
  # delete アクションを追加
  # delete_manage_user_path が route に追加される
  member_action :delete, method: :delete do
    user = User.find params[:id]
    user.touch(:deleted_at)
    redirect_to manage_user_path, :notice => "Deleted!"
  end

  index do
    column :id
    column :name

    actions defaults: false do |user|
      item I18n.t('active_admin.view'), manage_user_path(user), class: 'view_link member_link'
      item I18n.t('active_admin.delete'), delete_manage_user_path(user), class: 'delete_link member_link', method: :delete, data: { confirm: I18n.t('active_admin.delete_confirmation') } # クリック時に confirm も表示させた 
    end
  end
end
```

# まとめ

DSL なので、カスタマイズしたい場合に、どこまで出来るのか調べながら実装していくことになるので、
多少導入コストがかかるが、カスタマイズせず（一覧ページだけでいいとか）そのまま使う場合は、ホントに手軽にかつ素早く実装できる  

上記のパラメータを覚えるだけで、必要な機能の8割方の機能は実装できると思う

ActiveAdmin 関連はこちら

- [ActiveAdmin の actions のボタンをカスタマイズする](http://qiita.com/yutackall/items/95b79e4fbed3e47d886e)
- [ActiveAdmin の has_many の Form で NestedAttributes の画像 ( Paperclip ) リソースを更新する](http://qiita.com/yutackall/items/24da34d81902a139897f)