auditing 0.0.2 → 1.0.0
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/LICENSE +1 -1
- data/README.rdoc +27 -4
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/auditing.gemspec +9 -6
- data/lib/auditing/audit_relationship.rb +127 -0
- data/lib/auditing/auditor.rb +10 -3
- data/lib/auditing/base.rb +74 -0
- data/lib/auditing.rb +4 -2
- data/spec/auditing/audit_relationship_spec.rb +64 -0
- data/spec/auditing/auditor_spec.rb +89 -5
- data/spec/auditing/{auditing_spec.rb → base_spec.rb} +80 -10
- data/spec/schema.rb +33 -3
- metadata +10 -7
- data/lib/auditing/auditing.rb +0 -51
data/LICENSE
CHANGED
data/README.rdoc
CHANGED
@@ -1,10 +1,10 @@
|
|
1
|
-
= auditing (currently in development
|
1
|
+
= auditing (currently in development)
|
2
2
|
|
3
3
|
Auditing is a simple way to track and rollback changes to a record.
|
4
4
|
|
5
5
|
== Installation
|
6
6
|
|
7
|
-
gem install auditing
|
7
|
+
gem install auditing
|
8
8
|
|
9
9
|
You will have to supply a model named Audit in your rails application with the following migration
|
10
10
|
|
@@ -37,18 +37,41 @@ TODO: more to follow about tracking the user
|
|
37
37
|
To add auditing to all attributes on a model, simply add auditing to the model.
|
38
38
|
|
39
39
|
class School < ActiveRecord::Base
|
40
|
-
|
40
|
+
audit_enabled
|
41
41
|
end
|
42
42
|
|
43
43
|
If you want more control over which attributes are audited, you can define them
|
44
44
|
by supplying a fields hash
|
45
45
|
|
46
46
|
class School < ActiveRecord::Base
|
47
|
-
|
47
|
+
audit_enabled :fields => [:name, :established_on]
|
48
48
|
end
|
49
49
|
|
50
50
|
Auditing will not track "id", "created_at", or "updated_at" by default
|
51
51
|
|
52
|
+
== belongs_to relationships
|
53
|
+
|
54
|
+
Auditing a belongs_to relationship works by keeping track of the foreign_id attribute. You do
|
55
|
+
not need to add anything to the foreign model.
|
56
|
+
|
57
|
+
== has_many relationships
|
58
|
+
|
59
|
+
You may have the need to keep track of a has_many relationship. To accomplish this, we have
|
60
|
+
to enable a similar type of auditing on the other model(s)
|
61
|
+
|
62
|
+
class Company < ActiveRecord::Base
|
63
|
+
has_many :phone_numbers
|
64
|
+
audit_enabled
|
65
|
+
end
|
66
|
+
class PhoneNumber < ActiveRecord::Base
|
67
|
+
belongs_to :company
|
68
|
+
audit_relationship_enabled
|
69
|
+
end
|
70
|
+
|
71
|
+
As above, you can supply a :fields hash of which attributes will be logged.
|
72
|
+
If your relationship is polymorphic, you can supply an array of classes
|
73
|
+
that you :only want to log.
|
74
|
+
|
52
75
|
== Note on Patches/Pull Requests
|
53
76
|
|
54
77
|
* Fork the project.
|
data/Rakefile
CHANGED
@@ -9,7 +9,7 @@ begin
|
|
9
9
|
gem.description = %Q{acts_as_versioned is good. This allows an attribute level rollback instead}
|
10
10
|
gem.email = "brad.cantin@gmail.com"
|
11
11
|
gem.homepage = "http://github.com/bcantin/auditing"
|
12
|
-
gem.authors = ["Brad"]
|
12
|
+
gem.authors = ["Brad Cantin"]
|
13
13
|
gem.add_development_dependency "rspec", ">= 1.2.9"
|
14
14
|
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
15
15
|
end
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.0
|
1
|
+
1.0.0
|
data/auditing.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{auditing}
|
8
|
-
s.version = "0.0
|
8
|
+
s.version = "1.0.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
-
s.authors = ["Brad"]
|
12
|
-
s.date = %q{2010-
|
11
|
+
s.authors = ["Brad Cantin"]
|
12
|
+
s.date = %q{2010-10-09}
|
13
13
|
s.description = %q{acts_as_versioned is good. This allows an attribute level rollback instead}
|
14
14
|
s.email = %q{brad.cantin@gmail.com}
|
15
15
|
s.extra_rdoc_files = [
|
@@ -29,10 +29,12 @@ Gem::Specification.new do |s|
|
|
29
29
|
"VERSION",
|
30
30
|
"auditing.gemspec",
|
31
31
|
"lib/auditing.rb",
|
32
|
-
"lib/auditing/
|
32
|
+
"lib/auditing/audit_relationship.rb",
|
33
33
|
"lib/auditing/auditor.rb",
|
34
|
-
"
|
34
|
+
"lib/auditing/base.rb",
|
35
|
+
"spec/auditing/audit_relationship_spec.rb",
|
35
36
|
"spec/auditing/auditor_spec.rb",
|
37
|
+
"spec/auditing/base_spec.rb",
|
36
38
|
"spec/schema.rb",
|
37
39
|
"spec/spec_helper.rb"
|
38
40
|
]
|
@@ -42,8 +44,9 @@ Gem::Specification.new do |s|
|
|
42
44
|
s.rubygems_version = %q{1.3.7}
|
43
45
|
s.summary = %q{A gem to keep track of audit hisory of a record}
|
44
46
|
s.test_files = [
|
45
|
-
"spec/auditing/
|
47
|
+
"spec/auditing/audit_relationship_spec.rb",
|
46
48
|
"spec/auditing/auditor_spec.rb",
|
49
|
+
"spec/auditing/base_spec.rb",
|
47
50
|
"spec/schema.rb",
|
48
51
|
"spec/spec_helper.rb"
|
49
52
|
]
|
@@ -0,0 +1,127 @@
|
|
1
|
+
module Auditing
|
2
|
+
module AuditRelationship
|
3
|
+
|
4
|
+
# AuditRelationship creates audits for a has_many relationship.
|
5
|
+
#
|
6
|
+
# @examples
|
7
|
+
# class Company < ActiveRecord::Base
|
8
|
+
# has_many :phone_numbers
|
9
|
+
# audit_enabled
|
10
|
+
# end
|
11
|
+
# class PhoneNumber < ActiveRecord::Base
|
12
|
+
# belongs_to :company
|
13
|
+
# audit_relationship_enabled
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# valid options include:
|
17
|
+
# :only => [(array of models)]
|
18
|
+
# an array of models to only send an audit to
|
19
|
+
# :except => [(array of models)]
|
20
|
+
# an array of models to not send an audit to
|
21
|
+
# :fields => [(array of field names)]
|
22
|
+
# an array of field names. Each field name will be one audit item
|
23
|
+
def audit_relationship_enabled(opts={})
|
24
|
+
include InstanceMethods
|
25
|
+
|
26
|
+
class_inheritable_accessor :audit_enabled_models
|
27
|
+
class_inheritable_accessor :field_names
|
28
|
+
|
29
|
+
self.audit_enabled_models = gather_models(opts)
|
30
|
+
self.field_names = gather_fields_for_auditing(opts[:fields])
|
31
|
+
|
32
|
+
after_create :audit_relationship_create
|
33
|
+
before_update :audit_relationship_update
|
34
|
+
before_destroy :audit_relationship_destroy
|
35
|
+
end
|
36
|
+
|
37
|
+
def gather_fields_for_auditing(fields=nil)
|
38
|
+
poly_array = []
|
39
|
+
reflect_on_all_associations(:belongs_to).each do |assoc|
|
40
|
+
poly_array << assoc.name if assoc.options[:polymorphic]
|
41
|
+
end
|
42
|
+
|
43
|
+
unless fields
|
44
|
+
if poly_array.nil?
|
45
|
+
return default_columns
|
46
|
+
else
|
47
|
+
tmp_names = default_columns
|
48
|
+
poly_array.each do |poly|
|
49
|
+
tmp_names = tmp_names.reject {|column| column.match(/#{poly.to_s}*/)}
|
50
|
+
end
|
51
|
+
end
|
52
|
+
return tmp_names
|
53
|
+
else
|
54
|
+
fields.is_a?(Array) ? fields.map {|f| f.to_s} : [fields.to_s]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def default_columns
|
59
|
+
self.column_names - ["id", "created_at", "updated_at"]
|
60
|
+
end
|
61
|
+
|
62
|
+
def gather_models(opts={})
|
63
|
+
if opts[:only]
|
64
|
+
if opts[:only].is_a?(Array)
|
65
|
+
opts[:only].map {|c| c.to_s.underscore.to_sym}
|
66
|
+
else
|
67
|
+
[opts[:only].to_s.underscore.to_sym]
|
68
|
+
end
|
69
|
+
else
|
70
|
+
array,tmp = [], []
|
71
|
+
reflect_on_all_associations(:belongs_to).each do |assoc|
|
72
|
+
array << assoc.name
|
73
|
+
end
|
74
|
+
|
75
|
+
if opts[:except]
|
76
|
+
if opts[:except].is_a?(Array)
|
77
|
+
tmp = opts[:except].map {|c| c.to_s.underscore.to_sym}
|
78
|
+
else
|
79
|
+
tmp = [opts[:except].to_s.underscore.to_sym]
|
80
|
+
end
|
81
|
+
end
|
82
|
+
array - tmp
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
module InstanceMethods
|
87
|
+
|
88
|
+
def audit_relationship_create
|
89
|
+
audit_enabled_models.each do |model|
|
90
|
+
return unless send(model).respond_to?('log_association_create')
|
91
|
+
field_names.each do |field|
|
92
|
+
field_value = send(field)
|
93
|
+
next unless field_value
|
94
|
+
model_instance = send(model)
|
95
|
+
model_instance.log_association_create(self, {:field => field,
|
96
|
+
:value => field_value})
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def audit_relationship_update
|
102
|
+
if changed?
|
103
|
+
audit_enabled_models.each do |model|
|
104
|
+
return unless send(model).respond_to?('log_association_update')
|
105
|
+
changes.each.select {|k,v| field_names.include?(k)}.each do |field, change|
|
106
|
+
field_value = send(field)
|
107
|
+
model_instance = send(model)
|
108
|
+
model_instance.log_association_update(self, {:field => field,
|
109
|
+
:old_value => change[0],
|
110
|
+
:new_value => change[1]})
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def audit_relationship_destroy
|
117
|
+
audit_enabled_models.each do |model|
|
118
|
+
return unless send(model).respond_to?('log_association_destroy')
|
119
|
+
model_instance = send(model)
|
120
|
+
model_instance.log_association_destroy(self)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
end
|
data/lib/auditing/auditor.rb
CHANGED
@@ -2,21 +2,28 @@ module Auditing
|
|
2
2
|
module Auditor
|
3
3
|
|
4
4
|
def reversable?
|
5
|
-
|
5
|
+
undoable?
|
6
6
|
end
|
7
7
|
|
8
8
|
def show_action
|
9
9
|
action
|
10
10
|
end
|
11
11
|
|
12
|
+
def old_value
|
13
|
+
Marshal.load(read_attribute(:old_value))
|
14
|
+
end
|
15
|
+
|
16
|
+
def new_value
|
17
|
+
Marshal.load(read_attribute(:new_value))
|
18
|
+
end
|
19
|
+
|
12
20
|
def rollback
|
13
21
|
return unless reversable?
|
14
22
|
|
15
23
|
if association.blank?
|
16
24
|
auditable.update_attribute(field_name.to_sym, old_value)
|
17
25
|
else
|
18
|
-
|
19
|
-
# association.class.find(association_id).update_attribute(field_name.to_sym, old_value)
|
26
|
+
association.class.find(association_id).update_attribute(field_name.to_sym, old_value)
|
20
27
|
end
|
21
28
|
|
22
29
|
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
module Auditing
|
2
|
+
module Base
|
3
|
+
|
4
|
+
# Auditing creates audit objects for a record.
|
5
|
+
#
|
6
|
+
# @examples
|
7
|
+
# class School < ActiveRecord::Base
|
8
|
+
# audit_enabled
|
9
|
+
# end
|
10
|
+
# class School < ActiveRecord::Base
|
11
|
+
# audit_enabled :fields => [:name, :established_on]
|
12
|
+
# end
|
13
|
+
def audit_enabled(opts={})
|
14
|
+
include InstanceMethods
|
15
|
+
class_inheritable_accessor :auditing_fields
|
16
|
+
|
17
|
+
has_many :audits, :as => :auditable, :order => 'created_at DESC, id DESC'
|
18
|
+
|
19
|
+
self.auditing_fields = gather_fields_for_auditing(opts[:fields])
|
20
|
+
|
21
|
+
after_create :log_creation
|
22
|
+
after_update :log_update
|
23
|
+
end
|
24
|
+
|
25
|
+
def gather_fields_for_auditing(fields=nil)
|
26
|
+
return self.column_names - ["id", "created_at", "updated_at"] unless fields
|
27
|
+
fields.is_a?(Array) ? fields.map {|f| f.to_s} : [fields.to_s]
|
28
|
+
end
|
29
|
+
|
30
|
+
module InstanceMethods
|
31
|
+
def log_creation
|
32
|
+
add_audit(:action => 'created', :undoable => false)
|
33
|
+
end
|
34
|
+
|
35
|
+
def log_update
|
36
|
+
if changed?
|
37
|
+
changes.each.select {|k,v| auditing_fields.include?(k)}.each do |field, change|
|
38
|
+
next if change[0].to_s == change[1].to_s
|
39
|
+
add_audit(:action => 'updated',
|
40
|
+
:field_name => field,
|
41
|
+
:old_value => Marshal.dump(change[0]),
|
42
|
+
:new_value => Marshal.dump(change[1]) )
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def log_association_create(child_object, hash)
|
48
|
+
add_audit(:action => 'added',
|
49
|
+
:association => child_object,
|
50
|
+
:field_name => hash[:field],
|
51
|
+
:new_value => Marshal.dump(hash[:value]) )
|
52
|
+
end
|
53
|
+
|
54
|
+
def log_association_update(child_object, hash)
|
55
|
+
add_audit(:action => 'updated',
|
56
|
+
:association => child_object,
|
57
|
+
:field_name => hash[:field],
|
58
|
+
:old_value => Marshal.dump(hash[:old_value]),
|
59
|
+
:new_value => Marshal.dump(hash[:new_value]) )
|
60
|
+
end
|
61
|
+
|
62
|
+
def log_association_destroy(item)
|
63
|
+
add_audit(:action => 'removed', :association => item, :undoable => false)
|
64
|
+
# TODO: update all of this items previous audits to be undoable
|
65
|
+
end
|
66
|
+
|
67
|
+
private
|
68
|
+
def add_audit(hash={})
|
69
|
+
Audit.create!({:auditable => self}.merge(hash))
|
70
|
+
end
|
71
|
+
end # Auditing::InstanceMethods
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
data/lib/auditing.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
require 'auditing/
|
1
|
+
require 'auditing/base'
|
2
2
|
require 'auditing/auditor'
|
3
|
+
require 'auditing/audit_relationship'
|
3
4
|
|
4
|
-
ActiveRecord::Base.send(:extend, Auditing)
|
5
|
+
ActiveRecord::Base.send(:extend, Auditing::Base)
|
5
6
|
ActiveRecord::Base.send(:extend, Auditing::Auditor)
|
7
|
+
ActiveRecord::Base.send(:extend, Auditing::AuditRelationship)
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "AuditingRelationship" do
|
4
|
+
|
5
|
+
describe "adding audit_relationship_enabled on a model" do
|
6
|
+
before do
|
7
|
+
class Company < ActiveRecord::Base
|
8
|
+
has_many :phone_numbers, :as => :phoneable
|
9
|
+
audit_enabled
|
10
|
+
end
|
11
|
+
class PhoneNumber < ActiveRecord::Base
|
12
|
+
belongs_to :phoneable, :polymorphic => true
|
13
|
+
audit_relationship_enabled
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should respond to audit_relationship_enabled when we add audit_relationship_enabled" do
|
18
|
+
PhoneNumber.should respond_to(:audit_relationship_enabled)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should build an array of audit_enabled_models" do
|
22
|
+
PhoneNumber.audit_enabled_models.should == [:phoneable]
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should build an array of field_names" do
|
26
|
+
PhoneNumber.field_names.should =~ ['number', 'extension']
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "creating a new belongs_to object" do
|
30
|
+
before do
|
31
|
+
@company = Company.create(:name => 'Apple')
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should add an audit to the parent instance object (company) when we create a phone number" do
|
35
|
+
lambda {
|
36
|
+
@company.phone_numbers << PhoneNumber.new(:number => '1-800-orange')
|
37
|
+
}.should change{@company.audits.count}
|
38
|
+
end
|
39
|
+
|
40
|
+
it "should add an audit for each field" do
|
41
|
+
lambda {
|
42
|
+
@company.phone_numbers << PhoneNumber.new(:number => '1-800-orange', :extension => '1')
|
43
|
+
}.should change{@company.audits.count}.by(2)
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "updating an existing belongs_to object" do
|
47
|
+
before do
|
48
|
+
@ph = PhoneNumber.new(:number => '1-800-orange', :extension => '1')
|
49
|
+
@company.phone_numbers << @ph
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should add an audit to the parent instance when we update a phone number" do
|
53
|
+
lambda {@ph.update_attributes(:number => '1-800-apple')}.should change{@company.audits.count}
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "deleting a child_object" do
|
57
|
+
it "should add an audit to the parent instance when we delete a phone number" do
|
58
|
+
lambda {PhoneNumber.destroy(@ph)}.should change{@company.audits.count}
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -1,22 +1,21 @@
|
|
1
1
|
require 'spec_helper'
|
2
|
-
# require 'timecop'
|
3
2
|
|
4
3
|
describe "Auditor" do
|
5
|
-
it 'adds the
|
4
|
+
it 'adds the Auditing::Auditor module to the Audit class' do
|
6
5
|
Audit.new.should respond_to(:show_action)
|
7
6
|
end
|
8
7
|
|
9
8
|
describe "#rollback" do
|
10
9
|
before do
|
11
10
|
class School < ActiveRecord::Base
|
12
|
-
|
11
|
+
audit_enabled
|
13
12
|
end
|
14
13
|
@school = School.create(:name => 'PS118')
|
15
14
|
@school.update_attributes(:name => 'PS99')
|
16
15
|
@audit = @school.audits.first
|
17
16
|
end
|
18
17
|
|
19
|
-
it "the
|
18
|
+
it "the latest audit should be the audit we want to rollback" do
|
20
19
|
@audit.action.should == 'updated'
|
21
20
|
@audit.new_value.should == 'PS99'
|
22
21
|
@audit.old_value.should == 'PS118'
|
@@ -33,11 +32,96 @@ describe "Auditor" do
|
|
33
32
|
lambda { @audit.rollback }.should change { Audit.count }.by(1)
|
34
33
|
end
|
35
34
|
|
36
|
-
it "the
|
35
|
+
it "the latest audit after a rollback should contain the changed values" do
|
37
36
|
@audit.rollback
|
38
37
|
@school.reload
|
39
38
|
@school.audits.first.old_value.should == 'PS99'
|
40
39
|
@school.audits.first.new_value.should == 'PS118'
|
41
40
|
end
|
42
41
|
end
|
42
|
+
|
43
|
+
describe "#rollback belongs_to attribute" do
|
44
|
+
before do
|
45
|
+
class Car < ActiveRecord::Base
|
46
|
+
audit_enabled
|
47
|
+
belongs_to :auto_maker
|
48
|
+
end
|
49
|
+
class AutoMaker < ActiveRecord::Base
|
50
|
+
has_many :cars
|
51
|
+
end
|
52
|
+
@automaker = AutoMaker.create(:name => 'maker of fast cars')
|
53
|
+
@new_automaker = AutoMaker.create(:name => 'maker of safe cars')
|
54
|
+
|
55
|
+
@car = Car.create(:name => 'fast car', :auto_maker => @automaker)
|
56
|
+
@car.update_attributes(:auto_maker => @new_automaker)
|
57
|
+
@audit = @car.audits.first
|
58
|
+
end
|
59
|
+
|
60
|
+
it "the latest audit should be the audit we want to rollback" do
|
61
|
+
@audit.action.should == 'updated'
|
62
|
+
@audit.new_value.should == @new_automaker.id
|
63
|
+
@audit.old_value.should == @automaker.id
|
64
|
+
@audit.association.should == nil
|
65
|
+
@car.auto_maker.should == @new_automaker
|
66
|
+
end
|
67
|
+
|
68
|
+
it "performs the rollback" do
|
69
|
+
@audit.rollback
|
70
|
+
@car.reload
|
71
|
+
@car.auto_maker.should == @automaker
|
72
|
+
end
|
73
|
+
|
74
|
+
it "creates an audit when a rollback is performed" do
|
75
|
+
lambda { @audit.rollback }.should change { Audit.count }.by(1)
|
76
|
+
end
|
77
|
+
|
78
|
+
it "the latest audit after a rollback should contain the changed values" do
|
79
|
+
@audit.rollback
|
80
|
+
@car.reload
|
81
|
+
@car.audits.first.old_value.should == @new_automaker.id
|
82
|
+
@car.audits.first.new_value.should == @automaker.id
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe "rollback has_many attributes" do
|
87
|
+
before do
|
88
|
+
class Company < ActiveRecord::Base
|
89
|
+
has_many :phone_numbers, :as => :phoneable
|
90
|
+
audit_enabled
|
91
|
+
end
|
92
|
+
class PhoneNumber < ActiveRecord::Base
|
93
|
+
belongs_to :phoneable, :polymorphic => true
|
94
|
+
audit_relationship_enabled
|
95
|
+
end
|
96
|
+
@company = Company.create(:name => 'Apple')
|
97
|
+
@ph = PhoneNumber.new(:number => '1-800-orange')
|
98
|
+
@company.phone_numbers << @ph
|
99
|
+
@ph.update_attributes(:number => '1-800-call-apple')
|
100
|
+
@audit = @company.audits.first
|
101
|
+
end
|
102
|
+
|
103
|
+
it "the latest audit should be the audit we want to rollback" do
|
104
|
+
@audit.action.should == 'updated'
|
105
|
+
@audit.new_value.should == '1-800-call-apple'
|
106
|
+
@audit.old_value.should == '1-800-orange'
|
107
|
+
@audit.association.should == @ph
|
108
|
+
end
|
109
|
+
|
110
|
+
it "performs the rollback" do
|
111
|
+
@audit.rollback
|
112
|
+
@company.reload
|
113
|
+
@company.phone_numbers.first.number.should == '1-800-orange'
|
114
|
+
end
|
115
|
+
|
116
|
+
it "creates an audit when a rollback is performed" do
|
117
|
+
lambda { @audit.rollback }.should change { Audit.count }.by(1)
|
118
|
+
end
|
119
|
+
|
120
|
+
it "the latest audit after a rollback should contain the changed values" do
|
121
|
+
@audit.rollback
|
122
|
+
@company.reload
|
123
|
+
@company.audits.first.old_value.should == '1-800-call-apple'
|
124
|
+
@company.audits.first.new_value.should == '1-800-orange'
|
125
|
+
end
|
126
|
+
end
|
43
127
|
end
|
@@ -5,12 +5,12 @@ describe "Auditing" do
|
|
5
5
|
describe "auditing default values" do
|
6
6
|
before do
|
7
7
|
class School < ActiveRecord::Base
|
8
|
-
|
8
|
+
audit_enabled
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
|
-
it "responds to
|
13
|
-
School.should respond_to(:
|
12
|
+
it "responds to audit_enabled when auditing is added to an AR object" do
|
13
|
+
School.should respond_to(:audit_enabled)
|
14
14
|
end
|
15
15
|
|
16
16
|
it "responds to @auditing_fields" do
|
@@ -23,31 +23,31 @@ describe "Auditing" do
|
|
23
23
|
end
|
24
24
|
end # auditing default values
|
25
25
|
|
26
|
-
describe "
|
26
|
+
describe "audit_enabled :fields => [:foo,:bar]" do
|
27
27
|
it "accepts a single valude as a symbol" do
|
28
28
|
class School < ActiveRecord::Base
|
29
|
-
|
29
|
+
audit_enabled :fields => :name
|
30
30
|
end
|
31
31
|
School.auditing_fields.should == ['name']
|
32
32
|
end
|
33
33
|
|
34
34
|
it "accepts a single value as a string" do
|
35
35
|
class School < ActiveRecord::Base
|
36
|
-
|
36
|
+
audit_enabled :fields => 'name'
|
37
37
|
end
|
38
38
|
School.auditing_fields.should == ['name']
|
39
39
|
end
|
40
40
|
|
41
41
|
it "accepts an array of symbols" do
|
42
42
|
class School < ActiveRecord::Base
|
43
|
-
|
43
|
+
audit_enabled :fields => [:name, :established_on]
|
44
44
|
end
|
45
45
|
School.auditing_fields.should == ['name', 'established_on']
|
46
46
|
end
|
47
47
|
|
48
48
|
it "accepts an array of strings" do
|
49
49
|
class School < ActiveRecord::Base
|
50
|
-
|
50
|
+
audit_enabled :fields => ['name', 'established_on']
|
51
51
|
end
|
52
52
|
School.auditing_fields.should == ['name', 'established_on']
|
53
53
|
end
|
@@ -56,7 +56,7 @@ describe "Auditing" do
|
|
56
56
|
describe "creating a new instance" do
|
57
57
|
before do
|
58
58
|
class School < ActiveRecord::Base
|
59
|
-
|
59
|
+
audit_enabled
|
60
60
|
end
|
61
61
|
end
|
62
62
|
|
@@ -83,7 +83,7 @@ describe "Auditing" do
|
|
83
83
|
describe "updating an existing record" do
|
84
84
|
before do
|
85
85
|
class School < ActiveRecord::Base
|
86
|
-
|
86
|
+
audit_enabled :fields => 'name'
|
87
87
|
end
|
88
88
|
@school = School.create(:name => 'PS118')
|
89
89
|
end
|
@@ -128,4 +128,74 @@ describe "Auditing" do
|
|
128
128
|
end
|
129
129
|
end # updating an existing record
|
130
130
|
|
131
|
+
describe "belongs_to relationships" do
|
132
|
+
before do
|
133
|
+
class Car < ActiveRecord::Base
|
134
|
+
audit_enabled
|
135
|
+
belongs_to :auto_maker
|
136
|
+
end
|
137
|
+
class AutoMaker < ActiveRecord::Base
|
138
|
+
has_many :cars
|
139
|
+
end
|
140
|
+
|
141
|
+
@car = Car.create(:name => 'fast car')
|
142
|
+
@automaker = AutoMaker.create(:name => 'maker of fast cars')
|
143
|
+
end
|
144
|
+
|
145
|
+
it "creates an audit when we add a belongs_to relationship" do
|
146
|
+
lambda { @car.update_attributes(:auto_maker => @automaker) }.should change { Audit.count }.by(1)
|
147
|
+
end
|
148
|
+
|
149
|
+
describe "our latest audit" do
|
150
|
+
before do
|
151
|
+
@car.update_attributes(:auto_maker => @automaker)
|
152
|
+
end
|
153
|
+
|
154
|
+
it "the action should be 'updated" do
|
155
|
+
@car.audits.first.action.should == 'updated'
|
156
|
+
end
|
157
|
+
|
158
|
+
it "new_value should be an id" do
|
159
|
+
@car.audits.first.new_value.should == @automaker.id
|
160
|
+
end
|
161
|
+
|
162
|
+
it "should be reversable" do
|
163
|
+
@car.audits.first.reversable?.should == true
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
describe "updating a belongs_to" do
|
168
|
+
before do
|
169
|
+
@new_automaker = AutoMaker.create(:name => 'maker of safe car')
|
170
|
+
@car.update_attributes(:auto_maker => @automaker)
|
171
|
+
end
|
172
|
+
|
173
|
+
it "creates an audit when we change a belongs_to relationship" do
|
174
|
+
lambda { @car.update_attributes(:auto_maker => @new_automaker) }.should change { Audit.count }.by(1)
|
175
|
+
end
|
176
|
+
|
177
|
+
describe "our latest audit" do
|
178
|
+
before do
|
179
|
+
@car.update_attributes(:auto_maker => @new_automaker)
|
180
|
+
end
|
181
|
+
|
182
|
+
it "the action should be 'updated" do
|
183
|
+
@car.audits.first.action.should == 'updated'
|
184
|
+
end
|
185
|
+
|
186
|
+
it "new_value should be an id" do
|
187
|
+
@car.audits.first.new_value.should == @new_automaker.id
|
188
|
+
end
|
189
|
+
|
190
|
+
it "old_value should be an id" do
|
191
|
+
@car.audits.first.old_value.should == @automaker.id
|
192
|
+
end
|
193
|
+
|
194
|
+
it "should be reversable" do
|
195
|
+
@car.audits.first.reversable?.should == true
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end # belongs_to relationships
|
200
|
+
|
131
201
|
end
|
data/spec/schema.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
ActiveRecord::Schema.define(:version => 0) do
|
2
2
|
|
3
|
-
create_table :audits
|
3
|
+
create_table :audits do |t|
|
4
4
|
t.string :action
|
5
5
|
t.string :auditable_type
|
6
6
|
t.integer :auditable_id
|
@@ -14,11 +14,41 @@ ActiveRecord::Schema.define(:version => 0) do
|
|
14
14
|
t.timestamps
|
15
15
|
end
|
16
16
|
|
17
|
-
|
18
|
-
create_table :schools, :force => true do |t|
|
17
|
+
create_table :schools do |t|
|
19
18
|
t.string :name
|
20
19
|
t.datetime :established_on
|
21
20
|
t.timestamps
|
22
21
|
end
|
23
22
|
|
23
|
+
create_table :cars do |t|
|
24
|
+
t.string :name
|
25
|
+
t.integer :auto_maker_id
|
26
|
+
t.timestamps
|
27
|
+
end
|
28
|
+
|
29
|
+
create_table :auto_makers do |t|
|
30
|
+
t.string :name
|
31
|
+
t.timestamps
|
32
|
+
end
|
33
|
+
|
34
|
+
create_table :companies do |t|
|
35
|
+
t.string :name
|
36
|
+
t.timestamps
|
37
|
+
end
|
38
|
+
|
39
|
+
create_table :people do |t|
|
40
|
+
t.string :first_name
|
41
|
+
t.string :last_name
|
42
|
+
t.timestamps
|
43
|
+
end
|
44
|
+
|
45
|
+
create_table :phone_numbers do |t|
|
46
|
+
t.string :number
|
47
|
+
t.string :extension
|
48
|
+
t.string :phoneable_type
|
49
|
+
t.integer :phoneable_id
|
50
|
+
t.timestamps
|
51
|
+
end
|
52
|
+
|
53
|
+
|
24
54
|
end
|
metadata
CHANGED
@@ -3,18 +3,18 @@ name: auditing
|
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease: false
|
5
5
|
segments:
|
6
|
+
- 1
|
6
7
|
- 0
|
7
8
|
- 0
|
8
|
-
|
9
|
-
version: 0.0.2
|
9
|
+
version: 1.0.0
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
|
-
- Brad
|
12
|
+
- Brad Cantin
|
13
13
|
autorequire:
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-
|
17
|
+
date: 2010-10-09 00:00:00 -04:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -54,10 +54,12 @@ files:
|
|
54
54
|
- VERSION
|
55
55
|
- auditing.gemspec
|
56
56
|
- lib/auditing.rb
|
57
|
-
- lib/auditing/
|
57
|
+
- lib/auditing/audit_relationship.rb
|
58
58
|
- lib/auditing/auditor.rb
|
59
|
-
-
|
59
|
+
- lib/auditing/base.rb
|
60
|
+
- spec/auditing/audit_relationship_spec.rb
|
60
61
|
- spec/auditing/auditor_spec.rb
|
62
|
+
- spec/auditing/base_spec.rb
|
61
63
|
- spec/schema.rb
|
62
64
|
- spec/spec_helper.rb
|
63
65
|
has_rdoc: true
|
@@ -93,7 +95,8 @@ signing_key:
|
|
93
95
|
specification_version: 3
|
94
96
|
summary: A gem to keep track of audit hisory of a record
|
95
97
|
test_files:
|
96
|
-
- spec/auditing/
|
98
|
+
- spec/auditing/audit_relationship_spec.rb
|
97
99
|
- spec/auditing/auditor_spec.rb
|
100
|
+
- spec/auditing/base_spec.rb
|
98
101
|
- spec/schema.rb
|
99
102
|
- spec/spec_helper.rb
|
data/lib/auditing/auditing.rb
DELETED
@@ -1,51 +0,0 @@
|
|
1
|
-
module Auditing
|
2
|
-
|
3
|
-
# Auditing creates audit objects for a record.
|
4
|
-
#
|
5
|
-
# @examples
|
6
|
-
# class School < ActiveRecord::Base
|
7
|
-
# auditing
|
8
|
-
# end
|
9
|
-
# class School < ActiveRecord::Base
|
10
|
-
# auditing :fields => [:name, :established_on]
|
11
|
-
# end
|
12
|
-
def auditing(opts={})
|
13
|
-
include InstanceMethods
|
14
|
-
class_inheritable_accessor :auditing_fields
|
15
|
-
|
16
|
-
has_many :audits, :as => :auditable, :order => 'created_at DESC, id DESC'
|
17
|
-
|
18
|
-
self.auditing_fields = gather_fields_for_auditing(opts[:fields])
|
19
|
-
|
20
|
-
after_create :log_creation
|
21
|
-
after_update :log_update
|
22
|
-
|
23
|
-
end
|
24
|
-
|
25
|
-
def gather_fields_for_auditing(fields=nil)
|
26
|
-
return self.column_names - ["id", "created_at", "updated_at"] unless fields
|
27
|
-
fields.is_a?(Array) ? fields.map {|f| f.to_s} : [fields.to_s]
|
28
|
-
end
|
29
|
-
|
30
|
-
module InstanceMethods
|
31
|
-
def log_creation
|
32
|
-
add_audit(:action => 'created', :undoable => false)
|
33
|
-
end
|
34
|
-
|
35
|
-
def log_update
|
36
|
-
if changed?
|
37
|
-
changes.each.select {|k,v| auditing_fields.include?(k)}.each do |field, change|
|
38
|
-
next if change[0].to_s == change[1].to_s
|
39
|
-
add_audit(:action => 'updated', :field_name => field,
|
40
|
-
:old_value => change[0], :new_value => change[1])
|
41
|
-
end
|
42
|
-
end
|
43
|
-
end
|
44
|
-
|
45
|
-
private
|
46
|
-
def add_audit(hash={})
|
47
|
-
Audit.create!({:auditable => self}.merge(hash))
|
48
|
-
end
|
49
|
-
end # Auditing::InstanceMethods
|
50
|
-
|
51
|
-
end
|