couch_potato 1.1.4 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f8f5c231fcf0ed9657baf832752652a1b04ec331
4
- data.tar.gz: 1918a304d3b741fe970677e9ce4da467df692f11
3
+ metadata.gz: 2771fb2d4e5b08e8ce5a50afa1d6747a54682807
4
+ data.tar.gz: 1009498ac99d3d0b5e2bba6a7a5a4624ff5759be
5
5
  SHA512:
6
- metadata.gz: a10c00c94ef2642ff0c458a223e56a14e913d7202c07555e308266600ecfffaffec1f2595b09460da30ee22acc4cac132451b4292a6282bdb65f1468e994f5b6
7
- data.tar.gz: c9714c1620976ea921df67afe9e59058415c4e16006e71203b012eade16c09bcfecb1541c28960a7703deafee6135c401bddec7298c753734fa0d92dcd287a0c
6
+ metadata.gz: 665f44a87cc9d5f96b1c0ba3dc416849e6fc1b02888a96658e94e7a295601aa4e81d81423df6a8362e87c4feb41f1a738fa1bd08d18fe716a7d655b03714eff7
7
+ data.tar.gz: c53f3dba7f3fd8208182ecfc54a8e6d747af55d316ea2097df26762ab70e3544676575e36594922caf1b85033c9b6631e79d2876105b37c901a100f3ce6608f2
data/.travis.yml CHANGED
@@ -1,8 +1,7 @@
1
1
  rvm:
2
2
  - 1.9.3
3
3
  - 2.0.0
4
- - 2.1.0
5
- - 2.1.1
4
+ - 2.1.2
6
5
  - jruby-19mode
7
6
  - rbx-2.1.1
8
7
  gemfile:
data/CHANGES.md CHANGED
@@ -1,5 +1,10 @@
1
1
  ## Changes
2
2
 
3
+ ### 1.2.0
4
+
5
+ * adds optional deep dirty tracking (andymorris)
6
+ * fixes an exception when deleting an already deleted document (Alexander Lang)
7
+
3
8
  ### 1.1.4
4
9
 
5
10
  * Removes the dependency to json/add/core (cstettner)
data/README.md CHANGED
@@ -224,6 +224,49 @@ You can also force a dirty state:
224
224
  user.name_changed? # => false
225
225
  CouchPotato.database.save_document user # does nothing as no attributes are dirty
226
226
 
227
+ #### Optional Deep Dirty Tracking
228
+
229
+ In addition to standard dirty tracking, you can opt-in to more advanced dirty tracking for deeply structured documents by including the ```CouchPotato::DeepDirtyAttributes``` module in your models. This provides two benefits:
230
+
231
+ 1. Dirty checking for array and embedded document properties is more reliable, such that modifying elements in an array (by any means) or changing a property of an embedded document will make the root document be ```changed?```. With standard dirty checking, the ```#{property}=``` method must be called on the root document for it to be ```changed?```.
232
+ 2. It gives more useful and detailed change tracking for embedded documents, arrays of simple values, and arrays of embedded documents.
233
+
234
+ The ```#{property}_changed?``` and ```#{property}_was``` methods work the same as basic dirty checking, and the ```_was``` values are always deep clones of the original/previous value. The ```#{property}_change``` and ```changes``` methods differ from basic dirty checking for embedded documents and arrays, giving richer details of the changes instead of just the previous and current values. This makes generating detailed, human friendly audit trails of documents easy.
235
+
236
+ Tracking changes in embedded documents gives easy access to the changes in that document:
237
+
238
+ book = Book.new(:cover => Cover.new(:color => "red"))
239
+ book.cover.color = "blue"
240
+ book.cover_changed? # => true
241
+ book.cover_was # => <deep clone of original state of book.cover>
242
+ book.cover_change # => [<deep clone of original state of book.cover>, {:color => ["red", "blue"]}]
243
+
244
+ Tracking changes in arrays of simple properties gives easy access to added and removed items:
245
+
246
+ book = Book.new(:authors => ["Sarah", "Jane"])
247
+ book.authors.delete "Jane"
248
+ book.authors << "Sue"
249
+ book.authors_changed? # => true
250
+ book.authors_was # => ["Sarah", "Jane"]
251
+ book.authors_change # => [["Sarah", "Jane"], {:added => ["Sue"], :removed => ["Jane"]}]
252
+
253
+ Tracking changes in an array of embedded documents also gives changed items:
254
+
255
+ book = Book.new(:pages => [Page.new(:number => 1), Page.new(:number => 2)]
256
+ book.pages[0].title = "New title"
257
+ book.pages.delete_at 1
258
+ book.pages << Page.new(:number => 3)
259
+ book.pages_changed? # => true
260
+ book.pages_was # => <deep clone of original pages array>
261
+ book.pages_change[0] # => <deep clone of original pages array>
262
+ book.pages_change[1] # => {:added => [<page 3>], :removed => [<page 2>], :changed => [[<deep clone of original page 1>, {:title => [nil, "New title"]}]]}
263
+
264
+ For change tracking in nested documents and document arrays to work, the embedded documents **must** have unique ```_id``` values. This can be accomplished easily in your embedded CouchPotato models by overriding ```initialize```:
265
+
266
+ def initialize(*args)
267
+ self._id = SecureRandom.uuid
268
+ super
269
+ end
227
270
 
228
271
  #### Object validations
229
272
 
@@ -102,8 +102,7 @@ module CouchPotato
102
102
  begin
103
103
  destroy_document_without_conflict_handling document
104
104
  rescue RestClient::Conflict
105
- document = document.reload
106
- retry
105
+ retry if document = document.reload
107
106
  end
108
107
  end
109
108
  alias_method :destroy, :destroy_document
@@ -0,0 +1,180 @@
1
+ module CouchPotato
2
+ module Persistence
3
+ module DeepDirtyAttributes
4
+
5
+ def self.included(base) #:nodoc:
6
+ base.send :extend, ClassMethods
7
+ end
8
+
9
+ def initialize(*args, &block)
10
+ super(*args, &block)
11
+ reset_deep_dirty_attributes
12
+ end
13
+
14
+ def changed?
15
+ super || self.class.deep_tracked_properties.any? do |property|
16
+ send("#{property.name}_changed?")
17
+ end
18
+ end
19
+
20
+ def changes
21
+ changes = super
22
+ if @original_deep_values
23
+ self.class.deep_tracked_properties.each do |property|
24
+ if send("#{property.name}_changed?")
25
+ changes[property.name] = send("#{property.name}_change")
26
+ else
27
+ changes.delete property.name
28
+ end
29
+ end
30
+ end
31
+ changes
32
+ end
33
+
34
+ private
35
+
36
+ def reset_dirty_attributes
37
+ super
38
+ reset_deep_dirty_attributes
39
+ end
40
+
41
+ def reset_deep_dirty_attributes
42
+ @original_deep_values = HashWithIndifferentAccess.new
43
+ self.class.deep_tracked_properties.each do |property|
44
+ value = send(property.name)
45
+ if value
46
+ if doc?(value)
47
+ value.send(:reset_dirty_attributes)
48
+ elsif value.respond_to?(:each)
49
+ value.each do |item|
50
+ item.send(:reset_dirty_attributes) if doc?(item)
51
+ end
52
+ end
53
+ end
54
+ @original_deep_values[property.name] = clone_attribute(value)
55
+ end
56
+ end
57
+
58
+ def doc_changed?(name)
59
+ old, new = @original_deep_values[name], send(name)
60
+ if old.nil? && new.nil?
61
+ false
62
+ elsif old.nil? ^ new.nil?
63
+ true
64
+ else
65
+ (doc?(new) && new.changed?) || old.to_hash != new.to_hash
66
+ end
67
+ end
68
+
69
+ def simple_array_changed?(name)
70
+ @original_deep_values[name] != send(name)
71
+ end
72
+
73
+ def doc_array_changed?(name)
74
+ old, new = @original_deep_values[name], send(name)
75
+ if old.blank? && new.blank?
76
+ false
77
+ elsif old.blank? ^ new.blank?
78
+ true
79
+ else
80
+ old != new || old.map(&:to_hash) != new.map(&:to_hash)
81
+ end
82
+ end
83
+
84
+ def doc?(value)
85
+ value && value.respond_to?(:changed?)
86
+ end
87
+
88
+ def doc_change(name)
89
+ old, new = @original_deep_values[name], send(name)
90
+ if !old || !new || old != new
91
+ [old, new]
92
+ else
93
+ [old, doc_diff(old, new)]
94
+ end
95
+ end
96
+
97
+ def doc_diff(old, new)
98
+ clone = clone_attribute(old)
99
+ clone.attributes = new.attributes
100
+ clone.changes
101
+ end
102
+
103
+ def simple_array_change(name)
104
+ value = send(name) || []
105
+ old = @original_deep_values[name] || []
106
+ changes = HashWithIndifferentAccess.new :added => value - old, :removed => old - value
107
+ [old, changes]
108
+ end
109
+
110
+ def doc_array_change(name)
111
+ old = @original_deep_values[name] || []
112
+ value = send(name)
113
+
114
+ added = value - old
115
+ removed = old - value
116
+ changed = value.map do |value_item|
117
+ old_item = old.detect {|i| i == value_item}
118
+ if old_item
119
+ changes = doc_diff(old_item, value_item)
120
+ unless changes.empty?
121
+ [old_item, changes]
122
+ end
123
+ end
124
+ end.compact
125
+ changes = HashWithIndifferentAccess.new(:added => added, :removed => removed, :changed => changed)
126
+
127
+ [old, changes]
128
+ end
129
+
130
+ module ClassMethods #:nodoc:
131
+ def property(name, options = {})
132
+ super
133
+ if deep_trackable_type?(options[:type])
134
+ index = properties.find_index {|p| p.name == name}
135
+ properties.list[index] = DeepTrackedProperty.new(self, name, options)
136
+ end
137
+ remove_attribute_dirty_methods_from_activesupport_module
138
+ end
139
+
140
+ def remove_attribute_dirty_methods_from_activesupport_module
141
+ methods = deep_tracked_property_names.flat_map {|n| ["#{n}_changed?", "#{n}_change", "#{n}_was"]}.map(&:to_sym)
142
+ activesupport_modules = ancestors.select {|m| m.name.nil? && (methods - m.instance_methods).empty?}
143
+ activesupport_modules.each do |mod|
144
+ methods.each do |method|
145
+ mod.send :remove_method, method if mod.instance_methods.include?(method)
146
+ end
147
+ end
148
+ end
149
+
150
+ def doc_array_type?(type)
151
+ type && type.is_a?(Array) && doc_type?(type[0])
152
+ end
153
+
154
+ def simple_array_type?(type)
155
+ type && type.is_a?(Array) && !doc_type?(type[0])
156
+ end
157
+
158
+ def doc_type?(type)
159
+ type &&
160
+ type.respond_to?(:included_modules) &&
161
+ type.included_modules.include?(DirtyAttributes)
162
+ end
163
+
164
+ def deep_trackable_type?(type)
165
+ type && type.is_a?(Array) || doc_type?(type)
166
+ end
167
+
168
+ def deep_tracked_properties
169
+ properties.select do |property|
170
+ property.is_a? DeepTrackedProperty
171
+ end
172
+ end
173
+
174
+ def deep_tracked_property_names
175
+ deep_tracked_properties.map(&:name)
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
@@ -0,0 +1,56 @@
1
+ module CouchPotato
2
+ module Persistence
3
+ class DeepTrackedProperty < SimpleProperty
4
+
5
+ def initialize(owner_clazz, name, options = {})
6
+ super
7
+ define_accessors deep_accessors_module_for(owner_clazz), name, options
8
+ end
9
+
10
+ private
11
+
12
+ def deep_accessors_module_for(clazz)
13
+ module_for(clazz, "DeepAccessorMethods")
14
+ end
15
+
16
+ def define_accessors(base, name, options)
17
+ base.class_eval do
18
+ define_method "#{name}=" do |value|
19
+ typecasted_value = type_caster.cast(value, options[:type])
20
+ self.instance_variable_set("@#{name}", typecasted_value)
21
+ end
22
+
23
+ define_method "#{name}_changed?" do
24
+ if self.class.doc_array_type?(options[:type])
25
+ doc_array_changed?(name)
26
+ elsif self.class.simple_array_type?(options[:type])
27
+ simple_array_changed?(name)
28
+ elsif self.class.doc_type?(options[:type])
29
+ doc_changed?(name)
30
+ else
31
+ super()
32
+ end
33
+ end
34
+
35
+ define_method "#{name}_was" do
36
+ @original_deep_values[name] if send("#{name}_changed?")
37
+ end
38
+
39
+ define_method "#{name}_change" do
40
+ if !send("#{name}_changed?")
41
+ nil
42
+ elsif self.class.doc_array_type?(options[:type])
43
+ doc_array_change(name)
44
+ elsif self.class.simple_array_type?(options[:type])
45
+ simple_array_change(name)
46
+ elsif self.class.doc_type?(options[:type])
47
+ doc_change(name)
48
+ else
49
+ super()
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -10,10 +10,6 @@ module CouchPotato
10
10
  end
11
11
  end
12
12
 
13
- def initialize(attributes = {})
14
- super
15
- end
16
-
17
13
  # returns true if a model has dirty attributes, i.e. their value has changed since the last save
18
14
  def dirty?
19
15
  changed? || @forced_dirty
@@ -1,4 +1,5 @@
1
1
  require File.dirname(__FILE__) + '/simple_property'
2
+ require File.dirname(__FILE__) + '/deep_tracked_property'
2
3
 
3
4
  module CouchPotato
4
5
  module Persistence
@@ -42,8 +42,8 @@ module CouchPotato
42
42
 
43
43
  private
44
44
 
45
- def accessors_module_for(clazz)
46
- module_name = "#{clazz.name.to_s.gsub('::', '__')}AccessorMethods"
45
+ def module_for(clazz, module_name)
46
+ module_name = "#{clazz.name.to_s.gsub('::', '__')}#{module_name}"
47
47
  unless clazz.const_defined?(module_name)
48
48
  accessors_module = clazz.const_set(module_name, Module.new)
49
49
  clazz.send(:include, accessors_module)
@@ -51,6 +51,10 @@ module CouchPotato
51
51
  clazz.const_get(module_name)
52
52
  end
53
53
 
54
+ def accessors_module_for(clazz)
55
+ module_for(clazz, "AccessorMethods")
56
+ end
57
+
54
58
  def define_accessors(base, name, options)
55
59
  base.class_eval do
56
60
  define_method "#{name}" do
@@ -6,6 +6,7 @@ require File.dirname(__FILE__) + '/persistence/magic_timestamps'
6
6
  require File.dirname(__FILE__) + '/persistence/callbacks'
7
7
  require File.dirname(__FILE__) + '/persistence/json'
8
8
  require File.dirname(__FILE__) + '/persistence/dirty_attributes'
9
+ require File.dirname(__FILE__) + '/persistence/deep_dirty_attributes'
9
10
  require File.dirname(__FILE__) + '/persistence/ghost_attributes'
10
11
  require File.dirname(__FILE__) + '/persistence/attachments'
11
12
  require File.dirname(__FILE__) + '/persistence/type_caster'
@@ -40,7 +40,7 @@ module CouchPotato::RSpec
40
40
 
41
41
  module StubDb
42
42
  def stub_db(options = {})
43
- db = stub('db', options)
43
+ db = double('db', options)
44
44
  db.extend CouchPotato::RSpec::StubView
45
45
  CouchPotato.stub(:database => db)
46
46
  db
@@ -1,3 +1,3 @@
1
1
  module CouchPotato
2
- VERSION = "1.1.4"
2
+ VERSION = "1.2.0"
3
3
  end
@@ -419,3 +419,17 @@ describe CouchPotato::Database, 'view' do
419
419
  @db.view(@spec)
420
420
  end
421
421
  end
422
+
423
+ describe CouchPotato::Database, '#destroy' do
424
+ it 'does not try to delete an already deleted document' do
425
+ couchrest_db = double(:couchrest_db)
426
+ couchrest_db.stub(:delete_doc).and_raise(RestClient::Conflict)
427
+ db = CouchPotato::Database.new couchrest_db
428
+ document = double(:document, reload: nil).as_null_object
429
+ document.stub(:run_callbacks).and_yield
430
+
431
+ expect {
432
+ db.destroy document
433
+ }.to_not raise_error
434
+ end
435
+ end
@@ -0,0 +1,434 @@
1
+ require 'spec_helper'
2
+
3
+ class Cover
4
+ include CouchPotato::Persistence
5
+
6
+ def initialize(*args)
7
+ self._id = SecureRandom.uuid
8
+ super
9
+ end
10
+
11
+ property :color
12
+ end
13
+
14
+ class Page
15
+ include CouchPotato::Persistence
16
+
17
+ def initialize(*args)
18
+ self._id = SecureRandom.uuid
19
+ super
20
+ end
21
+
22
+ property :number
23
+ property :headline
24
+ end
25
+
26
+ class Book
27
+ include CouchPotato::Persistence
28
+ include CouchPotato::Persistence::DeepDirtyAttributes
29
+
30
+ property :title
31
+ property :cover, :type => Cover
32
+ property :authors, :type => [String]
33
+ property :pages, :type => [Page]
34
+ end
35
+
36
+ class TextBook < Book
37
+ property :edition
38
+ end
39
+
40
+ describe "deep dirty attribute tracking" do
41
+ describe "standard dirty checking" do
42
+ describe "_changed?" do
43
+ it "should return true if only root simple properties have changed" do
44
+ book = Book.new(:title => "A")
45
+ book.title = "B"
46
+ book.should be_title_changed
47
+ book.should be_changed
48
+ end
49
+ end
50
+
51
+ describe "_was" do
52
+ it "gives access to old values of simple root properties" do
53
+ book = Book.new(:title => "A")
54
+ book.title = "B"
55
+ book.title_was.should == "A"
56
+ book.title_change.should == ["A", "B"]
57
+ end
58
+ end
59
+
60
+ describe "_change" do
61
+ it "returns standard _change" do
62
+ book = Book.new(:title => "A")
63
+ book.title = "B"
64
+ book.title_change.should == ["A", "B"]
65
+ end
66
+ end
67
+ end
68
+
69
+ describe "single nested document" do
70
+ describe "_changed?" do
71
+ it "should return true if a nested attribute has changed" do
72
+ book = Book.new(:cover => Cover.new(:color => "red"))
73
+ book.cover.color = "blue"
74
+ book.should be_cover_changed
75
+ book.should be_changed
76
+ end
77
+
78
+ it "should return true if changed to a different document" do
79
+ book = Book.new(:cover => Cover.new(:color => "red"))
80
+ book.cover = Cover.new(:color => "blue")
81
+ book.should be_cover_changed
82
+ book.should be_changed
83
+ end
84
+
85
+ it "should return false if changed to a clone of the original document" do
86
+ book = Book.new(:cover => Cover.new(:color => "red"))
87
+ book.cover = book.cover.clone
88
+ book.should_not be_cover_changed
89
+ book.should_not be_changed
90
+ end
91
+
92
+ it "should return false if set to nil and unchanged" do
93
+ book = Book.new
94
+ book.should_not be_cover_changed
95
+ book.should_not be_changed
96
+ end
97
+
98
+ it "should return true when reassigned with changes but the same _id" do
99
+ book = Book.new(:cover => Cover.new(:_id => "cid", :color => "red"))
100
+ book.cover = Cover.new(:_id => "cid", :color => "blue")
101
+ book.should be_cover_changed
102
+ book.should be_changed
103
+ end
104
+ end
105
+
106
+ describe "_was" do
107
+ it "gives access to the old value" do
108
+ book = Book.new(:cover => Cover.new(:color => "red"))
109
+ book.cover.color = "blue"
110
+ book.cover_was.color.should == "red"
111
+ end
112
+ end
113
+
114
+ describe "_change" do
115
+ it "should return the standard changes when a nested document is reassigned" do
116
+ book = Book.new(:cover => Cover.new(:color => "red"))
117
+ book.cover = Cover.new(:color => "blue")
118
+ book.cover_change[0].should be_a Cover
119
+ book.cover_change[0].color.should == "red"
120
+ book.cover_change[1].should be_a Cover
121
+ book.cover_change[1].color.should == "blue"
122
+ end
123
+
124
+ it "should return the standard changes when a nested document is reassigned from nil" do
125
+ book = Book.new
126
+ book.cover = Cover.new
127
+ book.cover_change[0].should == nil
128
+ book.cover_change[1].should == book.cover
129
+ end
130
+
131
+ it "should return the standard changes when a nested document is reassigned to nil" do
132
+ cover = Cover.new
133
+ book = Book.new(:cover => cover)
134
+ book.cover = nil
135
+ book.cover_change[0].should == cover
136
+ book.cover_change[1].should == nil
137
+ end
138
+
139
+ it "should return the nested changes when a nested document is changed" do
140
+ book = Book.new(:cover => Cover.new(:color => "red"))
141
+ book.cover.color = "blue"
142
+ book.cover_change[0].should be_a Cover
143
+ book.cover_change[0].color.should == "red"
144
+ book.cover_change[1].should == book.cover.changes
145
+ end
146
+
147
+ it "should return the nested changes when reassigned with changes but the same _id" do
148
+ book = Book.new(:cover => Cover.new(:_id => "cid", :color => "red"))
149
+ book.cover = Cover.new(:_id => "cid", :color => "blue")
150
+ book.cover_change[0].should be_a Cover
151
+ book.cover_change[0].color.should == "red"
152
+ book.cover_change[1].should == {"color" => ["red", "blue"]}
153
+ end
154
+ end
155
+ end
156
+
157
+ describe "simple array" do
158
+ describe "_changed?" do
159
+ it "returns true if the array is reassigned" do
160
+ book = Book.new(:authors => ["Sarah"])
161
+ book.authors = ["Jane"]
162
+ book.should be_authors_changed
163
+ end
164
+
165
+ it "returns true if an item is added" do
166
+ book = Book.new(:authors => ["Jane"])
167
+ book.authors << "Sue"
168
+ book.should be_authors_changed
169
+ book.should be_changed
170
+ end
171
+
172
+ it "returns true if an item is removed" do
173
+ book = Book.new(:authors => ["Sue"])
174
+ book.authors.delete "Sue"
175
+ book.should be_authors_changed
176
+ book.should be_changed
177
+ end
178
+
179
+ it "returns false if an empty array is unchanged" do
180
+ book = Book.new(:authors => [])
181
+ book.authors = []
182
+ book.should_not be_authors_changed
183
+ book.should_not be_changed
184
+ end
185
+ end
186
+
187
+ describe "_was" do
188
+ it "gives access to the old values" do
189
+ book = Book.new(:authors => ["Jane"])
190
+ book.authors << "Sue"
191
+ book.authors_was.should == ["Jane"]
192
+ end
193
+ end
194
+
195
+ describe "_change" do
196
+ it "returns a hash of added and removed items" do
197
+ book = Book.new(:authors => ["Jane"])
198
+ book.authors << "Sue"
199
+ book.authors.delete "Jane"
200
+ book.authors_change[0].should == ["Jane"]
201
+ book.authors_change[1].should be_a HashWithIndifferentAccess
202
+ book.authors_change[1][:added].should == ["Sue"]
203
+ book.authors_change[1][:removed].should == ["Jane"]
204
+ end
205
+
206
+ it "returns a hash of added and removed items when the array is reassigned" do
207
+ book = Book.new(:authors => ["Jane"])
208
+ book.authors = ["Sue"]
209
+ book.authors_change[0].should == ["Jane"]
210
+ book.authors_change[1].should be_a HashWithIndifferentAccess
211
+ book.authors_change[1][:added].should == ["Sue"]
212
+ book.authors_change[1][:removed].should == ["Jane"]
213
+ end
214
+
215
+ it "returns a hash of added items when the value is changed from nil to an array" do
216
+ book = Book.new
217
+ book.authors = ["Sue"]
218
+ book.authors_change[0].should == []
219
+ book.authors_change[1].should be_a HashWithIndifferentAccess
220
+ book.authors_change[1][:added].should == ["Sue"]
221
+ book.authors_change[1][:removed].should == []
222
+ end
223
+
224
+ it "returns a hash of removed items when the value is changed from an array to nil" do
225
+ book = Book.new(:authors => ["Jane"])
226
+ book.authors = nil
227
+ book.authors_change[0].should == ["Jane"]
228
+ book.authors_change[1].should be_a HashWithIndifferentAccess
229
+ book.authors_change[1][:added].should == []
230
+ book.authors_change[1][:removed].should == ["Jane"]
231
+ end
232
+ end
233
+ end
234
+
235
+ describe "document array" do
236
+ describe "_changed?" do
237
+ it "returns true if an item is changed" do
238
+ book = Book.new(:pages => [Page.new(:number => 1)])
239
+ book.pages[0].number = 2
240
+ book.should be_pages_changed
241
+ book.should be_changed
242
+ end
243
+
244
+ it "returns true if an item is added" do
245
+ book = Book.new(:pages => [Page.new(:number => 1)])
246
+ book.pages << Page.new(:number => 2)
247
+ book.should be_pages_changed
248
+ book.should be_changed
249
+ end
250
+
251
+ it "returns true if an items is removed" do
252
+ book = Book.new(:pages => [Page.new(:number => 1)])
253
+ book.pages.delete_at 0
254
+ book.should be_pages_changed
255
+ book.should be_changed
256
+ end
257
+
258
+ it "returns true if an item is replaced" do
259
+ book = Book.new(:pages => [Page.new(:number => 1)])
260
+ book.pages[0] = Page.new(:number => 2)
261
+ book.should be_pages_changed
262
+ book.should be_changed
263
+ end
264
+
265
+ it "returns false if an item is replaced with a clone" do
266
+ book = Book.new(:pages => [Page.new(:number => 1)])
267
+ book.pages[0] = book.pages[0].clone
268
+ book.should_not be_pages_changed
269
+ book.should_not be_changed
270
+ end
271
+
272
+ it "returns true if an item is replaced with changes but the same _id" do
273
+ book = Book.new(:pages => [Page.new(:_id => "pid", :number => 1)])
274
+ book.pages[0] = Page.new(:_id => "pid", :number => 2)
275
+ book.should be_pages_changed
276
+ book.should be_changed
277
+ end
278
+
279
+ it "returns false if an empty array is unchanged" do
280
+ book = Book.new(:pages => [])
281
+ book.pages = []
282
+ book.should_not be_authors_changed
283
+ book.should_not be_changed
284
+ end
285
+ end
286
+
287
+ describe "_was" do
288
+ it "gives access to the old values" do
289
+ book = Book.new(:pages => [Page.new(:number => 1)])
290
+ book.pages[0].number = 2
291
+ book.pages_was[0].number.should == 1
292
+ end
293
+ end
294
+
295
+ describe "_change" do
296
+ it "returns a changes hash with added, removed, and changed items" do
297
+ p1 = Page.new
298
+ p2 = Page.new(:headline => "A")
299
+ p3 = Page.new
300
+ book = Book.new(:pages => [p1, p2])
301
+ pages = book.pages.clone
302
+ book.pages = [p2]
303
+ p2.headline = "B"
304
+ book.pages << p3
305
+ book.pages_change[0].should == pages
306
+ book.pages_change[1].should be_a HashWithIndifferentAccess
307
+ book.pages_change[1][:added].should == [p3]
308
+ book.pages_change[1][:removed].should == [p1]
309
+ book.pages_change[1][:changed][0][0].should be_a Page
310
+ book.pages_change[1][:changed][0][0].headline.should == "A"
311
+ book.pages_change[1][:changed][0][1].should == p2.changes
312
+ end
313
+
314
+ it "returns added items when changing from nil to an array" do
315
+ p1 = Page.new
316
+ p2 = Page.new(:headline => "A")
317
+ book = Book.new
318
+ book.pages = [p1, p2]
319
+ book.pages_change[0].should == []
320
+ book.pages_change[1].should be_a HashWithIndifferentAccess
321
+ book.pages_change[1][:added].should == [p1, p2]
322
+ book.pages_change[1][:removed].should == []
323
+ book.pages_change[1][:changed].should == []
324
+ end
325
+
326
+ it "does not return unchanged cloned items as changes" do
327
+ book = Book.new(:pages => [Page.new(:number => 1)])
328
+ book.pages[0] = book.pages[0].clone
329
+ book.pages_change.should be_nil
330
+ end
331
+
332
+ it "returns changes if an item is replaced with changes but the same _id" do
333
+ book = Book.new(:pages => [Page.new(:_id => "pid", :number => 1)])
334
+ pages = book.pages.clone
335
+ book.pages[0] = Page.new(:_id => "pid", :number => 2)
336
+ book.pages_change[0].should == pages
337
+ book.pages_change[1].should be_a HashWithIndifferentAccess
338
+ book.pages_change[1][:added].should == []
339
+ book.pages_change[1][:removed].should == []
340
+ book.pages_change[1][:changed].should == [[pages[0], {"number" => [1, 2]}]]
341
+ end
342
+ end
343
+ end
344
+
345
+ describe "changes" do
346
+ it "includes simple property changes" do
347
+ book = Book.new(:title => "Title A")
348
+ book.title = "Title B"
349
+ book.changes[:title].should == book.title_change
350
+ end
351
+
352
+ it "includes embedded document changes" do
353
+ book = Book.new(:cover => Cover.new(:color => "red"))
354
+ cover = book.cover.clone
355
+ book.cover.color = "blue"
356
+ book.changes[:cover].should == book.cover_change
357
+ end
358
+
359
+ it "does not include unchanged embedded documents" do
360
+ book = Book.new(:cover => Cover.new(:color => "red"))
361
+ book.changes.should_not have_key :cover
362
+ end
363
+
364
+ it "includes simple array changes" do
365
+ book = Book.new(:authors => ["Sarah"])
366
+ book.authors = ["Jane"]
367
+ book.changes[:authors].should == book.authors_change
368
+ end
369
+
370
+ it "does not include unchanged simple arrays" do
371
+ book = Book.new(:authors => ["Sarah"])
372
+ book.changes.should_not have_key :authors
373
+ end
374
+
375
+ it "includes document array changes" do
376
+ book = Book.new(:pages => [Page.new(:number => 1)])
377
+ book.pages = [Page.new(:number => 2)]
378
+ book.changes[:pages].should == book.pages_change
379
+ end
380
+
381
+ it "does not include unchanged document arrays" do
382
+ book = Book.new(:pages => [Page.new(:number => 1)])
383
+ book.changes.should_not have_key :pages
384
+ end
385
+ end
386
+
387
+ describe "after save" do
388
+ before :each do
389
+ book = Book.json_create(:_id => "1", :title => "A", :cover => {:color => "red"}, :pages => [{:_id => "p1", :number => 1}, {:_id => "p2", :number => 2}])
390
+ @couchrest_db = stub('database', :info => nil, :save_doc => {}, :get => book)
391
+ @db = CouchPotato::Database.new(@couchrest_db)
392
+ @book = @db.load_document "1"
393
+ end
394
+
395
+ it "should reset all attributes to not dirty" do
396
+ @book.title = "B"
397
+ @book.cover.color = "blue"
398
+ @db.save! @book
399
+ @book.should_not be_dirty
400
+ @book.cover.should_not be_dirty
401
+ end
402
+
403
+ it "should reset all elements in a document array" do
404
+ @book.pages.each(&:is_dirty)
405
+ @db.save! @book
406
+ @book.should_not be_dirty
407
+ @book.pages.each do |page|
408
+ page.should_not be_dirty
409
+ end
410
+ end
411
+
412
+ it "should reset a forced dirty state" do
413
+ @book.is_dirty
414
+ @db.save! @book
415
+ @book.should_not be_dirty
416
+ end
417
+
418
+ it "clears old values" do
419
+ @book.cover.color = "blue"
420
+ @db.save! @book
421
+ @book.cover_was.should be_nil
422
+ @book.cover_change.should be_nil
423
+ end
424
+ end
425
+
426
+ describe "on inherited models" do
427
+ it "still uses deep dirty tracking" do
428
+ book = TextBook.new(:pages => [Page.new(:number => 1)])
429
+ book.pages[0].number = 2
430
+ book.should be_pages_changed
431
+ book.should be_changed
432
+ end
433
+ end
434
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: couch_potato
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.4
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alexander Lang
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-03-24 00:00:00.000000000 Z
11
+ date: 2014-05-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -135,6 +135,8 @@ files:
135
135
  - lib/couch_potato/persistence/active_model_compliance.rb
136
136
  - lib/couch_potato/persistence/attachments.rb
137
137
  - lib/couch_potato/persistence/callbacks.rb
138
+ - lib/couch_potato/persistence/deep_dirty_attributes.rb
139
+ - lib/couch_potato/persistence/deep_tracked_property.rb
138
140
  - lib/couch_potato/persistence/dirty_attributes.rb
139
141
  - lib/couch_potato/persistence/ghost_attributes.rb
140
142
  - lib/couch_potato/persistence/json.rb
@@ -186,6 +188,7 @@ files:
186
188
  - spec/unit/custom_views_spec.rb
187
189
  - spec/unit/database_spec.rb
188
190
  - spec/unit/date_spec.rb
191
+ - spec/unit/deep_dirty_attributes_spec.rb
189
192
  - spec/unit/dirty_attributes_spec.rb
190
193
  - spec/unit/forbidden_attributes_protection_spec.rb
191
194
  - spec/unit/initialize_spec.rb
@@ -251,6 +254,7 @@ test_files:
251
254
  - spec/unit/custom_views_spec.rb
252
255
  - spec/unit/database_spec.rb
253
256
  - spec/unit/date_spec.rb
257
+ - spec/unit/deep_dirty_attributes_spec.rb
254
258
  - spec/unit/dirty_attributes_spec.rb
255
259
  - spec/unit/forbidden_attributes_protection_spec.rb
256
260
  - spec/unit/initialize_spec.rb