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 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