rollbacker 1.0.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,16 +1,121 @@
1
- ## Rollbacker ##
1
+ Rollbacker
2
+ ==========
2
3
 
3
- Rollbacker is a manage tool for auditing changes to your ActiveRecord.
4
+ **Rollbacker** is a manage tool for auditing changes to your ActiveRecord.
4
5
  The changes of objects are added to a queue where the auditor can approve and reject those changes.
5
6
 
6
- ## Installation ##
7
+ ## Installation
7
8
 
8
9
  To use it with your Rails 3 project, add the following line to your Gemfile
9
10
 
10
- gem 'rollbacker'
11
+ ```ruby
12
+ gem 'rollbacker'
13
+ ```
11
14
 
12
15
  Generate the migration and create the rollbacker_changes table
16
+ ```bash
17
+ $ rails generate rollbacker:migration
18
+ $ rake db:migrate
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Simply call `rollbacker` on your models:
24
+
25
+ ```ruby
26
+ class Model < ActiveRecord::Base
27
+ rollbacker(:create, :update, :destroy)
28
+ end
29
+ ```
30
+
31
+ After that, whenever a model is created, updated or destroyed, a new RollbackerChange record is created.
32
+
33
+ ```ruby
34
+ # Create Model
35
+ > Model.count
36
+ => 0
37
+ > Model.create(name: 'Name', value: 'Value')
38
+ => #<Model id: nil, name: "Name", value: "Value">
39
+ > Model.count
40
+ => 0
41
+ > RollbackerChange.last.approve
42
+ > Model.count
43
+ => 1
44
+
45
+ # Update Model
46
+ > m = Model.first
47
+ => #<Model id: 1, name: "Name", value: "Value">
48
+ > m.update_attribute(:name, 'New')
49
+ > m.reload
50
+ => #<Model id: 1, name: "Name", value: "Value">
51
+ > m.rollbacker_changes.last.rollbacked_changes
52
+ => {"name"=>["Name", "New"]}
53
+ > m.update_attributes(:name=>'Newer', :value => 'New')
54
+ > m.rollbacker_changes.last.rollbacked_changes
55
+ => {"name"=>["Name", "Newer"], "value"=>["Value", "New"]}
56
+ > m.rollbacker_changes.last.approve('value')
57
+ > m.reload
58
+ => #<Model id: 1, name: "Name", value: "New">
59
+ > m.rollbacker_changes.last.rollbacked_changes
60
+ => {"name"=>["Name", "Newer"]}
61
+ > m.rollbacker_changes.last.reject
62
+ > m.reload
63
+ => #<Model id: 1, name: "Name", value: "New">
64
+
65
+ # Destroy Model
66
+ > m.destroy
67
+ > Model.count
68
+ => 1
69
+ > m.rollbacker_changes.last.approve
70
+ > Model.count
71
+ => 0
72
+ ```
73
+
74
+ The rollbacked_changes column automatically serializes the changes of any model attributes modified during the action. If there are only a few attributes you want to track or a couple that you want to prevent from being tracked, you can specify that in the rollbacker call. For example
75
+ ```ruby
76
+ rollbacker(:create, :destroy, :except => [:title])
77
+ rollbacker(:update, :only => :title)
78
+ ```
79
+
80
+ ### Current User Tracking
81
+
82
+ If you're using Rollbacker in a Rails application, all changes made within a request will automatically be attributed to the current user. By default, Rollbacker uses the `current_user` method in your controller.
83
+
84
+ ```ruby
85
+ class PostsController < ApplicationController
86
+ def create
87
+ current_user # => #<User name: "Steve">
88
+ @post = Post.create(params[:post])
89
+ @post.rollbacker_changes.last.user # => #<User name: "Steve">
90
+ end
91
+ end
92
+ ```
93
+
94
+ ### Integration
95
+ There may be some instances where you need to perform an action on your model object without Rollbacker. In those cases you can include the Rollbacker::Status module for help.
96
+ ```ruby
97
+ class PostsController < ApplicationController
98
+ include Rollbacker::Status
99
+
100
+ def update
101
+ post = Post.find(params[:id])
102
+ without_rollbacker { post.update_attributes(params[:post]) } # Rollbacker is disabled for the entire block
103
+ end
104
+ end
105
+ ```
106
+
107
+ You can also force Rollbacker to track any actions within a block as a specified user.
108
+ ```ruby
109
+ class PostsController < ApplicationController
110
+ include Rollbacker::Status
111
+
112
+ def update
113
+ post = Post.find(params[:id])
114
+ rollbacker_as(another_user) { post.update_attributes(params[:post]) }
115
+ end
116
+ end
117
+ ```
118
+
119
+ For more details, I suggest you check out the test examples in the spec folder itself.
13
120
 
14
- rails generate rollbacker:migration
15
- rake db:migrate
16
121
 
@@ -23,7 +23,7 @@ module Rollbacker
23
23
 
24
24
  config = Rollbacker::Config.new(*args)
25
25
  config.actions.each do |action|
26
- send "around_#{action}", Rollbacker::DatabaseRollback.new(config.options)
26
+ send "before_#{action}", Rollbacker::DatabaseRollback.new(config.options)
27
27
  # send :after_rollback, Rollbacker::Recorder.new(action, config.options, &blk), :on => action
28
28
  end
29
29
  send :after_rollback, Rollbacker::Recorder.new(config.options, &blk)
@@ -10,7 +10,7 @@ module Rollbacker
10
10
  end
11
11
 
12
12
  [:create, :update, :destroy].each do |action|
13
- define_method("around_#{action}") do |model|
13
+ define_method("before_#{action}") do |model|
14
14
  rollback_model_changes(model, action)
15
15
  end
16
16
  end
@@ -1,7 +1,9 @@
1
1
  require 'active_record'
2
2
  require 'rollbacker/config'
3
+ require 'rollbacker/status'
3
4
 
4
5
  class RollbackerChange < ActiveRecord::Base
6
+ include Rollbacker::Status
5
7
  belongs_to :rollbackable, :polymorphic => true
6
8
  belongs_to :user, :polymorphic => true
7
9
 
@@ -9,14 +11,65 @@ class RollbackerChange < ActiveRecord::Base
9
11
 
10
12
  serialize :rollbacked_changes
11
13
 
12
- def new_attributes
14
+ def new_attributes(*fields)
13
15
  (rollbacked_changes || {}).inject({}.with_indifferent_access) do |attrs,(attr,values)|
14
16
  attrs[attr] = values.is_a?(Array) ? values.last : values
15
17
  attrs
18
+ end.select do |k,v|
19
+ fields.blank? ? true : fields.map(&:to_s).include?(k)
16
20
  end
17
21
  end
18
22
 
23
+
24
+ def reject(*fields)
25
+ without_rollbacker do
26
+ case self.action.to_s
27
+ when 'update', 'create'
28
+ update_changes_after_approve_or_reject(*fields)
29
+ when 'destroy'
30
+ self.rollbacked_changes = nil
31
+ self.destroy
32
+ end
33
+ end
34
+ end
35
+
36
+ def approve(*fields)
37
+ without_rollbacker do
38
+ edits = self.new_attributes(*fields)
39
+ case self.action.to_s
40
+ when 'update'
41
+ if edits.present? && self.rollbackable && self.rollbackable.update_attributes(edits)
42
+ update_changes_after_approve_or_reject(*fields)
43
+ return true
44
+ end
45
+ when 'create'
46
+ if edits.present? && self.rollbackable_type.constantize.create(edits)
47
+ self.rollbacked_changes = nil
48
+ self.destroy
49
+ end
50
+ when 'destroy'
51
+ if self.rollbackable && self.rollbackable.destroy
52
+ self.rollbacked_changes = nil
53
+ self.destroy
54
+ end
55
+ end
56
+ end
57
+ end
58
+
59
+ def create_action?; self[:action].to_s == 'create' end
60
+ def update_action?; self[:action].to_s == 'update' end
61
+ def destroy_action?; self[:action].to_s == 'destroy' end
62
+
19
63
  private
64
+ def update_changes_after_approve_or_reject(*fields)
65
+ edits = fields.blank? ? nil : rollbacked_changes.delete_if{|k,v| fields.map(&:to_s).include?(k) }
66
+ if edits.present?
67
+ self.update_attribute(:rollbacked_changes, edits)
68
+ else
69
+ self.rollbacked_changes = nil
70
+ self.destroy
71
+ end
72
+ end
20
73
 
21
74
  def set_user
22
75
  self.user = Rollbacker::User.current_user if self.user_id.nil?
@@ -1,3 +1,3 @@
1
1
  module Rollbacker
2
- VERSION = "1.0.0"
2
+ VERSION = "1.0.1"
3
3
  end
@@ -11,11 +11,11 @@ describe Rollbacker::DatabaseRollback do
11
11
  end
12
12
 
13
13
  [:create, :update, :destroy].each do |action|
14
- it "should respond with around_#{action}" do
14
+ it "should respond with before_#{action}" do
15
15
  model = Model.new(name: 'name')
16
16
  config = Rollbacker::Config.new(action)
17
17
  callback = Rollbacker::DatabaseRollback.new(config.options)
18
- callback.should respond_to("around_#{action}")
18
+ callback.should respond_to("before_#{action}")
19
19
  end
20
20
  end
21
21
 
@@ -27,7 +27,7 @@ describe Rollbacker::DatabaseRollback do
27
27
 
28
28
  lambda {
29
29
  without_rollbacker do
30
- callback.send :around_create, model
30
+ callback.send :before_create, model
31
31
  end
32
32
  }.should_not raise_exception(ActiveRecord::Rollback)
33
33
  end
@@ -39,7 +39,7 @@ describe Rollbacker::DatabaseRollback do
39
39
  callback = Rollbacker::DatabaseRollback.new(config.options)
40
40
 
41
41
  lambda {
42
- callback.send :around_create, model
42
+ callback.send :before_create, model
43
43
  }.should raise_exception(ActiveRecord::Rollback)
44
44
  end
45
45
  end
@@ -15,5 +15,284 @@ describe RollbackerChange do
15
15
  record = RollbackerChange.create(:rollbackable => @rollbackable, :user => @user, :action => :create)
16
16
  record.user.should == @user
17
17
  end
18
+
19
+ describe '#new_attributes' do
20
+ it "should collect all new values" do
21
+ record = RollbackerChange.new(rollbacked_changes: {'name'=>[nil, 'Name'], 'value'=>[nil, 'Value']})
22
+ record.new_attributes.should == {'name'=>'Name','value'=>'Value'}
23
+ end
24
+
25
+ it "should select only specific attributes" do
26
+ record = RollbackerChange.new(rollbacked_changes: {'name'=>[nil, 'Name'], 'value'=>[nil, 'Value']})
27
+ record.new_attributes('name').should == {'name'=>'Name'}
28
+ record.new_attributes(:name).should == {'name'=>'Name'}
29
+ end
30
+ end
31
+
32
+ describe '#update_changes_after_approve_or_reject' do
33
+ it "should remove edits from rollbacked_changes field" do
34
+ record = RollbackerChange.new(rollbacked_changes: {'name'=>[nil, 'Name'], 'value'=>[nil, 'Value']})
35
+ record.send(:update_changes_after_approve_or_reject, 'name')
36
+ record.rollbacked_changes.should == {'value'=>[nil, 'Value']}
37
+ record.should_not be_destroyed
38
+ end
39
+
40
+ it "should be able update multiple fields" do
41
+ record = RollbackerChange.new(rollbacked_changes: {'name'=>[nil, 'Name'], 'value'=>[nil, 'Value']})
42
+ record.send(:update_changes_after_approve_or_reject, 'name', 'value')
43
+ record.rollbacked_changes.should be_nil
44
+ record.should be_destroyed
45
+ end
46
+
47
+ it "should destroy record and remove all changes with no arguments" do
48
+ record = RollbackerChange.new(rollbacked_changes: {'name'=>[nil, 'Name'], 'value'=>[nil, 'Value']})
49
+ record.send(:update_changes_after_approve_or_reject)
50
+ record.rollbacked_changes.should be_nil
51
+ record.should be_destroyed
52
+ end
53
+
54
+ it "should destroy record if no more changes" do
55
+ record = RollbackerChange.create(rollbacked_changes: {'name'=>[nil, 'Name']})
56
+ record.send(:update_changes_after_approve_or_reject, 'name')
57
+ record.rollbacked_changes.should be_nil
58
+ record.should be_destroyed
59
+ end
60
+
61
+ it "should destroy record if no more changes" do
62
+ record = RollbackerChange.create(rollbacked_changes: {'name'=>[nil, 'Name']})
63
+ record.send(:update_changes_after_approve_or_reject, :name)
64
+ record.rollbacked_changes.should be_nil
65
+ record.should be_destroyed
66
+ end
67
+ end
68
+
69
+ describe "reject" do
70
+ context "on :update" do
71
+ let(:action) { :update }
72
+ it "should not change anything with a has no change about the field" do
73
+ record = RollbackerChange.create({
74
+ :rollbacked_changes => {'name'=>[nil, 'Name']},
75
+ :rollbackable => @rollbackable,
76
+ :user => @user,
77
+ :action => action
78
+ })
79
+ record.rollbackable.should be_present
80
+ record.should be_update_action
81
+ lambda {
82
+ record.reject('value').should be_true
83
+ }.should_not change(RollbackerChange, :count)
84
+ record.reload.rollbacked_changes.should == {'name'=>[nil, 'Name']}
85
+ end
86
+ it "should NOT apply the change to the auditable model" do
87
+ record = RollbackerChange.create({
88
+ :rollbacked_changes => {'name'=>[nil, 'Name'], 'value'=>[nil, 'Value']},
89
+ :rollbackable => @rollbackable,
90
+ :user => @user,
91
+ :action => action
92
+ })
93
+ record.rollbackable.should be_present
94
+ record.should be_update_action
95
+ lambda {
96
+ record.reject('name').should be_true
97
+ }.should_not change(RollbackerChange, :count)
98
+ @rollbackable.reload.name.should be_nil
99
+ record.rollbacked_changes.should == {'value'=>[nil, 'Value']}
100
+ record.should_not be_destroyed
101
+ end
102
+ end
103
+
104
+ context "on :create" do
105
+ let(:action) { :create }
106
+ it "should NOT be created if has no change about the field" do
107
+ record = RollbackerChange.create({
108
+ :rollbacked_changes => {'name'=>[nil, 'Name']},
109
+ :rollbackable_type => 'Model',
110
+ :user => @user,
111
+ :action => action
112
+ })
113
+ record.should be_create_action
114
+ lambda {
115
+ lambda {
116
+ record.reject('value').should be_true
117
+ }.should_not change(RollbackerChange, :count)
118
+ }.should_not change(Model, :count)
119
+ record.reload.rollbacked_changes.should == {'name'=>[nil, 'Name']}
120
+ end
121
+ it "should be able to only update changes without insert a new record" do
122
+ record = RollbackerChange.create({
123
+ :rollbacked_changes => {'name'=>[nil, 'Name'], 'value'=>[nil, 'Value']},
124
+ :rollbackable_type => 'Model',
125
+ :user => @user,
126
+ :action => action
127
+ })
128
+ record.should be_create_action
129
+ lambda {
130
+ lambda {
131
+ record.reject('value').should be_true
132
+ }.should_not change(RollbackerChange, :count)
133
+ }.should_not change(Model, :count)
134
+ record.reload.rollbacked_changes.should == {'name'=>[nil, 'Name']}
135
+ end
136
+ end
137
+ context "on :destroy" do
138
+ let(:action) { :destroy }
139
+ it "should only remove rollbacker_change record and discart rollbacked_changes" do
140
+ record = RollbackerChange.create({
141
+ :rollbacked_changes => {'name'=>[nil, 'Name'], 'value'=>[nil, 'Value']},
142
+ :rollbackable => @rollbackable,
143
+ :user => @user,
144
+ :action => action
145
+ })
146
+ record.rollbackable.should be_present
147
+ record.should be_destroy_action
148
+ lambda {
149
+ lambda {
150
+ record.reject.should be_true
151
+ }.should change(RollbackerChange, :count).by(-1)
152
+ }.should_not change(Model, :count)
153
+ @rollbackable.reload.name.should be_nil
154
+ @rollbackable.reload.value.should be_nil
155
+ record.rollbacked_changes.should be_nil
156
+ record.should be_destroyed
157
+ end
158
+ end
159
+ end
160
+
161
+ describe "approve" do
162
+ context "on :update" do
163
+ let(:action) { :update}
164
+ it "should not change anything with a has no change about the field" do
165
+ record = RollbackerChange.create({
166
+ :rollbacked_changes => {'name'=>[nil, 'Name']},
167
+ :rollbackable => @rollbackable,
168
+ :user => @user,
169
+ :action => action
170
+ })
171
+ record.rollbackable.should be_present
172
+ record.should be_update_action
173
+ lambda {
174
+ lambda {
175
+ record.approve('value').should be_nil
176
+ }.should_not change(RollbackerChange, :count)
177
+ }.should_not change(Model, :count)
178
+ record.reload.rollbacked_changes.should == {'name'=>[nil, 'Name']}
179
+ end
180
+ it "should apply the change to the auditable model from a specific field" do
181
+ record = RollbackerChange.create({
182
+ :rollbacked_changes => {'name'=>[nil, 'Name'], 'value'=>[nil, 'Value']},
183
+ :rollbackable => @rollbackable,
184
+ :user => @user,
185
+ :action => action
186
+ })
187
+ record.rollbackable.should be_present
188
+ record.should be_update_action
189
+ lambda {
190
+ lambda {
191
+ record.approve('name').should be_true
192
+ }.should_not change(RollbackerChange, :count)
193
+ }.should_not change(Model, :count)
194
+ @rollbackable.reload.name.should == 'Name'
195
+ record.reload.rollbacked_changes.should == {'value'=>[nil, 'Value']}
196
+ record.should_not be_destroyed
197
+ end
198
+ it "should apply all changes to the auditable model" do
199
+ record = RollbackerChange.create({
200
+ :rollbacked_changes => {'name'=>[nil, 'Name'], 'value'=>[nil, 'Value']},
201
+ :rollbackable => @rollbackable,
202
+ :user => @user,
203
+ :action => action
204
+ })
205
+ record.rollbackable.should be_present
206
+ record.should be_update_action
207
+ lambda {
208
+ lambda {
209
+ record.approve.should be_true
210
+ }.should change(RollbackerChange, :count).by(-1)
211
+ }.should_not change(Model, :count)
212
+ @rollbackable.reload.name.should == 'Name'
213
+ @rollbackable.reload.value.should == 'Value'
214
+ record.rollbacked_changes.should be_nil
215
+ record.should be_destroyed
216
+ end
217
+ end
218
+
219
+ context "on :create" do
220
+ let(:action) { :create }
221
+ it "should not change anything with a has no change about the field" do
222
+ record = RollbackerChange.create({
223
+ :rollbacked_changes => {'name'=>[nil, 'Name']},
224
+ :rollbackable_type => 'Model',
225
+ :user => @user,
226
+ :action => action
227
+ })
228
+ record.should be_create_action
229
+ lambda {
230
+ lambda {
231
+ record.approve('value').should be_nil
232
+ }.should_not change(RollbackerChange, :count)
233
+ }.should_not change(Model, :count)
234
+ record.reload.rollbacked_changes.should == {'name'=>[nil, 'Name']}
235
+ end
236
+ it "should be created a rollbackable model from specific fields" do
237
+ record = RollbackerChange.create({
238
+ :rollbacked_changes => {'name'=>[nil, 'Newer Name'], 'value'=>[nil,'Newer Value']},
239
+ :rollbackable_type => 'Model',
240
+ :user => @user,
241
+ :action => action
242
+ })
243
+ record.should be_create_action
244
+ lambda {
245
+ lambda {
246
+ record.approve('name').should be_true
247
+ }.should change(RollbackerChange, :count).by(-1)
248
+ }.should change(Model, :count).by(1)
249
+ record.rollbacked_changes.should be_nil
250
+ record.should be_destroyed
251
+ rollbackable = Model.last
252
+ rollbackable.name.should == 'Newer Name'
253
+ rollbackable.value.should be_nil
254
+ end
255
+ it "should be created a rollbackable model with all fields" do
256
+ record = RollbackerChange.create({
257
+ :rollbacked_changes => {'name'=>[nil, 'Name'], 'value'=>[nil,'Value']},
258
+ :rollbackable_type => 'Model',
259
+ :user => @user,
260
+ :action => action
261
+ })
262
+ record.should be_create_action
263
+ lambda {
264
+ lambda {
265
+ record.approve.should be_true
266
+ }.should change(RollbackerChange, :count).by(-1)
267
+ }.should change(Model, :count).by(1)
268
+ record.rollbacked_changes.should be_nil
269
+ record.should be_destroyed
270
+ rollbackable = Model.last
271
+ rollbackable.name.should == 'Name'
272
+ rollbackable.value.should == 'Value'
273
+ end
274
+ end
275
+ context "on :destroy" do
276
+ let(:action) { :destroy }
277
+ it "should not change anything with a has no change about the field" do
278
+ record = RollbackerChange.create({
279
+ :rollbacked_changes => nil,
280
+ :rollbackable => @rollbackable,
281
+ :user => @user,
282
+ :action => action
283
+ })
284
+ record.rollbackable.should be_present
285
+ record.should be_destroy_action
286
+ lambda {
287
+ lambda {
288
+ record.approve.should be_true
289
+ }.should change(RollbackerChange, :count).by(-1)
290
+ }.should change(Model, :count).by(-1)
291
+ record.rollbacked_changes.should == nil
292
+ record.should be_destroyed
293
+ @rollbackable.should be_destroyed
294
+ end
295
+ end
296
+ end
18
297
  end
19
298
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rollbacker
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.1
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -140,3 +140,4 @@ test_files:
140
140
  - spec/support/model_setup.rb
141
141
  - spec/support/transactional_specs.rb
142
142
  - spec/user_spec.rb
143
+ has_rdoc: