audited_change_set 0.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/.document +5 -0
- data/.gitignore +22 -0
- data/.rspec +1 -0
- data/LICENSE +20 -0
- data/README.markdown +10 -0
- data/Rakefile +37 -0
- data/VERSION +1 -0
- data/audited_change_set.gemspec +68 -0
- data/lib/audited_change_set.rb +6 -0
- data/lib/audited_change_set/change.rb +134 -0
- data/lib/audited_change_set/change_set.rb +31 -0
- data/spec/audited_change_set/change_set_spec.rb +103 -0
- data/spec/audited_change_set/change_spec.rb +395 -0
- data/spec/db/schema.rb +15 -0
- data/spec/spec_helper.rb +15 -0
- data/specs.watchr +59 -0
- metadata +127 -0
data/.document
ADDED
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 David Chelimsky, Brian Tatnall, Corey Haines, Nate Jackson
|
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.
|
data/README.markdown
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "audited_change_set"
|
8
|
+
gem.summary = %Q{change_set for acts_as_audited}
|
9
|
+
gem.description = gem.summary
|
10
|
+
gem.email = "dchelimsky@gmail.com"
|
11
|
+
gem.homepage = "http://github.com/dchelimsky/audited_change_set"
|
12
|
+
gem.authors = ["David Chelimsky","Brian Tatnall", "Nate Jackson", "Corey Haines"]
|
13
|
+
gem.add_dependency "acts_as_audited", ">= 1.1.1"
|
14
|
+
gem.add_development_dependency "rspec", ">= 2.0.0.beta.8"
|
15
|
+
gem.add_development_dependency "sqlite3-ruby", ">= 1.2.5"
|
16
|
+
end
|
17
|
+
Jeweler::GemcutterTasks.new
|
18
|
+
rescue LoadError
|
19
|
+
puts "Jeweler (or a dependency) not available. Install it with: gem install jeweler"
|
20
|
+
end
|
21
|
+
|
22
|
+
require 'rspec/core/rake_task'
|
23
|
+
Rspec::Core::RakeTask.new(:spec)
|
24
|
+
|
25
|
+
task :spec => :check_dependencies
|
26
|
+
|
27
|
+
task :default => :spec
|
28
|
+
|
29
|
+
require 'rake/rdoctask'
|
30
|
+
Rake::RDocTask.new do |rdoc|
|
31
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
32
|
+
|
33
|
+
rdoc.rdoc_dir = 'rdoc'
|
34
|
+
rdoc.title = "audited_change_set #{version}"
|
35
|
+
rdoc.rdoc_files.include('README*')
|
36
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
37
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1
|
@@ -0,0 +1,68 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{audited_change_set}
|
8
|
+
s.version = "0.0.1"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["David Chelimsky", "Brian Tatnall", "Nate Jackson", "Corey Haines"]
|
12
|
+
s.date = %q{2010-05-13}
|
13
|
+
s.description = %q{change_set for acts_as_audited}
|
14
|
+
s.email = %q{dchelimsky@gmail.com}
|
15
|
+
s.extra_rdoc_files = [
|
16
|
+
"LICENSE",
|
17
|
+
"README.markdown"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".document",
|
21
|
+
".gitignore",
|
22
|
+
".rspec",
|
23
|
+
"LICENSE",
|
24
|
+
"README.markdown",
|
25
|
+
"Rakefile",
|
26
|
+
"VERSION",
|
27
|
+
"audited_change_set.gemspec",
|
28
|
+
"lib/audited_change_set.rb",
|
29
|
+
"lib/audited_change_set/change.rb",
|
30
|
+
"lib/audited_change_set/change_set.rb",
|
31
|
+
"spec/audited_change_set/change_set_spec.rb",
|
32
|
+
"spec/audited_change_set/change_spec.rb",
|
33
|
+
"spec/db/schema.rb",
|
34
|
+
"spec/spec_helper.rb",
|
35
|
+
"specs.watchr"
|
36
|
+
]
|
37
|
+
s.homepage = %q{http://github.com/dchelimsky/audited_change_set}
|
38
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
39
|
+
s.require_paths = ["lib"]
|
40
|
+
s.rubygems_version = %q{1.3.6}
|
41
|
+
s.summary = %q{change_set for acts_as_audited}
|
42
|
+
s.test_files = [
|
43
|
+
"spec/audited_change_set/change_set_spec.rb",
|
44
|
+
"spec/audited_change_set/change_spec.rb",
|
45
|
+
"spec/db/schema.rb",
|
46
|
+
"spec/spec_helper.rb"
|
47
|
+
]
|
48
|
+
|
49
|
+
if s.respond_to? :specification_version then
|
50
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
51
|
+
s.specification_version = 3
|
52
|
+
|
53
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
54
|
+
s.add_runtime_dependency(%q<acts_as_audited>, [">= 1.1.1"])
|
55
|
+
s.add_development_dependency(%q<rspec>, [">= 2.0.0.beta.8"])
|
56
|
+
s.add_development_dependency(%q<sqlite3-ruby>, [">= 1.2.5"])
|
57
|
+
else
|
58
|
+
s.add_dependency(%q<acts_as_audited>, [">= 1.1.1"])
|
59
|
+
s.add_dependency(%q<rspec>, [">= 2.0.0.beta.8"])
|
60
|
+
s.add_dependency(%q<sqlite3-ruby>, [">= 1.2.5"])
|
61
|
+
end
|
62
|
+
else
|
63
|
+
s.add_dependency(%q<acts_as_audited>, [">= 1.1.1"])
|
64
|
+
s.add_dependency(%q<rspec>, [">= 2.0.0.beta.8"])
|
65
|
+
s.add_dependency(%q<sqlite3-ruby>, [">= 1.2.5"])
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module AuditedChangeSet
|
2
|
+
class Change
|
3
|
+
module Hooks
|
4
|
+
def self.included(mod)
|
5
|
+
mod.extend ClassMethods
|
6
|
+
end
|
7
|
+
|
8
|
+
module ClassMethods
|
9
|
+
def hooks
|
10
|
+
@hooks ||= {}
|
11
|
+
end
|
12
|
+
|
13
|
+
def hook(method, &block)
|
14
|
+
hooks[method] = block
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def hooks
|
19
|
+
self.class.hooks
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def hook(key, *args)
|
25
|
+
hooks[key] && instance_exec(*args, &hooks[key])
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
include Enumerable
|
31
|
+
include Hooks
|
32
|
+
|
33
|
+
class Field
|
34
|
+
include Hooks
|
35
|
+
|
36
|
+
attr_reader :name, :old_value, :new_value
|
37
|
+
|
38
|
+
def initialize(name, new_val, old_val=nil)
|
39
|
+
@name = name.to_s
|
40
|
+
@new_value, @old_value = [new_val, old_val].map {|val| transform_value(val) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def transform_value(val)
|
44
|
+
hook(:transform_value, val) || (association_field? ? get_associated_object(val).to_s : val.to_s)
|
45
|
+
end
|
46
|
+
|
47
|
+
def association_class
|
48
|
+
@association_class ||= begin
|
49
|
+
name.to_s =~ /(.*)_id$/
|
50
|
+
$1.camelize.constantize
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def get_associated_object(id)
|
55
|
+
hook(:get_associated_object, id) || association_class.find_by_id(id)
|
56
|
+
end
|
57
|
+
|
58
|
+
def association_field?
|
59
|
+
name.ends_with? "_id"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
class << self
|
64
|
+
def for_audits(audits, fields=nil, unfiltered_change_id=nil)
|
65
|
+
audits_to_changes(audits, fields, unfiltered_change_id).select(&:relevant?).reverse
|
66
|
+
end
|
67
|
+
|
68
|
+
def field_names_for_audits(audits)
|
69
|
+
audits_to_changes(audits).map(&:field_names).flatten.uniq.sort
|
70
|
+
end
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def audits_to_changes(audits, fields=nil, unfiltered_change_id=nil)
|
75
|
+
audits.map do |a|
|
76
|
+
filter = (a.id == unfiltered_change_id.to_i) ? nil : fields
|
77
|
+
new(a, filter)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def initialize(audit, fields=nil)
|
83
|
+
@audit = audit
|
84
|
+
@fields = fields
|
85
|
+
end
|
86
|
+
|
87
|
+
def create_field(name, changes)
|
88
|
+
Field.new(name,*[changes].flatten.reverse)
|
89
|
+
end
|
90
|
+
|
91
|
+
delegate :id, :to => :@audit
|
92
|
+
|
93
|
+
delegate :action, :to => :@audit
|
94
|
+
|
95
|
+
def username
|
96
|
+
if @audit.user
|
97
|
+
hook(:username, @audit.user) || @audit.username
|
98
|
+
else
|
99
|
+
'unknown'
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def date
|
104
|
+
@audit.created_at
|
105
|
+
end
|
106
|
+
|
107
|
+
def relevant?
|
108
|
+
any?(&:present?)
|
109
|
+
end
|
110
|
+
|
111
|
+
def relevant_field?(field)
|
112
|
+
@fields ? @fields.map(&:downcase).include?(field.name) : true
|
113
|
+
end
|
114
|
+
|
115
|
+
def field_names
|
116
|
+
non_empty_fields.map { |name, vals| name }
|
117
|
+
end
|
118
|
+
|
119
|
+
def each(&block)
|
120
|
+
changed_fields.each(&block)
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
def changed_fields
|
126
|
+
@changes_fields ||= non_empty_fields.map { |name, vals| create_field(name, vals) }.select {|field| relevant_field?(field) }
|
127
|
+
end
|
128
|
+
|
129
|
+
def non_empty_fields
|
130
|
+
@audit[:changes].reject { |name, val| val.to_s.empty? }
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module AuditedChangeSet
|
2
|
+
class ChangeSet
|
3
|
+
include Enumerable
|
4
|
+
|
5
|
+
def self.for_auditable(klass, id, fields=nil, change_id=nil)
|
6
|
+
new(klass.find(id), fields, change_id)
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(auditable, fields=nil, change_id=nil)
|
10
|
+
@auditable = auditable
|
11
|
+
@fields = fields
|
12
|
+
@change_id = change_id
|
13
|
+
end
|
14
|
+
|
15
|
+
delegate :name, :to => :@auditable, :prefix => :auditable
|
16
|
+
|
17
|
+
def each(&block)
|
18
|
+
changes.each(&block)
|
19
|
+
end
|
20
|
+
|
21
|
+
def changed_fields
|
22
|
+
Change.field_names_for_audits(@auditable.audits).sort
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def changes
|
28
|
+
@changes ||= Change.for_audits(@auditable.audits, @fields, @change_id)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
module AuditedChangeSet
|
4
|
+
describe ChangeSet do
|
5
|
+
describe "::for_auditable" do
|
6
|
+
let(:klass) { double(Class) }
|
7
|
+
let(:auditable) { double('auditable') }
|
8
|
+
before { klass.stub(:find) { auditable } }
|
9
|
+
|
10
|
+
it "returns the change_set for the given auditable object" do
|
11
|
+
ChangeSet.should_receive(:new).with(auditable, nil, nil).and_return(change_set = double(ChangeSet))
|
12
|
+
ChangeSet.for_auditable(klass, 37).should be(change_set)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "passes the fields into the ChangeSet constructor" do
|
16
|
+
fields = ["field"]
|
17
|
+
ChangeSet.should_receive(:new).with(auditable, fields, nil)
|
18
|
+
ChangeSet.for_auditable(klass, 37, fields)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "passes the change id into the ChangeSet constructor" do
|
22
|
+
fields = ["field"]
|
23
|
+
ChangeSet.should_receive(:new).with(auditable, fields, 42)
|
24
|
+
ChangeSet.for_auditable(klass, 37, fields, 42)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "#auditable_name" do
|
29
|
+
let(:auditable) { double('auditable', :name => 'irrelevant') }
|
30
|
+
let(:change_set) { ChangeSet.new(auditable) }
|
31
|
+
|
32
|
+
it "returns the name of the auditable object" do
|
33
|
+
change_set.auditable_name.should == 'irrelevant'
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
context "change_set for an auditable model" do
|
38
|
+
let(:auditable_model) { stub(AuditableModel, :name => 'irrelevant') }
|
39
|
+
let(:change_set) { ChangeSet.new(auditable_model) }
|
40
|
+
|
41
|
+
describe "#each" do
|
42
|
+
context "without specified fields" do
|
43
|
+
it "yields changes for the auditable_model audits" do
|
44
|
+
audits = [double(Audit), double(Audit)]
|
45
|
+
auditable_model.stub(:audits).and_return(audits)
|
46
|
+
|
47
|
+
changes = [stub(Change), stub(Change)]
|
48
|
+
Change.should_receive(:for_audits).with(audits, nil, nil).and_return(changes)
|
49
|
+
|
50
|
+
yielded_changes = []
|
51
|
+
change_set.each do |change|
|
52
|
+
yielded_changes << change
|
53
|
+
end
|
54
|
+
|
55
|
+
yielded_changes.should == changes
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
context "with specified fields" do
|
60
|
+
let(:fields) { ["name", "intent"] }
|
61
|
+
let(:change_set) { ChangeSet.new(auditable_model, fields) }
|
62
|
+
it "provides the specified fields to the changes factory" do
|
63
|
+
audits = [double(Audit), double(Audit)]
|
64
|
+
auditable_model.stub(:audits).and_return(audits)
|
65
|
+
|
66
|
+
changes = [stub(Change), stub(Change)]
|
67
|
+
Change.should_receive(:for_audits).with(audits, fields, nil).and_return(changes)
|
68
|
+
change_set.each do
|
69
|
+
#just need to invoke this
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context "and an change id" do
|
74
|
+
let(:change_id) { 42 }
|
75
|
+
let(:change_set) { ChangeSet.new(auditable_model, fields, change_id) }
|
76
|
+
it "provides the change id to the changes factory" do
|
77
|
+
audits = [double(Audit), double(Audit)]
|
78
|
+
auditable_model.stub(:audits).and_return(audits)
|
79
|
+
|
80
|
+
changes = [stub(Change), stub(Change)]
|
81
|
+
Change.should_receive(:for_audits).with(audits, fields, change_id).and_return(changes)
|
82
|
+
change_set.each do
|
83
|
+
#just need to invoke this
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "#changed_fields" do
|
92
|
+
it "returns all of the changed field names" do
|
93
|
+
audits = [double(Audit), double(Audit)]
|
94
|
+
auditable_model.stub(:audits).and_return(audits)
|
95
|
+
Change.stub(:field_names_for_audits).with(audits).and_return(["intent", "executive_status"])
|
96
|
+
|
97
|
+
change_set.changed_fields.should == ["executive_status", "intent"]
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,395 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module AuditedChangeSet
|
4
|
+
describe Change do
|
5
|
+
|
6
|
+
let(:audit) { double(Audit) }
|
7
|
+
|
8
|
+
describe "::for_audits" do
|
9
|
+
let(:audits) {[double(Audit), double(Audit)]}
|
10
|
+
|
11
|
+
it "constructs an array of changes from the audits" do
|
12
|
+
changes = [double(Change), double(Change)]
|
13
|
+
changes.each do |change|
|
14
|
+
change.stub(:relevant?).and_return(true)
|
15
|
+
end
|
16
|
+
audits.each_with_index do |audit, index|
|
17
|
+
audit.stub(:id).and_return(53)
|
18
|
+
Change.stub(:new).with(audit, nil).and_return(changes[index])
|
19
|
+
end
|
20
|
+
Change.for_audits(audits).should == changes.reverse
|
21
|
+
end
|
22
|
+
|
23
|
+
context "with specified fields" do
|
24
|
+
let(:audits) { [double(Audit), double(Audit)] }
|
25
|
+
let(:fields) { ["name", "intent"] }
|
26
|
+
let(:changes) { [double(Change), double(Change)] }
|
27
|
+
|
28
|
+
it "constructs an array of changes filtered by the specified fields from the audits" do
|
29
|
+
audits.each_with_index do |audit, index|
|
30
|
+
audit.stub(:id).and_return(53)
|
31
|
+
Change.stub(:new).with(audit, fields).and_return(changes[index])
|
32
|
+
end
|
33
|
+
changes.first.should_receive(:relevant?).and_return(true)
|
34
|
+
changes.last.should_receive(:relevant?).and_return(false)
|
35
|
+
Change.for_audits(audits, fields).should == [changes.first]
|
36
|
+
end
|
37
|
+
|
38
|
+
context "with a change id" do
|
39
|
+
it "doesn't pass the fields into any audits whose id matches the change id" do
|
40
|
+
change_id = 42
|
41
|
+
audits.first.stub(:id).and_return(53)
|
42
|
+
audits.second.stub(:id).and_return(change_id)
|
43
|
+
|
44
|
+
changes.first.stub(:relevant?).and_return(true)
|
45
|
+
changes.second.stub(:relevant?).and_return(true)
|
46
|
+
|
47
|
+
Change.should_receive(:new).with(audits.first, fields).and_return(changes.first)
|
48
|
+
Change.should_receive(:new).with(audits.second, nil).and_return(changes.second)
|
49
|
+
|
50
|
+
Change.for_audits(audits, fields, change_id.to_s) # this just needs to get invoked
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "::field_names_for_audits" do
|
57
|
+
let(:audits) {[double(Audit), double(Audit)]}
|
58
|
+
it "returns an array of the fields which have changed across the audits" do
|
59
|
+
changes = [double(Change), double(Change)]
|
60
|
+
audits.each_with_index do |audit, index|
|
61
|
+
audit.stub(:id).and_return(53)
|
62
|
+
Change.stub(:new).with(audit, nil).and_return(changes[index])
|
63
|
+
end
|
64
|
+
changes.first.stub(:field_names).and_return(["intent", "name"])
|
65
|
+
changes.last.stub(:field_names).and_return(["name", "executive_status"])
|
66
|
+
Change.field_names_for_audits(audits).should == ["executive_status", "intent", "name"]
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe "#field_names" do
|
71
|
+
it "returns a list of field names for this change" do
|
72
|
+
audit_changes = {'intent' => [nil, 'irrelevant'], 'name' => [nil, 'irrelevant'], 'executive_status' => [nil, '']}
|
73
|
+
audit.stub(:[]).with(:changes).and_return(audit_changes)
|
74
|
+
change = Change.new(audit)
|
75
|
+
change.field_names.should == ['name', 'intent']
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "#username" do
|
80
|
+
it "returns user.username" do
|
81
|
+
audit.stub(:user) { double("user") }
|
82
|
+
audit.stub(:username) { "Example User" }
|
83
|
+
change = Change.new(audit)
|
84
|
+
change.username.should == "Example User"
|
85
|
+
end
|
86
|
+
|
87
|
+
it "returns 'unknown' if no user associated" do
|
88
|
+
audit.stub(:user).and_return nil
|
89
|
+
change = Change.new(audit)
|
90
|
+
change.username.should == 'unknown'
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
it "#date returns the date when the audit was made" do
|
95
|
+
d = DateTime.now
|
96
|
+
audit.stub(:created_at).and_return(d)
|
97
|
+
change = Change.new(audit)
|
98
|
+
change.date.should == d
|
99
|
+
end
|
100
|
+
|
101
|
+
it "#id returns the audit id" do
|
102
|
+
audit.stub(:id).and_return(42)
|
103
|
+
change = Change.new(audit)
|
104
|
+
change.id.should == audit.id
|
105
|
+
end
|
106
|
+
|
107
|
+
it "#action returns the audit action" do
|
108
|
+
audit.stub(:action).and_return("create")
|
109
|
+
change = Change.new(audit)
|
110
|
+
change.action.should == "create"
|
111
|
+
end
|
112
|
+
|
113
|
+
describe "#relevant?" do
|
114
|
+
context "any of the audits match the specified fields" do
|
115
|
+
it "returns true" do
|
116
|
+
audit.stub(:[]).with(:changes).and_return("intent" => "irrelevant")
|
117
|
+
Change.new(audit, ["intent"]).should be_relevant
|
118
|
+
end
|
119
|
+
end
|
120
|
+
context "none of the audits match the specified fields" do
|
121
|
+
it "returns false" do
|
122
|
+
audit.stub(:[]).with(:changes).and_return("name" => "irrelevant")
|
123
|
+
Change.new(audit, ["intent"]).should_not be_relevant
|
124
|
+
end
|
125
|
+
end
|
126
|
+
context "no specified fields" do
|
127
|
+
it "returns true" do
|
128
|
+
audit.stub(:[]).with(:changes).and_return("intent" => "irrelevant")
|
129
|
+
Change.new(audit).should be_relevant
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
describe "#each yields fields for each changed attribute" do
|
135
|
+
Rspec::Matchers.define :yield_these do |field_values|
|
136
|
+
match do |change|
|
137
|
+
@yielded_fields = []
|
138
|
+
change.each do |field|
|
139
|
+
@yielded_fields << field
|
140
|
+
end
|
141
|
+
matching_fields = @yielded_fields.select do |field|
|
142
|
+
field_values[field.name].present? &&
|
143
|
+
field_values[field.name].first == field.old_value &&
|
144
|
+
field_values[field.name].second == field.new_value
|
145
|
+
end
|
146
|
+
matching_fields.size == field_values.size && @yielded_fields.size == field_values.size
|
147
|
+
end
|
148
|
+
|
149
|
+
failure_message_for_should do |changes_hash|
|
150
|
+
"expected Change to yield #{field_values.to_a.inspect}, but got #{@yielded_fields.map{|f| [f.name, [f.old_value, f.new_value]]}.inspect}"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context "change is a create" do
|
155
|
+
before(:each) do
|
156
|
+
audit.stub(:action).and_return("create")
|
157
|
+
end
|
158
|
+
|
159
|
+
it "sets old value to nil and new value to attribute value" do
|
160
|
+
changes = { "name" => "new name", "intent" => "new intent"}
|
161
|
+
|
162
|
+
yielded_fields ={"name" => ["", "new name"], "intent" => ["", "new intent"]}
|
163
|
+
|
164
|
+
audit.stub(:[]).and_return(changes)
|
165
|
+
Change.new(audit).should yield_these(yielded_fields)
|
166
|
+
end
|
167
|
+
|
168
|
+
it "does not show empty string values" do
|
169
|
+
changes = { "blank_field" => "", "non_blank" => "something"}
|
170
|
+
yielded_fields = { "non_blank" => ["", "something"] }
|
171
|
+
|
172
|
+
audit.stub(:[]).and_return(changes)
|
173
|
+
Change.new(audit).should yield_these(yielded_fields)
|
174
|
+
end
|
175
|
+
|
176
|
+
it "does show 'false' values" do
|
177
|
+
changes = { "false_field" => false}
|
178
|
+
yielded_fields = { "false_field" => ["", "false"] }
|
179
|
+
|
180
|
+
audit.stub(:[]).and_return(changes)
|
181
|
+
Change.new(audit).should yield_these(yielded_fields)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
context "change is an update" do
|
186
|
+
before(:each) do
|
187
|
+
audit.stub(:action).and_return("update")
|
188
|
+
end
|
189
|
+
|
190
|
+
it "sets the old and new values" do
|
191
|
+
changes = { "name" => ["old name", "changed name"] }
|
192
|
+
yielded_fields = {"name" => ["old name", "changed name"] }
|
193
|
+
|
194
|
+
audit.stub(:[]).and_return(changes)
|
195
|
+
Change.new(audit).should yield_these(yielded_fields)
|
196
|
+
end
|
197
|
+
|
198
|
+
it "shows if we changed to empty string" do
|
199
|
+
changes = { "changed_to_empty" => ["not empty", ""]}
|
200
|
+
yielded_fields = {"changed_to_empty" => ["not empty", ""]}
|
201
|
+
|
202
|
+
audit.stub(:[]).and_return(changes)
|
203
|
+
Change.new(audit).should yield_these(yielded_fields)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
context "field is an association" do
|
208
|
+
|
209
|
+
before :each do
|
210
|
+
AuditableModel.stub(:find_by_id).and_return(nil)
|
211
|
+
AuditableModel.stub(:find_by_id).with("1").and_return(double(AuditableModel, :to_s => 'to_s_ified'))
|
212
|
+
end
|
213
|
+
|
214
|
+
it "uses associated object for new value" do
|
215
|
+
changes = { "auditable_model_id" => "1"}
|
216
|
+
yielded_fields ={"auditable_model_id" => ["", "to_s_ified"]}
|
217
|
+
|
218
|
+
audit.stub(:action).and_return("create")
|
219
|
+
audit.stub(:[]).and_return(changes)
|
220
|
+
Change.new(audit).should yield_these(yielded_fields)
|
221
|
+
end
|
222
|
+
|
223
|
+
it "uses associated object for old value" do
|
224
|
+
changes = { "auditable_model_id" => ["1", nil]}
|
225
|
+
yielded_fields ={"auditable_model_id" => ["to_s_ified", ""]}
|
226
|
+
|
227
|
+
audit.stub(:action).and_return("create")
|
228
|
+
audit.stub(:[]).and_return(changes)
|
229
|
+
Change.new(audit).should yield_these(yielded_fields)
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
context "fields are specified" do
|
234
|
+
it "uses only the relevant fields" do
|
235
|
+
changes = { "name" => "irrelevant", "intent" => "win"}
|
236
|
+
yielded_fields ={"intent" => ["", "win"]}
|
237
|
+
|
238
|
+
audit.stub(:action).and_return("create")
|
239
|
+
audit.stub(:[]).and_return(changes)
|
240
|
+
Change.new(audit, ["intent"]).should yield_these(yielded_fields)
|
241
|
+
end
|
242
|
+
|
243
|
+
it "uses the relevant fields after downcasing" do
|
244
|
+
changes = { "name" => "irrelevant", "intent" => "win"}
|
245
|
+
yielded_fields ={"intent" => ["", "win"]}
|
246
|
+
|
247
|
+
audit.stub(:action).and_return("create")
|
248
|
+
audit.stub(:[]).and_return(changes)
|
249
|
+
Change.new(audit, ["Intent"]).should yield_these(yielded_fields)
|
250
|
+
end
|
251
|
+
|
252
|
+
it "uses only the relevant fields that are associations" do
|
253
|
+
models = [
|
254
|
+
double(AuditableModel, :to_s => "more revenue"),
|
255
|
+
double(AuditableModel, :to_s => "less cost")
|
256
|
+
]
|
257
|
+
AuditableModel.stub(:find_by_id) do |options|
|
258
|
+
models.shift
|
259
|
+
end
|
260
|
+
|
261
|
+
changes = { "title" => "irrelevant", "auditable_model_id" => ["1", "2"]}
|
262
|
+
yielded_fields ={"auditable_model_id" => ["less cost", "more revenue"]}
|
263
|
+
|
264
|
+
audit.stub(:action).and_return("create")
|
265
|
+
audit.stub(:[]).and_return(changes)
|
266
|
+
Change.new(audit, ["auditable_model_id"]).should yield_these(yielded_fields)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
|
271
|
+
describe "hooks" do
|
272
|
+
let(:field_class) { Class.new(Change::Field) }
|
273
|
+
let(:change_class) { Class.new(Change) }
|
274
|
+
|
275
|
+
describe "Field::transform_value" do
|
276
|
+
it "yields the new and old values" do
|
277
|
+
yielded_args = []
|
278
|
+
|
279
|
+
field_class::hook(:transform_value) do |block_arg|
|
280
|
+
yielded_args << block_arg
|
281
|
+
end
|
282
|
+
|
283
|
+
field_class.new("anything", "new", "old")
|
284
|
+
yielded_args.should == ["new", "old"]
|
285
|
+
end
|
286
|
+
|
287
|
+
context "given the callback returns a non-nil value" do
|
288
|
+
it "uses the returned value" do
|
289
|
+
field_class::hook(:transform_value) do |block_arg|
|
290
|
+
"#{block_arg} modified by callback"
|
291
|
+
end
|
292
|
+
|
293
|
+
field = field_class.new("anything", "new value", "old value")
|
294
|
+
|
295
|
+
field.new_value.should == "new value modified by callback"
|
296
|
+
field.old_value.should == "old value modified by callback"
|
297
|
+
end
|
298
|
+
end
|
299
|
+
|
300
|
+
context "given the callback returns nil" do
|
301
|
+
it "uses it's unhooked value" do
|
302
|
+
field_class::hook(:transform_value) do |block_arg|
|
303
|
+
nil
|
304
|
+
end
|
305
|
+
|
306
|
+
field = field_class.new("anything", "new value", "old value")
|
307
|
+
|
308
|
+
field.new_value.should == "new value"
|
309
|
+
field.old_value.should == "old value"
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
describe "Field::get_associated_object" do
|
315
|
+
it "yields the id of the associated object" do
|
316
|
+
yielded_args = []
|
317
|
+
|
318
|
+
field_class::hook(:get_associated_object) do |block_arg|
|
319
|
+
yielded_args << block_arg
|
320
|
+
end
|
321
|
+
|
322
|
+
field_class.new("audited_model_id", 37, 42)
|
323
|
+
yielded_args.should == [37, 42]
|
324
|
+
end
|
325
|
+
|
326
|
+
context "given the callback returns a non-nil value" do
|
327
|
+
it "uses the returned value" do
|
328
|
+
field_class::hook(:get_associated_object) do |block_arg|
|
329
|
+
"#{block_arg} returned by callback"
|
330
|
+
end
|
331
|
+
|
332
|
+
field = field_class.new("anything_id", "new value", "old value")
|
333
|
+
|
334
|
+
field.new_value.should == "new value returned by callback"
|
335
|
+
field.old_value.should == "old value returned by callback"
|
336
|
+
end
|
337
|
+
end
|
338
|
+
|
339
|
+
context "given the callback returns nil" do
|
340
|
+
it "uses the default strategy to find the associated object" do
|
341
|
+
returned_object = Object.new
|
342
|
+
AuditableModel.stub(:find_by_id).with(37) { returned_object }
|
343
|
+
field_class::hook(:get_associated_object) do |block_arg|
|
344
|
+
nil
|
345
|
+
end
|
346
|
+
|
347
|
+
field = field_class.new("auditable_model_id", 37)
|
348
|
+
field.new_value.should == returned_object.to_s
|
349
|
+
end
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
describe "Change::username" do
|
354
|
+
let(:user) { double("User") }
|
355
|
+
let(:audit) { double("Audit", :user => user, :username => "supplied username") }
|
356
|
+
|
357
|
+
it "yields the user" do
|
358
|
+
yielded_args = []
|
359
|
+
|
360
|
+
change_class::hook(:username) do |user_arg|
|
361
|
+
yielded_args << user_arg
|
362
|
+
end
|
363
|
+
|
364
|
+
change = change_class.new(audit)
|
365
|
+
change.username # to invoke the hook
|
366
|
+
yielded_args.should == [user]
|
367
|
+
end
|
368
|
+
|
369
|
+
context "given the callback returns a non-nil value" do
|
370
|
+
it "uses the returned value" do
|
371
|
+
change_class::hook(:username) do |user_arg|
|
372
|
+
"returned username"
|
373
|
+
end
|
374
|
+
|
375
|
+
change = change_class.new(audit)
|
376
|
+
change.username.should == "returned username"
|
377
|
+
end
|
378
|
+
end
|
379
|
+
|
380
|
+
context "given the callback returns nil" do
|
381
|
+
it "uses the audit's username" do
|
382
|
+
yielded_args = []
|
383
|
+
|
384
|
+
change_class::hook(:username) do |user_arg|
|
385
|
+
nil
|
386
|
+
end
|
387
|
+
|
388
|
+
change = change_class.new(audit)
|
389
|
+
change.username.should == "supplied username"
|
390
|
+
end
|
391
|
+
end
|
392
|
+
end
|
393
|
+
end
|
394
|
+
end
|
395
|
+
end
|
data/spec/db/schema.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
ActiveRecord::Schema.define(:version => 0) do
|
2
|
+
create_table :audits, :force => true do |t|
|
3
|
+
t.column :auditable_id, :integer
|
4
|
+
t.column :auditable_type, :string
|
5
|
+
t.column :user_id, :integer
|
6
|
+
t.column :user_type, :string
|
7
|
+
t.column :username, :string
|
8
|
+
t.column :action, :string
|
9
|
+
t.column :changes, :text
|
10
|
+
t.column :version, :integer, :default => 0
|
11
|
+
t.column :created_at, :datetime
|
12
|
+
end
|
13
|
+
|
14
|
+
create_table :auditable_models do; end
|
15
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
require "audited_change_set"
|
3
|
+
require "rspec"
|
4
|
+
require "sqlite3"
|
5
|
+
|
6
|
+
ActiveRecord::Base.logger = Logger.new(File.dirname(__FILE__) + "/debug.log")
|
7
|
+
ActiveRecord::Base.establish_connection(
|
8
|
+
:adapter => "sqlite3",
|
9
|
+
:database => ":memory:"
|
10
|
+
)
|
11
|
+
ActiveRecord::Migration.verbose = false
|
12
|
+
load(File.dirname(__FILE__) + "/db/schema.rb")
|
13
|
+
|
14
|
+
class AuditableModel < ActiveRecord::Base; end
|
15
|
+
|
data/specs.watchr
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# Run me with:
|
2
|
+
#
|
3
|
+
# $ watchr specs.watchr
|
4
|
+
|
5
|
+
# --------------------------------------------------
|
6
|
+
# Convenience Methods
|
7
|
+
# --------------------------------------------------
|
8
|
+
def all_spec_files
|
9
|
+
Dir['spec/**/*_spec.rb']
|
10
|
+
end
|
11
|
+
|
12
|
+
def run_spec_matching(thing_to_match)
|
13
|
+
matches = all_spec_files.grep(/#{thing_to_match}/i)
|
14
|
+
if matches.empty?
|
15
|
+
puts "Sorry, thanks for playing, but there were no matches for #{thing_to_match}"
|
16
|
+
else
|
17
|
+
run matches.join(' ')
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def run(files_to_run)
|
22
|
+
puts("Running: #{files_to_run}")
|
23
|
+
system("clear;rspec -cfs #{files_to_run}")
|
24
|
+
no_int_for_you
|
25
|
+
end
|
26
|
+
|
27
|
+
def run_all_specs
|
28
|
+
run(all_spec_files.join(' '))
|
29
|
+
end
|
30
|
+
|
31
|
+
# --------------------------------------------------
|
32
|
+
# Watchr Rules
|
33
|
+
# --------------------------------------------------
|
34
|
+
watch('^spec/(.*)_spec\.rb') { |m| run_spec_matching(m[1]) }
|
35
|
+
watch('^lib/(.*)\.rb') { |m| run_spec_matching(m[1]) }
|
36
|
+
watch('^spec/spec_helper\.rb') { run_all_specs }
|
37
|
+
watch('^spec/support/.*\.rb') { run_all_specs }
|
38
|
+
|
39
|
+
# --------------------------------------------------
|
40
|
+
# Signal Handling
|
41
|
+
# --------------------------------------------------
|
42
|
+
|
43
|
+
def no_int_for_you
|
44
|
+
@sent_an_int = nil
|
45
|
+
end
|
46
|
+
|
47
|
+
Signal.trap 'INT' do
|
48
|
+
if @sent_an_int then
|
49
|
+
puts " A second INT? Ok, I get the message. Shutting down now."
|
50
|
+
exit
|
51
|
+
else
|
52
|
+
puts " Did you just send me an INT? Ugh. I'll quit for real if you do it again."
|
53
|
+
@sent_an_int = true
|
54
|
+
Kernel.sleep 1.5
|
55
|
+
run_all_specs
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
# vim:ft=ruby
|
metadata
ADDED
@@ -0,0 +1,127 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: audited_change_set
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 1
|
9
|
+
version: 0.0.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- David Chelimsky
|
13
|
+
- Brian Tatnall
|
14
|
+
- Nate Jackson
|
15
|
+
- Corey Haines
|
16
|
+
autorequire:
|
17
|
+
bindir: bin
|
18
|
+
cert_chain: []
|
19
|
+
|
20
|
+
date: 2010-05-13 00:00:00 -05:00
|
21
|
+
default_executable:
|
22
|
+
dependencies:
|
23
|
+
- !ruby/object:Gem::Dependency
|
24
|
+
name: acts_as_audited
|
25
|
+
prerelease: false
|
26
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
27
|
+
requirements:
|
28
|
+
- - ">="
|
29
|
+
- !ruby/object:Gem::Version
|
30
|
+
segments:
|
31
|
+
- 1
|
32
|
+
- 1
|
33
|
+
- 1
|
34
|
+
version: 1.1.1
|
35
|
+
type: :runtime
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: rspec
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
segments:
|
45
|
+
- 2
|
46
|
+
- 0
|
47
|
+
- 0
|
48
|
+
- beta
|
49
|
+
- 8
|
50
|
+
version: 2.0.0.beta.8
|
51
|
+
type: :development
|
52
|
+
version_requirements: *id002
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: sqlite3-ruby
|
55
|
+
prerelease: false
|
56
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
segments:
|
61
|
+
- 1
|
62
|
+
- 2
|
63
|
+
- 5
|
64
|
+
version: 1.2.5
|
65
|
+
type: :development
|
66
|
+
version_requirements: *id003
|
67
|
+
description: change_set for acts_as_audited
|
68
|
+
email: dchelimsky@gmail.com
|
69
|
+
executables: []
|
70
|
+
|
71
|
+
extensions: []
|
72
|
+
|
73
|
+
extra_rdoc_files:
|
74
|
+
- LICENSE
|
75
|
+
- README.markdown
|
76
|
+
files:
|
77
|
+
- .document
|
78
|
+
- .gitignore
|
79
|
+
- .rspec
|
80
|
+
- LICENSE
|
81
|
+
- README.markdown
|
82
|
+
- Rakefile
|
83
|
+
- VERSION
|
84
|
+
- audited_change_set.gemspec
|
85
|
+
- lib/audited_change_set.rb
|
86
|
+
- lib/audited_change_set/change.rb
|
87
|
+
- lib/audited_change_set/change_set.rb
|
88
|
+
- spec/audited_change_set/change_set_spec.rb
|
89
|
+
- spec/audited_change_set/change_spec.rb
|
90
|
+
- spec/db/schema.rb
|
91
|
+
- spec/spec_helper.rb
|
92
|
+
- specs.watchr
|
93
|
+
has_rdoc: true
|
94
|
+
homepage: http://github.com/dchelimsky/audited_change_set
|
95
|
+
licenses: []
|
96
|
+
|
97
|
+
post_install_message:
|
98
|
+
rdoc_options:
|
99
|
+
- --charset=UTF-8
|
100
|
+
require_paths:
|
101
|
+
- lib
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - ">="
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
segments:
|
107
|
+
- 0
|
108
|
+
version: "0"
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
110
|
+
requirements:
|
111
|
+
- - ">="
|
112
|
+
- !ruby/object:Gem::Version
|
113
|
+
segments:
|
114
|
+
- 0
|
115
|
+
version: "0"
|
116
|
+
requirements: []
|
117
|
+
|
118
|
+
rubyforge_project:
|
119
|
+
rubygems_version: 1.3.6
|
120
|
+
signing_key:
|
121
|
+
specification_version: 3
|
122
|
+
summary: change_set for acts_as_audited
|
123
|
+
test_files:
|
124
|
+
- spec/audited_change_set/change_set_spec.rb
|
125
|
+
- spec/audited_change_set/change_spec.rb
|
126
|
+
- spec/db/schema.rb
|
127
|
+
- spec/spec_helper.rb
|