Skip to content

Commit

Permalink
added callback-methods
Browse files Browse the repository at this point in the history
  • Loading branch information
Ninigi committed Sep 10, 2015
1 parent a97850a commit 9008d67
Show file tree
Hide file tree
Showing 10 changed files with 243 additions and 17 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ None

### Added

- Added callback-methods `paper_trail_update` `paper_trail_create` `paper_trail_destroy`
instead of has_paper_trail
[#593](https://github.com/airblade/paper_trail/pull/607)
- Added `unversioned_attributes` option to `reify`.
[#579](https://github.com/airblade/paper_trail/pull/579)

Expand Down
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,30 @@ a.versions.size # 3
a.versions.last.event # 'update'
```

You can also use the corresponding callback-methods seperately instead of using
the :on option. If you choose to use the callback-methods, PaperTrail will only
track the according events - so `paper_trail_create` is basically the same as
`has_paper_trail :on => :create`.

```ruby
class Article < ActiveRecord::Base
paper_trail_destroy
# or paper_trail_after_destroy
# paper_trail_destroy(:before) will create the version before the actual
# destroy event

paper_trail_update
paper_trail_create
end
```

The `paper_trail_destroy` method can be configured to be called `:before` or `:after` the
destroy event. This can be usefull if you are using a third party tool that alters the
destroy method (for example paranoia). If you do not pass an argument, it will default
to after_destroy.
`paper_trail_after_destroy` and `paper_trail_before_destroy` are alias methods for
`paper_trail_destroy(:before/:after)`.

## Choosing When To Save New Versions

You can choose the conditions when to add new versions with the `if` and
Expand Down
11 changes: 8 additions & 3 deletions gemfiles/3.0.gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,14 @@ group :development, :test do
gem 'timecop'
end

platforms :ruby do
platforms :ruby do
gem 'sqlite3', '~> 1.2'
gem 'mysql2', '~> 0.3'

# We would prefer to only constrain mysql2 to '~> 0.3',
# but a rails bug (https://github.com/rails/rails/issues/21544)
# requires us to constrain to '~> 0.3.20' for now.
gem 'mysql2', '~> 0.3.20'

gem 'pg', '~> 0.17.1'
end

Expand All @@ -42,7 +47,7 @@ group :development, :test do
gem 'shoulda-matchers', '~> 1.5'
end

platforms :jruby do
platforms :jruby do
# Use jRuby's sqlite3 adapter for jRuby
gem 'activerecord-jdbcsqlite3-adapter', '~> 1.3'
gem 'activerecord-jdbcpostgresql-adapter', '~> 1.3'
Expand Down
65 changes: 65 additions & 0 deletions lib/paper_trail/callbacks.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
module PaperTrail
module Callbacks
def setup_callbacks_from_options(options_on, options = {})
options_on.each do |option|
send "paper_trail_#{option}", options
end
end

# Record version before or after "destroy" event
def paper_trail_destroy(options = {})
setup_model_if_necessary options
recording_order = options[:recording_order] || 'after'

unless %(after before).include?(recording_order.to_s)
fail ArgumentError, 'recording order can only be "after" or "before"'
end

send "#{recording_order}_destroy",
:record_destroy,
:if => :save_version?
end

# Record version after "destroy" event
def paper_trail_after_destroy(options = {})
options[:recording_order] = :after
paper_trail_destroy options
end

# Record version before "destroy" event
def paper_trail_before_destroy(options = {})
options[:recording_order] = :before
paper_trail_destroy options
end

# Record version after "update" event
def paper_trail_update(options = {})
setup_model_if_necessary options
before_save :reset_timestamp_attrs_for_update_if_needed!,
:on => :update
after_update :record_update,
:if => :save_version?
after_update :clear_version_instance!
end

# Record version after "create" event
def paper_trail_create(options = {})
setup_model_if_necessary options
after_create :record_create,
:if => :save_version?
end

private

def setup_model_if_necessary(options)
return true if model_set_up?

setup_model_for_paper_trail options
@_set_up = true
end

def model_set_up?
@_set_up
end
end
end
28 changes: 14 additions & 14 deletions lib/paper_trail/has_paper_trail.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'active_support/core_ext/object' # provides the `try` method
require 'paper_trail/callbacks'

module PaperTrail
module Model
Expand All @@ -8,6 +9,7 @@ def self.included(base)
end

module ClassMethods
include Callbacks
# Declare this in your model to track every create, update, and destroy.
# Each version of the model is available in the `versions` association.
#
Expand Down Expand Up @@ -46,6 +48,18 @@ module ClassMethods
# column if it exists. Default is true
#
def has_paper_trail(options = {})
setup_model_for_paper_trail(options)

options[:on] ||= [:create, :update, :destroy]

# Wrap the :on option in an array if necessary. This allows a single
# symbol to be passed in.
options_on = Array(options[:on])

setup_callbacks_from_options options_on, options
end

def setup_model_for_paper_trail(options = {})
# Lazily include the instance methods so we don't clutter up
# any more ActiveRecord models than we have to.
send :include, InstanceMethods
Expand Down Expand Up @@ -87,20 +101,6 @@ def has_paper_trail(options = {})
:order => self.paper_trail_version_class.timestamp_sort_order
end

options[:on] ||= [:create, :update, :destroy]

# Wrap the :on option in an array if necessary. This allows a single
# symbol to be passed in.
options_on = Array(options[:on])

after_create :record_create, :if => :save_version? if options_on.include?(:create)
if options_on.include?(:update)
before_save :reset_timestamp_attrs_for_update_if_needed!, :on => :update
after_update :record_update, :if => :save_version?
after_update :clear_version_instance!
end
after_destroy :record_destroy, :if => :save_version? if options_on.include?(:destroy)

# Reset the transaction id when the transaction is closed.
after_commit :reset_transaction_id
after_rollback :reset_transaction_id
Expand Down
77 changes: 77 additions & 0 deletions spec/models/callback_modifier_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
require 'rails_helper'
require 'support/callback_modifier'

describe CallbackModifier, :type => :model do
with_versioning do
describe 'callback-methods', :versioning => true do
describe 'paper_trail_destroy' do
context 'when :before' do
it 'should create the version before destroy' do
modifier = BeforeDestroyModifier.create!(:some_content => Faker::Lorem.sentence)
modifier.test_destroy
expect(modifier.versions.last.reify).not_to be_flagged_deleted
end
end

context 'when :after' do
it 'should create the version after destroy' do
modifier = AfterDestroyModifier.create!(:some_content => Faker::Lorem.sentence)
modifier.test_destroy
expect(modifier.versions.last.reify).to be_flagged_deleted
end
end

context 'when no argument' do
it 'should default to after destroy' do
modifier = NoArgDestroyModifier.create!(:some_content => Faker::Lorem.sentence)
modifier.test_destroy
expect(modifier.versions.last.reify).to be_flagged_deleted
end
end
end

describe 'paper_trail_update' do
it 'should create a version' do
modifier = UpdateModifier.create!(:some_content => Faker::Lorem.sentence)
modifier.update_attributes! :some_content => 'modified'
expect(modifier.versions.last.event).to eq 'update'
end
end

describe 'paper_trail_create' do
it 'should create a version' do
modifier = CreateModifier.create!(:some_content => Faker::Lorem.sentence)
expect(modifier.versions.last.event).to eq 'create'
end
end

context 'when no callback-method used' do
it 'should default to track destroy' do
modifier = DefaultModifier.create!(:some_content => Faker::Lorem.sentence)
modifier.destroy
expect(modifier.versions.last.event).to eq 'destroy'
end

it 'should default to track update' do
modifier = DefaultModifier.create!(:some_content => Faker::Lorem.sentence)
modifier.update_attributes! :some_content => 'modified'
expect(modifier.versions.last.event).to eq 'update'
end

it 'should default to track create' do
modifier = DefaultModifier.create!(:some_content => Faker::Lorem.sentence)
expect(modifier.versions.last.event).to eq 'create'
end
end

context 'when only one callback-method' do
it 'does only track the corresponding event' do
modifier = CreateModifier.create!(:some_content => Faker::Lorem.sentence)
modifier.update_attributes!(:some_content => 'modified')
modifier.test_destroy
expect(modifier.versions.last.event).to eq 'create'
end
end
end
end
end
23 changes: 23 additions & 0 deletions spec/support/callback_modifier.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
class BeforeDestroyModifier < CallbackModifier
paper_trail_before_destroy
end

class AfterDestroyModifier < CallbackModifier
paper_trail_after_destroy
end

class NoArgDestroyModifier < CallbackModifier
paper_trail_destroy
end

class UpdateModifier < CallbackModifier
paper_trail_update
end

class CreateModifier < CallbackModifier
paper_trail_create
end

class DefaultModifier < CallbackModifier
has_paper_trail
end
18 changes: 18 additions & 0 deletions test/dummy/app/models/callback_modifier.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class CallbackModifier < ActiveRecord::Base
# This will not be directly instantiated, but we need to set paper_trail up
# before we can run tests on fake classes inheriting from CallbackModifier
has_paper_trail :on => []

def test_destroy
transaction do
run_callbacks(:destroy) do
self.deleted = true
save!
end
end
end

def flagged_deleted?
deleted?
end
end
6 changes: 6 additions & 0 deletions test/dummy/db/migrate/20110208155312_set_up_test_tables.rb
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,11 @@ def self.up
t.string :name
t.boolean :scoped, :default => true
end

create_table :callback_modifiers, :force => true do |t|
t.string :some_content
t.boolean :deleted, :default => false
end
end

def self.down
Expand Down Expand Up @@ -250,5 +255,6 @@ def self.down
remove_index :version_associations, :column => [:version_id]
remove_index :version_associations, :name => 'index_version_associations_on_foreign_key'
drop_table :version_associations
drop_table :filter_modifier
end
end
5 changes: 5 additions & 0 deletions test/dummy/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@
t.boolean "scoped", default: true
end

create_table "callback_modifiers", force: :cascade do |t|
t.string "some_content"
t.boolean "deleted", default: false
end

create_table "customers", force: :cascade do |t|
t.string "name"
end
Expand Down

0 comments on commit 9008d67

Please sign in to comment.