changeling 0.0.3 → 0.0.4

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/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
- def records_for(object, length = nil)
46
+ # TODO: Refactor me! More specs!
47
+ def records_for(object, length = nil, field = nil)
45
48
  self.tire.index.refresh
46
- results = self.search do
47
- query do
48
- filtered do
49
- query { all }
50
- filter :terms, :klass => [Logling.klassify(object).to_s.underscore]
51
- filter :terms, :oid => [object.id.to_s]
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
- sort { by :modified_at, "desc" }
56
- end.results
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
- _run_save_callbacks {}
136
+ unless self.modifications.empty?
137
+ _run_save_callbacks {}
138
+ end
116
139
  end
117
140
  end
118
141
  end
@@ -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
@@ -5,7 +5,7 @@ module Changeling
5
5
  end
6
6
 
7
7
  def save_logling
8
- if self.changes
8
+ if self.changes && !self.changes.empty?
9
9
  logling = Changeling::Models::Logling.create(self)
10
10
  end
11
11
  end
@@ -1,3 +1,3 @@
1
1
  module Changeling
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
@@ -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.save!
16
+ @object.run_after_callbacks(:create)
21
17
  end
22
18
 
23
- it "should create a logling with the changed attributes of an object when it is saved" do
24
- # Persist object to DB so we can update it.
25
- @object.save!
26
-
27
- @klass.should_receive(:create).with(@object)
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
- @changes.each_pair do |k, v|
30
- @object.send("#{k}=", v[1])
31
- end
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
- @object.save!
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.3
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-11-30 00:00:00.000000000 Z
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: &70333793166600 !ruby/object:Gem::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: *70333793166600
24
+ version_requirements: *70120925957500
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: activemodel
27
- requirement: &70333793164960 !ruby/object:Gem::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: *70333793164960
35
+ version_requirements: *70120925956900
36
36
  - !ruby/object:Gem::Dependency
37
37
  name: mongoid
38
- requirement: &70333793164040 !ruby/object:Gem::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: *70333793164040
46
+ version_requirements: *70120925956080
47
47
  - !ruby/object:Gem::Dependency
48
48
  name: activerecord
49
- requirement: &70333793163020 !ruby/object:Gem::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: *70333793163020
57
+ version_requirements: *70120925955460
58
58
  - !ruby/object:Gem::Dependency
59
59
  name: debugger
60
- requirement: &70333793178260 !ruby/object:Gem::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: *70333793178260
68
+ version_requirements: *70120925954960
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: rake
71
- requirement: &70333793176620 !ruby/object:Gem::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: *70333793176620
79
+ version_requirements: *70120925954180
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: rspec
82
- requirement: &70333793174580 !ruby/object:Gem::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: *70333793174580
90
+ version_requirements: *70120925953640
91
91
  - !ruby/object:Gem::Dependency
92
92
  name: bson_ext
93
- requirement: &70333793173940 !ruby/object:Gem::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: *70333793173940
101
+ version_requirements: *70120925953060
102
102
  - !ruby/object:Gem::Dependency
103
103
  name: database_cleaner
104
- requirement: &70333793173400 !ruby/object:Gem::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: *70333793173400
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