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 +4 -4
- data/.travis.yml +1 -2
- data/CHANGES.md +5 -0
- data/README.md +43 -0
- data/lib/couch_potato/database.rb +1 -2
- data/lib/couch_potato/persistence/deep_dirty_attributes.rb +180 -0
- data/lib/couch_potato/persistence/deep_tracked_property.rb +56 -0
- data/lib/couch_potato/persistence/dirty_attributes.rb +0 -4
- data/lib/couch_potato/persistence/properties.rb +1 -0
- data/lib/couch_potato/persistence/simple_property.rb +6 -2
- data/lib/couch_potato/persistence.rb +1 -0
- data/lib/couch_potato/rspec/stub_db.rb +1 -1
- data/lib/couch_potato/version.rb +1 -1
- data/spec/unit/database_spec.rb +14 -0
- data/spec/unit/deep_dirty_attributes_spec.rb +434 -0
- metadata +6 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2771fb2d4e5b08e8ce5a50afa1d6747a54682807
|
4
|
+
data.tar.gz: 1009498ac99d3d0b5e2bba6a7a5a4624ff5759be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 665f44a87cc9d5f96b1c0ba3dc416849e6fc1b02888a96658e94e7a295601aa4e81d81423df6a8362e87c4feb41f1a738fa1bd08d18fe716a7d655b03714eff7
|
7
|
+
data.tar.gz: c53f3dba7f3fd8208182ecfc54a8e6d747af55d316ea2097df26762ab70e3544676575e36594922caf1b85033c9b6631e79d2876105b37c901a100f3ce6608f2
|
data/.travis.yml
CHANGED
data/CHANGES.md
CHANGED
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
|
@@ -42,8 +42,8 @@ module CouchPotato
|
|
42
42
|
|
43
43
|
private
|
44
44
|
|
45
|
-
def
|
46
|
-
module_name = "#{clazz.name.to_s.gsub('::', '__')}
|
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'
|
data/lib/couch_potato/version.rb
CHANGED
data/spec/unit/database_spec.rb
CHANGED
@@ -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.
|
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-
|
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
|