rails/PaperTrail+ActiveScaffold のバックアップの現在との差分(No.15)

更新


  • 追加された行はこの色です。
  • 削除された行はこの色です。
[[公開メモ]]

#contents

* 概要 [#wf74cdc5]

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

** Rails の管理画面と言えば rails_admin [#h76532c9]

[[rails_admin>https://github.com/sferik/rails_admin]] を使うと、Rails で動く Web アプリケーションに、データ管理画面を簡単に作成できます。

** データの更新履歴を残したい paper_trail + paranoia [#yf81ef16]

一方で、

- [[paper_trail>https://github.com/airblade/paper_trail]]: データの更新履歴を取る
- [[paranoia>https://github.com/radar/paranoia]]: 削除済みデータを閲覧・復帰可能にする

を用いると、レコードの各カラムの変更履歴を残し、さらに削除済みのデータを閲覧可能になります。

** データ更新履歴を見られる rails_admin ってないの? [#c2673708]

paper_trail + paranoia に対応した rails_admin (みたいなもの)って無いんでしょうか?

見つけられなかったため、そういう管理画面を active_scaffold で作ろうともがいているのがこの記事です。

** active_scaffold [#v10db601]

[[active_scaffold>https://github.com/activescaffold/active_scaffold]] を使うことで、レコードに対する更新履歴のような
has_many 子要素まで対応可能な閲覧・編集画面を簡単に作成できます。

** ということで [#w821c2a1]

paper_trail + paranoia でデータの更新履歴を残しつつ、
そのデータを active_scaffold で閲覧・編集できるようにしようともがいているのが以下の記事です。

そこそこ動くようになったところから読みたい方は、[[無理矢理ライブラリ化してみた>#l2259cc9]] からどうぞ。

なんかいろいろ車輪の再発明をしている気がするのですが、
こういう機能を簡単に持たせることって、もっと簡単にできたりしないんでしょうか???

** 考えてみると [#z06bdbb9]

active_scaffold で1から作るという方向の他に、

http://qiita.com/joker1007/items/4ac31f081c44634a5e90 などを参考にして rails_admin 
を paper_trail + paranoia に対応させるという方向もあって、

考えてみるとそちらの方が需要多かったのかもしれないです?いまさらですが orz

一応、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

 LANG:ruby
 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%);

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

* 削除されたレコードも見たい2 [#ed5d780c]

acts_as_paranoid を入れて論理削除を使えばもっと簡単になるのかも。

ということで、paranoia を入れてみました -> https://github.com/radar/paranoia

1つ問題になるのは、~
https://groups.google.com/forum/#!topic/activescaffold/_ocZQgVjLl8 ~
にあるように、ActiveScaffold で default_scope を無視した表示がしにくいことのようです。

** acts_as_paranoid の設定 [#ad305b63]

 LANG:console
 $ rails generate migration AddDeletedAtToTests deleted_at:datetime:index
 $ rake db:migrate

app/models/test.rb

 LANG:ruby
 class Test < ActiveRecord::Base
   acts_as_paranoid
   validates :email, presence: true, uniqueness: {scope: :deleted_at}
 
   ...
 
 end

acts_as_paranoid と validates uniqueness を付け加え、
削除と、削除済みを考慮した重複検出が行えていることを確認できました。

** 削除済みレコードを表示する [#g0902b38]

このままだと削除されたレコードが一覧に出てこないので、

app/controllers/tests_controller.rb

 LANG:ruby
 class TestsController < ApplicationController
 
   ...
   
   def beginning_of_chain
     super.unscoped
   end
 
 end

とすることで、削除済みのアイテムを表示できました。

*** 2014-10-28 追記 [#t9278ff4]

上記だと親レコードからのリンクで子レコードの一覧を表示する際などに
「親レコードの子である」という制約も unscoped で外れてしまい、
全件が表示されてしまうという問題が発生することが分かりました。

 LANG:ruby
     super.unscoped

の代わりに

 LANG:ruby
     nested? ? super : super.unscoped

とするのが良いようでした。

** histories を一覧 [#s25fec03]

上記だけだと histories のリンクで Request Failed となってしまいます。

 LANG:console
 Started GET "/tests/4/histories?association=histories&parent_scaffold=tests&adapter=_list_inline_adapter" for 192.168.133.1 at 2014-04-27 14:50:44 +0900
 Processing by Test::HistoriesController#index as JS
   Parameters: {"association"=>"histories", "parent_scaffold"=>"tests", "adapter"=>"_list_inline_adapter", "test_id"=>"4"}
   Test Load (0.2ms)  SELECT "tests".* FROM "tests" WHERE "tests"."deleted_at" IS NULL AND "tests"."id" = ? LIMIT 1  [["id", "4"]]
 Completed 404 Not Found in 3ms
 
 ActiveRecord::RecordNotFound (Couldn't find Test with id=4 [WHERE "tests"."deleted_at" IS NULL]):
   activerecord (4.0.4) lib/active_record/relation/finder_methods.rb:199:in `raise_record_not_found_exception!'
   activerecord (4.0.4) lib/active_record/relation/finder_methods.rb:285:in `find_one'
   activerecord (4.0.4) lib/active_record/relation/finder_methods.rb:269:in `find_with_ids'
   activerecord (4.0.4) lib/active_record/relation/finder_methods.rb:36:in `find'
   activerecord-deprecated_finders (1.0.3) lib/active_record/deprecated_finders/relation.rb:122:in `find'
   activerecord (4.0.4) lib/active_record/querying.rb:3:in `find'
   /home/osamu/.bundler/ruby/2.0.0/active_scaffold-6b895cd14a2a/lib/active_scaffold/finder.rb:325:in `find_if_allowed'
   /home/osamu/.bundler/ruby/2.0.0/active_scaffold-6b895cd14a2a/lib/active_scaffold/actions/nested.rb:97:in `nested_parent_record'

lib/active_scaffold/actions/nested.rb:97:in `nested_parent_record' で find_if_allowed 
に渡されている nested.parent_model にスコープが付いているのがいけないようなので、

 LANG:ruby
 class Test::HistoriesController < ApplicationController
 
   ...
 
   def nested_parent_record(crud = :read)
 #   @nested_parent_record ||= find_if_allowed(nested.parent_id, crud, nested.parent_model)
     @nested_parent_record ||= find_if_allowed(nested.parent_id, crud, nested.parent_model.unscoped)
   end
 
 end

としたところ、ちゃんと表示されるようになりました。

&attachref(acts_as_paranoid.png,,50%);

** delete 済みのレコードに edit と delete のアクションが表示される件 [#d048d220]

2つの方法があるようです。

*** authorized_for で無効にする [#u527a884]

app/models/test.rb

 LANG:ruby
 class Test < ActiveRecord::Base
   ...
    
   def authorized_for_delete?
     not deleted_at
   end
   
    def authorized_for_update?
      not deleted_at
   end
 
 end

とすることで、Edit や Delete の文字は残しつつ、
グレーアウトさせることができました。

*** skip_action_link? でフィルター [#t214ef36]

アクションを完全に消すには、
skip_action_link? でフィルターしてやればいいようです。

呼び出される際は、args[0] にレコードが渡されたり渡されなかったりするので、
渡された場合かつ deleted_at が null でない場合かつ crud_type が read 以外の場合に
表示を非許可にしました。

app/helpers/tests_helper.rb

 LANG:ruby
 module TestsHelper
 
   ...
    
   def skip_action_link?(link, *args)
     (args.size > 0) and args[0].respond_to?(:deleted_at) and args[0].deleted_at and (link.crud_type!=:read)
   end
 
 end

これで、削除済みのレコードに対してのみ Show 以外のアクションを完全に消せました。

** 削除時にレコードが消えてしまう [#u96f4983]

削除したレコードも表示して欲しいので、
削除成功時には行を消去する代わりに表示し直すことにします。

削除失敗時にはエラーメッセージを表示するため、普通に 'destroy' を呼びます。

app/controllers/tests_controller.rb

 class TestsController < ApplicationController
   ...
 
   protected
 
   def destroy_respond_to_js
     if successful?
       render :action => 'update_row'
     else
       # エラーメッセージを表示する
       render :action => 'destroy'
     end
   end
 
 end

** 削除済みのレコードに Restore アクションを追加する [#aa1a8d15]

app/controllers/tests_controller.rb

 LANG:ruby
 class TestsController < ApplicationController
   active_scaffold :"test" do |conf|
     ...
  
     conf.action_links.add 'restore', :label => 'Restore', :type => :member, 
             :confirm => 'Are you sure you want to restore the record?', 
             :method => :put, :position => :false
   end
   ...
 
   def restore
 
   end
  
 end

config/routes.rb

 LANG:ruby
   resources :tests do 
     member do
       get 'restore'
     end
     as_routes
     resources :histories, controller: 'test/histories' do
       as_routes
     end
   end

app/helpers/tests_helper.rb

 LANG:ruby
 module TestsHelper
   ...
 
   def skip_action_link?(link, *args)
     return false unless (args.size > 0) and args[0].respond_to?(:deleted_at)
     
     return true if args[0].deleted_at and (link.action=='destroy' or link.action=='edit')
     return true if not args[0].deleted_at and (link.action=='restore')
   end
 end

これで、削除済みのレコードにのみ Restore が表示されました。

TestsController::restore の中身は
active_scaffold/lib/active_scaffold/actions/update.rb
を参考にして、以下のようにすれば良いようでした。

app/controllers/tests_controller.rb

 LANG:ruby
 class TestsController < ApplicationController
   ...
 
   def restore
     do_restore
     respond_to_action(:restore)
   end
  
   protected
 
   def do_restore
     do_edit
     @record.deleted_at = nil
     update_save(no_record_param_update: true)
   end
 
   def restore_respond_to_js
     update_row
   end
 
   def destroy_respond_to_js
     # Update row instead of deleting
     update_row
   end
 
   def update_row
     if successful?
       render :action => 'update_row'
     else
       # エラーメッセージを表示する
       render :action => 'destroy'
     end
   end
 
 end

これで、

- acts_as_paranoid による restore 可能な削除を実現
- 作成・編集・削除・リストアすべてを paper_trail で追跡
- 作成・編集・削除・リストア・履歴閲覧が ActiveScaffold 上で可能

になりました。

&attachref(acts_as_paranoid2.png,,50%);

リストア時に uniqueness 制約に引っかかれば、ちゃんとエラーメッセージも表示されます。

* 無理矢理ライブラリ化してみた [#l2259cc9]

複数のモデルの履歴情報を管理者画面で閲覧するのに、
個々のモデルに上記の Controller/Helper を実装するのはばからしいので、
上記を無理矢理ライブラリ化しました。

ruby の開発に慣れていないため随所に常識破りがあると思いますがご容赦ください。
こうすればいいのに、などアドバイスをいただけると大歓迎です。

config/initializers/trailed_paranoid_scaffold.rb
 LANG:ruby(linenumber)
 # autoload によりライブラリを読み込む
 TrailedParanoidScaffold

lib/trailed_paranoid_scaffold.rb

 LANG:ruby(linenumber)
 # coding: utf-8
 #
 # Osamu Takeuchi <osamu@big.jp> 
 #
 # 2014.05.02 初期リリース
 # 2014.05.16 build, as_tpsf_routes を実装した
 #            ヘルパにユーティリティ関数を定義した
 #            as_tpsf_routes のコードをファイル末尾に取り込んだ
 # 2014.05.20 const_get 関連の改善
 # 2014.10.28 nested リスト表示の際の検索条件を正しくした
 #
 module TrailedParanoidScaffold
 
   #
   # Model に include する
   #
   module Model
     def self.included(base)
       base.class_eval do
         # 論理削除を使う
         acts_as_paranoid
         
         # 履歴情報を持たせる
         if @paper_trail_ignore
             has_paper_trail :ignore => @paper_trail_ignore
         else
             has_paper_trail :ignore => [:id, :created_at, :updated_at]
         end
         
         # http://stackoverflow.com/questions/3194290/why-cant-there-be-classes-inside-methods-in-ruby
         base.const_set :History, Class.new(PaperTrailHistory) do
         end
   
         has_many :histories, -> { where "item_type = \"#{base.to_s}\"" },
             foreign_key: 'item_id', class_name: "#{base.to_s}::History"
       end
     end
   end
 
   class PaperTrailHistory < PaperTrail::Version
     def method_missing(action)
       unless self.reify.attributes.keys.index(action.to_s).nil?
         self.reify[action]
       else
         super
       end
     end
   end
 
   #
   # ModelsController に include する
   #
   module Controller
   
     module ClassMethods
       def active_scaffold(model_id, &block)
         super(model_id) do |conf|
           conf.columns.exclude :versions
           conf.show.columns.add :created_at, :updated_at
           conf.create.columns.exclude :histories, :deleted_at
           conf.update.columns.exclude :histories, :deleted_at
           conf.list.columns.add :updated_at
           conf.action_links.add 'restore', :label => 'Restore', :type => :member, 
                   :confirm => 'Are you sure you want to restore the record?', 
                   :method => :put, :position => :false
           block.call(conf)
         end
       end
     end
   
     def self.included(base)
       base.class_eval do
         extend ClassMethods
   
         # 削除を元に戻すアクション
         def restore
           do_restore
           respond_to_action(:restore)
         end
         
         protected
         
         # 削除済みのレコードも表示する
         def beginning_of_chain
           nested? ? super() : super().unscoped
         end
   
         # acts_as_paranoid で deleted_at が立てられたレコードを復帰させる
         def do_restore
           do_edit
           @record.deleted_at = nil
           update_save(no_record_param_update: true)
         end
         
         def restore_respond_to_js
            update_row
         end
   
         def destroy_respond_to_js
           # Update row instead of deleting
           update_row
         end
   
         def update_row
           if successful?
             # 行を更新
             render :action => 'update_row'
           else
             # エラーメッセージを表示する
             render :action => 'destroy'
           end
         end
       end
     end
     
   end
 
   #
   # ModelsHelper に include する
   #
   module Helper
 
     # 削除済みのレコードに Edit および Delete のリンクを付けない
     # 削除済みのレコードだけに Restore のリンクを付ける
     def skip_action_link?(link, *args)
       return false unless (args.size > 0) and args[0].respond_to?(:deleted_at)
       return true if args[0].deleted_at and (link.action=='destroy' or link.action=='edit')
       return true if not args[0].deleted_at and (link.action=='restore')
     end
     
     def self.included(base)
       base.class_eval do
         # ヘルパークラス名( = SomeScope::SomeModelsHelper )からモデル名を取り出す
         model_id = base.to_s.split(/::/).last.gsub(/Helper$/,'').underscore.singularize
 
         # histories の表示を "# 数字" にする
         define_method("#{model_id}_histories_column") do |record, column|
           "# " + record.versions.size.to_s
         end
       end
     end
   
   end
 
   #
   # HistoriesController に include する
   #
   module HistoriesController
   
     module ClassMethods
       def active_scaffold(model_id, &block)
         super model_id 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]
           
           block.call(conf)
         end
       end
     end
   
     def self.included(base)
       base.class_eval do
         extend ClassMethods
         
         protected
         
         # 逆順で表示
         def custom_finder_options
           {reorder: "id DESC"}
         end
 
         # 親クラスは削除済みのレコードも検索する
         def nested_parent_record(crud = :read)
           @nested_parent_record ||= 
               find_if_allowed(nested.parent_id, crud, nested.parent_model.unscoped)
         end
       end
     end
     
   end
   
   #
   # HistoriesHelper に include する
   #
   module HistoriesHelper
   
     def self.included(base)
       base.class_eval do
         # ヘルパークラス名 (= SomeScope::SomeModel::HistoriesHelper) からモデル名を取り出す
         model_id = base.to_s.gsub(/::(?=[^:]+$)|Helper$/,'').sub(/.*::/,'').underscore.singularize
       
         # 更新履歴の表示形式
         define_method("#{model_id}_object_changes_column") do |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
       end
     end
     
   end
   
   #
   # Controller, Helper を作成する関数
   #
   # app/controllers/admin/models_controller.rb
   # app/helpers/admin/models_helper.rb
   # app/controllers/admin/model/histories_controller.rb
   # app/helpers/admin/model/histories_helper.rb
   #
   # といったファイルから、
   #
   # TrailedParanoidScaffold::build(__FILE__) do
   # end
   #
   # の形で呼び出すことを想定している。
   #
   def self.build(file, parent = nil, &block)
     file = File.expand_path(file).sub(/\.rb\z/,'')
     unless file =~ /([^\/]+)(\/histories)?_(controller|helper)\z/
       throw "unexpected file name #{file}"
     end
 
     # 必要な情報をファイル名から取り出す
     model = $1                  # "model" or "models"
     histories = $2              # "histories" or nil
     controller_or_helper = $3   # "controller" or "helper"
     model = model.singularize unless histories
     
     *path, name = file.
              sub(/.*\/app\/#{controller_or_helper}s\//,'').
              split(File::Separator).
              map{|s| s.classify}
 
     # 無名クラス・モジュールを作成
     if controller_or_helper == "controller"
       new_class = Class.new(parent)
     else
       new_class = Module.new()
     end
 
     # Admin::ModelsController
     # Admin::ModelsHelper
     # Admin::Model::HistoriesController
     # Admin::Model::HistoriesHelper
     # といった定数に結びつける
     path.reduce(Object){|obj, p| obj.const_get(p)}.const_set name, new_class
     
     # 新しく作ったクラス・モジュールに必要な機能を追加
     new_class.class_eval do
       # TrailedParanoidScaffold::Controller
       # TrailedParanoidScaffold::Helper
       # TrailedParanoidScaffold::HistoriesController
       # TrailedParanoidScaffold::HistoriesHelper
       # を include する
       include TrailedParanoidScaffold.const_get(
           (histories ? "Histories" : "") + controller_or_helper.classify
       )
       
       # モデル名も計算しておく
      if controller_or_helper == "controller"
        @active_scaffold_model =
                histories ? "#{model}/history".to_sym : model.to_sym
      else
        # ヘルパにユーティリティ関数を定義する
        # 例えば def_column を使うと、
        #   def_column(:column_name) do |record, column|
        #     # def {record_name}_{column_name}_column(record, column)
        #     # として定義するのと同じ結果を得られる
        #   end
        [:column, :column_attributes, 
            :form_column, :show_column, :search_column].each do |fname|
          new_class.class_eval <<-"EOS"
            def self.def_#{fname}(name, &block)
              define_method :"#{model.underscore}_\#{name}_#{fname}", &block
            end
          EOS
        end
 
      end
       
       # 与えられたブロックをクラス・モジュールのスコープで評価
       new_class.class_eval &block
     end
 
   end
 
 end
 
 module ActionDispatch
   module Routing
     class Mapper
       module Base
       
         # trails_paranoid_scaffold 用の as_routes
         #
         # options には、
         #   options[:histories_resources] = {}
         #   options[:histories_routes] = {}
         # の形で、下位層へのオプションも与えられる
         #
         def as_tpsf_routes(options = {:association => true})
           options = options.dup.stringify_keys
 
           histories_resources = { 
             controller: "#{@scope[:controller].singularize}/histories"
           }.merge(options.delete(:histories_resources) || {})
 
           histories_routes = { 
             :association => true
           }.merge(options.delete(:histories_routes) || {})
           
           
           ### ここからが実際のルート指定
           
           # active_scaffold の基本
           as_routes options
           
           # 消去済みレコードの復帰用アクション
           member do 
             put 'restore' 
           end
 
           # histories へのルート
           resources :histories, histories_resources do
             as_routes histories_routes
           end
           
         end
 
       end
     end
   end
 end

** 想定する利用方法 [#id4456c4]

config/application.rb
 LANG:ruby(linenumber)
     # lib 以下のモジュールを自動ロードする
     config.autoload_paths += %W(#{config.root}/lib)

app/models/some_model.rb
 LANG:ruby(linenumber)
 # coding:utf-8
 class SomeModel < ActiveRecord::Base
 
   # 必要に応じて、paper_trail で無視する column を指定する
   # 指定しなければ [:id, :created_at, :updated_at] になる
   @paper_trail_ignore = [:id, :created_at, :updated_at, :some_other_column]
 
   # 論理削除+履歴情報 を持たせる
   include TrailedParanoidScaffold::Model 
 
   # もし uniqueness が必要な場合には必ず scope: :deleted_at を付ける
   validates :some_unique_column, presence: true, uniqueness: {scope: :deleted_at}
  
 end

SomeModel に act_as_paranoid 用のための deleted_at カラムを追加する
 LANG:console
 $ rails generate migration AddDeletedAtToSomeModels deleted_at:datetime:index
 $ rake db:migrate

config/routes.rb
 LANG:ruby(linenumber)
   resources :some_models do
     # TrailedParanoidScaffold 用のルート指定
     as_tpsf_routes
   end

app/controllers/some_models_controller.rb
 LANG:ruby(linenumber)
 # coding:utf-8
 class SomeModelsController < ApplicationController
   # 上記の機能を取り込む
   include TrailedParanoidScaffold::Controller
 
   active_scaffold :"some_model" do |conf|
     # 追加で必要な設定があればここで行う
   end
   # 必要ならここでも
 end

app/helpers/some_models_helper.rb
 LANG:ruby(linenumber)
 # coding:utf-8
 module SomeModelsHelper
   # 上記の機能を取り込む
   include TrailedParanoidScaffold::Helper
 
   # 日付表示の設定などがあればここで行う
   def test_updated_at_column(record, column)
     record.updated_at ? record.updated_at.localtime.strftime('%m/%d') : ''
   end
 
 end 

app/controllers/some_model/histories_controller.rb
 LANG:ruby(linenumber)
 # coding:utf-8
 # ソースファイル名を元に自動でコントローラを設定する
 # 親にしたいコントローラを第2引数として与えることができる
 # 第2引数を省略すれば ApplicationController が親になる
 class SomeModel::HistoriesController < ApplicationController
   # 上記の機能を取り込む
   include TrailedParanoidScaffold::HistoriesController
 
   active_scaffold :"some_model/history" do |conf|
     # 追加の設定があればここで
   end
   # 必要ならここでも
 end

app/helpers/some_model/histories_helper.rb
 LANG:ruby(linenumber)
 # coding:utf-8
 module SomeModel::HistoriesHelper
   # 上記の機能を取り込む
   include TrailedParanoidScaffold::HistoriesHelper
 
   # 日付表示の設定など必要があれば別途
   def some_model_history_created_at_column(record, column)
     record.created_at ? record.created_at.localtime.strftime('%m/%d %H:%M') : ''
   end
   
 end

* TrailedParanoidScaffold.build を使って簡単に書く [#z6de664d]

モデル数が増えてくると、各モデル毎にコントローラ、
ヘルパを2個ずつ書くのが面倒になるので、
TrailedParanoidScaffold::build という関数を実装しました。

コントローラ、ヘルパを実装すべき、

- app/controllers/some_models_controller.rb
- app/helpers/some_models_helper.rb
- app/controllers/some_model/histories_controller.rb
- app/helpers/some_model/histories_helper.rb

のようなファイルからこの関数を _FILE_ を与えて呼び出せば、それぞれ

- SomeModelsController
- SomeModelsHelper
- SomeModel::HistoriesController
- SomeModel::HistoriesHelper

を適切に設定できます。これを使うことでソースファイル内にモデル名を
書かずに済むので、多数のモデルに対してコピペでコントローラ・ヘルパを量産できます。

本当はこれ専用の rails generate ができるようになればその方が良いのかもしれませんが、
現状ではそこまで手が回らなかったので、その場しのぎです。

app/controllers/some_models_controller.rb
 LANG:ruby(linenumber)
 # coding:utf-8
 # ソースファイル名を元に自動でコントローラを設定する
 # 親にしたいコントローラを第2引数として与える
 # 第2引数を省略すれば ApplicationController が親になる
 TrailedParanoidScaffold::build(__FILE__, AdminController) do
   active_scaffold @active_scaffold_model do |conf|
     # 追加で必要な設定があればここで行う
   end
   # 必要ならここでも
 end

app/helpers/some_models_helper.rb
 LANG:ruby(linenumber)
 # coding:utf-8
 # ソースファイル名を元に自動でヘルパーを設定する
 TrailedParanoidScaffold::build(__FILE__) do
   
   # 日付表示の設定などがあればここで行う
   def test_updated_at_column(record, column)
     record.updated_at ? record.updated_at.localtime.strftime('%m/%d') : ''
   end
 
 end 

app/controllers/some_model/histories_controller.rb
 LANG:ruby(linenumber)
 # coding:utf-8
 # ソースファイル名を元に自動でコントローラを設定する
 # 親にしたいコントローラを第2引数として与えることができる
 # 第2引数を省略すれば ApplicationController が親になる
 TrailedParanoidScaffold::build(__FILE__, AdminController) do
   active_scaffold @active_scaffold_model do |conf|
     # 追加の設定があればここで
   end
   # 必要ならここでも
 end

app/helpers/some_model/histories_helper.rb
 LANG:ruby(linenumber)
 # coding:utf-8
 # ソースファイル名を元に自動でヘルパーを設定する
 TrailedParanoidScaffold::build(__FILE__) do
 
   # 日付表示の設定など必要があれば別途
   def some_model_history_created_at_column(record, column)
     record.created_at ? record.created_at.localtime.strftime('%m/%d %H:%M') : ''
   end
   
 end

caller 情報を使えば _FILE_ を渡さなくて良い仕様にできるのですが、
それだとさすがに黒魔術になりすぎると思い、意図的に _FILE_ を渡すようにしています。

** AdminController の下で動かす場合 [#h95a093f]

この機能を使うのは主に管理者画面だと思うので、

- /admin/some_models
- /admin/another_models

のように、admin パスの下で動かす需要も多いかもしれません。

その場合には以下のようにします。

*** ファイル構成 [#vf5cc95a]

ファイル構成は以下の通りです。

- config/routes.rb
- app/controllers/admin_controller.rb
- app/controllers/admin/some_models_controller.rb
- app/controllers/admin/some_model/histories_controller.rb
- app/controllers/admin/another_models_controller.rb
- app/controllers/admin/another_model/histories_controller.rb
- app/helpers/admin/some_models_controller.rb
- app/helpers/admin/some_model/histories_controller.rb
- app/helpers/admin/another_models_controller.rb
- app/helpers/admin/another_model/histories_controller.rb

*** ルーティング & AdminController [#h4f60e41]

config/routes.rb
 LANG:ruby(linenumber)
 namespace 'admin' do
   resources :some_models do 
     as_tpsf_routes
   end
   resources :another_models do
     as_tpsf_routes
   end
 end

app/controllers/admin_controller.rb
 LANG:ruby(linenumber)
 class AdminController < ApplicationController
   # 管理者のみがアクセス可能なようにする
   before_filter :assert_admin
 end

*** これだけ [#ya3fd174]

変更点はこれだけです。というのも、

- app/controllers/admin/XXXXs_controller.rb
- app/helpers/admin/XXXXs_controller.rb
- app/controllers/admin/XXXX/histories_controller.rb
- app/helpers/admin/XXXX/histories_controller.rb

の中身は上記と同じでOKなのです。

builder 関数がそれぞれファイル名から適切に判断して、

- class Admin::XXXXsController < AdminController
- module Admin::XXXXsHelper
- class Admin::XXXX::HistoriesController < AdminController
- module Admin::XXXX::HistoriesHelper

を実装してくれます。

XXXXsController, HistoriesController が AdminController を継承しているのは
builder の第2引数に AdminController を指定しているためです。

これを使えば、たとえば履歴を残したいモデルが10個程度あっても
1時間もかからずに管理画面を作成できると思います。

* その他の Tips [#u145463c]

** 削除済みレコードを淡色表示 [#ge63559b]

削除済みレコードを見やすく(見にくく)するために、
ApplicationHelper に column_attributes を仕込み、

app/controllers/application_controller.rb
 LANG:ruby
 module ApplicationHelper
   ...
 
   def column_attributes(column, record)
     if record.respond_to?(:deleted_at) and record.deleted_at
       super
       column.css_class << "record_deleted"
 #      { :record_deleted => true }.merge(super)  # attribute を設定する場合
     else
       super
     end
   end
   ...
 
 end

css で薄くできました。

app/assets/stylesheets/application.css.less
 LANG:css
 div.as_content table {
 
   .record_deleted {
     opacity: 0.3;
   }
 
 /* attribute を設定する場合
   [record_deleted="true"] {
     opacity: 0.3;
   }
 */
 }

ただこれだと、データ管理画面以外で active_scaffold を使っていると
バッティングする可能性があります。

例えば AdminHelper に column_attributes を置いて、
取り込んで使えればよいと思うのですが、
include などで取り込んだ際の method_missing の動作を良く理解していないため、
正しいやり方を見つけられていません。

** 子要素の数でソートする [#g15055d8]

has_many: subitems を持つモデルについて、list の表示を subitems.count でソートしたい場合、
[[sort_by>https://github.com/activescaffold/active_scaffold/wiki/API%3A-Column#sort_by]] の
method オプションを使えば次のように簡単にできるのだけれど、

 LANG:ruby
   active_scaffold tpsf.model_id do |conf|
     conf.columns[:subitems].sort_by :method => 'subitems.count'
   end

これでは無駄なクエリが乱発されるので、大きなテーブルに対してはパフォーマンスに問題が生じそうです。

これを1回の sql 発行で済ますには、
[[beginning_of_chain>https://github.com/activescaffold/active_scaffold/wiki/API%3A-List#beginning_of_chain-controller-method-v23]] をいじって LEFT OUTER JOIN で 
subitem 数を求める方法があるようです。

app/controllers/some_models_controller.rb
 LANG:ruby
 TrailedParanoidScaffold::build(__FILE__, AdminController) do
 
   def beginning_of_chain
     # subitems_count というカラムを追加しておく
    super().unscoped.joins(:presentations).select('accounts.*').
            select('COUNT(presentations.id) AS presentations_count')
   end
 
   active_scaffold @active_scaffold_model do |conf|
     # 追加した subitems_count でソートする
     conf.columns[:subitems].sort_by :sql => 'subitems_count'
   end
 
 end

同様に、beginning_of_chain にて .select を使えば、
テーブルにはないカラムを好きなだけ追加できるので、
いろいろ凝ったこともできそうです。

** 削除済みレコードから親レコードへのリンクを表示しない [#y040d177]

削除済み deleted_at!=NULL のレコードから belongs_to 経由で親へのリンクをたどろうとすると、
自身がすでに削除されていることから、親レコードが見付からないというエラーになってしまいます。

そこで以下のように、削除済みレコードからは親へのリンクを張らないようにしました。
削除されていなければ、表示用のリンクを張ります。

app/helpers/subitems_helper.rb
 LANG:ruby
  def subitem_parent_model_column do |rec, col|
    if rec.deleted_at
      # 一時的に無効にする良い方法が分からなかったため
      # このようにしている
      col.set_link ->(dummy){ nil }
    else
      col.set_link :show
 #     col.set_link :edit  # 編集リンクを張るならこちら
    end
 
    rec.to_s # 表示内容
  end

リンクをしない設定はこれで正しいんだろうか?~
[[ソースコード>https://github.com/activescaffold/active_scaffold/blob/5066df89173db70f5e96f6a89f175bee7ca9061c/lib/active_scaffold/data_structures/column.rb]] を読んでも、どうするのが正式なのかが分からない???

* ruby 2.4.0p0 + Rails5 環境では動かなくなってしまいました [#ufe1d06d]

いくつか問題があるようです。

- とりあえず turbolinks5 は off にした
- histories のリンクが機能していない
- edit → update で失敗する

** histories のリンクが機能していない [#aafbacd1]

500 Internal Server Error が出てしまいました。

 LANG:console
 Processing by Admin::Account::HistoriesController#index as JS
   Parameters: {"association"=>"histories", "parent_scaffold"=>"admin/accounts", "adapter"=>"_list_inline_adapter", "account_id"=>"1"}
   Account Load (0.3ms)  SELECT  "accounts".* FROM "accounts" WHERE "accounts"."deleted_at" IS NULL AND "accounts"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
   Account Load (0.2ms)  SELECT  "accounts".* FROM "accounts" WHERE "accounts"."id" = ? LIMIT ?  [["id", 1], ["LIMIT", 1]]
 Completed 500 Internal Server Error in 6ms (ActiveRecord: 0.5ms)
 
 NoMethodError (undefined method `[]' for nil:NilClass):
 
 /home/takeuchi/.rvm/gems/ruby-2.4.0@ICSPM25/bundler/gems/active_scaffold-ab5840e57e91/lib/active_scaffold/data_structures/nested_info.rb:109:in `default_sorting'
 /home/takeuchi/.rvm/gems/ruby-2.4.0@ICSPM25/bundler/gems/active_scaffold-ab5840e57e91/lib/active_scaffold/data_structures/nested_info.rb:102:in `sorted?'
 /home/takeuchi/.rvm/gems/ruby-2.4.0@ICSPM25/bundler/gems/active_scaffold-ab5840e57e91/lib/active_scaffold/actions/nested.rb:39:in `configure_nested'
 activesupport (5.1.2) lib/active_support/callbacks.rb:413:in `block in make_lambda'
 activesupport (5.1.2) lib/active_support/callbacks.rb:197:in `block (2 levels) in halting'
 ...

https://github.com/osamutake/active_scaffold/blob/master/lib/active_scaffold/data_structures/nested_info.rb#L109

 LANG:ruby
     def sorted?(chain)
       default_sorting(chain).present?
     end
 
     def default_sorting(chain)
       return @default_sorting if defined? @default_sorting
       if association.scope.is_a?(Proc) && chain.respond_to?(:values)
         @default_sorting = chain.values[:order]
         @default_sorting = @default_sorting.map(&:to_sql) if @default_sorting[0].is_a? Arel::Nodes::Node
         @default_sorting = @default_sorting.join(', ')
       end
     end

の真ん中の @default_sorting[0].is_a? Arel::Nodes::Node で、
@default_sorting が nil になって失敗しているみたい

上記の chain は

https://github.com/osamutake/active_scaffold/blob/master/lib/active_scaffold/actions/nested.rb#L39

によると 

https://github.com/osamutake/active_scaffold/blob/master/lib/active_scaffold/actions/nested.rb#L82

の beginning_of_chain で作られているのだけれど、
必ずしも :order が設定されていない可能性がある、ということなのかも?

byebug 仕込んで見てみると、

  LANG:console
 [103, 112] in /home/takeuchi/.rvm/gems/ruby-2.4.0@ICSPM25/bundler/gems/active_scaffold-ab5840e57e91/lib/active_scaffold/data_structures/nested_info.rb
    103:     end
    104:
    105:     def default_sorting(chain)
    106:       return @default_sorting if defined? @default_sorting
    107: byebug
 => 108:       if association.scope.is_a?(Proc) && chain.respond_to?(:values)
    109:         @default_sorting = chain.values[:order]
    110:         @default_sorting = @default_sorting.map(&:to_sql) if @default_sorting[0].is_a? Arel::Nodes::Node
    111:         @default_sorting = @default_sorting.join(', ')
    112:       end
 (byebug) chain
  CACHE Account::History Load (0.0ms)  SELECT  "versions".* FROM "versions" WHERE "versions"."item_id" = ? AND (item_type = "Account") LIMIT ?  [["item_id", 1], ["LIMIT", 11]]
#<ActiveRecord::Associations::CollectionProxy [#<Account::History id: 1, item_type: "Account", item_id: 1, event: "create", whodunnit: nil, object: nil, created_at: "2017-07-13 00:39:52", object_changes: "---\nemail:\n- \n- takeuchi@bk.tsukuba.ac.jp\npassword...", transaction_id: 1>]>

なので、確かに ORDER BY 節が存在しない。

えーと・・・これ、以前のバージョンだとどうして動いてたんだ???

あれ、ruby や Rails のバージョン違いじゃなくて、
active_scaffold 自体のバージョンが上がっている問題なのかも???

以前使っていた active_scaffold (3.4.39) だと、

https://github.com/activescaffold/active_scaffold/blob/5c5c18ccaf28ef7ab6a4f0564912bb4e0fbb8e4f/lib/active_scaffold/data_structures/nested_info.rb#L121

のようになっていて、全然違うコードだ。

 # TODO: remove when rails 3 compatibility is removed

というコメントが付いてある部分を何とかしようとして、バグが混入したのかもしれない?

たぶん、以下のようにすれば良さそう。

 LANG:ruby
     def default_sorting(chain)
       return @default_sorting if defined? @default_sorting
 -     if association.scope.is_a?(Proc) && chain.respond_to?(:values)
 +     if association.scope.is_a?(Proc) && chain.respond_to?(:values) && chain.values[:order]
         @default_sorting = chain.values[:order]
         @default_sorting = @default_sorting.map(&:to_sql) if @default_sorting[0].is_a? Arel::Nodes::Node
         @default_sorting = @default_sorting.join(', ')
       end
     end

とりあえず、それらしく動くことを確認。

これでいいのかどうかよく分からないけど、一応、報告してみました。~
https://github.com/activescaffold/active_scaffold/issues/560

** edit → update で失敗する [#g2da7dc2]

まず、なぜか更新ボタンを1回押しただけで同時に2つのリクエストが飛んでいる。

 LANG:console
 Started PATCH "/event/ICSPM25/ja/admin/invited_speakers/13?utf8=%E2%9C%93" for 127.0.0.1 at 2017-07-21 11:35:30 +0900
 Started PATCH "/event/ICSPM25/ja/admin/invited_speakers/13?utf8=%E2%9C%93" for 127.0.0.1 at 2017-07-21 11:35:30 +0900
 Processing by Admin::InvitedSpeakersController#update as JS
 Processing by Admin::InvitedSpeakersController#update as JS

form の submit に jquery_ujs のイベント

 LANG:javascript
 function(e) {
   var form = $(this),
     remote = rails.isRemote(form),
     blankRequiredInputs,
     nonBlankFileInputs;
 
   if (!rails.allowAction(form)) return rails.stopEverything(e);
 
   // Skip other logic when required values are missing or file upload is present
   if (form.attr('novalidate') === undefined) {
     if (form.data('ujs:formnovalidate-button') === undefined) {
       blankRequiredInputs = rails.blankInputs(form, rails.requiredInputSelector, false);
       if (blankRequiredInputs && rails.fire(form, 'ajax:aborted:required', [blankRequiredInputs])) {
         return rails.stopEverything(e);
       }
     } else {
       // Clear the formnovalidate in case the next button click is not on a formnovalidate button
       // Not strictly necessary to do here, since it is also reset on each button click, but just to be certain
       form.data('ujs:formnovalidate-button', undefined);
     }
   }
 
   if (remote) {
     nonBlankFileInputs = rails.nonBlankInputs(form, rails.fileInputSelector);
     if (nonBlankFileInputs) {
       // Slight timeout so that the submit button gets properly serialized
       // (make it easy for event handler to serialize form without disabled values)
       setTimeout(function() {
         rails.disableFormElements(form);
       }, 13);
       var aborted = rails.fire(form, 'ajax:aborted:file', [nonBlankFileInputs]);
 
       // Re-enable form elements if event bindings return false (canceling normal form submission)
       if (!aborted) {
         setTimeout(function() {
           rails.enableFormElements(form);
         }, 13);
       }
 
       return aborted;
     }
 
     rails.handleRemote(form);
     return false;
 
   } else {
     // Slight timeout so that the submit button gets properly serialized
     setTimeout(function() {
       rails.disableFormElements(form);
     }, 13);
   }
 }

form の 更新ボタン の click に jquery_ujs の

 LANG:javascript
 function(event) {
   var button = $(this);
 
   if (!rails.allowAction(button)) return rails.stopEverything(event);
 
   // Register the pressed submit button
   var name = button.attr('name'),
     data = name ? {
       name: name,
       value: button.val()
     } : null;
 
   var form = button.closest('form');
   if (form.length === 0) {
     form = $('#' + button.attr('form'));
   }
   form.data('ujs:submit-button', data);
 
   // Save attributes from button
   form.data('ujs:formnovalidate-button', button.attr('formnovalidate'));
   form.data('ujs:submit-button-formaction', button.attr('formaction'));
   form.data('ujs:submit-button-formmethod', button.attr('formmethod'));
 }

あれ?jquery_ujs と rails-ujs ってのが両方入ってるけど、
これっていいのか?

http://qiita.com/itkrt2y/items/7e999836f460fb9c005d

jquery_ujs を削ったらそもそも form が表示されなくなってしまった。

rails-ujs を削ったら正しく動いた。

えーと・・・どうなってるんだっけ?

Gemfile を見直す必要がありそう。

* Rails 5 でも動くようにしたい (2017-11) [#l94df97e]

ruby, rails, bundler とも現時点での最新版を使ってみます。

 LANG:console
 $ cat ~/.gemrc
  install: --no-document
  update: --no-document
 $ rvm --version
  rvm 1.26.11 (latest) by Wayne E. Seguin <wayneeseguin@gmail.com>, Michal Papis <mpapis@gmail.com> [https://rvm.io/]
 $ cat .ruby-version
  ruby-2.4.2
 $ ruby --version
  ruby 2.4.2p198 (2017-09-14 revision 59899) [i686-linux]
  rvm 1.26.11 (latest) by Wayne E. Seguin <wayneeseguin@gmail.com>, Michal Papis <mpapis@gmail.com> 
 $ echo "ruby-2.4.2" > .ruby-version
 $ echo "tpsf" > .ruby-gemset
 $ cd .
  ruby-2.4.2 is not installed.
  To install do: 'rvm install ruby-2.4.2'
 $ rvm install ruby-2.4.2
 $ cd .
 $ gem --version
  2.6.13
  2.6.14
 $ gem install rails
 $ rails --version
  Rails 5.1.4
 $ bundler --version
  Bundler version 1.16.0
 $ rails new tpsf
  ...
 
  The dependency tzinfo-data (>= 0) will be unused by any of the platforms Bundler is installing for.
  Bundler is installing for ruby but the dependency is only for x86-mingw32, x86-mswin32, x64-mingw32, java. 
  To add those platforms to the bundle, run `bundle lock --add-platform x86-mingw32 x86-mswin32 x64-mingw32 java`.
  ...
 
  Bundle complete! 16 Gemfile dependencies, 70 gems now installed.
  Use `bundle info [gemname]` to see where a bundled gem is installed.
           run  bundle exec spring binstub --all
  * bin/rake: spring inserted
  * bin/rails: spring inserted
 $ cd tpsf
 $ git add .
 $ git commit -m "initial commit"
 $ jed Gemfile
  - gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
  + #gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]
  +
  + gem 'paper_trail'
  + gem 'active_scaffold'
  + gem 'active_scaffold', git: 'https://github.com/activescaffold/active_scaffold.git', branch: 'master', ref: 'b3a4859'
  + gem 'paranoia'
  + gem 'jquery-rails'
  + gem 'jquery-ui-rails'
 $ bundle install
 $ git commit -a -m "gem tzinfo-data removed / gems paper_trail, active_scaffold and paranoia installed"

とりあえずインストール終了。

** モデルの作成をしようとすると active_scaffold が文句を言う [#d02f64f5]

仕方が無いので最新版をインストール

 LANG:console
 $ rails g active_scaffold Test name:string email:string
  /home/osamu/.rvm/gems/ruby-2.4.2@album/gems/bundler-1.16.0/lib/bundler/runtime.rb:84:in `rescue in block (2 levels) in require': 
  There was an error while trying to load the gem 'active_scaffold'.
  Gem Load Error is: This version of ActiveScaffold requires Rails 3.1 or higher.  
  Please use an earlier version.
 $ jed Gemfile
  - gem 'active_scaffold'
  + gem 'active_scaffold', git: 'https://github.com/activescaffold/active_scaffold.git', branch: 'master', ref: 'b3a4859'
 $ # https://github.com/activescaffold/active_scaffold#quick-start
 $ bundle install
 $ rails g active_scaffold:install
  Running via Spring preloader in process 13454
         route  concern :active_scaffold, ActiveScaffold::Routing::Basic.new(association: true)
         route  concern :active_scaffold_association, ActiveScaffold::Routing::Association.new
        insert  app/assets/javascripts/application.js
        insert  app/assets/stylesheets/application.css
 $ rails g paper_trail:install --with-changes
  Running via Spring preloader in process 14299
        create  db/migrate/20171110151301_create_versions.rb
        create  config/initializers/paper_trail.rb
 $ rails db:create
  Created database 'db/development.sqlite3'
  Created database 'db/test.sqlite3'
 $ rake db:migrate
  == 20171110151301 CreateVersions: migrating ===================================
  -- create_table(:versions)
     -> 0.0074s
  -- add_index(:versions, [:item_type, :item_id])
     -> 0.0031s
  == 20171110151301 CreateVersions: migrated (0.0112s) ==========================
 $ git add .
 $ git commit -m "gem tzinfo-data removed / gems jquery, jqyery-ui, paper_trail, active_scaffold and paranoia installed"

active_scaffold は新しいバージョンを入れないと動かない。

とりあえずインストール終了。

** モデルの作成 [#t681bc7f]

 LANG:console
 $ rails g active_scaffold:resource Test name:string email:string
  Running via Spring preloader in process 13486
        invoke  active_record
        create    db/migrate/20171110143107_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, concerns: :active_scaffold
        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
  == 20171110143107 CreateTests: migrating ======================================
  -- create_table(:tests)
     -> 0.0052s
  == 20171110143107 CreateTests: migrated (0.0056s) =============================
 $ git commit -a -m "active_scaffold を master ブランチの最新版にした"
 $ rails s 

http://localhost:3000/tests

へのアクセスで
へのアクセスで一覧画面が出た。

 uninitialized constant Jquery

が出た。

** jQuery のインストール [#c7402813]

 LANG:console
 $ jed Gemfile
  + gem 'jquery-rails'
  + gem 'jquery-ui-rails'
 $ bundle install
 $ git commit -a -m "gem jquery-rails, jquery-ui-rails added"
 $ rails s

これで一覧画面が出た。

&ref(tpsf-tests-list1.png,,75%);

** Create New でエラーが出る [#i673c95d]

[Create New] を押したら、

 Uncaught SyntaxError: Unexpected token <
     at processResponse (rails-ujs.self-817d9a8cb641f7125060cb18fefada3f35339170767c4e003105f92d4c204e39.js?body=1:246)
     at rails-ujs.self-817d9a8cb641f7125060cb18fefada3f35339170767c4e003105f92d4c204e39.js?body=1:173
     at XMLHttpRequest.xhr.onreadystatechange (rails-ujs.self-817d9a8cb641f7125060cb18fefada3f35339170767c4e003105f92d4c204e39.js?body=1:230)

エラーが出たのは、

 LANG:javascript
          } else if (type.match(/\b(?:java|ecma)script\b/)) {
            script = document.createElement('script');
            script.text = response;
            document.head.appendChild(script).parentNode.removeChild(script);

appendChild の所なので、javascript を期待してたのに html が帰ってきてしまったという感じ。

えー、そんなぁ。

** javascript の require を直す [#be38a622]

 LANG:console
 $ jed app/assets/javascripts/application.js
  - // require rails-ujs
  + //# require rails-ujs
  + //= require jquery
  + //= require jquery_ujs
  + //= require active_scaffold
 $ git commit -a -m "active_scaffold が動くように application.js の require を修正"

Create New が動くようになった。

&ref(tpsf-tests-new1.png,,75%);

** Paper Trail による履歴管理を追加 [#j4e8a399]
* TrailedParanoidScaffold を仕込む [#bd13c5be]

 LANG:console
 $ rails g paper_trail:install
  Running via Spring preloader in process 14299
        create  db/migrate/20171110151301_create_versions.rb
        create  config/initializers/paper_trail.rb
 $ rake db:migrate
  == 20171110151301 CreateVersions: migrating ===================================
  -- create_table(:versions)
     -> 0.0074s
  -- add_index(:versions, [:item_type, :item_id])
     -> 0.0031s
  == 20171110151301 CreateVersions: migrated (0.0112s) ==========================
 $ jed app/models/test.rb
    class Test < ApplicationRecord
  +   has_paper_trail :ignore => [:id, :created_at, :updated_at]
    end
 $ jed app/controllers/tests_controller.rb
    class TestsController < ApplicationController
      active_scaffold :"test" do |conf|
  +     conf.columns.exclude :versions
      end
    end
 $ rails s

app/models/test.rb
app/models/trailed_paranoid_scaffold.rb
 LANG:ruby
    class Test < ApplicationRecord
  +   has_paper_trail :ignore => [:id, :created_at, :updated_at]
    end

app/controllers/tests_controller.rb
 LANG:ruby
    class TestsController < ApplicationController
      active_scaffold :"test" do |conf|
  +     conf.columns.exclude :versions
      end
    end

&ref(tpsf-list-listdeletedrecords.png,,75%);

なぜだか List Deleted Records というリンクができてて、しかも押すとエラーになる。

どうやら active_scaffold 自体に paper_trail への対応が含まれているらしく、
ただ残念ながらバグっているっぽい。

 Processing by TestsController#deleted as JS
   Parameters: {"adapter"=>"_list_inline_adapter"}
 Completed 500 Internal Server Error in 51ms (ActiveRecord: 1.4ms)
 # coding: utf-8
 #
 # Osamu Takeuchi <osamu@big.jp> 
 #
 # 2014.05.02 初期リリース
 # 2014.05.16 build を実装した
 # 2017.11.29 Rails 5 用に改修 & route 定義だけで使えるようにした
 #
 module TrailedParanoidScaffold
 
   #
   # Model に include する
   #
   module Model
     def self.included(base)
       base.class_eval do
         # 論理削除を使う
         acts_as_paranoid
         
         # 履歴情報を持たせる
         if @paper_trail_ignore
             has_paper_trail :ignore => @paper_trail_ignore
         else
             has_paper_trail :ignore => [:id, :created_at, :updated_at]
         end
         
         # http://stackoverflow.com/questions/3194290/why-cant-there-be-classes-inside-methods-in-ruby
         base.const_set :History, Class.new(PaperTrailHistory) do
         end
   
         has_many :histories, -> { where "item_type = \"#{base.to_s}\"" },
             foreign_key: 'item_id', class_name: "#{base.to_s}::History"
       end
     end
   end
 
   class PaperTrailHistory < PaperTrail::Version
     def method_missing(action)
       unless self.reify.attributes.keys.index(action.to_s).nil?
         self.reify[action]
       else
         super
       end
     end
   end
 
 TypeError (can't cast Class):
   #
   # ModelsController に include する
   #
   module Controller
   
     module ClassMethods
       def active_scaffold(model_id, &block)
         super(model_id) do |conf|
           conf.columns.exclude :versions
           conf.show.columns.add :created_at, :updated_at
           conf.create.columns.exclude :histories, :deleted_at
           conf.update.columns.exclude :histories, :deleted_at
           conf.list.columns.exclude :deleted_at, :created_at, :updated_at
           conf.action_links.add 'restore', :label => 'Restore', :type => :member, 
                   :confirm => 'Are you sure you want to restore the record?', 
                   :method => :put, :position => :false
           conf.actions.exclude :deleted_records
           block.call(conf) if block
         end
       end
     end
   
     def self.included(base)
 
 activerecord (5.1.4) lib/active_record/connection_adapters/abstract/quoting.rb:45:in `rescue in type_cast'
 activerecord (5.1.4) lib/active_record/connection_adapters/abstract/quoting.rb:32:in `type_cast'
 activerecord (5.1.4) lib/active_record/connection_adapters/abstract/quoting.rb:166:in `block in type_casted_binds'
 activerecord (5.1.4) lib/active_record/connection_adapters/abstract/quoting.rb:166:in `map'
 activerecord (5.1.4) lib/active_record/connection_adapters/abstract/quoting.rb:166:in `type_casted_binds'
 activerecord (5.1.4) lib/active_record/connection_adapters/sqlite3_adapter.rb:206:in `exec_query'
 activerecord (5.1.4) lib/active_record/connection_adapters/abstract/database_statements.rb:371:in `select'
 activerecord (5.1.4) lib/active_record/connection_adapters/abstract/database_statements.rb:42:in `select_all'
 activerecord (5.1.4) lib/active_record/connection_adapters/abstract/query_cache.rb:95:in `block in select_all'
 activerecord (5.1.4) lib/active_record/connection_adapters/abstract/query_cache.rb:117:in `block in cache_sql'
 /home/osamu/.rvm/rubies/ruby-2.4.2/lib/ruby/2.4.0/monitor.rb:214:in `mon_synchronize'
 activerecord (5.1.4) lib/active_record/connection_adapters/abstract/query_cache.rb:104:in `cache_sql'
 activerecord (5.1.4) lib/active_record/connection_adapters/abstract/query_cache.rb:95:in `select_all'
 activerecord (5.1.4) lib/active_record/relation/calculations.rb:253:in `execute_simple_calculation'
 activerecord (5.1.4) lib/active_record/relation/calculations.rb:209:in `perform_calculation'
 activerecord (5.1.4) lib/active_record/relation/calculations.rb:118:in `calculate'
 activerecord (5.1.4) lib/active_record/relation/calculations.rb:41:in `count'
 /home/osamu/.rvm/gems/ruby-2.4.2@album/bundler/gems/active_scaffold-b3a48596187f/lib/active_scaffold/bridges/paper_trail/actions.rb:15:in `deleted'
       base.class_eval do
 
         extend ClassMethods
 
         # 削除を元に戻すアクション
         def restore
           do_restore
           respond_to_action(:restore)
         end
         
         protected
         
         # 削除済みのレコードも表示する
         def beginning_of_chain
           nested? ? super() : super().unscoped
         end
   
         # acts_as_paranoid で deleted_at が立てられたレコードを復帰させる
         def do_restore
           do_edit
           @record.deleted_at = nil
           update_save(no_record_param_update: true)
         end
         
         def restore_respond_to_js
            update_row
         end
   
         def destroy_respond_to_js
           # Update row instead of deleting
           update_row
         end
   
         def update_row
           if successful?
             # 行を更新
             render :action => 'update_row'
           else
             # エラーメッセージを表示する
             render :action => 'destroy'
           end
         end
       end
     end
     
   end
 
   #
   # ModelsHelper に include する
   #
   module Helper
 
     # 削除済みのレコードに Edit および Delete のリンクを付けない
     # 削除済みのレコードだけに Restore のリンクを付ける
     def skip_action_link?(link, *args)
       return false unless (args.size > 0) and args[0].respond_to?(:deleted_at)
       return true if args[0].deleted_at and (link.action=='destroy' or link.action=='edit')
       return true if not args[0].deleted_at and (link.action=='restore')
     end
     
     def self.included(base)
       base.class_eval do
         # ヘルパークラス名( = SomeScope::SomeModelsHelper )からモデル名を取り出す
         model_id = base.to_s.split(/::/).last.gsub(/Helper$/,'').underscore.singularize
 
         # histories の表示を "# 数字" にする
         define_method("#{model_id}_histories_column") do |record, column|
           ("# #{record.versions.size.to_s} @ #{l record.updated_at, format: :short}" +
           ( record.versions.last.whodunnit ? "<br> by #{record.versions.last.whodunnit}" : '')).html_safe
         end
       end
     end
   
   end
 
   #
   # HistoriesController に include する
   #
   module HistoriesController
   
     module ClassMethods
       def active_scaffold(model_id, &block)
         super model_id 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.list.sorting= { :created_at => :desc }
     
           # 検索をしやすくする
           conf.actions.exclude :search
           conf.actions.add :field_search
           conf.field_search.columns = [:event,:created_at,:object_changes]
           
           block.call(conf) if block
         end
       end
     end
   
     def self.included(base)
       base.class_eval do
         extend ClassMethods
         
         protected
         
         # 親クラスは削除済みのレコードも検索する
         def nested_parent_record(crud = :read)
           @nested_parent_record ||= 
               find_if_allowed(nested.parent_id, crud, nested.parent_model.unscoped)
         end
       end
     end
     
   end
   
   #
   # HistoriesHelper に include する
   #
   module HistoriesHelper
   
     def self.included(base)
       base.class_eval do
         # ヘルパークラス名 (= SomeScope::SomeModel::HistoriesHelper) からモデル名を取り出す
         model_id = base.to_s.gsub(/::(?=[^:]+$)|Helper$/,'').sub(/.*::/,'').underscore.singularize
       
         # 更新履歴の表示形式
         define_method("#{model_id}_object_changes_column") do |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
         
         define_method("#{model_id}_created_at_column") do |record, column|
           ( l(record.created_at)+"<br>"+
             (record.whodunnit ? " by " + record.whodunnit : "")
           ).html_safe
         end
       end
     end
     
   end
   
   #
   # Controller, Helper を作成する関数
   #
   # app/controllers/admin/models_controller.rb
   # app/helpers/admin/models_helper.rb
   # app/controllers/admin/model/histories_controller.rb
   # app/helpers/admin/model/histories_helper.rb
   #
   # といったファイルから、
   #
   # TrailedParanoidScaffold::build(__FILE__) do
   # end
   #
   # の形で呼び出すことを想定している。
   #
   def self.build(file, parent = nil, &block)
     file = File.expand_path(file).sub(/.*\/app\/(controllers|helpers)\//,'').sub(/\.rb\z/,'')
     self.build_sub(file, parent, &block)
   end
 
   # admin/models_controller.rb
   # admin/models_helper.rb
   # admin/model/histories_controller.rb
   # admin/model/histories_helper.rb
   # のような文字列を渡す
   def self.build_sub(file, parent = nil, &block)
     unless file =~ /([^\/]+)(\/histories)?_(controller|helper)\z/
       throw "unexpected file name #{file}"
     end
 
     # 必要な情報をファイル名から取り出す
     controller_or_helper = $3   # "controller" or "helper"
     histories = $2              # "histories" or nil
     model = $1                  # "model"
     model = model.singularize unless histories
 
     *path, name = file.split(File::Separator).map{|s| s.classify}
     
     # Admin::ModelsController
     # Admin::ModelsHelper
     # Admin::Model::HistoriesController
     # Admin::Model::HistoriesHelper
     # といった定数に結びつけたい
     # このとき Controller は AdminController を継承する
 
     # そのために Admin や AdminController のようなモジュールや
     # クラスを定義する
 
     # c = controller, n = namespace
     c, n = path.reduce([ApplicationController, Object]){|list, p| 
       c, n = list
 
       if controller_or_helper == "controller"
         c = if Object.const_defined? (p + 'Controller'), false
               Object.const_get (p + 'Controller'), false
             else
               Object.const_set p + 'Controller', Class.new(c)
             end
       end
 
       n = if n.const_defined? p, false
             n.const_get(p, false)
           else
             n.const_set p, Module.new()
           end
       [c, n]
     }
 
     # ApplicationController を参照すると Helper が読み込まれるので
     # 必要な Controller, Helper をその前に登録する
 
     if controller_or_helper == "controller"
       new_class = Class.new(c)
     else
       new_class = Module.new()
     end
 
     # 
     n.const_set name, new_class
 
     # 新しく作ったクラス・モジュールに必要な機能を追加
     new_class.class_eval do
       # TrailedParanoidScaffold::Controller
       # TrailedParanoidScaffold::Helper
       # TrailedParanoidScaffold::HistoriesController
       # TrailedParanoidScaffold::HistoriesHelper
       # を include する
       include TrailedParanoidScaffold.const_get(
           (histories ? "Histories" : "") + controller_or_helper.classify
       )
       
       # クラスメソッド tpsf を追加
       # カラム名などに関する情報が格納される
       @tpsf = {
           model:    model,
           model_id: ( histories ? "#{model}/history".to_sym : model.to_sym )
       }
       def @tpsf.method_missing(name, *other)
         if has_key? name
           self[name]
         else
           super name, *other
         end
       end
       def self.tpsf
         @tpsf
       end
       
        # モデル名も計算しておく
       if controller_or_helper == "controller"
         @active_scaffold_model =
                 histories ? "#{model}/history".to_sym : model.to_sym
       else
         # ヘルパにユーティリティ関数を定義する
         # 例えば def_column を使うと、
         #   def_column(:column_name) do |record, column|
         #     # def {record_name}_{column_name}_column(record, column)
         #     # として定義するのと同じ結果を得られる
         #   end
         [:column, :column_attributes, 
             :form_column, :show_column, :search_column].each do |fname|
           new_class.class_eval <<-"EOS"
             def self.def_#{fname}(name, &block)
               define_method :"#{model.underscore}_\#{name}_#{fname}", &block
             end
           EOS
         end
  
       end
       
       # 与えられたブロックをクラス・モジュールのスコープで評価
       new_class.class_eval &block if block
     end
 
   end
 
   # 必要な Model / Controller / Helper が定義されていなければ定義する
   def self.build_all(path)
       path =~ /^\/((.*)\/)?([^\/]+)$/
       model = $3
       path = $1 || ''
 
       # Model::History
       TrailedParanoidScaffold::unless_defined(model + '/history') do |name|
         model.classify.constantize.class_eval do
           include TrailedParanoidScaffold::Model
         end
       end
 
       # Model::HistoriesHelper
       unless_defined(path + model + '/histories_helper') do |name|
         TrailedParanoidScaffold.build_sub(name) { }
       end
 
       # ModelsHelper
       TrailedParanoidScaffold::unless_defined(path + model.pluralize + '_helper') do |name|
         TrailedParanoidScaffold.build_sub(name) { }
       end
 
       # ModelsController
       unless_defined(model.pluralize + '_controller') do |name|
         TrailedParanoidScaffold.build_sub(path + name) do |conf|
           # 自動的には読み込まれないため手動でヘルパーを読み込む
           add_template_helper (path + model.pluralize + '_helper').classify.constantize
 
           # active_scaffold を呼ぶ
           active_scaffold @active_scaffold_model
         end
       end
 
       # Model::HistoriesController
       unless_defined(model + '/histories_controller') do |name|
         TrailedParanoidScaffold.build_sub(path + name) do |conf|
           # 自動的には読み込まれないため手動でヘルパーを読み込む
           add_template_helper (path + model + '/histories_helper').classify.constantize
 
           # active_scaffold を呼ぶ
           active_scaffold @active_scaffold_model
         end
       end
   end
 
   # id.classify で指定された定数が見つからなければ与えられたブロックを実行する
   # ブロックには指定された定数が渡される
   def self.unless_defined(id)
     defined = false
     begin
       id.classify.constantize
       # was defined
       defined = true
     rescue NameError
       # was not defined
     end
 
     yield id unless defined
   end
 
 end
 
 module ActionDispatch
   module Routing
     class Mapper
       module Base
         # trails_paranoid_scaffold 用の as_routes
         #
         # options には、
         #   options[:histories_resources] = {}
         #   options[:histories_routes] = {}
         # の形で、下位層へのオプションも与えられる
         #
         def as_tpsf_routes(options = {:association => true})
           options = options.dup.stringify_keys
 
           histories_resources = { 
             controller: "#{@scope[:controller].singularize}/histories"
           }.merge(options.delete(:histories_resources) || {})
 
           histories_routes = { 
             :association => true
           }.merge(options.delete(:histories_routes) || {})
           
           
           ### ここからが実際のルート指定
           
           # active_scaffold の基本
           as_routes options
           
           # 消去済みレコードの復帰用アクション
           member do 
             put 'restore' 
           end
 
           # histories へのルート
           resources :histories, histories_resources do
             as_routes histories_routes
           end
 
           # 必要な Controller / Helper を定義する
           TrailedParanoidScaffold::build_all(( @scope[:path] || '' ) + '/' + @scope[:controller].singularize)
         end
 
       end
     end
   end
 end

 LANG:ruby
    def deleted
      query = PaperTrail::Version.destroys.where(:item_type => active_scaffold_config.model)
      query = query.where_object(nested.child_association.foreign_key => nested.parent_id) if nested? && nested.child_association.belongs_to? && PaperTrail::Version.respond_to?(:where_$
 >    pager = Paginator.new(query.count, active_scaffold_config.list.per_page) do |offset, per_page|
        query.offset(offset).limit(per_page).map(&:reify)
      end
      @pagination_action = :deleted
      @page = pager.page(params[:page] || 1)
      @records = @page.items
      respond_to_action(:list)
    end
paranoid のためのカラムを追加

ここでは消去済みレコードは paranoia で管理する予定なので、
リンクを消してしまうだけで良い?
 LANG:console
 $ rails generate migration AddDeletedAtToTests deleted_at:datetime:index
 $ rake db:migrate

app/controllers/tests_controller.rb
confit/routes.rb
 LANG:ruby
    class TestsController < ApplicationController
      active_scaffold :"test" do |conf|
        conf.actions.exclude :deleted_records
        conf.columns.exclude :versions
      end
    end
   #  resources :tests, concerns: :active_scaffold
     resources :tests, concerns: :active_scaffold do
       as_tpsf_routes
     end

** モデルに履歴情報へのリンクを持たせる [#nefe1550]
これだけで動くようにした。

app/models/test.rb
History や Controller, Helper は定義されていなければ勝手に定義する。

** Controller や Helper に手を加えたい場合 [#o890da96]

app/controllers/tests_controller.rb
 LANG:ruby
   class Test < ApplicationRecord
     has_paper_trail
 + 
 +   class History < ItemHistory
 +   end
 +   has_many :histories, -> { where 'item_type = "Test"' }, foreign_key: 'item_id', class_name: 'Test::History'
 TrailedParanoidScaffold::build(__FILE__) do
   active_scaffold @active_scaffold_model do |conf|
     # ここに active_scaffold の column 定義など
   end
   # ここに追加の action など
 end

app/models/item_history.rb
app/helpers/tests_helper.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
 TrailedParanoidScaffold::build(__FILE__) do
   # ここに追加の設定を
 end

config/routes.rb
app/controllers/test/histories_controller.rb
 LANG:ruby
   concern :active_scaffold_association, ActiveScaffold::Routing::Association.new
   concern :active_scaffold, ActiveScaffold::Routing::Basic.new(association: true)
   resources :tests, concerns: :active_scaffold
 + resources :tests do
 +   as_routes
 +   resources :histories, controller: 'test/histories' do
 +     as_routes
 +   end
 + end
 TrailedParanoidScaffold::build(__FILE__) do
   active_scaffold @active_scaffold_model do |conf|
     # ここに active_scaffold の column 定義など
   end
   # ここに追加の action など
 end

app/controllers/test/histories_controller.rb
app/helpers/test/histories_helper.rb
 LANG:ruby
 + class Test::HistoriesController < ApplicationController
 +   active_scaffold :"test/history" do |conf|
 +   end
 + end
 TrailedParanoidScaffold::build(__FILE__) do
   # ここに追加の設定を
 end

あれ、エラーが出るな

** 時刻の表示方法を設定 [#y5977f34]

https://ja.stackoverflow.com/questions/8161/created-at%E3%81%AE%E3%82%BF%E3%82%A4%E3%83%A0%E3%82%BE%E3%83%BC%E3%83%B3%E3%81%A8%E3%83%95%E3%82%A9%E3%83%BC%E3%83%9E%E3%83%83%E3%83%88%E3%82%92%E5%A4%89%E6%8F%9B%E3%81%99%E3%82%8B を参考に

config/application.rb
 LANG:ruby
   module Album
     class Application < Rails::Application
        ...
   
 +     config.time_zone = 'Tokyo'
 +     config.i18n.default_locale = :ja
     end
   end

config/locales/ja.yml
 LANG:ruby
 + ja:
 +   time:
 +     formats:
 +       default: ! '%Y/%m/%d %H:%M:%S'

* 質問・コメント [#t616f744]

#article_kcaptcha


Counter: 18911 (from 2010/06/03), today: 6, yesterday: 0