htanata-acts_as_audited 1.1.1

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_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({:version => version}))
58
+ end
59
+ end
60
+
61
+ def ancestors
62
+ self.class.find(:all, :order => 'version',
63
+ :conditions => ['auditable_id = ? and auditable_type = ? and version <= ?',
64
+ auditable_id, auditable_type, 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.kind_of?(Time) ? values : Array(values).last
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!(:version => 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_version_number
106
+ max = self.class.maximum(:version,
107
+ :conditions => {
108
+ :auditable_id => auditable_id,
109
+ :auditable_type => auditable_type
110
+ }) || 0
111
+ self.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,380 @@
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 }
26
+
27
+ should_change 'Audit.count', :by => 1
28
+
29
+ should 'create associated audit' do
30
+ @user.audits.count.should == 1
31
+ end
32
+
33
+ should "set the action to 'create'" do
34
+ @user.audits.first.action.should == 'create'
35
+ end
36
+
37
+ should "store all the audited attributes" do
38
+ @user.audits.first.changes.should == @user.audited_attributes
39
+ end
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
+ should "not save an audit if only specified on update and on destroy" do
47
+ lambda { on_update_destroy = OnUpdateDestroy.create(:name => 'Bart') }.should_not change { Audit.count }
48
+ end
49
+ end
50
+
51
+ context "on update" do
52
+ setup do
53
+ @user = create_user(:name => 'Brandon')
54
+ end
55
+
56
+ should "save an audit" do
57
+ lambda { @user.update_attribute(:name, "Someone") }.should change { @user.audits.count }.by(1)
58
+ lambda { @user.update_attribute(:name, "Someone else") }.should change { @user.audits.count }.by(1)
59
+ end
60
+
61
+ should "not save an audit if the record is not changed" do
62
+ lambda { @user.save! }.should_not change { Audit.count }
63
+ end
64
+
65
+ should "set the action to 'update'" do
66
+ @user.update_attributes :name => 'Changed'
67
+ @user.audits.last.action.should == 'update'
68
+ end
69
+
70
+ should "store the changed attributes" do
71
+ @user.update_attributes :name => 'Changed'
72
+ @user.audits.last.changes.should == {'name' => ['Brandon', 'Changed']}
73
+ end
74
+
75
+ # Dirty tracking in Rails 2.0-2.2 had issues with type casting
76
+ if ActiveRecord::VERSION::STRING >= '2.3'
77
+ should "not save an audit if the value doesn't change after type casting" do
78
+ @user.update_attributes! :logins => 0, :activated => true
79
+ lambda { @user.update_attribute :logins, '0' }.should_not change { Audit.count }
80
+ lambda { @user.update_attribute :activated, 1 }.should_not change { Audit.count }
81
+ lambda { @user.update_attribute :activated, '1' }.should_not change { Audit.count }
82
+ end
83
+ end
84
+
85
+ should "not save an audit if only specified on create and on destroy" do
86
+ on_create_destroy = OnCreateDestroy.create(:name => 'Bart')
87
+ lambda { on_create_destroy.update_attributes :name => 'Changed' }.should_not change { Audit.count }
88
+ end
89
+ end
90
+
91
+ context "on destroy" do
92
+ setup do
93
+ @user = create_user
94
+ end
95
+
96
+ should "save an audit" do
97
+ lambda { @user.destroy }.should change { Audit.count }.by(1)
98
+ @user.audits.size.should == 2
99
+ end
100
+
101
+ should "set the action to 'destroy'" do
102
+ @user.destroy
103
+ @user.audits.last.action.should == 'destroy'
104
+ end
105
+
106
+ should "store all of the audited attributes" do
107
+ @user.destroy
108
+ @user.audits.last.changes.should == @user.audited_attributes
109
+ end
110
+
111
+ should "be able to reconstruct destroyed record without history" do
112
+ @user.audits.delete_all
113
+ @user.destroy
114
+ revision = @user.audits.first.revision
115
+ revision.name.should == @user.name
116
+ end
117
+
118
+ should "not save an audit if only specified on create and on update" do
119
+ on_create_update = OnCreateUpdate.create(:name => 'Bart')
120
+ lambda { on_create_update.destroy }.should_not change { Audit.count }
121
+ end
122
+ end
123
+
124
+ context "dirty tracking" do
125
+ setup do
126
+ @user = create_user
127
+ end
128
+
129
+ should "not be changed when the record is saved" do
130
+ u = User.new(:name => 'Brandon')
131
+ u.changed?.should be(true)
132
+ u.save
133
+ u.changed?.should be(false)
134
+ end
135
+
136
+ should "be changed when an attribute has been changed" do
137
+ @user.name = "Bobby"
138
+ @user.changed?.should be(true)
139
+ @user.name_changed?.should be(true)
140
+ @user.username_changed?.should be(false)
141
+ end
142
+
143
+ # Dirty tracking in Rails 2.0-2.2 had issues with type casting
144
+ if ActiveRecord::VERSION::STRING >= '2.3'
145
+ should "not be changed if the value doesn't change after type casting" do
146
+ @user.update_attributes! :logins => 0, :activated => true
147
+ @user.logins = '0'
148
+ @user.changed?.should be(false)
149
+ end
150
+ end
151
+
152
+ end
153
+
154
+ context "revisions" do
155
+ setup do
156
+ @user = create_versions
157
+ end
158
+
159
+ should "be an Array of Users" do
160
+ @user.revisions.should be_kind_of(Array)
161
+ @user.revisions.each {|version| version.should be_kind_of(User) }
162
+ end
163
+
164
+ should "have one revision for a new record" do
165
+ create_user.revisions.size.should == 1
166
+ end
167
+
168
+ should "have one revision for each audit" do
169
+ @user.revisions.size.should == @user.audits.size
170
+ end
171
+
172
+ should "set the attributes for each revision" do
173
+ u = User.create(:name => 'Brandon', :username => 'brandon')
174
+ u.update_attributes :name => 'Foobar'
175
+ u.update_attributes :name => 'Awesome', :username => 'keepers'
176
+
177
+ u.revisions.size.should == 3
178
+
179
+ u.revisions[0].name.should == 'Brandon'
180
+ u.revisions[0].username.should == 'brandon'
181
+
182
+ u.revisions[1].name.should == 'Foobar'
183
+ u.revisions[1].username.should == 'brandon'
184
+
185
+ u.revisions[2].name.should == 'Awesome'
186
+ u.revisions[2].username.should == 'keepers'
187
+ end
188
+
189
+ should "access to only recent revisions" do
190
+ u = User.create(:name => 'Brandon', :username => 'brandon')
191
+ u.update_attributes :name => 'Foobar'
192
+ u.update_attributes :name => 'Awesome', :username => 'keepers'
193
+
194
+ u.revisions(2).size.should == 2
195
+
196
+ u.revisions(2)[0].name.should == 'Foobar'
197
+ u.revisions(2)[0].username.should == 'brandon'
198
+
199
+ u.revisions(2)[1].name.should == 'Awesome'
200
+ u.revisions(2)[1].username.should == 'keepers'
201
+ end
202
+
203
+ should "be empty if no audits exist" do
204
+ @user.audits.delete_all
205
+ @user.revisions.empty?.should be(true)
206
+ end
207
+
208
+ should "ignore attributes that have been deleted" do
209
+ @user.audits.last.update_attributes :changes => {:old_attribute => 'old value'}
210
+ lambda { @user.revisions }.should_not raise_error
211
+ end
212
+
213
+ end
214
+
215
+ context "revision" do
216
+ setup do
217
+ @user = create_versions(5)
218
+ end
219
+
220
+ should "maintain identity" do
221
+ @user.revision(1).should == @user
222
+ end
223
+
224
+ should "find the given revision" do
225
+ revision = @user.revision(3)
226
+ revision.should be_kind_of(User)
227
+ revision.version.should == 3
228
+ revision.name.should == 'Foobar 3'
229
+ end
230
+
231
+ should "find the previous revision with :previous" do
232
+ revision = @user.revision(:previous)
233
+ revision.version.should == 4
234
+ revision.should == @user.revision(4)
235
+ end
236
+
237
+ should "be able to get the previous revision repeatedly" do
238
+ previous = @user.revision(:previous)
239
+ previous.version.should == 4
240
+ previous.revision(:previous).version.should == 3
241
+ end
242
+
243
+ should "be able to set protected attributes" do
244
+ u = User.create(:name => 'Brandon')
245
+ u.update_attribute :logins, 1
246
+ u.update_attribute :logins, 2
247
+
248
+ u.revision(3).logins.should == 2
249
+ u.revision(2).logins.should == 1
250
+ u.revision(1).logins.should == 0
251
+ end
252
+
253
+ should "set attributes directly" do
254
+ u = User.create(:name => '<Joe>')
255
+ u.revision(1).name.should == '&lt;Joe&gt;'
256
+ end
257
+
258
+ should "set the attributes for each revision" do
259
+ u = User.create(:name => 'Brandon', :username => 'brandon')
260
+ u.update_attributes :name => 'Foobar'
261
+ u.update_attributes :name => 'Awesome', :username => 'keepers'
262
+
263
+ u.revision(3).name.should == 'Awesome'
264
+ u.revision(3).username.should == 'keepers'
265
+
266
+ u.revision(2).name.should == 'Foobar'
267
+ u.revision(2).username.should == 'brandon'
268
+
269
+ u.revision(1).name.should == 'Brandon'
270
+ u.revision(1).username.should == 'brandon'
271
+ end
272
+
273
+ should "be able to get datetime for first revision" do
274
+ suspended_at = Time.now
275
+ u = User.create(:suspended_at => suspended_at)
276
+ u.revision(1).suspended_at.should == suspended_at
277
+ end
278
+
279
+ should "not raise an error when no previous audits exist" do
280
+ @user.audits.destroy_all
281
+ lambda{ @user.revision(:previous) }.should_not raise_error
282
+ end
283
+
284
+ should "mark revision's attributes as changed" do
285
+ @user.revision(1).name_changed?.should be(true)
286
+ end
287
+
288
+ should "record new audit when saving revision" do
289
+ lambda { @user.revision(1).save! }.should change { @user.audits.count }.by(1)
290
+ end
291
+
292
+ end
293
+
294
+ context "revision_at" do
295
+ should "find the latest revision before the given time" do
296
+ u = create_user
297
+ Audit.update(u.audits.first.id, :created_at => 1.hour.ago)
298
+ u.update_attributes :name => 'updated'
299
+ u.revision_at(2.minutes.ago).version.should == 1
300
+ end
301
+
302
+ should "be nil if given a time before audits" do
303
+ create_user.revision_at(1.week.ago).should be(nil)
304
+ end
305
+
306
+ end
307
+
308
+ context "without auditing" do
309
+
310
+ should "not save an audit when calling #save_without_auditing" do
311
+ lambda {
312
+ u = User.new(:name => 'Brandon')
313
+ u.save_without_auditing.should be(true)
314
+ }.should_not change { Audit.count }
315
+ end
316
+
317
+ should "not save an audit inside of the #without_auditing block" do
318
+ lambda do
319
+ User.without_auditing { User.create(:name => 'Brandon') }
320
+ end.should_not change { Audit.count }
321
+ end
322
+ end
323
+
324
+ context "attr_protected and attr_accessible" do
325
+ class UnprotectedUser < ActiveRecord::Base
326
+ set_table_name :users
327
+ acts_as_audited :protect => false
328
+ attr_accessible :name, :username, :password
329
+ end
330
+ should "not raise error when attr_accessible is set and protected is false" do
331
+ lambda{
332
+ UnprotectedUser.new(:name => 'NO FAIL!')
333
+ }.should_not raise_error(RuntimeError)
334
+ end
335
+
336
+ class AccessibleUser < ActiveRecord::Base
337
+ set_table_name :users
338
+ attr_accessible :name, :username, :password # declare attr_accessible before calling aaa
339
+ acts_as_audited
340
+ end
341
+ should "not raise an error when attr_accessible is declared before acts_as_audited" do
342
+ lambda{
343
+ AccessibleUser.new(:name => 'NO FAIL!')
344
+ }.should_not raise_error
345
+ end
346
+ end
347
+
348
+ context "audit as" do
349
+ setup do
350
+ @user = User.create :name => 'Testing'
351
+ end
352
+
353
+ should "record user objects" do
354
+ Company.audit_as( @user ) do
355
+ company = Company.create :name => 'The auditors'
356
+ company.name = 'The Auditors'
357
+ company.save
358
+
359
+ company.audits.each do |audit|
360
+ audit.user.should == @user
361
+ end
362
+ end
363
+ end
364
+
365
+ should "record usernames" do
366
+ Company.audit_as( @user.name ) do
367
+ company = Company.create :name => 'The auditors'
368
+ company.name = 'The Auditors, Inc'
369
+ company.save
370
+
371
+ company.audits.each do |audit|
372
+ audit.username.should == @user.name
373
+ end
374
+ end
375
+ end
376
+ end
377
+
378
+ end
379
+ end
380
+ end