Skip to content

Commit de52843

Browse files
committed
Merge pull request #614 from Ninigi/master
Enable control over the order of AR callbacks
2 parents 207d2f5 + 8fe6ada commit de52843

9 files changed

Lines changed: 248 additions & 14 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ None
2828

2929
### Added
3030

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

README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,27 @@ a.versions.size # 3
330330
a.versions.last.event # 'update'
331331
```
332332

333+
### Controlling the Order of AR Callbacks
334+
335+
You can also use the corresponding callback-methods seperately instead of using
336+
the :on option. If you choose to use the callback-methods, PaperTrail will only
337+
track the according events - so `paper_trail_on_create` is basically the same as
338+
`has_paper_trail :on => :create`.
339+
340+
```ruby
341+
class Article < ActiveRecord::Base
342+
has_paper_trail :on => []
343+
paper_trail_on_destroy
344+
paper_trail_on_update
345+
paper_trail_on_create
346+
end
347+
```
348+
349+
The `paper_trail_on_destroy` method can be configured to be called `:before` or `:after` the
350+
destroy event. This can be usefull if you are using a third party tool that alters the
351+
destroy method (for example paranoia). If you do not pass an argument, it will default
352+
to after_destroy.
353+
333354
## Choosing When To Save New Versions
334355

335356
You can choose the conditions when to add new versions with the `if` and

lib/paper_trail/has_paper_trail.rb

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,18 @@ module ClassMethods
4646
# column if it exists. Default is true
4747
#
4848
def has_paper_trail(options = {})
49+
options[:on] ||= [:create, :update, :destroy]
50+
51+
# Wrap the :on option in an array if necessary. This allows a single
52+
# symbol to be passed in.
53+
options[:on] = Array(options[:on])
54+
55+
setup_model_for_paper_trail(options)
56+
57+
setup_callbacks_from_options options[:on]
58+
end
59+
60+
def setup_model_for_paper_trail(options = {})
4961
# Lazily include the instance methods so we don't clutter up
5062
# any more ActiveRecord models than we have to.
5163
send :include, InstanceMethods
@@ -60,6 +72,7 @@ def has_paper_trail(options = {})
6072
self.version_class_name = options[:class_name] || 'PaperTrail::Version'
6173

6274
class_attribute :paper_trail_options
75+
6376
self.paper_trail_options = options.dup
6477

6578
[:ignore, :skip, :only].each do |k|
@@ -87,26 +100,53 @@ def has_paper_trail(options = {})
87100
:order => self.paper_trail_version_class.timestamp_sort_order
88101
end
89102

90-
options[:on] ||= [:create, :update, :destroy]
91-
92-
# Wrap the :on option in an array if necessary. This allows a single
93-
# symbol to be passed in.
94-
options_on = Array(options[:on])
95-
96-
after_create :record_create, :if => :save_version? if options_on.include?(:create)
97-
if options_on.include?(:update)
98-
before_save :reset_timestamp_attrs_for_update_if_needed!, :on => :update
99-
after_update :record_update, :if => :save_version?
100-
after_update :clear_version_instance!
101-
end
102-
after_destroy :record_destroy, :if => :save_version? if options_on.include?(:destroy)
103-
104103
# Reset the transaction id when the transaction is closed.
105104
after_commit :reset_transaction_id
106105
after_rollback :reset_transaction_id
107106
after_rollback :clear_rolled_back_versions
108107
end
109108

109+
def setup_callbacks_from_options(options_on = [])
110+
options_on.each do |option|
111+
send "paper_trail_on_#{option}"
112+
end
113+
end
114+
115+
# Record version before or after "destroy" event
116+
def paper_trail_on_destroy(recording_order = 'after')
117+
unless %w[after before].include?(recording_order.to_s)
118+
fail ArgumentError, 'recording order can only be "after" or "before"'
119+
end
120+
121+
send "#{recording_order}_destroy",
122+
:record_destroy,
123+
:if => :save_version?
124+
125+
return if paper_trail_options[:on].include?(:destroy)
126+
paper_trail_options[:on] << :destroy
127+
end
128+
129+
# Record version after "update" event
130+
def paper_trail_on_update
131+
before_save :reset_timestamp_attrs_for_update_if_needed!,
132+
:on => :update
133+
after_update :record_update,
134+
:if => :save_version?
135+
after_update :clear_version_instance!
136+
137+
return if paper_trail_options[:on].include?(:update)
138+
paper_trail_options[:on] << :update
139+
end
140+
141+
# Record version after "create" event
142+
def paper_trail_on_create
143+
after_create :record_create,
144+
:if => :save_version?
145+
146+
return if paper_trail_options[:on].include?(:create)
147+
paper_trail_options[:on] << :create
148+
end
149+
110150
# Switches PaperTrail off for this class.
111151
def paper_trail_off!
112152
PaperTrail.enabled_for_model(self, false)

spec/models/animal_spec.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,5 +15,22 @@
1515
expect(dog).to be_instance_of(Dog)
1616
end
1717
end
18+
19+
context 'with callback-methods' do
20+
context 'when only has_paper_trail set in super class' do
21+
let(:callback_cat) { Cat.create(:name => 'Markus') }
22+
23+
it 'trails all events' do
24+
callback_cat.update_attributes(:name => 'Billie')
25+
callback_cat.destroy
26+
expect(callback_cat.versions.collect(&:event)).to eq %w(create update destroy)
27+
end
28+
29+
it 'does not break reify' do
30+
callback_cat.destroy
31+
expect { callback_cat.versions.last.reify }.not_to raise_error
32+
end
33+
end
34+
end
1835
end
1936
end
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
require 'rails_helper'
2+
require 'support/callback_modifier'
3+
4+
describe CallbackModifier, :type => :model do
5+
with_versioning do
6+
describe 'callback-methods', :versioning => true do
7+
describe 'paper_trail_on_destroy' do
8+
it 'should add :destroy to paper_trail_options[:on]' do
9+
modifier = NoArgDestroyModifier.create!(:some_content => Faker::Lorem.sentence)
10+
expect(modifier.paper_trail_options[:on]).to eq [:destroy]
11+
end
12+
13+
context 'when :before' do
14+
it 'should create the version before destroy' do
15+
modifier = BeforeDestroyModifier.create!(:some_content => Faker::Lorem.sentence)
16+
modifier.test_destroy
17+
expect(modifier.versions.last.reify).not_to be_flagged_deleted
18+
end
19+
end
20+
21+
context 'when :after' do
22+
it 'should create the version after destroy' do
23+
modifier = AfterDestroyModifier.create!(:some_content => Faker::Lorem.sentence)
24+
modifier.test_destroy
25+
expect(modifier.versions.last.reify).to be_flagged_deleted
26+
end
27+
end
28+
29+
context 'when no argument' do
30+
it 'should default to after destroy' do
31+
modifier = NoArgDestroyModifier.create!(:some_content => Faker::Lorem.sentence)
32+
modifier.test_destroy
33+
expect(modifier.versions.last.reify).to be_flagged_deleted
34+
end
35+
end
36+
end
37+
38+
describe 'paper_trail_on_update' do
39+
it 'should add :update to paper_trail_options[:on]' do
40+
modifier = UpdateModifier.create!(:some_content => Faker::Lorem.sentence)
41+
expect(modifier.paper_trail_options[:on]).to eq [:update]
42+
end
43+
44+
it 'should create a version' do
45+
modifier = UpdateModifier.create!(:some_content => Faker::Lorem.sentence)
46+
modifier.update_attributes! :some_content => 'modified'
47+
expect(modifier.versions.last.event).to eq 'update'
48+
end
49+
end
50+
51+
describe 'paper_trail_on_create' do
52+
it 'should add :create to paper_trail_options[:on]' do
53+
modifier = CreateModifier.create!(:some_content => Faker::Lorem.sentence)
54+
expect(modifier.paper_trail_options[:on]).to eq [:create]
55+
end
56+
57+
it 'should create a version' do
58+
modifier = CreateModifier.create!(:some_content => Faker::Lorem.sentence)
59+
expect(modifier.versions.last.event).to eq 'create'
60+
end
61+
end
62+
63+
context 'when no callback-method used' do
64+
it 'should set paper_trail_options[:on] to [:create, :update, :destroy]' do
65+
modifier = DefaultModifier.create!(:some_content => Faker::Lorem.sentence)
66+
expect(modifier.paper_trail_options[:on]).to eq [:create, :update, :destroy]
67+
end
68+
69+
it 'should default to track destroy' do
70+
modifier = DefaultModifier.create!(:some_content => Faker::Lorem.sentence)
71+
modifier.destroy
72+
expect(modifier.versions.last.event).to eq 'destroy'
73+
end
74+
75+
it 'should default to track update' do
76+
modifier = DefaultModifier.create!(:some_content => Faker::Lorem.sentence)
77+
modifier.update_attributes! :some_content => 'modified'
78+
expect(modifier.versions.last.event).to eq 'update'
79+
end
80+
81+
it 'should default to track create' do
82+
modifier = DefaultModifier.create!(:some_content => Faker::Lorem.sentence)
83+
expect(modifier.versions.last.event).to eq 'create'
84+
end
85+
end
86+
87+
context 'when only one callback-method' do
88+
it 'does only track the corresponding event' do
89+
modifier = CreateModifier.create!(:some_content => Faker::Lorem.sentence)
90+
modifier.update_attributes!(:some_content => 'modified')
91+
modifier.test_destroy
92+
expect(modifier.versions.collect(&:event)).to eq ['create']
93+
end
94+
end
95+
end
96+
end
97+
end

spec/support/callback_modifier.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
class BeforeDestroyModifier < CallbackModifier
2+
has_paper_trail :on => []
3+
paper_trail_on_destroy :before
4+
end
5+
6+
class AfterDestroyModifier < CallbackModifier
7+
has_paper_trail :on => []
8+
paper_trail_on_destroy :after
9+
end
10+
11+
class NoArgDestroyModifier < CallbackModifier
12+
has_paper_trail :on => []
13+
paper_trail_on_destroy
14+
end
15+
16+
class UpdateModifier < CallbackModifier
17+
has_paper_trail :on => []
18+
paper_trail_on_update
19+
end
20+
21+
class CreateModifier < CallbackModifier
22+
has_paper_trail :on => []
23+
paper_trail_on_create
24+
end
25+
26+
class DefaultModifier < CallbackModifier
27+
has_paper_trail
28+
end
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
class CallbackModifier < ActiveRecord::Base
2+
has_paper_trail :on => []
3+
4+
def test_destroy
5+
transaction do
6+
run_callbacks(:destroy) do
7+
self.deleted = true
8+
save!
9+
end
10+
end
11+
end
12+
13+
def flagged_deleted?
14+
deleted?
15+
end
16+
end

test/dummy/db/migrate/20110208155312_set_up_test_tables.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,11 @@ def self.up
212212
t.boolean :scoped, :default => true
213213
end
214214

215+
create_table :callback_modifiers, :force => true do |t|
216+
t.string :some_content
217+
t.boolean :deleted, :default => false
218+
end
219+
215220
create_table :chapters, :force => true do |t|
216221
t.string :name
217222
end
@@ -277,5 +282,7 @@ def self.down
277282
remove_index :version_associations, :column => [:version_id]
278283
remove_index :version_associations, :name => 'index_version_associations_on_foreign_key'
279284
drop_table :version_associations
285+
drop_table :filter_modifier
286+
drop_table :callback_modifiers
280287
end
281288
end

test/dummy/db/schema.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@
5555
t.boolean "scoped", default: true
5656
end
5757

58+
create_table "callback_modifiers", force: :cascade do |t|
59+
t.string "some_content"
60+
t.boolean "deleted", default: false
61+
end
62+
5863
create_table "customers", force: :cascade do |t|
5964
t.string "name"
6065
end

0 commit comments

Comments
 (0)