audited_change_set 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ ## MAC OS
2
+ .DS_Store
3
+
4
+ ## TEXTMATE
5
+ *.tmproj
6
+ tmtags
7
+
8
+ ## EMACS
9
+ *~
10
+ \#*
11
+ .\#*
12
+
13
+ ## VIM
14
+ *.swp
15
+
16
+ ## PROJECT::GENERAL
17
+ coverage
18
+ rdoc
19
+ pkg
20
+
21
+ ## PROJECT::SPECIFIC
22
+ *log
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
@@ -0,0 +1,10 @@
1
+ # audited_change_set
2
+
3
+ Change sets for acts_as_audited.
4
+
5
+ More info coming soon. In the mean time, feel free to look, poke, use.
6
+
7
+ ## Copyright
8
+
9
+ Copyright (c) 2010 David Chelimsky, Brian Tatnall, Corey Haines, Nate Jackson
10
+ See LICENSE for details.
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,6 @@
1
+ require "active_record"
2
+ require "active_record/base"
3
+ require "acts_as_audited"
4
+ require "acts_as_audited/audit"
5
+ require "audited_change_set/change"
6
+ require "audited_change_set/change_set"
@@ -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
@@ -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