acts_as_audited 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/.gitignore +4 -0
- data/CHANGELOG +25 -0
- data/LICENSE +19 -0
- data/README +84 -0
- data/Rakefile +57 -0
- data/VERSION +1 -0
- data/acts_as_audited.gemspec +72 -0
- data/generators/audited_migration/USAGE +7 -0
- data/generators/audited_migration/audited_migration_generator.rb +7 -0
- data/generators/audited_migration/templates/migration.rb +23 -0
- data/init.rb +1 -0
- data/lib/acts_as_audited/audit.rb +122 -0
- data/lib/acts_as_audited/audit_sweeper.rb +79 -0
- data/lib/acts_as_audited.rb +265 -0
- data/rails/init.rb +9 -0
- data/test/acts_as_audited_test.rb +365 -0
- data/test/audit_sweeper_test.rb +29 -0
- data/test/audit_test.rb +179 -0
- data/test/db/database.yml +21 -0
- data/test/db/schema.rb +31 -0
- data/test/test_helper.rb +53 -0
- metadata +109 -0
@@ -0,0 +1,265 @@
|
|
1
|
+
# Copyright (c) 2006 Brandon Keepers
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
# a copy of this software and associated documentation files (the
|
5
|
+
# "Software"), to deal in the Software without restriction, including
|
6
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
# the following conditions:
|
10
|
+
#
|
11
|
+
# The above copyright notice and this permission notice shall be
|
12
|
+
# included in all copies or substantial portions of the Software.
|
13
|
+
#
|
14
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
21
|
+
|
22
|
+
module CollectiveIdea #:nodoc:
|
23
|
+
module Acts #:nodoc:
|
24
|
+
# Specify this act if you want changes to your model to be saved in an
|
25
|
+
# audit table. This assumes there is an audits table ready.
|
26
|
+
#
|
27
|
+
# class User < ActiveRecord::Base
|
28
|
+
# acts_as_audited
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# See <tt>CollectiveIdea::Acts::Audited::ClassMethods#acts_as_audited</tt>
|
32
|
+
# for configuration options
|
33
|
+
module Audited #:nodoc:
|
34
|
+
CALLBACKS = [:audit_create, :audit_update, :audit_destroy]
|
35
|
+
|
36
|
+
def self.included(base) # :nodoc:
|
37
|
+
base.extend ClassMethods
|
38
|
+
end
|
39
|
+
|
40
|
+
module ClassMethods
|
41
|
+
# == Configuration options
|
42
|
+
#
|
43
|
+
#
|
44
|
+
# * +only+ - Only audit the given attributes
|
45
|
+
# * +except+ - Excludes fields from being saved in the audit log.
|
46
|
+
# By default, acts_as_audited will audit all but these fields:
|
47
|
+
#
|
48
|
+
# [self.primary_key, inheritance_column, 'lock_version', 'created_at', 'updated_at']
|
49
|
+
# You can add to those by passing one or an array of fields to skip.
|
50
|
+
#
|
51
|
+
# class User < ActiveRecord::Base
|
52
|
+
# acts_as_audited :except => :password
|
53
|
+
# end
|
54
|
+
# * +protect+ - If your model uses +attr_protected+, set this to false to prevent Rails from
|
55
|
+
# raising an error. If you declare +attr_accessibe+ before calling +acts_as_audited+, it
|
56
|
+
# will automatically default to false. You only need to explicitly set this if you are
|
57
|
+
# calling +attr_accessible+ after.
|
58
|
+
#
|
59
|
+
# class User < ActiveRecord::Base
|
60
|
+
# acts_as_audited :protect => false
|
61
|
+
# attr_accessible :name
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
def acts_as_audited(options = {})
|
65
|
+
# don't allow multiple calls
|
66
|
+
return if self.included_modules.include?(CollectiveIdea::Acts::Audited::InstanceMethods)
|
67
|
+
|
68
|
+
options = {:protect => accessible_attributes.nil?}.merge(options)
|
69
|
+
|
70
|
+
class_inheritable_reader :non_audited_columns
|
71
|
+
class_inheritable_reader :auditing_enabled
|
72
|
+
|
73
|
+
if options[:only]
|
74
|
+
except = self.column_names - options[:only].flatten.map(&:to_s)
|
75
|
+
else
|
76
|
+
except = [self.primary_key, inheritance_column, 'lock_version', 'created_at', 'updated_at']
|
77
|
+
except |= Array(options[:except]).collect(&:to_s) if options[:except]
|
78
|
+
end
|
79
|
+
write_inheritable_attribute :non_audited_columns, except
|
80
|
+
|
81
|
+
has_many :audits, :as => :auditable, :order => "#{Audit.quoted_table_name}.version"
|
82
|
+
attr_protected :audit_ids if options[:protect]
|
83
|
+
Audit.audited_class_names << self.to_s
|
84
|
+
|
85
|
+
after_create :audit_create_callback
|
86
|
+
before_update :audit_update_callback
|
87
|
+
after_destroy :audit_destroy_callback
|
88
|
+
|
89
|
+
attr_accessor :version
|
90
|
+
|
91
|
+
extend CollectiveIdea::Acts::Audited::SingletonMethods
|
92
|
+
include CollectiveIdea::Acts::Audited::InstanceMethods
|
93
|
+
|
94
|
+
write_inheritable_attribute :auditing_enabled, true
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
module InstanceMethods
|
99
|
+
|
100
|
+
# Temporarily turns off auditing while saving.
|
101
|
+
def save_without_auditing
|
102
|
+
without_auditing { save }
|
103
|
+
end
|
104
|
+
|
105
|
+
# Executes the block with the auditing callbacks disabled.
|
106
|
+
#
|
107
|
+
# @foo.without_auditing do
|
108
|
+
# @foo.save
|
109
|
+
# end
|
110
|
+
#
|
111
|
+
def without_auditing(&block)
|
112
|
+
self.class.without_auditing(&block)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Gets an array of the revisions available
|
116
|
+
#
|
117
|
+
# user.revisions.each do |revision|
|
118
|
+
# user.name
|
119
|
+
# user.version
|
120
|
+
# end
|
121
|
+
#
|
122
|
+
def revisions(from_version = 1)
|
123
|
+
audits = self.audits.find(:all, :conditions => ['version >= ?', from_version])
|
124
|
+
return [] if audits.empty?
|
125
|
+
revision = self.audits.find_by_version(from_version).revision
|
126
|
+
Audit.reconstruct_attributes(audits) {|attrs| revision.revision_with(attrs) }
|
127
|
+
end
|
128
|
+
|
129
|
+
# Get a specific revision specified by the version number, or +:previous+
|
130
|
+
def revision(version)
|
131
|
+
revision_with Audit.reconstruct_attributes(audits_to(version))
|
132
|
+
end
|
133
|
+
|
134
|
+
def revision_at(date_or_time)
|
135
|
+
audits = self.audits.find(:all, :conditions => ["created_at <= ?", date_or_time])
|
136
|
+
revision_with Audit.reconstruct_attributes(audits) unless audits.empty?
|
137
|
+
end
|
138
|
+
|
139
|
+
def audited_attributes
|
140
|
+
attributes.except(*non_audited_columns)
|
141
|
+
end
|
142
|
+
|
143
|
+
protected
|
144
|
+
|
145
|
+
def revision_with(attributes)
|
146
|
+
returning self.dup do |revision|
|
147
|
+
revision.send :instance_variable_set, '@attributes', self.attributes_before_type_cast
|
148
|
+
Audit.assign_revision_attributes(revision, attributes)
|
149
|
+
|
150
|
+
# Remove any association proxies so that they will be recreated
|
151
|
+
# and reference the correct object for this revision. The only way
|
152
|
+
# to determine if an instance variable is a proxy object is to
|
153
|
+
# see if it responds to certain methods, as it forwards almost
|
154
|
+
# everything to its target.
|
155
|
+
for ivar in revision.instance_variables
|
156
|
+
proxy = revision.instance_variable_get ivar
|
157
|
+
if !proxy.nil? and proxy.respond_to? :proxy_respond_to?
|
158
|
+
revision.instance_variable_set ivar, nil
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
def audited_changes
|
167
|
+
changed_attributes.except(*non_audited_columns).inject({}) do |changes,(attr, old_value)|
|
168
|
+
changes[attr] = [old_value, self[attr]]
|
169
|
+
changes
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def audits_to(version = nil)
|
174
|
+
if version == :previous
|
175
|
+
version = if self.version
|
176
|
+
self.version - 1
|
177
|
+
else
|
178
|
+
previous = audits.find(:first, :offset => 1,
|
179
|
+
:order => "#{Audit.quoted_table_name}.version DESC")
|
180
|
+
previous ? previous.version : 1
|
181
|
+
end
|
182
|
+
end
|
183
|
+
audits.find(:all, :conditions => ['version <= ?', version])
|
184
|
+
end
|
185
|
+
|
186
|
+
def audit_create(user = nil)
|
187
|
+
write_audit(:action => 'create', :changes => audited_attributes, :user => user)
|
188
|
+
end
|
189
|
+
|
190
|
+
def audit_update(user = nil)
|
191
|
+
unless (changes = audited_changes).empty?
|
192
|
+
write_audit(:action => 'update', :changes => changes, :user => user)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
|
196
|
+
def audit_destroy(user = nil)
|
197
|
+
write_audit(:action => 'destroy', :user => user, :changes => audited_attributes)
|
198
|
+
end
|
199
|
+
|
200
|
+
def write_audit(attrs)
|
201
|
+
self.audits.create attrs if auditing_enabled
|
202
|
+
end
|
203
|
+
|
204
|
+
CALLBACKS.each do |attr_name|
|
205
|
+
alias_method "#{attr_name}_callback".to_sym, attr_name
|
206
|
+
end
|
207
|
+
|
208
|
+
def empty_callback #:nodoc:
|
209
|
+
end
|
210
|
+
|
211
|
+
end # InstanceMethods
|
212
|
+
|
213
|
+
module SingletonMethods
|
214
|
+
# Returns an array of columns that are audited. See non_audited_columns
|
215
|
+
def audited_columns
|
216
|
+
self.columns.select { |c| !non_audited_columns.include?(c.name) }
|
217
|
+
end
|
218
|
+
|
219
|
+
# Executes the block with auditing disabled.
|
220
|
+
#
|
221
|
+
# Foo.without_auditing do
|
222
|
+
# @foo.save
|
223
|
+
# end
|
224
|
+
#
|
225
|
+
def without_auditing(&block)
|
226
|
+
auditing_was_enabled = auditing_enabled
|
227
|
+
disable_auditing
|
228
|
+
returning(block.call) { enable_auditing if auditing_was_enabled }
|
229
|
+
end
|
230
|
+
|
231
|
+
def disable_auditing
|
232
|
+
write_inheritable_attribute :auditing_enabled, false
|
233
|
+
end
|
234
|
+
|
235
|
+
def enable_auditing
|
236
|
+
write_inheritable_attribute :auditing_enabled, true
|
237
|
+
end
|
238
|
+
|
239
|
+
def disable_auditing_callbacks
|
240
|
+
class_eval do
|
241
|
+
CALLBACKS.each do |attr_name|
|
242
|
+
alias_method "#{attr_name}_callback", :empty_callback
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
247
|
+
def enable_auditing_callbacks
|
248
|
+
class_eval do
|
249
|
+
CALLBACKS.each do |attr_name|
|
250
|
+
alias_method "#{attr_name}_callback".to_sym, attr_name
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
|
255
|
+
# All audit operations during the block are recorded as being
|
256
|
+
# made by +user+. This is not model specific, the method is a
|
257
|
+
# convenience wrapper around #Audit.as_user.
|
258
|
+
def audit_as( user, &block )
|
259
|
+
Audit.as_user( user, &block )
|
260
|
+
end
|
261
|
+
|
262
|
+
end
|
263
|
+
end
|
264
|
+
end
|
265
|
+
end
|
data/rails/init.rb
ADDED
@@ -0,0 +1,9 @@
|
|
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
|
+
ActionController::Base.send :include, CollectiveIdea::ActionController::Audited
|
9
|
+
end
|
@@ -0,0 +1,365 @@
|
|
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', '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
|
+
end
|
41
|
+
|
42
|
+
context "on update" do
|
43
|
+
setup do
|
44
|
+
@user = create_user(:name => 'Brandon')
|
45
|
+
end
|
46
|
+
|
47
|
+
should "save an audit" do
|
48
|
+
lambda { @user.update_attribute(:name, "Someone") }.should change { @user.audits.count }.by(1)
|
49
|
+
lambda { @user.update_attribute(:name, "Someone else") }.should change { @user.audits.count }.by(1)
|
50
|
+
end
|
51
|
+
|
52
|
+
should "not save an audit if the record is not changed" do
|
53
|
+
lambda { @user.save! }.should_not change { Audit.count }
|
54
|
+
end
|
55
|
+
|
56
|
+
should "set the action to 'update'" do
|
57
|
+
@user.update_attributes :name => 'Changed'
|
58
|
+
@user.audits.last.action.should == 'update'
|
59
|
+
end
|
60
|
+
|
61
|
+
should "store the changed attributes" do
|
62
|
+
@user.update_attributes :name => 'Changed'
|
63
|
+
@user.audits.last.changes.should == {'name' => ['Brandon', 'Changed']}
|
64
|
+
end
|
65
|
+
|
66
|
+
# Dirty tracking in Rails 2.0-2.2 had issues with type casting
|
67
|
+
if ActiveRecord::VERSION::STRING >= '2.3'
|
68
|
+
should "not save an audit if the value doesn't change after type casting" do
|
69
|
+
@user.update_attributes! :logins => 0, :activated => true
|
70
|
+
lambda { @user.update_attribute :logins, '0' }.should_not change { Audit.count }
|
71
|
+
lambda { @user.update_attribute :activated, 1 }.should_not change { Audit.count }
|
72
|
+
lambda { @user.update_attribute :activated, '1' }.should_not change { Audit.count }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
end
|
77
|
+
|
78
|
+
context "on destroy" do
|
79
|
+
setup do
|
80
|
+
@user = create_user
|
81
|
+
end
|
82
|
+
|
83
|
+
should "save an audit" do
|
84
|
+
lambda { @user.destroy }.should change { Audit.count }.by(1)
|
85
|
+
@user.audits.size.should == 2
|
86
|
+
end
|
87
|
+
|
88
|
+
should "set the action to 'destroy'" do
|
89
|
+
@user.destroy
|
90
|
+
@user.audits.last.action.should == 'destroy'
|
91
|
+
end
|
92
|
+
|
93
|
+
should "store all of the audited attributes" do
|
94
|
+
@user.destroy
|
95
|
+
@user.audits.last.changes.should == @user.audited_attributes
|
96
|
+
end
|
97
|
+
|
98
|
+
should "be able to reconstruct destroyed record without history" do
|
99
|
+
@user.audits.delete_all
|
100
|
+
@user.destroy
|
101
|
+
revision = @user.audits.first.revision
|
102
|
+
revision.name.should == @user.name
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context "dirty tracking" do
|
107
|
+
setup do
|
108
|
+
@user = create_user
|
109
|
+
end
|
110
|
+
|
111
|
+
should "not be changed when the record is saved" do
|
112
|
+
u = User.new(:name => 'Brandon')
|
113
|
+
u.changed?.should be(true)
|
114
|
+
u.save
|
115
|
+
u.changed?.should be(false)
|
116
|
+
end
|
117
|
+
|
118
|
+
should "be changed when an attribute has been changed" do
|
119
|
+
@user.name = "Bobby"
|
120
|
+
@user.changed?.should be(true)
|
121
|
+
@user.name_changed?.should be(true)
|
122
|
+
@user.username_changed?.should be(false)
|
123
|
+
end
|
124
|
+
|
125
|
+
# Dirty tracking in Rails 2.0-2.2 had issues with type casting
|
126
|
+
if ActiveRecord::VERSION::STRING >= '2.3'
|
127
|
+
should "not be changed if the value doesn't change after type casting" do
|
128
|
+
@user.update_attributes! :logins => 0, :activated => true
|
129
|
+
@user.logins = '0'
|
130
|
+
@user.changed?.should be(false)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|
135
|
+
|
136
|
+
context "revisions" do
|
137
|
+
setup do
|
138
|
+
@user = create_versions
|
139
|
+
end
|
140
|
+
|
141
|
+
should "be an Array of Users" do
|
142
|
+
@user.revisions.should be_kind_of(Array)
|
143
|
+
@user.revisions.each {|version| version.should be_kind_of(User) }
|
144
|
+
end
|
145
|
+
|
146
|
+
should "have one revision for a new record" do
|
147
|
+
create_user.revisions.size.should == 1
|
148
|
+
end
|
149
|
+
|
150
|
+
should "have one revision for each audit" do
|
151
|
+
@user.revisions.size.should == @user.audits.size
|
152
|
+
end
|
153
|
+
|
154
|
+
should "set the attributes for each revision" do
|
155
|
+
u = User.create(:name => 'Brandon', :username => 'brandon')
|
156
|
+
u.update_attributes :name => 'Foobar'
|
157
|
+
u.update_attributes :name => 'Awesome', :username => 'keepers'
|
158
|
+
|
159
|
+
u.revisions.size.should == 3
|
160
|
+
|
161
|
+
u.revisions[0].name.should == 'Brandon'
|
162
|
+
u.revisions[0].username.should == 'brandon'
|
163
|
+
|
164
|
+
u.revisions[1].name.should == 'Foobar'
|
165
|
+
u.revisions[1].username.should == 'brandon'
|
166
|
+
|
167
|
+
u.revisions[2].name.should == 'Awesome'
|
168
|
+
u.revisions[2].username.should == 'keepers'
|
169
|
+
end
|
170
|
+
|
171
|
+
should "access to only recent revisions" do
|
172
|
+
u = User.create(:name => 'Brandon', :username => 'brandon')
|
173
|
+
u.update_attributes :name => 'Foobar'
|
174
|
+
u.update_attributes :name => 'Awesome', :username => 'keepers'
|
175
|
+
|
176
|
+
u.revisions(2).size.should == 2
|
177
|
+
|
178
|
+
u.revisions(2)[0].name.should == 'Foobar'
|
179
|
+
u.revisions(2)[0].username.should == 'brandon'
|
180
|
+
|
181
|
+
u.revisions(2)[1].name.should == 'Awesome'
|
182
|
+
u.revisions(2)[1].username.should == 'keepers'
|
183
|
+
end
|
184
|
+
|
185
|
+
should "be empty if no audits exist" do
|
186
|
+
@user.audits.delete_all
|
187
|
+
@user.revisions.empty?.should be(true)
|
188
|
+
end
|
189
|
+
|
190
|
+
should "ignore attributes that have been deleted" do
|
191
|
+
@user.audits.last.update_attributes :changes => {:old_attribute => 'old value'}
|
192
|
+
lambda { @user.revisions }.should_not raise_error
|
193
|
+
end
|
194
|
+
|
195
|
+
end
|
196
|
+
|
197
|
+
context "revision" do
|
198
|
+
setup do
|
199
|
+
@user = create_versions(5)
|
200
|
+
end
|
201
|
+
|
202
|
+
should "maintain identity" do
|
203
|
+
@user.revision(1).should == @user
|
204
|
+
end
|
205
|
+
|
206
|
+
should "find the given revision" do
|
207
|
+
revision = @user.revision(3)
|
208
|
+
revision.should be_kind_of(User)
|
209
|
+
revision.version.should == 3
|
210
|
+
revision.name.should == 'Foobar 3'
|
211
|
+
end
|
212
|
+
|
213
|
+
should "find the previous revision with :previous" do
|
214
|
+
revision = @user.revision(:previous)
|
215
|
+
revision.version.should == 4
|
216
|
+
revision.should == @user.revision(4)
|
217
|
+
end
|
218
|
+
|
219
|
+
should "be able to get the previous revision repeatedly" do
|
220
|
+
previous = @user.revision(:previous)
|
221
|
+
previous.version.should == 4
|
222
|
+
previous.revision(:previous).version.should == 3
|
223
|
+
end
|
224
|
+
|
225
|
+
should "be able to set protected attributes" do
|
226
|
+
u = User.create(:name => 'Brandon')
|
227
|
+
u.update_attribute :logins, 1
|
228
|
+
u.update_attribute :logins, 2
|
229
|
+
|
230
|
+
u.revision(3).logins.should == 2
|
231
|
+
u.revision(2).logins.should == 1
|
232
|
+
u.revision(1).logins.should == 0
|
233
|
+
end
|
234
|
+
|
235
|
+
should "set attributes directly" do
|
236
|
+
u = User.create(:name => '<Joe>')
|
237
|
+
u.revision(1).name.should == '<Joe>'
|
238
|
+
end
|
239
|
+
|
240
|
+
should "set the attributes for each revision" do
|
241
|
+
u = User.create(:name => 'Brandon', :username => 'brandon')
|
242
|
+
u.update_attributes :name => 'Foobar'
|
243
|
+
u.update_attributes :name => 'Awesome', :username => 'keepers'
|
244
|
+
|
245
|
+
u.revision(3).name.should == 'Awesome'
|
246
|
+
u.revision(3).username.should == 'keepers'
|
247
|
+
|
248
|
+
u.revision(2).name.should == 'Foobar'
|
249
|
+
u.revision(2).username.should == 'brandon'
|
250
|
+
|
251
|
+
u.revision(1).name.should == 'Brandon'
|
252
|
+
u.revision(1).username.should == 'brandon'
|
253
|
+
end
|
254
|
+
|
255
|
+
should "not raise an error when no previous audits exist" do
|
256
|
+
@user.audits.destroy_all
|
257
|
+
lambda{ @user.revision(:previous) }.should_not raise_error
|
258
|
+
end
|
259
|
+
|
260
|
+
should "mark revision's attributes as changed" do
|
261
|
+
@user.revision(1).name_changed?.should be(true)
|
262
|
+
end
|
263
|
+
|
264
|
+
should "record new audit when saving revision" do
|
265
|
+
lambda { @user.revision(1).save! }.should change { @user.audits.count }.by(1)
|
266
|
+
end
|
267
|
+
|
268
|
+
end
|
269
|
+
|
270
|
+
context "revision_at" do
|
271
|
+
should "find the latest revision before the given time" do
|
272
|
+
u = create_user
|
273
|
+
Audit.update(u.audits.first.id, :created_at => 1.hour.ago)
|
274
|
+
u.update_attributes :name => 'updated'
|
275
|
+
u.revision_at(2.minutes.ago).version.should == 1
|
276
|
+
end
|
277
|
+
|
278
|
+
should "be nil if given a time before audits" do
|
279
|
+
create_user.revision_at(1.week.ago).should be(nil)
|
280
|
+
end
|
281
|
+
|
282
|
+
end
|
283
|
+
|
284
|
+
context "without auditing" do
|
285
|
+
|
286
|
+
should "not save an audit when calling #save_without_auditing" do
|
287
|
+
lambda {
|
288
|
+
u = User.new(:name => 'Brandon')
|
289
|
+
u.save_without_auditing.should be(true)
|
290
|
+
}.should_not change { Audit.count }
|
291
|
+
end
|
292
|
+
|
293
|
+
should "not save an audit inside of the #without_auditing block" do
|
294
|
+
lambda do
|
295
|
+
User.without_auditing { User.create(:name => 'Brandon') }
|
296
|
+
end.should_not change { Audit.count }
|
297
|
+
end
|
298
|
+
|
299
|
+
should "not save an audit when callbacks are disabled" do
|
300
|
+
begin
|
301
|
+
User.disable_auditing_callbacks
|
302
|
+
lambda { create_user }.should_not change { Audit.count }
|
303
|
+
ensure
|
304
|
+
User.enable_auditing_callbacks
|
305
|
+
end
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
context "attr_protected and attr_accessible" do
|
310
|
+
class UnprotectedUser < ActiveRecord::Base
|
311
|
+
set_table_name :users
|
312
|
+
acts_as_audited :protect => false
|
313
|
+
attr_accessible :name, :username, :password
|
314
|
+
end
|
315
|
+
should "not raise error when attr_accessible is set and protected is false" do
|
316
|
+
lambda{
|
317
|
+
UnprotectedUser.new(:name => 'NO FAIL!')
|
318
|
+
}.should_not raise_error(RuntimeError)
|
319
|
+
end
|
320
|
+
|
321
|
+
class AccessibleUser < ActiveRecord::Base
|
322
|
+
set_table_name :users
|
323
|
+
attr_accessible :name, :username, :password # declare attr_accessible before calling aaa
|
324
|
+
acts_as_audited
|
325
|
+
end
|
326
|
+
should "not raise an error when attr_accessible is declared before acts_as_audited" do
|
327
|
+
lambda{
|
328
|
+
AccessibleUser.new(:name => 'NO FAIL!')
|
329
|
+
}.should_not raise_error
|
330
|
+
end
|
331
|
+
end
|
332
|
+
|
333
|
+
context "audit as" do
|
334
|
+
setup do
|
335
|
+
@user = User.create :name => 'Testing'
|
336
|
+
end
|
337
|
+
|
338
|
+
should "record user objects" do
|
339
|
+
Company.audit_as( @user ) do
|
340
|
+
company = Company.create :name => 'The auditors'
|
341
|
+
company.name = 'The Auditors'
|
342
|
+
company.save
|
343
|
+
|
344
|
+
company.audits.each do |audit|
|
345
|
+
audit.user.should == @user
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
should "record usernames" do
|
351
|
+
Company.audit_as( @user.name ) do
|
352
|
+
company = Company.create :name => 'The auditors'
|
353
|
+
company.name = 'The Auditors, Inc'
|
354
|
+
company.save
|
355
|
+
|
356
|
+
company.audits.each do |audit|
|
357
|
+
audit.username.should == @user.name
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
end
|
364
|
+
end
|
365
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/test_helper')
|
2
|
+
|
3
|
+
class AuditsController < ActionController::Base
|
4
|
+
audit Company
|
5
|
+
|
6
|
+
def audit
|
7
|
+
@company = Company.create
|
8
|
+
render :nothing => true
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
attr_accessor :current_user
|
13
|
+
end
|
14
|
+
AuditsController.view_paths = [File.dirname(__FILE__)]
|
15
|
+
ActionController::Routing::Routes.draw {|m| m.connect ':controller/:action/:id' }
|
16
|
+
|
17
|
+
class AuditsControllerTest < ActionController::TestCase
|
18
|
+
|
19
|
+
should "call acts as audited on non audited models" do
|
20
|
+
Company.should be_kind_of(CollectiveIdea::Acts::Audited::SingletonMethods)
|
21
|
+
end
|
22
|
+
|
23
|
+
should "audit user" do
|
24
|
+
user = @controller.send(:current_user=, create_user)
|
25
|
+
lambda { post :audit }.should change { Audit.count }
|
26
|
+
assigns(:company).audits.last.user.should == user
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|