rails/PaperTrail+ActiveScaffold のバックアップソース(No.3)
更新- バックアップ一覧
- 差分 を表示
- 現在との差分 を表示
- バックアップ を表示
- ソフトウェア/rails/PaperTrail+ActiveScaffold へ行く。
[[公開メモ]] #contents * 概要 [#wf74cdc5] Rails4 でモデルの履歴情報を Paper Trail で追跡し、Active Scaffold により閲覧したい。 * 削除されたレコードが見えなくなっても良い場合 [#i772c7bc] ** モデルの作成 [#s2bf952d] Active Scaffold で管理される Test というモデルを作ります。 LANG:console $ rails g active_scaffold Test name:string email:string invoke active_record create db/migrate/20140424103245_create_tests.rb create app/models/test.rb invoke test_unit create test/models/test_test.rb create test/fixtures/tests.yml route resources :tests do as_routes end invoke active_scaffold_controller create app/controllers/tests_controller.rb create app/helpers/tests_helper.rb invoke test_unit create test/controllers/tests_controller_test.rb create app/views/tests $ rake db:migrate == 20140424103245 CreateTests: migrating ====================================== -- create_table(:tests) -> 0.0043s == 20140424103245 CreateTests: migrated (0.0044s) ============================= ** 編集可能になった [#l51cb401] &attachref(pure_test.png,,50%); ** Paper Trail による履歴管理を追加 [#m3ac8bd5] このモデルに has_paper_trail を追加して、履歴情報を保存すると共に、 ActiveScaffold からその履歴情報へアクセスするための histories というプロパティを定義します。 app/models/test.rb LANG:ruby class Test < ActiveRecord::Base has_paper_trail :ignore => [:id, :created_at, :updated_at] end ただし、id, created_at, updated_at の各カラムは履歴管理の必要がないため除外します。 ** 編集不可能になった [#i7fcd2b8] &attachref(trailed_test_error.png,,50%); ActionView::Template::Error (Could not find ::PaperTrail::VersionsController or ::PaperTrail::VersionController or PaperTrail::VersionsController or PaperTrail::VersionController): ** Tests コントローラを編集 [#qb6d56f5] app/controllers/tests_controller.rb LANG:ruby class TestsController < ApplicationController active_scaffold :"test" do |conf| conf.columns.exclude :versions end end ActiveScaffold が versions を表示できないので除外しました。 ** 編集可能になった [#r089a68a] 1つレコードを追加した後、編集中。 &attachref(trailed_test_editing.png,,50%); ** モデルに履歴情報へのリンクを持たせます [#rc6f8637] http://kingyo-bachi.blogspot.jp/2012/10/activescaffoldpapertrail.html を参考にしつつ、 多数のモデルの履歴を取るのに便利なように工夫しています。 LANG:ruby class Test < ActiveRecord::Base has_paper_trail class History < ItemHistory end has_many :histories, -> { where 'item_type = "Test"' }, foreign_key: 'item_id', class_name: 'Test::History' end app/models/item_history.rb LANG:ruby class ItemHistory < PaperTrail::Version def method_missing(action) unless self.reify.attributes.keys.index(action.to_s).nil? self.reify[action] else super end end end PaperTrail::Version には、複数のモデルの履歴がまぜこぜに追加されるので、 自分の履歴情報のみを取り出すため、item_type="Test" を指定しています。 ** routes の設定 [#y1a0afa0] 上記履歴情報へ /tests/:test_id/histories というパスでアクセスできるようにします。 コントローラ名はモデル名 Test::History に合わせなければならないため、 Test::HistoriesController とします。 config/routes.rb resources :tests do as_routes resources :histories, controller: 'test/histories' do as_routes end end ** 履歴情報のコントローラ [#c82488bd] app/controllers/test/histories_controller.rb LANG:ruby class Test::HistoriesController < ApplicationController active_scaffold :"test/history" do |conf| end end ** 履歴情報へのリンクが追加されました [#u3e87868] &attachref(link_to_histories.png,,50%); クリックすると、 &attachref(histories_list_original.png,,50%); ちゃんと履歴情報が表示されます。 ** 履歴情報の見た目を変更 [#f8397d0b] 上記のままだと何が変更されたか見にくく、 また、履歴情報の編集ができてしまうので、いろいろ直します。 app/controllers/test/histories_controller.rb LANG:ruby # coding:utf-8 class Test::HistoriesController < ApplicationController active_scaffold :"test/history" do |conf| # 編集を不可にする conf.actions.exclude :create, :update, :delete # 履歴に日時を表示する conf.columns.add :created_at conf.columns[:created_at].label = "Time" # リストに表示する内容と順番を指定 conf.list.columns= [:item, :event, :created_at, :object_changes] # 検索をしやすくする conf.actions.exclude :search conf.actions.add :field_search conf.field_search.columns = [:item_id,:event,:created_at] end end app/helpers/test/histories_helper.rb LANG:ruby module Test::HistoriesHelper def test_history_created_at_column(record, column) # record.created_at.in_time_zone(@user.time_zone) のようにしても良い record.created_at ? record.created_at.localtime.strftime('%m/%d %H:%M') : '' end def test_history_object_changes_column(record, column) # まるっきり美しくないけれどとりあえず result = "<table>" record.changeset.each do |k,v| result += "<tr><td style=\"text-align:right;border:0px;width:100px;\">"+ "<b>#{k}</b><td style=\"border:0px;\">#{v}</tr>\n" end result += "</table>" result.html_safe end def test_history_item_column(record, column) "#"+record.item_id.to_s end end ** 履歴部分が分かりやすくなりました [#g2eb8780] &attachref(histories_list_modified.png,,50%); ** モデル側の調整 [#vcc49aa0] - histories を編集不能にする - created_at, updated_at を表示する - histories の見た目を調整 app/controllers/tests_controller.rb LANG:ruby class TestsController < ApplicationController active_scaffold :"test" do |conf| conf.columns.exclude :versions conf.show.columns.add :created_at, :updated_at conf.create.columns.exclude :histories conf.update.columns.exclude :histories end end app/helpers/tests_helper.rb LANG:ruby module TestsHelper def test_updated_at_column(record, column) record.updated_at ? record.updated_at.localtime.strftime('%m/%d') : '' end def test_created_at_column(record, column) record.created_at ? record.created_at.localtime.strftime('%m/%d') : '' end def test_histories_column(record, column) "# " + record.versions.size.to_s end end ** だいたいOK [#qc347365] /tests/1/histories への参照がエラーになるなど腑に落ちない点は残りますが、 履歴情報を閲覧するという目的に限れば、これでほぼ問題なく使えると思います。 &attachref(nicely_working.png,,50%); /tests/1/histories への参照は、 No route matches {:action=>"edit", :association=>:item, :controller=>"test/tests", :id=>"--CHILD_ID--", :parent_scaffold=>"test/histories", :test_id=>"1", :version_id=>"--ID--"} というエラーを生じました。{:controller=>"test/tests"} がまずいのですが、 正攻法での回避方法は分かりませんでした。 この問題はクラス階層を正しく扱えないことが原因なので、 - 履歴モデルを Test::History ではなく TestHistory にする - コントローラ等を Test::Histories ではなく TestHistories にする ことにより解決されるようです。 ** 削除したレコードの履歴がたどれない [#c92859b1] 削除したレコードの履歴もたどりたければ、上記では対応できません。 ではどうするかということで考えたのが以下になります。 * 削除されたレコードも見たい場合 [#u89cbff5] 上記の設定に加えて、歴史上存在した Test レコードをすべて一覧する、 HistoricalTest モデルを作成します。 LANG:console $ rails g active_scaffold HistoricalTest invoke active_record create db/migrate/20140424121702_create_historical_tests.rb create app/models/historical_test.rb invoke test_unit create test/models/historical_test_test.rb create test/fixtures/historical_tests.yml route resources :historical_tests do as_routes end invoke active_scaffold_controller create app/controllers/historical_tests_controller.rb create app/helpers/historical_tests_helper.rb invoke test_unit create test/controllers/historical_tests_controller_test.rb create app/views/historical_tests データベースには historical_tests テーブルを作成する代わりに、 履歴情報を取り出すための view を同名で作成します。 db/migrate/20140424121702_create_historical_tests.rb LANG:ruby # coding: utf-8 class CreateHistoricalTests < ActiveRecord::Migration def up # 各レコードに対する最後の更新履歴を取り出す execute <<-SQL CREATE VIEW historical_tests AS SELECT versions.id, versions.created_at as last_event_at, versions.event as last_event, versions.item_id, tests.* FROM versions LEFT JOIN tests ON tests.id=versions.item_id WHERE versions.id IN (SELECT MAX(id) FROM versions WHERE item_type = "Test" GROUP BY item_id); SQL end def down execute <<-SQL DROP VIEW historical_tests; SQL end end LANG:console $ rake db:migrate == 20140424121702 CreateHistoricalTests: migrating ============================ -- execute(" CREATE VIEW historical_tests AS \n SELECT versions.id, versions.created_at as last_event_at, versions.event as last_event, \n versions.item_id, tests.* FROM versions \n LEFT JOIN tests ON tests.id=versions.item_id\n WHERE versions.id IN (SELECT MAX(id) FROM versions WHERE item_type = \"Test\" GROUP BY item_id);\n") -> 0.0004s == 20140424121702 CreateHistoricalTests: migrated (0.0005s) =================== ** 歴代レコードのモデル [#bdde6f1d] Test と Test::History への参照を持たせます。 app/models/historical_test.rb LANG:ruby class HistoricalTest < ActiveRecord::Base belongs_to :test, foreign_key: 'item_id' has_many :histories, -> { where 'item_type = "Test"' }, primary_key: 'item_id', foreign_key: 'item_id', class_name: 'Test::History' end ** routes の設定 [#tad85ef8] config/routes.rb LANG:ruby resources :tests do as_routes end resources :historical_tests do as_routes resources :histories, controller: 'test/histories' do as_routes end end ** とりあえず表示できるようになりました [#i48d78eb] ここまでで以下が可能です。 - destroy されたレコードの履歴をたどる - 存在するレコードを更新 &attachref(historical_test.png,,50%); ** 表示を整える [#zf558dd6] app/controller/historical_tests_controller.rb LANG:ruby class HistoricalTestsController < ApplicationController active_scaffold :"historical_test" do |conf| conf.actions.exclude :search,:create,:update,:delete,:show conf.columns.exclude 'id:1' conf.actions.add :field_search conf.field_search.columns.exclude :test,:histories conf.columns[:test].actions_for_association_links = [:show] conf.columns[:last_event_at].label = 'Modified' conf.list.columns= [:item_id, :test, :last_event_at, :histories] end end app/helper/historical_tests_helper.rb LANG:ruby module HistoricalTestsHelper def historical_test_last_event_at_column(record, column) record.last_event_at ? record.last_event_at.localtime.strftime('%y/%m/%d %H:%M') : '' end def historical_test_test_column(record, column) if record.last_event.to_s == 'destroy' "-- destroyed --" else "#{record.test.name} <#{record.test.email}>" end end def historical_test_histories_column(record, column) "# #{record.histories.count}" end def last_event_search_column(record, input_name) select :record, :last_event, options_for_select(['create','update','destroy']), {:include_blank => as_(:_select_)}, input_name end end ** 良い感じ [#faf5fe70] &attachref(historical_test_formatted.png,,50%); これで削除済みのレコードを含めて、すべての履歴を参照できるようになりました。 * 質問・コメント [#t616f744] #article_kcaptcha
Counter: 18970 (from 2010/06/03),
today: 1,
yesterday: 0