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