rails/PaperTrail+ActiveScaffold のバックアップ(No.2)

更新


公開メモ

概要

Rails4 でモデルの履歴情報を Paper Trail で追跡し、Active Scaffold により閲覧したい。

削除されたレコードが見えなくなっても良い場合

モデルの作成

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) =============================

編集可能になった

pure_test.png

Paper Trail による履歴管理を追加

このモデルに 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 の各カラムは履歴管理の必要がないため除外します。

編集不可能になった

trailed_test_error.png

ActionView::Template::Error (Could not find ::PaperTrail::VersionsController or ::PaperTrail::VersionController or PaperTrail::VersionsController or PaperTrail::VersionController):

Tests コントローラを編集

app/controllers/tests_controller.rb

LANG:ruby
class TestsController < ApplicationController
  active_scaffold :"test" do |conf|
    conf.columns.exclude :versions
  end
end

ActiveScaffold が versions を表示できないので除外しました。

編集可能になった

1つレコードを追加した後、編集中。

trailed_test_editing.png

モデルに履歴情報へのリンクを持たせます

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 の設定

上記履歴情報へ /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

履歴情報のコントローラ

app/controllers/test/histories_controller.rb

LANG:ruby
class Test::HistoriesController < ApplicationController
  active_scaffold :"test/history" do |conf|
  end
end

履歴情報へのリンクが追加されました

link_to_histories.png

クリックすると、

histories_list_original.png

ちゃんと履歴情報が表示されます。

履歴情報の見た目を変更

上記のままだと何が変更されたか見にくく、 また、履歴情報の編集ができてしまうので、いろいろ直します。

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

履歴部分が分かりやすくなりました

histories_list_modified.png

モデル側の調整

  • 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

/tests/1/histories への参照がエラーになるなど腑に落ちない点は残りますが、 履歴情報を閲覧するという目的に限れば、これでほぼ問題なく使えると思います。

nicely_working.png

/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 にする

ことにより解決されるようです。

削除したレコードの履歴がたどれない

削除したレコードの履歴もたどりたければ、上記では対応できません。

ではどうするかということで考えたのが以下になります。

削除されたレコードも見たい場合

上記の設定に加えて、歴史上存在した 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_test テーブルを作成する代わりに、 履歴情報を取り出すための 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) ===================

歴代レコードのモデル

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 の設定

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

とりあえず表示できるようになりました

ここまでで以下が可能です。

  • destroy されたレコードの履歴をたどる
  • 存在するレコードを更新

historical_test.png

表示を整える

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

良い感じ

historical_test_formatted.png

これで削除済みのレコードを含めて、すべての履歴を参照できるようになりました。

質問・コメント





Counter: 18912 (from 2010/06/03), today: 1, yesterday: 0