ericperko-acts_as_audited 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,119 @@
1
+ require 'set'
2
+
3
+ # Audit saves the changes to ActiveRecord models. It has the following attributes:
4
+ #
5
+ # * <tt>auditable</tt>: the ActiveRecord model that was changed
6
+ # * <tt>user</tt>: the user that performed the change; a string or an ActiveRecord model
7
+ # * <tt>action</tt>: one of create, update, or delete
8
+ # * <tt>changes</tt>: a serialized hash of all the changes
9
+ # * <tt>created_at</tt>: Time that the change was performed
10
+ #
11
+ class Audit < ActiveRecord::Base
12
+ belongs_to :auditable, :polymorphic => true
13
+ belongs_to :user, :polymorphic => true
14
+
15
+ before_create :set_audit_version_number, :set_audit_user
16
+
17
+ serialize :changes
18
+
19
+ cattr_accessor :audited_class_names
20
+ self.audited_class_names = Set.new
21
+
22
+ def self.audited_classes
23
+ self.audited_class_names.map(&:constantize)
24
+ end
25
+
26
+ # All audits made during the block called will be recorded as made
27
+ # by +user+. This method is hopefully threadsafe, making it ideal
28
+ # for background operations that require audit information.
29
+ def self.as_user(user, &block)
30
+ Thread.current[:acts_as_audited_user] = user
31
+
32
+ yield
33
+
34
+ Thread.current[:acts_as_audited_user] = nil
35
+ end
36
+
37
+ # Allows user to be set to either a string or an ActiveRecord object
38
+ def user_as_string=(user) #:nodoc:
39
+ # reset both either way
40
+ self.user_as_model = self.username = nil
41
+ user.is_a?(ActiveRecord::Base) ?
42
+ self.user_as_model = user :
43
+ self.username = user
44
+ end
45
+ alias_method :user_as_model=, :user=
46
+ alias_method :user=, :user_as_string=
47
+
48
+ def user_as_string #:nodoc:
49
+ self.user_as_model || self.username
50
+ end
51
+ alias_method :user_as_model, :user
52
+ alias_method :user, :user_as_string
53
+
54
+ def revision
55
+ clazz = auditable_type.constantize
56
+ returning clazz.find_by_id(auditable_id) || clazz.new do |m|
57
+ Audit.assign_revision_attributes(m, self.class.reconstruct_attributes(ancestors).merge({:audit_version => audit_version}))
58
+ end
59
+ end
60
+
61
+ def ancestors
62
+ self.class.find(:all, :order => 'audit_version',
63
+ :conditions => ['auditable_id = ? and auditable_type = ? and audit_version <= ?',
64
+ auditable_id, auditable_type, audit_version])
65
+ end
66
+
67
+ # Returns a hash of the changed attributes with the new values
68
+ def new_attributes
69
+ (changes || {}).inject({}.with_indifferent_access) do |attrs,(attr,values)|
70
+ attrs[attr] = values.is_a?(Array) ? values.last : values
71
+ attrs
72
+ end
73
+ end
74
+
75
+ # Returns a hash of the changed attributes with the old values
76
+ def old_attributes
77
+ (changes || {}).inject({}.with_indifferent_access) do |attrs,(attr,values)|
78
+ attrs[attr] = Array(values).first
79
+ attrs
80
+ end
81
+ end
82
+
83
+ def self.reconstruct_attributes(audits)
84
+ attributes = {}
85
+ result = audits.collect do |audit|
86
+ attributes.merge!(audit.new_attributes).merge!(:audit_version => audit.audit_version)
87
+ yield attributes if block_given?
88
+ end
89
+ block_given? ? result : attributes
90
+ end
91
+
92
+ def self.assign_revision_attributes(record, attributes)
93
+ attributes.each do |attr, val|
94
+ if record.respond_to?("#{attr}=")
95
+ record.attributes.has_key?(attr.to_s) ?
96
+ record[attr] = val :
97
+ record.send("#{attr}=", val)
98
+ end
99
+ end
100
+ record
101
+ end
102
+
103
+ private
104
+
105
+ def set_audit_version_number
106
+ max = self.class.maximum(:audit_version,
107
+ :conditions => {
108
+ :auditable_id => auditable_id,
109
+ :auditable_type => auditable_type
110
+ }) || 0
111
+ self.audit_version = max + 1
112
+ end
113
+
114
+ def set_audit_user
115
+ self.user = Thread.current[:acts_as_audited_user] if Thread.current[:acts_as_audited_user]
116
+ nil # prevent stopping callback chains
117
+ end
118
+
119
+ end
@@ -0,0 +1,36 @@
1
+ module CollectiveIdea #:nodoc:
2
+ module ActionController #:nodoc:
3
+ module Audited #:nodoc:
4
+ def audit(*models)
5
+ ActiveSupport::Deprecation.warn("#audit is deprecated. Declare #acts_as_audited in your models.", caller)
6
+
7
+ options = models.extract_options!
8
+
9
+ # Parse the options hash looking for classes
10
+ options.each_key do |key|
11
+ models << [key, options.delete(key)] if key.is_a?(Class)
12
+ end
13
+
14
+ models.each do |(model, model_options)|
15
+ model.send :acts_as_audited, model_options || {}
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ class AuditSweeper < ActionController::Caching::Sweeper #:nodoc:
23
+ def before_create(audit)
24
+ audit.user ||= current_user
25
+ end
26
+
27
+ def current_user
28
+ controller.send :current_user if controller.respond_to?(:current_user, true)
29
+ end
30
+ end
31
+
32
+ ActionController::Base.class_eval do
33
+ extend CollectiveIdea::ActionController::Audited
34
+ cache_sweeper :audit_sweeper
35
+ end
36
+ Audit.add_observer(AuditSweeper.instance)
@@ -0,0 +1,8 @@
1
+ require 'acts_as_audited/audit'
2
+ require 'acts_as_audited'
3
+
4
+ ActiveRecord::Base.send :include, CollectiveIdea::Acts::Audited
5
+
6
+ if defined?(ActionController) and defined?(ActionController::Base)
7
+ require 'acts_as_audited/audit_sweeper'
8
+ end
@@ -0,0 +1,438 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/test_helper')
2
+
3
+ module CollectiveIdea
4
+ module Acts
5
+ class AuditedTest < Test::Unit::TestCase
6
+ should "include instance methods" do
7
+ User.new.should be_kind_of(CollectiveIdea::Acts::Audited::InstanceMethods)
8
+ end
9
+
10
+ should "extend singleton methods" do
11
+ User.should be_kind_of(CollectiveIdea::Acts::Audited::SingletonMethods)
12
+ end
13
+
14
+ ['created_at', 'updated_at', 'created_on', 'updated_on', 'lock_version', 'id', 'password'].each do |column|
15
+ should "not audit #{column}" do
16
+ User.non_audited_columns.should include(column)
17
+ end
18
+ end
19
+
20
+ should "not save non-audited columns" do
21
+ create_user.audits.first.changes.keys.any?{|col| ['created_at', 'updated_at', 'password'].include? col}.should be(false)
22
+ end
23
+
24
+ context "on create" do
25
+ setup { @user = create_user :audit_comment => "Create" }
26
+
27
+ should_change 'Audit.count', :by => 1
28
+
29
+ should 'create associated audit' do
30
+ @user.audits.count.should == 1
31
+ end
32
+ should "set the action to 'create'" do
33
+ @user.audits.first.action.should == 'create'
34
+ end
35
+
36
+ should "store all the audited attributes" do
37
+ @user.audits.first.changes.should == @user.audited_attributes
38
+ end
39
+
40
+
41
+ should "not audit an attribute which is excepted if specified on create and on destroy" do
42
+ on_create_destroy_except_name = OnCreateDestroyExceptName.create(:name => 'Bart')
43
+ on_create_destroy_except_name.audits.first.changes.keys.any?{|col| ['name'].include? col}.should be(false)
44
+ end
45
+
46
+
47
+ should "store comment" do
48
+ @user.audits.first.comment.should == "Create"
49
+ end
50
+
51
+
52
+ should "not save an audit if only specified on update and on destroy" do
53
+ lambda { on_update_destroy = OnUpdateDestroy.create(:name => 'Bart') }.should_not change { Audit.count }
54
+ end
55
+ end
56
+
57
+ context "on update" do
58
+ setup do
59
+ @user = create_user(:name => 'Brandon', :audit_comment => "Update")
60
+ end
61
+
62
+ should "save an audit" do
63
+ lambda { @user.update_attribute(:name, "Someone") }.should change { @user.audits.count }.by(1)
64
+ lambda { @user.update_attribute(:name, "Someone else") }.should change { @user.audits.count }.by(1)
65
+ end
66
+
67
+ should "not save an audit if the record is not changed" do
68
+ lambda { @user.save! }.should_not change { Audit.count }
69
+ end
70
+
71
+ should "set the action to 'update'" do
72
+ @user.update_attributes :name => 'Changed'
73
+ @user.audits.last.action.should == 'update'
74
+ end
75
+
76
+ should "store the changed attributes" do
77
+ @user.update_attributes :name => 'Changed'
78
+ @user.audits.last.changes.should == {'name' => ['Brandon', 'Changed']}
79
+ end
80
+
81
+ should "store audit comment" do
82
+ @user.audits.last.comment.should == "Update"
83
+ end
84
+
85
+ # Dirty tracking in Rails 2.0-2.2 had issues with type casting
86
+ if ActiveRecord::VERSION::STRING >= '2.3'
87
+ should "not save an audit if the value doesn't change after type casting" do
88
+ @user.update_attributes! :logins => 0, :activated => true
89
+ lambda { @user.update_attribute :logins, '0' }.should_not change { Audit.count }
90
+ lambda { @user.update_attribute :activated, 1 }.should_not change { Audit.count }
91
+ lambda { @user.update_attribute :activated, '1' }.should_not change { Audit.count }
92
+ end
93
+ end
94
+
95
+ should "not save an audit if only specified on create and on destroy" do
96
+ on_create_destroy = OnCreateDestroy.create(:name => 'Bart')
97
+ lambda { on_create_destroy.update_attributes :name => 'Changed' }.should_not change { Audit.count }
98
+ end
99
+ end
100
+
101
+ context "on destroy" do
102
+ setup do
103
+ @user = create_user
104
+ end
105
+
106
+ should "save an audit" do
107
+ lambda { @user.destroy }.should change { Audit.count }.by(1)
108
+ @user.audits.size.should == 2
109
+ end
110
+
111
+ should "set the action to 'destroy'" do
112
+ @user.destroy
113
+ @user.audits.last.action.should == 'destroy'
114
+ end
115
+
116
+ should "store all of the audited attributes" do
117
+ @user.destroy
118
+ @user.audits.last.changes.should == @user.audited_attributes
119
+ end
120
+
121
+ should "be able to reconstruct destroyed record without history" do
122
+ @user.audits.delete_all
123
+ @user.destroy
124
+ revision = @user.audits.first.revision
125
+ revision.name.should == @user.name
126
+ end
127
+
128
+ should "not save an audit if only specified on create and on update" do
129
+ on_create_update = OnCreateUpdate.create(:name => 'Bart')
130
+ lambda { on_create_update.destroy }.should_not change { Audit.count }
131
+ end
132
+ end
133
+
134
+ context "dirty tracking" do
135
+ setup do
136
+ @user = create_user
137
+ end
138
+
139
+ should "not be changed when the record is saved" do
140
+ u = User.new(:name => 'Brandon')
141
+ u.changed?.should be(true)
142
+ u.save
143
+ u.changed?.should be(false)
144
+ end
145
+
146
+ should "be changed when an attribute has been changed" do
147
+ @user.name = "Bobby"
148
+ @user.changed?.should be(true)
149
+ @user.name_changed?.should be(true)
150
+ @user.username_changed?.should be(false)
151
+ end
152
+
153
+ # Dirty tracking in Rails 2.0-2.2 had issues with type casting
154
+ if ActiveRecord::VERSION::STRING >= '2.3'
155
+ should "not be changed if the value doesn't change after type casting" do
156
+ @user.update_attributes! :logins => 0, :activated => true
157
+ @user.logins = '0'
158
+ @user.changed?.should be(false)
159
+ end
160
+ end
161
+
162
+ end
163
+
164
+ context "revisions" do
165
+ setup do
166
+ @user = create_versions
167
+ end
168
+
169
+ should "be an Array of Users" do
170
+ @user.revisions.should be_kind_of(Array)
171
+ @user.revisions.each {|version| version.should be_kind_of(User) }
172
+ end
173
+
174
+ should "have one revision for a new record" do
175
+ create_user.revisions.size.should == 1
176
+ end
177
+
178
+ should "have one revision for each audit" do
179
+ @user.revisions.size.should == @user.audits.size
180
+ end
181
+
182
+ should "set the attributes for each revision" do
183
+ u = User.create(:name => 'Brandon', :username => 'brandon')
184
+ u.update_attributes :name => 'Foobar'
185
+ u.update_attributes :name => 'Awesome', :username => 'keepers'
186
+
187
+ u.revisions.size.should == 3
188
+
189
+ u.revisions[0].name.should == 'Brandon'
190
+ u.revisions[0].username.should == 'brandon'
191
+
192
+ u.revisions[1].name.should == 'Foobar'
193
+ u.revisions[1].username.should == 'brandon'
194
+
195
+ u.revisions[2].name.should == 'Awesome'
196
+ u.revisions[2].username.should == 'keepers'
197
+ end
198
+
199
+ should "access to only recent revisions" do
200
+ u = User.create(:name => 'Brandon', :username => 'brandon')
201
+ u.update_attributes :name => 'Foobar'
202
+ u.update_attributes :name => 'Awesome', :username => 'keepers'
203
+
204
+ u.revisions(2).size.should == 2
205
+
206
+ u.revisions(2)[0].name.should == 'Foobar'
207
+ u.revisions(2)[0].username.should == 'brandon'
208
+
209
+ u.revisions(2)[1].name.should == 'Awesome'
210
+ u.revisions(2)[1].username.should == 'keepers'
211
+ end
212
+
213
+ should "be empty if no audits exist" do
214
+ @user.audits.delete_all
215
+ @user.revisions.empty?.should be(true)
216
+ end
217
+
218
+ should "ignore attributes that have been deleted" do
219
+ @user.audits.last.update_attributes :changes => {:old_attribute => 'old value'}
220
+ lambda { @user.revisions }.should_not raise_error
221
+ end
222
+
223
+ end
224
+
225
+ context "revision" do
226
+ setup do
227
+ @user = create_versions(5)
228
+ end
229
+
230
+ should "maintain identity" do
231
+ @user.revision(1).should == @user
232
+ end
233
+
234
+ should "find the given revision" do
235
+ revision = @user.revision(3)
236
+ revision.should be_kind_of(User)
237
+ revision.audit_version.should == 3
238
+ revision.name.should == 'Foobar 3'
239
+ end
240
+
241
+ should "find the previous revision with :previous" do
242
+ revision = @user.revision(:previous)
243
+ revision.audit_version.should == 4
244
+ revision.should == @user.revision(4)
245
+ end
246
+
247
+ should "be able to get the previous revision repeatedly" do
248
+ previous = @user.revision(:previous)
249
+ previous.audit_version.should == 4
250
+ previous.revision(:previous).audit_version.should == 3
251
+ end
252
+
253
+ should "be able to set protected attributes" do
254
+ u = User.create(:name => 'Brandon')
255
+ u.update_attribute :logins, 1
256
+ u.update_attribute :logins, 2
257
+
258
+ u.revision(3).logins.should == 2
259
+ u.revision(2).logins.should == 1
260
+ u.revision(1).logins.should == 0
261
+ end
262
+
263
+ should "set attributes directly" do
264
+ u = User.create(:name => '<Joe>')
265
+ u.revision(1).name.should == '&lt;Joe&gt;'
266
+ end
267
+
268
+ should "set the attributes for each revision" do
269
+ u = User.create(:name => 'Brandon', :username => 'brandon')
270
+ u.update_attributes :name => 'Foobar'
271
+ u.update_attributes :name => 'Awesome', :username => 'keepers'
272
+
273
+ u.revision(3).name.should == 'Awesome'
274
+ u.revision(3).username.should == 'keepers'
275
+
276
+ u.revision(2).name.should == 'Foobar'
277
+ u.revision(2).username.should == 'brandon'
278
+
279
+ u.revision(1).name.should == 'Brandon'
280
+ u.revision(1).username.should == 'brandon'
281
+ end
282
+
283
+ should "be able to get time for first revision" do
284
+ suspended_at = Time.now
285
+ u = User.create(:suspended_at => suspended_at)
286
+ u.revision(1).suspended_at.should == suspended_at
287
+ end
288
+
289
+ should "not raise an error when no previous audits exist" do
290
+ @user.audits.destroy_all
291
+ lambda{ @user.revision(:previous) }.should_not raise_error
292
+ end
293
+
294
+ should "mark revision's attributes as changed" do
295
+ @user.revision(1).name_changed?.should be(true)
296
+ end
297
+
298
+ should "record new audit when saving revision" do
299
+ lambda { @user.revision(1).save! }.should change { @user.audits.count }.by(1)
300
+ end
301
+
302
+ end
303
+
304
+ context "revision_at" do
305
+ should "find the latest revision before the given time" do
306
+ u = create_user
307
+ Audit.update(u.audits.first.id, :created_at => 1.hour.ago)
308
+ u.update_attributes :name => 'updated'
309
+ u.revision_at(2.minutes.ago).audit_version.should == 1
310
+ end
311
+
312
+ should "be nil if given a time before audits" do
313
+ create_user.revision_at(1.week.ago).should be(nil)
314
+ end
315
+
316
+ end
317
+
318
+ context "without auditing" do
319
+
320
+ should "not save an audit when calling #save_without_auditing" do
321
+ lambda {
322
+ u = User.new(:name => 'Brandon')
323
+ u.save_without_auditing.should be(true)
324
+ }.should_not change { Audit.count }
325
+ end
326
+
327
+ should "not save an audit inside of the #without_auditing block" do
328
+ lambda do
329
+ User.without_auditing { User.create(:name => 'Brandon') }
330
+ end.should_not change { Audit.count }
331
+ end
332
+ end
333
+
334
+ context "comment required" do
335
+ class CommentRequiredUser < ActiveRecord::Base
336
+ set_table_name :users
337
+ acts_as_audited :comment_required => true
338
+ end
339
+
340
+ context "on create" do
341
+ should "not validate when audit_comment is not supplied" do
342
+ CommentRequiredUser.new.valid?.should == false
343
+ end
344
+
345
+ should "validate when audit_comment is supplied" do
346
+ CommentRequiredUser.new(:audit_comment => "Create").valid?.should == true
347
+ end
348
+ end
349
+
350
+ context "on update" do
351
+ setup do
352
+ @user = CommentRequiredUser.create(:audit_comment => "Create")
353
+ end
354
+ should "not validate when audit_comment is not supplied" do
355
+ @user.update_attributes(:name => "Test").should == false
356
+ end
357
+
358
+ should "validate when audit_comment is supplied" do
359
+ @user.update_attributes(:name => "foo", :audit_comment => "Update").should == true
360
+ @user.audits.last.comment.should == "Update"
361
+ end
362
+
363
+ end
364
+
365
+ context "on destroy" do
366
+ setup do
367
+ @user = CommentRequiredUser.create(:audit_comment => "Create")
368
+ end
369
+
370
+ should "not validate when audit_comment is unset" do
371
+ @user.destroy.should == false
372
+ end
373
+
374
+ should "validate when audit_comment is supplied" do
375
+ @user.audit_comment = "Destroy"
376
+ @user.destroy.should == @user
377
+ end
378
+ end
379
+
380
+ end
381
+
382
+ context "attr_protected and attr_accessible" do
383
+ class UnprotectedUser < ActiveRecord::Base
384
+ set_table_name :users
385
+ acts_as_audited :protect => false
386
+ attr_accessible :name, :username, :password
387
+ end
388
+ should "not raise error when attr_accessible is set and protected is false" do
389
+ lambda{
390
+ UnprotectedUser.new(:name => 'NO FAIL!')
391
+ }.should_not raise_error(RuntimeError)
392
+ end
393
+
394
+ class AccessibleUser < ActiveRecord::Base
395
+ set_table_name :users
396
+ attr_accessible :name, :username, :password # declare attr_accessible before calling aaa
397
+ acts_as_audited
398
+ end
399
+ should "not raise an error when attr_accessible is declared before acts_as_audited" do
400
+ lambda{
401
+ AccessibleUser.new(:name => 'NO FAIL!')
402
+ }.should_not raise_error
403
+ end
404
+ end
405
+
406
+ context "audit as" do
407
+ setup do
408
+ @user = User.create :name => 'Testing'
409
+ end
410
+
411
+ should "record user objects" do
412
+ Company.audit_as( @user ) do
413
+ company = Company.create :name => 'The auditors'
414
+ company.name = 'The Auditors'
415
+ company.save
416
+
417
+ company.audits.each do |audit|
418
+ audit.user.should == @user
419
+ end
420
+ end
421
+ end
422
+
423
+ should "record usernames" do
424
+ Company.audit_as( @user.name ) do
425
+ company = Company.create :name => 'The auditors'
426
+ company.name = 'The Auditors, Inc'
427
+ company.save
428
+
429
+ company.audits.each do |audit|
430
+ audit.username.should == @user.name
431
+ end
432
+ end
433
+ end
434
+ end
435
+
436
+ end
437
+ end
438
+ end