changeling 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +12 -1
- data/lib/changeling/models/logling.rb +38 -15
- data/lib/changeling/probeling.rb +4 -0
- data/lib/changeling/trackling.rb +1 -1
- data/lib/changeling/version.rb +1 -1
- data/spec/lib/changeling/models/logling_spec.rb +31 -9
- data/spec/lib/changeling/probeling_spec.rb +86 -0
- data/spec/lib/changeling/trackling_spec.rb +15 -14
- metadata +26 -20
data/README.md
CHANGED
@@ -93,6 +93,17 @@ Access all of an objects history:
|
|
93
93
|
@post.all_history
|
94
94
|
```
|
95
95
|
|
96
|
+
Access all of an objects history where a specific field was changed:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
@post.history_for_field(:title)
|
100
|
+
# Or if you prefer stringified fields:
|
101
|
+
@post.history_for_field('title')
|
102
|
+
|
103
|
+
# You can also pass in a number to limit your results
|
104
|
+
@post.history_for_field(:title, 10)
|
105
|
+
```
|
106
|
+
|
96
107
|
Properties of Loglings (history objects):
|
97
108
|
|
98
109
|
```ruby
|
@@ -102,7 +113,7 @@ log.klass # class of the object that the Logling is tracking.
|
|
102
113
|
=> Post
|
103
114
|
|
104
115
|
log.oid # the ID of the object that the Logling is tracking.
|
105
|
-
# Note: integer type IDs will be integers. Non-integer types (such as Mongo's IDs) will be represented as a string
|
116
|
+
# Note: integer type IDs will be integers. Non-integer types (such as Mongo's IDs) will be represented as a string.
|
106
117
|
=> 1
|
107
118
|
|
108
119
|
log.before # what the before state of the object was.
|
@@ -2,7 +2,7 @@ module Changeling
|
|
2
2
|
module Models
|
3
3
|
class Logling
|
4
4
|
extend ActiveModel::Naming
|
5
|
-
attr_accessor :klass, :oid, :modifications, :before, :after, :modified_at
|
5
|
+
attr_accessor :klass, :oid, :modifications, :before, :after, :modified_at, :modified_fields
|
6
6
|
|
7
7
|
include Tire::Model::Search
|
8
8
|
include Tire::Model::Persistence
|
@@ -10,12 +10,14 @@ module Changeling
|
|
10
10
|
property :klass, :type => 'string'
|
11
11
|
property :oid, :type => 'string'
|
12
12
|
property :modifications, :type => 'string'
|
13
|
+
property :modified_fields, :type => 'string', :analyzer => 'keyword'
|
13
14
|
property :modified_at, :type => 'date'
|
14
15
|
|
15
16
|
mapping do
|
16
17
|
indexes :klass, :type => "string"
|
17
18
|
indexes :oid, :type => "string"
|
18
19
|
indexes :modifications, :type => 'string'
|
20
|
+
indexes :modified_fields, :type => 'string', :analyzer => 'keyword'
|
19
21
|
indexes :modified_at, :type => 'date'
|
20
22
|
end
|
21
23
|
|
@@ -41,19 +43,36 @@ module Changeling
|
|
41
43
|
object.class
|
42
44
|
end
|
43
45
|
|
44
|
-
|
46
|
+
# TODO: Refactor me! More specs!
|
47
|
+
def records_for(object, length = nil, field = nil)
|
45
48
|
self.tire.index.refresh
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
49
|
+
|
50
|
+
if field
|
51
|
+
results = self.search do
|
52
|
+
query do
|
53
|
+
filtered do
|
54
|
+
query { all }
|
55
|
+
filter :terms, :klass => [Logling.klassify(object).to_s.underscore]
|
56
|
+
filter :terms, :oid => [object.id.to_s]
|
57
|
+
filter :terms, :modified_fields => [field]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
sort { by :modified_at, "desc" }
|
62
|
+
end.results
|
63
|
+
else
|
64
|
+
results = self.search do
|
65
|
+
query do
|
66
|
+
filtered do
|
67
|
+
query { all }
|
68
|
+
filter :terms, :klass => [Logling.klassify(object).to_s.underscore]
|
69
|
+
filter :terms, :oid => [object.id.to_s]
|
70
|
+
end
|
52
71
|
end
|
53
|
-
end
|
54
72
|
|
55
|
-
|
56
|
-
|
73
|
+
sort { by :modified_at, "desc" }
|
74
|
+
end.results
|
75
|
+
end
|
57
76
|
|
58
77
|
if length
|
59
78
|
results.take(length)
|
@@ -68,7 +87,8 @@ module Changeling
|
|
68
87
|
:klass => self.klass.to_s.underscore,
|
69
88
|
:oid => self.oid.to_s,
|
70
89
|
:modifications => self.modifications.to_json,
|
71
|
-
:modified_at => self.modified_at
|
90
|
+
:modified_at => self.modified_at,
|
91
|
+
:modified_fields => self.modified_fields
|
72
92
|
}.to_json
|
73
93
|
end
|
74
94
|
|
@@ -87,19 +107,20 @@ module Changeling
|
|
87
107
|
self.klass = object['klass'].camelize.constantize
|
88
108
|
self.oid = object['oid'].to_i.to_s == object['oid'] ? object['oid'].to_i : object['oid']
|
89
109
|
self.modifications = changes
|
110
|
+
self.modified_fields = self.modifications.keys
|
90
111
|
|
91
112
|
self.before, self.after = Logling.parse_changes(changes)
|
92
113
|
|
93
114
|
self.modified_at = DateTime.parse(object['modified_at'])
|
94
115
|
else
|
95
|
-
changes = object.changes
|
96
|
-
|
116
|
+
changes = object.changes.reject { |k, v| v.nil? }
|
97
117
|
# Remove updated_at field.
|
98
118
|
changes.delete("updated_at")
|
99
119
|
|
100
120
|
self.klass = Logling.klassify(object)
|
101
121
|
self.oid = object.id
|
102
122
|
self.modifications = changes
|
123
|
+
self.modified_fields = self.modifications.keys
|
103
124
|
|
104
125
|
self.before, self.after = Logling.parse_changes(changes)
|
105
126
|
|
@@ -112,7 +133,9 @@ module Changeling
|
|
112
133
|
end
|
113
134
|
|
114
135
|
def save
|
115
|
-
|
136
|
+
unless self.modifications.empty?
|
137
|
+
_run_save_callbacks {}
|
138
|
+
end
|
116
139
|
end
|
117
140
|
end
|
118
141
|
end
|
data/lib/changeling/probeling.rb
CHANGED
@@ -7,5 +7,9 @@ module Changeling
|
|
7
7
|
def history(records = 10)
|
8
8
|
Changeling::Models::Logling.records_for(self, records.to_i)
|
9
9
|
end
|
10
|
+
|
11
|
+
def history_for_field(field_name, records = nil)
|
12
|
+
Changeling::Models::Logling.records_for(self, records ? records.to_i : nil, field_name.to_s)
|
13
|
+
end
|
10
14
|
end
|
11
15
|
end
|
data/lib/changeling/trackling.rb
CHANGED
data/lib/changeling/version.rb
CHANGED
@@ -38,15 +38,7 @@ describe Changeling::Models::Logling do
|
|
38
38
|
"klass"=>"BlogPost",
|
39
39
|
"oid"=>"50b8355f7a93d04908000001",
|
40
40
|
"modifications"=>"{\"public\":[true,false]}",
|
41
|
-
"modified_at"=>"2012-11-29T20:26:07-08:00"
|
42
|
-
"id"=>"UU2YGr1WSbilNUlyM1ja8g",
|
43
|
-
"_score"=>nil,
|
44
|
-
"_type"=>"changeling/models/logling",
|
45
|
-
"_index"=>"changeling_test_changeling_models_loglings",
|
46
|
-
"_version"=>1,
|
47
|
-
"sort"=>[1354249567000],
|
48
|
-
"highlight"=>nil,
|
49
|
-
"_explanation"=>nil
|
41
|
+
"modified_at"=>"2012-11-29T20:26:07-08:00"
|
50
42
|
}
|
51
43
|
|
52
44
|
@logling = @klass.new(@object)
|
@@ -72,6 +64,10 @@ describe Changeling::Models::Logling do
|
|
72
64
|
@logling.modifications.should == @changes
|
73
65
|
end
|
74
66
|
|
67
|
+
it "should set the modified_fields as the keys of the modifications" do
|
68
|
+
@logling.modified_fields.should == @changes.keys
|
69
|
+
end
|
70
|
+
|
75
71
|
it "should set before and after based on .parse_changes" do
|
76
72
|
@logling.before.should == @before
|
77
73
|
@logling.after.should == @after
|
@@ -104,6 +100,22 @@ describe Changeling::Models::Logling do
|
|
104
100
|
@logling.after.should == @after
|
105
101
|
end
|
106
102
|
|
103
|
+
it "should set the modified_fields as the keys of the modifications" do
|
104
|
+
@logling.modified_fields.should == @changes.keys
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should ignore changes that are nil" do
|
108
|
+
changes = {}
|
109
|
+
|
110
|
+
@changes.keys.each do |key|
|
111
|
+
changes[key] = nil
|
112
|
+
end
|
113
|
+
|
114
|
+
@object.stub(:changes).and_return(changes)
|
115
|
+
|
116
|
+
@klass.new(@object).modifications.should be_empty
|
117
|
+
end
|
118
|
+
|
107
119
|
it "should set modified_at to the object's time of update if the object responds to the updated_at method" do
|
108
120
|
@object.should_receive(:respond_to?).with(:updated_at).and_return(true)
|
109
121
|
|
@@ -154,6 +166,7 @@ describe Changeling::Models::Logling do
|
|
154
166
|
end
|
155
167
|
end
|
156
168
|
|
169
|
+
# TODO: More specs!
|
157
170
|
describe ".records_for" do
|
158
171
|
before(:each) do
|
159
172
|
@index = @klass.tire.index
|
@@ -204,6 +217,10 @@ describe Changeling::Models::Logling do
|
|
204
217
|
@logling.should_receive(:modified_at)
|
205
218
|
end
|
206
219
|
|
220
|
+
it "should include an array of the object's modified fields" do
|
221
|
+
@logling.should_receive(:modified_fields)
|
222
|
+
end
|
223
|
+
|
207
224
|
after(:each) do
|
208
225
|
@logling.to_indexed_json
|
209
226
|
end
|
@@ -240,6 +257,11 @@ describe Changeling::Models::Logling do
|
|
240
257
|
@logling.should_receive(:_run_save_callbacks)
|
241
258
|
end
|
242
259
|
|
260
|
+
it "should not update the index if there are no changes" do
|
261
|
+
@logling.stub(:modifications).and_return({})
|
262
|
+
@logling.should_not_receive(:_run_save_callbacks)
|
263
|
+
end
|
264
|
+
|
243
265
|
after(:each) do
|
244
266
|
@logling.save
|
245
267
|
end
|
@@ -57,4 +57,90 @@ describe Changeling::Probeling do
|
|
57
57
|
@object.history(20).count.should == 4
|
58
58
|
end
|
59
59
|
end
|
60
|
+
|
61
|
+
describe ".history_for_field" do
|
62
|
+
it "should query Logling with it's class name, and it's own ID, and a field name" do
|
63
|
+
@klass.should_receive(:records_for).with(@object, nil, "field")
|
64
|
+
@object.history_for_field("field")
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should be able to take a length to specify amount of loglings to return" do
|
68
|
+
@klass.should_receive(:records_for).with(@object, 5, "field")
|
69
|
+
@object.history_for_field("field", 5)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should handle non-integer arguments for length" do
|
73
|
+
@klass.should_receive(:records_for).with(@object, 5, "field")
|
74
|
+
@object.history_for_field("field", "5")
|
75
|
+
end
|
76
|
+
|
77
|
+
it "should handle symbol arguments for field" do
|
78
|
+
@klass.should_receive(:records_for).with(@object, nil, "field")
|
79
|
+
@object.history_for_field(:field)
|
80
|
+
end
|
81
|
+
|
82
|
+
it "should only return loglings where the specified field has changed" do
|
83
|
+
models.values.each do |value|
|
84
|
+
value[:changes].keys.each do |field|
|
85
|
+
@object.history_for_field(field).each do |logling|
|
86
|
+
logling.modifications.keys.should include(field)
|
87
|
+
end
|
88
|
+
|
89
|
+
@object.history_for_field(field).count.should == 2
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
it "should be able to find loglings even if there was more than one change logged in the logling" do
|
95
|
+
models.each_pair do |model, args|
|
96
|
+
@object = model.new(args[:options])
|
97
|
+
@object.save!
|
98
|
+
|
99
|
+
args[:changes].each do |field, values|
|
100
|
+
values.reverse.each do |value|
|
101
|
+
@object.send("#{field}=", value)
|
102
|
+
@object.save!
|
103
|
+
# Sleep to guarantee saves are not within the same second.
|
104
|
+
sleep 1
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
# This should make 2 changes at the same time then save it
|
109
|
+
args[:changes].each do |field, values|
|
110
|
+
@object.send("#{field}=", values.last)
|
111
|
+
end
|
112
|
+
|
113
|
+
@object.save!
|
114
|
+
end
|
115
|
+
|
116
|
+
models.values.each do |value|
|
117
|
+
value[:changes].keys.each do |field|
|
118
|
+
# Reverse chronological order: first object is the last inserted, which should have 2 changes.
|
119
|
+
# The last object should have 1 change.
|
120
|
+
@object.history_for_field(field).last.modifications.keys.count.should == 1
|
121
|
+
@object.history_for_field(field).first.modifications.keys.count.should == 2
|
122
|
+
|
123
|
+
@object.history_for_field(field).count.should == 3
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should only return the specified amount of loglings" do
|
129
|
+
models.values.each do |value|
|
130
|
+
value[:changes].keys.each do |field|
|
131
|
+
@object.history_for_field(field).count.should == 2
|
132
|
+
@object.history_for_field(field, 1).count.should == 1
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should not error out if the record count desired is more than the total number of loglings" do
|
138
|
+
models.values.each do |value|
|
139
|
+
value[:changes].keys.each do |field|
|
140
|
+
@object.history_for_field(field).count.should == 2
|
141
|
+
@object.history_for_field(field, 10).count.should == 2
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
60
146
|
end
|
@@ -11,26 +11,27 @@ describe Changeling::Trackling do
|
|
11
11
|
end
|
12
12
|
|
13
13
|
describe "callbacks" do
|
14
|
-
before(:each) do
|
15
|
-
@changes = args[:changes]
|
16
|
-
end
|
17
|
-
|
18
14
|
it "should not create a logling when doing the initial save of a new object" do
|
19
15
|
@klass.should_not_receive(:create)
|
20
|
-
@object.
|
16
|
+
@object.run_after_callbacks(:create)
|
21
17
|
end
|
22
18
|
|
23
|
-
it "should create a logling
|
24
|
-
|
25
|
-
@object.
|
26
|
-
|
27
|
-
|
19
|
+
it "should create a logling when updating an object and changes are made" do
|
20
|
+
@klass.should_receive(:create)
|
21
|
+
@object.stub(:changes).and_return({ :field => 'value' })
|
22
|
+
@object.run_after_callbacks(:update)
|
23
|
+
end
|
28
24
|
|
29
|
-
|
30
|
-
|
31
|
-
|
25
|
+
it "should create a logling when updating an object and changes are empty" do
|
26
|
+
@klass.should_not_receive(:create)
|
27
|
+
@object.stub(:changes).and_return({})
|
28
|
+
@object.run_after_callbacks(:update)
|
29
|
+
end
|
32
30
|
|
33
|
-
|
31
|
+
it "should not create a logling when updating an object and no changes have been made" do
|
32
|
+
@klass.should_not_receive(:create)
|
33
|
+
@object.stub(:changes).and_return(nil)
|
34
|
+
@object.run_after_callbacks(:update)
|
34
35
|
end
|
35
36
|
end
|
36
37
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: changeling
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,11 +9,11 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2012-
|
12
|
+
date: 2012-12-05 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: tire
|
16
|
-
requirement: &
|
16
|
+
requirement: &70120925957500 !ruby/object:Gem::Requirement
|
17
17
|
none: false
|
18
18
|
requirements:
|
19
19
|
- - ! '>='
|
@@ -21,10 +21,10 @@ dependencies:
|
|
21
21
|
version: '0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
|
-
version_requirements: *
|
24
|
+
version_requirements: *70120925957500
|
25
25
|
- !ruby/object:Gem::Dependency
|
26
26
|
name: activemodel
|
27
|
-
requirement: &
|
27
|
+
requirement: &70120925956900 !ruby/object:Gem::Requirement
|
28
28
|
none: false
|
29
29
|
requirements:
|
30
30
|
- - ! '>='
|
@@ -32,10 +32,10 @@ dependencies:
|
|
32
32
|
version: '0'
|
33
33
|
type: :runtime
|
34
34
|
prerelease: false
|
35
|
-
version_requirements: *
|
35
|
+
version_requirements: *70120925956900
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: mongoid
|
38
|
-
requirement: &
|
38
|
+
requirement: &70120925956080 !ruby/object:Gem::Requirement
|
39
39
|
none: false
|
40
40
|
requirements:
|
41
41
|
- - =
|
@@ -43,10 +43,10 @@ dependencies:
|
|
43
43
|
version: 3.0.3
|
44
44
|
type: :development
|
45
45
|
prerelease: false
|
46
|
-
version_requirements: *
|
46
|
+
version_requirements: *70120925956080
|
47
47
|
- !ruby/object:Gem::Dependency
|
48
48
|
name: activerecord
|
49
|
-
requirement: &
|
49
|
+
requirement: &70120925955460 !ruby/object:Gem::Requirement
|
50
50
|
none: false
|
51
51
|
requirements:
|
52
52
|
- - =
|
@@ -54,10 +54,10 @@ dependencies:
|
|
54
54
|
version: 3.2.7
|
55
55
|
type: :development
|
56
56
|
prerelease: false
|
57
|
-
version_requirements: *
|
57
|
+
version_requirements: *70120925955460
|
58
58
|
- !ruby/object:Gem::Dependency
|
59
59
|
name: debugger
|
60
|
-
requirement: &
|
60
|
+
requirement: &70120925954960 !ruby/object:Gem::Requirement
|
61
61
|
none: false
|
62
62
|
requirements:
|
63
63
|
- - ! '>='
|
@@ -65,10 +65,10 @@ dependencies:
|
|
65
65
|
version: '0'
|
66
66
|
type: :development
|
67
67
|
prerelease: false
|
68
|
-
version_requirements: *
|
68
|
+
version_requirements: *70120925954960
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
70
|
name: rake
|
71
|
-
requirement: &
|
71
|
+
requirement: &70120925954180 !ruby/object:Gem::Requirement
|
72
72
|
none: false
|
73
73
|
requirements:
|
74
74
|
- - ! '>='
|
@@ -76,10 +76,10 @@ dependencies:
|
|
76
76
|
version: '0'
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
|
-
version_requirements: *
|
79
|
+
version_requirements: *70120925954180
|
80
80
|
- !ruby/object:Gem::Dependency
|
81
81
|
name: rspec
|
82
|
-
requirement: &
|
82
|
+
requirement: &70120925953640 !ruby/object:Gem::Requirement
|
83
83
|
none: false
|
84
84
|
requirements:
|
85
85
|
- - ! '>='
|
@@ -87,10 +87,10 @@ dependencies:
|
|
87
87
|
version: '0'
|
88
88
|
type: :development
|
89
89
|
prerelease: false
|
90
|
-
version_requirements: *
|
90
|
+
version_requirements: *70120925953640
|
91
91
|
- !ruby/object:Gem::Dependency
|
92
92
|
name: bson_ext
|
93
|
-
requirement: &
|
93
|
+
requirement: &70120925953060 !ruby/object:Gem::Requirement
|
94
94
|
none: false
|
95
95
|
requirements:
|
96
96
|
- - ! '>='
|
@@ -98,10 +98,10 @@ dependencies:
|
|
98
98
|
version: '0'
|
99
99
|
type: :development
|
100
100
|
prerelease: false
|
101
|
-
version_requirements: *
|
101
|
+
version_requirements: *70120925953060
|
102
102
|
- !ruby/object:Gem::Dependency
|
103
103
|
name: database_cleaner
|
104
|
-
requirement: &
|
104
|
+
requirement: &70120925952440 !ruby/object:Gem::Requirement
|
105
105
|
none: false
|
106
106
|
requirements:
|
107
107
|
- - ! '>='
|
@@ -109,7 +109,7 @@ dependencies:
|
|
109
109
|
version: '0'
|
110
110
|
type: :development
|
111
111
|
prerelease: false
|
112
|
-
version_requirements: *
|
112
|
+
version_requirements: *70120925952440
|
113
113
|
description: A simple, yet flexible solution to tracking changes made to objects in
|
114
114
|
your database.
|
115
115
|
email:
|
@@ -151,12 +151,18 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
151
151
|
- - ! '>='
|
152
152
|
- !ruby/object:Gem::Version
|
153
153
|
version: '0'
|
154
|
+
segments:
|
155
|
+
- 0
|
156
|
+
hash: -473586463949581456
|
154
157
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
155
158
|
none: false
|
156
159
|
requirements:
|
157
160
|
- - ! '>='
|
158
161
|
- !ruby/object:Gem::Version
|
159
162
|
version: '0'
|
163
|
+
segments:
|
164
|
+
- 0
|
165
|
+
hash: -473586463949581456
|
160
166
|
requirements: []
|
161
167
|
rubyforge_project:
|
162
168
|
rubygems_version: 1.8.15
|