ActiveAdminを対象のクラスの詳細を見ずにドバリと設定したい。

ActiveAdminはポン付けでそれなりに動く管理画面Gemとして有名ですが、ちょっとだけ手を入れるというのがわりとめんどくさい面もあります。

しかしポン付けを希望している以上、必要以上に対象モデルの詳細を読んだりしたくないので、ActiveAdmin.register内で取得できる情報でなんとかやっていきたいと思います。

駆け足で調べたので、浅い実装に対するポン付けにしか対応していない可能性があります(polymorphicとか試していない)。

その前に

ActiveAdminで使用されている記法は他のGem由来のものが多いので、ActiveAdmin自体のドキュメントを追うよりも、依存しているGemを追ったほうが早い場合があります。

検索フィールドの機微に関してはRansack activerecord-hackery/ransackが参考になります。

作成、編集フォームの描画メソッドについてはFormtastic justinfrench/formtasticが参考になります。

filter

関連に関する検索セレクトボックスを消す

一見便利な機能ですが、レコードの数が増えてくるとそれを全てセレクトボックス内に描画してしまうため、結構重くなるようです。

ActiveAdmin.register ModelA do
  ModelA.reflections.keys.each { |key| remove_filter(key) }
end

消えました。

関連に関する検索はしたいんですが

hoge_idを用いた検索ならばセレクトボックスが出てきませんので、DBへのアクセスや描画などの心配もありません。

filterをそのまま使うと、filterで設定した分の検索フィールドしか表示しません。 preserve_default_filters!を用いれば、もとの検索フィールドはそのままに追加できます。

ActiveAdmin.register ModelA do
  preserve_default_filters!
  
  ModelA.reflections.each_pair do |name, config|
    remove_filter name
    if config.macro == :belongs_to
      filter config.foreign_key, label: config.foreign_key
    else
      through = config.options[:through]
      if !!through
        filter "#{through}_#{name.singularize}_id", label: "#{name}_id", as: :numeric
      else
        filter "#{name}_id", label: "#{name}_id", as: :numeric
      end
    end
  end
end

has_manythroughあたりは実際に描画された検索フィールドから勘で書いているので、もっと適切な引数があるかもしれません。

form

出てこないフィールドがあるんですけど

ActiveAdminは編集フィールドの作成の元ネタとして、ActiveRecord::ModelSchema.content_columnsを使用しています。これは

      # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count",
      # and columns used for single table inheritance have been removed.
      def content_columns
        @content_columns ||= columns.reject { |c| c.name == primary_key || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
      end

ということで、_id_count末尾のカラムのフォームは表示してくれません。inheritance_columnは、デフォルトでは値がtypeです。誰もがつまづく噂のアイツですね。

さて、本来ならばこれでも問題のないところですが、時々間違えて_countカラムを作って出てこないとか、そういうことがあるみたいです。

というわけで全部入りのやつでフォームを作り直しましょう。

つくる

content_columnsの元ネタになっている生columnsを使います。

上述のように、名称からはそれが関連のためのカラムであるかどうか、確信できません。

そこでremove_filterでも用いたreflectionsを参照して、関連に使用されているカラムであれば、カラム名ではなく関連名を与えます。

ActiveAdmin.register ModelA do
  selectable = ModelA.reflections.inject({}) { |a, (name, config)| a.merge!(config.foreign_key => name) }

  columns = ModelA.columns.map(&:name).tap { |names|
    names.delete(ModelA.primary_key)
  }.map { |name|
    if selectable.key?(name)
      selectable[name]
    else
      name
    end
  }

  form do |f|
    f.inputs '', *columns
  end
end

indexと同じく、関連セレクトボックスが不要

ActiveAdmin.register ModelA do
  selectable = ModelA.reflections.inject({}) { |a, (name, config)| a.merge!(config.foreign_key => name) }

  columns = ModelA.columns.map(&:name).tap { |names|
    names.delete(ModelA.primary_key)
  }.select { |name|
    name unless selectable.key?(name)
  }

  form do |f|
    f.inputs '', *columns
  end
end

_idで略

ActiveAdmin.register ModelA do
  columns = ModelA.columns.map(&:name).tap do |names|
    names.delete(ModelA.primary_key)
  end

  form do |f|
    f.inputs '', *columns
  end
end

乱暴にpermit_params

controller {}内でparams.permit!というのもありましたが、さすがにブルータルすぎるのでやめました。

ActiveAdmin.register ModelA do
  columns = ModelA.columns.map(&:name).tap do |names|
    names.delete(ModelA.primary_key)
  end

  permit_params columns
end

おまけ: THE 手作業

ActiveAdminの関連セレクトボックスは、先のクラスにname系のメソッドなどがないと"#<AdminUser:0x007fd6302cf318>"などと言いだしてだるいことで有名です。

class_evalでかましていく

app/admin/dashboard.rbなどで先のクラスにnameメソッドをつっこんでやりましょう。

AdminUser.class_eval do
  return if method_defined?(:name)

  def name
    "AdminUser #{email}"
  end
end

先のクラスにActiveAdminのためのコードが侵入しないのはいいですが、行儀が悪いですね。ただ、filterform両方をさわる必要がないので、早いといえば早い。

セレクトボックス個別にちゃんとやる

filterにはpreserve_default_filters!があるので、これをかました後該当セレクトボックスをremove_filter、あとはfilterでアレします。

一方formformを呼んだが最後、全部消えます。幸い引数なしのf.inputsで全てのフィールドを描画、exceptオプションで除外設定が出来ます。

残念ながらつけたすオプションは見つけられなかったので、見た目的に別のボックスになっちゃいますが以下のとおりです。

ActiveAdmin.register ModelA do
  form do |f|
    f.inputs do
      f.input :admin_user, as: :select, collection: AdminUser.pluck(:email, :id)
    end
    f.inputs except: 'admin_user'
    f.actions
  end
end

こつは、exceptの値をStringで渡すことです。びっくりした(関連以外だとSymbolで消える)。

これで大体いける

管理で雑に値を入れたり消したりする分には、たぶんこれで十分です。こういうササッと終わらせる場所に時間や労力を使わずに、やっていきましょう。

一応今回のは全部まとめてmmmpa/active_admin_relation_removerにしました。

# インデックスの関連を`id`化
ActiveAdmin.register ModelA do
  ActiveAdminRelationRemover.prepare(self) do
    filter!
  end
end

# フォームの関連を`id`化
ActiveAdmin.register ModelB do
  ActiveAdminRelationRemover.prepare(self) do
    form!
    permit!
  end
end

# 全部
ActiveAdmin.register ModelC do
  ActiveAdminRelationRemover.prepare(self).brutal!
end

ノーテストなので動いたり動かなかったりします。

勢いまかせで設定する参考程度にしてください。