rollbacker 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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: