Webのシステムを作成するときに、なんだかんだで必要になる管理画面ですが
railsでもいくつか有名なgemが出ています。
結局はその中でtypusを選んで現在でも使用しているのですか、そこから得られた知見から、こういう管理画面作成ツールが欲しいな、というイメージを固めたので、ついでではなせればな、と感じて記事にしました。
先ほど、話したように現在はtypusを管理画面作成ツールとして主に使っていて、ときどきコーナーケースでバグを踏んでは、pull requestを送ったりしています。
これを採用している最大の理由はカスタマイズ性と、その設計指針の正しさで
- 設定ファイルで大まかな設定を記述してある
- どんな機能も上書き可能
- view-helper-viewの入れ子構造での部品化
特に、各々の表示項目を細かく部品化して、viewの一部のみを変更したいときに、そこのpartialだけを変更すればうまくいくように設計してあるのは、他の管理画面作成ツールではない特徴でした。
これから上げていく箇所もありますが、実際に案件を回していくときは、非エンジニアにも使いやすい画面を提供するには、設定ファイルに記述するだけではまかり通らないことは多いので、非常に助かっています。
例えば、2つ以上のテーブルの管理を、事情があってユーザーのログイン情報とその他の情報を別のテーブルに分けているけど、表示は一つの画面に押し込みたいときとか、設定ファイルを超えた処理が必要になります。
同じようにユーザー検索機能で、FacebookIDでユーザー検索をしたいという要望が出たときも、omniauthの規約にそってテーブルを作ると、usersテーブルにデータがないのは、避けられないパターンです。
こういうのも柔軟になんとかしてくれるのがtypusの強みですが、実際にカスタマイズをしながらその変遷に付き合っていると、辛いところもあるので、紹介していきます。
基本編
そもそも学習コストが高い
表示のここだけ直したいんだよなー、という需要でも柔軟に答えてくれるのがtypusの長所ですが逆を言えば、欲しい所だけをカスタマイズするには、view-helper-viewの繰り返しを解読したあとに一度全体を考えてからカスタマイズを行う必要があります。
実際には、3案件位使っていかないと、かけた学習コストに対してペイしない感じがします。
そこまでやるのもどうかと思いますので、実際にはあまり解読に手間をかけないでべたっとかくのも最初は手段かと思います。
コード管理編
rubygemsにリリースしてくれない
未だに、3.1とかのバージョンで、rails4向けがrubygemsにリリースされていないのは何かあるんでしょうか?
Gemfileにgithubのアドレス直書きするのもう辛いです
gem "typus", github:"typus/typus"
時々バグを踏む
最近自分で踏んでpull requestしたものだと、読み取りのみの権限でもboolean型のデータはindexアクションでは編集出来るようにリンクが出ていたりしました。
そもそも、細かい編集権限を持たせると、表示のみと編集可能の場合で2つの管理をしないといけないので大変なのですが、管理画面では普通データの編集したいものなので、showアクション回りのコードは蔑ろにされている感じ。
※ 現在でも残っているのでは、datetime型のカラムで値がNULLの場合、落ちます。
ブランチの管理が甘い
以前、githubのmasterブランチの特定のhash値をgemfileに直に指定していたときに
bundle installが動かなくなって調べたら、指定したhash値が消えていた事がある。
masterブランチにforce pushされた様子で不通に辛かった。
明らかなバグ修正以外はpull requestを受け付けていない
最近はリファクタリングのコードを送っても、mergeされない傾向にある。
明らかなバグはその日のうちにmergeされているので、基準はあるのでしょうが少し活動が鈍くなっている印象を受ける。
設計の老朽化問題
Railsの標準とメソッドの名前が衝突してしまった
typusは、フォームにセレクトボックスを出して、指定の範囲から選択させたいときは、セレクトボックスを使わせてくれる設計になっています。
# app/models/video.rb
class Video < ActiveRecord::Base
STATUS = %w(pending encoding encoded error published)
validates_inclusion_of :status, :in => STATUS
def self.statuses
STATUS
end
end
モデルにメソッドを太らせる、という欠点こそ若干ありましたが、そこまで問題を感じず使ってきていたのですが、ActiveRedord::Enum
が登場してきてからは、全ての事情が変わってしまいました。本来ActiveRedord::Enum
に与えられるべきメソッド名が既に使われてしまっている状態で、これをどう対処しようかと悩み倒す羽目に。
rails4.1以上の機能なので、今削ると4.0系に手段がなくなったり、必ずしも消して正解とは言わないのですが、こうなってしまったのは、typusの歴史から来る負債なのでしょう。
enumのI18nに対応していない
これも上記の問題と同じ、Rails4.1になって、ActiveRedord::Enum
が導入されていますが、Rails本体ではI18nを提供していないので、セレクトボックスでは、日本語化されていない文字列が表示されてしまいます。
これは、そもそもI18nは提供しないけど、Enumだけ中途半端に提供したRailsの方がむしろ問題なのですが、それを理解しつつもI18nの機能を独自に提供しない管理画面フレームワークも片手落ちと言えます。
まぁ、どちらかというと、railsの方が悪いんですけどね。
ルーティングが固定化されている
typusはinstallを行うと、/admin/
以下に自動的に管理画面のルーティングを設定してくれます。その上で、基本的には設定ファイルなどでこれを変更する事はできません。
これは、そもそもtypusのそこの設計が古くなっていて、Mountable Engineで、ルーティング設定の塊をつくる事が出来る様になる以前から存在するからです。
いい加減ここも切り替えるべきなんですけどね。
設定ファイル編
showアクションがカスタマイズできない
本家のWikiを参照すると、default,list,formの名前で表示項目を指定できますが
これだとshowアクションでの表示項目が指定できません。
Photo:
fields:
default: name
list: name, created_at, category, status
form: name, body, created_at, status
csv: name, body
xml: name, created_at, status
defaultで全アクション共通の設定は記述できるのだから、listとformでindexアクション、new,editアクションの表示項目を指定すれば残りはshowアクションの表示項目だけだろう、と思いきや、実はhas_manyなどの関連を指定していると、そこの表示項目もdefaultから引き抜かれるので、結局showアクションのみの指定は出来ないんですよね。
showアクション、関連に関しても個別に指定できるようにしてほしいところです。
※ ちなみに同じように、newアクションとeditアクションでの編集項目を切り分けることもymlでは出来ない
同じ設定コピペする問題
アクセス権限などを設定する場合は
- マスターデータは基本表示だけ、管理者以外は変更できない
- ログに値するデータは管理者も含め閲覧のみ、変更はできない
という風に、役割によってテンプレート的な権限を張り付けてしまいたくなる場合があるのですが、そのときも、これを外部に変数として切り分ける、というのがYAMLだと出来ないんですよね、地味に不便です。
アクセス権限にロジックを組み込めない
例えば、データは管理者か作成した本人しか編集出来ないというアクセス権限や、自分とその部下に関するデータしか閲覧ができないというアクセス権限を作りたいときは、typus標準のアクセス権限設置方法では対処が出来ませんね。意外にYAMLで設定できる範囲を超えるユースケースが多い割には、controllerにアクセス権限に関するロジックを、直に書かないといけないのは不便に思うときも多いはず。
※ そもそも現在のバージョンでも、admin_usersテーブルの他人のパスワード変更できる状態が異常な気しかしませんね。
View編
一覧テーブルを、モデル毎にカスタマイズ出来ない。
一覧ページのカスタマイズを行っていた時に、非表示のステータスになっている行だけ、灰色背景にしたい、という要望が出た。
trタグに適切なクラスを埋め込むのが正解だが、他の画面にこのためのロジックを喰わせたくない。
なんとかして、このモデルだけ適応できないかなと思った。
結局、helper関数だけ機能を上書きして対処。
# helper関数のbuild_tableだけを上書きして、app/views/admin/users/_table.html.erb等、モデル独自に定義したtableで上書き出来るように変更をかける
unless defined?(Admin::Resources::TableHelper)
typus_helper_base_path = $LOAD_PATH.select{ |path| path.match(/typus.+\/helpers$/) }
helper_path = File.join(typus_helper_base_path, "admin/resources/table_helper.rb")
load helper_path
end
Admin::Resources::TableHelper.module_eval do
def build_table(model, fields, items, link_options = {}, association = nil, association_name = nil)
locals = {
model: model,
fields: fields,
items: items,
link_options: link_options,
headers: table_header(model, fields),
association_name: association_name,
}
render "admin/#{resource.try(:name).to_s.tableize}/table", locals
rescue ActionView::MissingTemplate
render 'helpers/admin/resources/table', locals
end
end
テーブルに依存したフィルターの書き換えができない
これも上の項目と同じで、フィルターを一つ追加したいときに、フィルターをモデル独自に作りたくなったのですが、そういう仕掛けがなかったので、フィルターを読み込んでくるhelper関数を上書きして対処。
そもそも、フィルター一個追加するためにフィルター作っている箇所全部書き換え自体がおかしいのですけどね。モデル毎にカスタマイズ可能な様に設計されていない事も問題でしょう。
# helper関数のbuild_tableだけを上書きして、app/views/admin/users/_filters.html.erb等、モデル独自に定義したtableで上書き出来るように変更をかける
unless defined?(Admin::Resources::FiltersHelper)
typus_helper_base_path = $LOAD_PATH.select{ |path| path.match(/typus.+\/helpers$/) }
helper_path = File.join(typus_helper_base_path, "admin/resources/filters_helper.rb")
load helper_path
end
Admin::Resources::FiltersHelper.module_eval do
def build_filters(resource = @resource, params = self.params)
locals = {
filters: [],
hidden_filters: [],
}
typus_filters = resource.typus_filters
locals[:filters] = typus_filters.map do |key, value|
{ key: key, value: send("#{value}_filter", key) }
end
begin
rejections = %w(controller action locale utf8 sort_order order_by subdomain)
locals[:hidden_filters] = params.dup.delete_if { |k, _| rejections.include?(k) }
render "filters", locals
rescue ActionView::MissingTemplate
if typus_filters.any?
rejections = %w(controller action locale utf8 sort_order order_by subdomain)
locals[:hidden_filters] = params.dup.delete_if { |k, _| (rejections + locals[:filters].map { |f| f[:key] }).include?(k) }
render 'helpers/admin/resources/filters', locals
end
end
end
end
showアクションで項目一つの表示を書き換えるのに全部修正
editやshowアクションで表示を行いたい場合、表示項目を一か所だけカスタマイズしたい、こればっかりは、typusの提供している機能ではカスタマイズ出来ない、というときにview自体を上書き出来るのですが、一度、すべてのカラムの呼び出し規則をバラバラに切りなおす羽目に。
下の例は、usersテーブルにprofilesというテーブルが属しているので、一つの画面に納めた例ですが
性別をセレクトボックスで変更できる様にしていたりYAMLで設定していた様な設定をhelper関数の引数にカラムの名前と、表示方法を引数として直接指定をし直す辛みを感じました。
<%= build_form(fields, form) %>
<h2><%= t("activerecord.models.profile") %></h2>
<%= form.fields_for :setting do |profile_form| %>
<%= typus_custom_template_field(:nickname, :string, profile_form) %>
<%= typus_custom_template_field(:sex, :selector, profile_form) %>
…(以下画面に含めたいカラムが続く)
<% end %>
結論:管理画面のあるべき設計手法
というわけで文句を書き連ねてきたtypusの話ですが、では、これの変りになるgemは見つかっているのか、というと見つかっていないわけで、まだこれに付き合っているのですが
※ administrateは代わりになる可能性を期待しつつまだ触っていない
typusの「すべての機能がオーバーライド可能なように設計されて、部品化されている」というコンセプトは賛成せざるおえないのです。
逆を言えば、今までのカスタマイズの辛みも、オーバーライド可能だったからこそ、それでなんとかやってこれていた箇所でしたから。
問題は、どのように部品化するべきか、という部分に関して知見が足りていないという事がtypusに対する文句になっているので、現在からの後方互換性を考えないのであれば次のような形で部品化するのが望ましいのではないのかな、と考えています。
設定はYAMLファイルではなく、専用のモデルに記述を行う。
結局のところ、YAMLにはロジックも変数も記述できないので限界が訪れるので、はじめからチューリング完全なプログラミング言語に、DSLをかぶせるのが記述性と表現力のバランスをとる最も良い方法と言えるんでしょう。
DSLはこれまでのYAMLの書式をほぼ踏襲して作成を行って、文句を言った、showアクションの表示項目や関連での表示項目の設定を可能にする、等の修正を行って、セレクトボックスの選択項目なども、こちらの方に記述を行うルールにすべきだと考えます。
そうすれば、モデルが太ることもなくなりますから。
viewはモデルの情報をなるべく遅く取得して、ブロック構造にする
typusの場合は、その設計でできるだけ早い段階で、モデルのカラムの情報とYAMLの設定ファイルの情報を取得して、その情報をhelper関数にひきまわしていく設計になっていましたが、実際に触ってみると、表示設定を引数として引き回すのが重荷になっていました。
今の設計はデバッグをやりやすいという意味では、正しいですけど、一つだけ表示項目をカスタマイズしたい、というときに実装を展開していく手間を支払う必要がある設計になっていました。
自分なりに、これを解決する方法を考えるなら、管理画面は次のように項目をツリー構造に綺麗に分けることが出来て、それぞれの項目は基本的に関係はないです。
* 画面
- 検索バー
- フィルター
* フィルター1
* フィルター2
* …
- テーブル
+ 列1
* アイテム1
* アイテム2
+ 列2
* アイテム1
* アイテム2
+ …
なら、表示するべきhtmlを作成するhelperは、表示したいデータのインスタンスとそのカラムの名前だけを引き継いで出来るだけ最後の部分で設定とモデルの情報を調べる設計にすれば、合成しやすい表示項目が出来るのではないかな、と考えます。
最後に
つまり誰かそういうの作ってくれ、以上!!
Slideshareに要約をアップロードしました