Skip to content

Commit 692335b

Browse files
committed
MONGOID-5888 make sure nested attributes trigger validations on deeply nested children
1 parent f16b297 commit 692335b

File tree

9 files changed

+306
-7
lines changed

9 files changed

+306
-7
lines changed

lib/mongoid/association/nested/many.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,8 @@ def update_nested_relation(parent, id, attrs)
190190
update_document(doc, attrs)
191191
existing.push(doc) unless destroyable?(attrs)
192192
end
193+
194+
parent.children_may_have_changed!
193195
end
194196
end
195197
end

lib/mongoid/association/nested/one.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def build(parent)
3737
parent.send(association.setter, nil)
3838
else
3939
check_for_id_violation!
40-
end
40+
end.tap { parent.children_may_have_changed! }
4141
end
4242

4343
# Create the new builder for nested attributes on one-to-one

lib/mongoid/changeable.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ def changed
1515
changed_attributes.keys.select { |attr| attribute_change(attr) }
1616
end
1717

18+
# Indicates that the children of this document may have changed, and
19+
# ought to be checked when the document is validated.
20+
def children_may_have_changed!(flag = true)
21+
@children_may_have_changed = flag
22+
end
23+
1824
# Has the document changed?
1925
#
2026
# @example Has the document changed?
@@ -31,7 +37,7 @@ def changed?
3137
#
3238
# @return [ true | false ] If any children have changed.
3339
def children_changed?
34-
_children.any?(&:changed?)
40+
@children_may_have_changed || _children.any?(&:changed?)
3541
end
3642

3743
# Get the attribute changes.
@@ -69,6 +75,7 @@ def move_changes
6975
@previous_changes = changes
7076
@attributes_before_last_save = @previous_attributes
7177
@previous_attributes = attributes.dup
78+
@children_may_have_changed = false
7279
reset_atomic_updates!
7380
changed_attributes.clear
7481
end

spec/integration/associations/embeds_many_spec.rb

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,24 @@
33

44
require 'spec_helper'
55

6+
module EmbedsManySpec
7+
class Post
8+
include Mongoid::Document
9+
field :title, type: String
10+
embeds_many :comments, class_name: 'EmbedsManySpec::Comment', as: :container
11+
accepts_nested_attributes_for :comments
12+
end
13+
14+
class Comment
15+
include Mongoid::Document
16+
field :content, type: String
17+
validates :content, presence: true
18+
embedded_in :container, polymorphic: true
19+
embeds_many :comments, class_name: 'EmbedsManySpec::Comment', as: :container
20+
accepts_nested_attributes_for :comments
21+
end
22+
end
23+
624
describe 'embeds_many associations' do
725

826
context 're-associating the same object' do
@@ -258,4 +276,55 @@
258276
expect(klass.new.addresses.build).to be_a Address
259277
end
260278
end
279+
280+
context 'with deeply nested trees' do
281+
let(:post) { EmbedsManySpec::Post.create!(title: 'Post') }
282+
let(:child) { post.comments.create!(content: 'Child') }
283+
284+
# creating grandchild will cascade to create the other documents
285+
let!(:grandchild) { child.comments.create!(content: 'Grandchild') }
286+
287+
let(:updated_parent_title) { 'Post Updated' }
288+
let(:updated_grandchild_content) { 'Grandchild Updated' }
289+
290+
context 'with nested attributes' do
291+
let(:attributes) do
292+
{
293+
title: updated_parent_title,
294+
comments_attributes: [
295+
{
296+
# no change for comment1
297+
_id: child.id,
298+
comments_attributes: [
299+
{
300+
_id: grandchild.id,
301+
content: updated_grandchild_content,
302+
}
303+
]
304+
}
305+
]
306+
}
307+
end
308+
309+
context 'when the grandchild is invalid' do
310+
let(:updated_grandchild_content) { '' } # invalid value
311+
312+
it 'will not save the parent' do
313+
expect(post.update(attributes)).to be_falsey
314+
expect(post.errors).not_to be_empty
315+
expect(post.reload.title).not_to eq(updated_parent_title)
316+
expect(grandchild.reload.content).not_to eq(updated_grandchild_content)
317+
end
318+
end
319+
320+
context 'when the grandchild is valid' do
321+
it 'will save the parent' do
322+
expect(post.update(attributes)).to be_truthy
323+
expect(post.errors).to be_empty
324+
expect(post.reload.title).to eq(updated_parent_title)
325+
expect(grandchild.reload.content).to eq(updated_grandchild_content)
326+
end
327+
end
328+
end
329+
end
261330
end

spec/integration/associations/has_and_belongs_to_many_spec.rb

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,38 @@ class Attachment
2323
include Mongoid::Document
2424
field :file, type: String
2525
end
26+
27+
class Item
28+
include Mongoid::Document
29+
30+
field :title, type: String
31+
32+
has_and_belongs_to_many :colors, class_name: 'HabtmSpec::Color', inverse_of: :items
33+
34+
accepts_nested_attributes_for :colors
35+
end
36+
37+
class Beam
38+
include Mongoid::Document
39+
40+
field :name, type: String
41+
validates :name, presence: true
42+
43+
has_and_belongs_to_many :colors, class_name: 'HabtmSpec::Color', inverse_of: :beams
44+
45+
accepts_nested_attributes_for :colors
46+
end
47+
48+
class Color
49+
include Mongoid::Document
50+
51+
field :name, type: String
52+
53+
has_and_belongs_to_many :items, class_name: 'HabtmSpec::Item', inverse_of: :colors
54+
has_and_belongs_to_many :beams, class_name: 'HabtmSpec::Beam', inverse_of: :colors
55+
56+
accepts_nested_attributes_for :items, :beams
57+
end
2658
end
2759

2860
describe 'has_and_belongs_to_many associations' do
@@ -59,4 +91,53 @@ class Attachment
5991
expect { image_block.save! }.not_to raise_error
6092
end
6193
end
94+
95+
context 'with deeply nested trees' do
96+
let(:item) { HabtmSpec::Item.create!(title: 'Item') }
97+
let(:beam) { HabtmSpec::Beam.create!(name: 'Beam') }
98+
let!(:color) { HabtmSpec::Color.create!(name: 'Red', items: [ item ], beams: [ beam ]) }
99+
100+
let(:updated_item_title) { 'Item Updated' }
101+
let(:updated_beam_name) { 'Beam Updated' }
102+
103+
context 'with nested attributes' do
104+
let(:attributes) do
105+
{
106+
title: updated_item_title,
107+
colors_attributes: [
108+
{
109+
# no change for color
110+
_id: color.id,
111+
beams_attributes: [
112+
{
113+
_id: beam.id,
114+
name: updated_beam_name,
115+
}
116+
]
117+
}
118+
]
119+
}
120+
end
121+
122+
context 'when the beam is invalid' do
123+
let(:updated_beam_name) { '' } # invalid value
124+
125+
it 'will not save the parent' do
126+
expect(item.update(attributes)).to be_falsey
127+
expect(item.errors).not_to be_empty
128+
expect(item.reload.title).not_to eq(updated_item_title)
129+
expect(beam.reload.name).not_to eq(updated_beam_name)
130+
end
131+
end
132+
133+
context 'when the beam is valid' do
134+
it 'will save the parent' do
135+
expect(item.update(attributes)).to be_truthy
136+
expect(item.errors).to be_empty
137+
expect(item.reload.title).to eq(updated_item_title)
138+
expect(beam.reload.name).to eq(updated_beam_name)
139+
end
140+
end
141+
end
142+
end
62143
end

spec/integration/associations/has_many_spec.rb

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,4 +126,60 @@
126126
end
127127
end
128128
end
129+
130+
context 'with deeply nested trees' do
131+
let(:post) { HmmPost.create!(title: 'Post') }
132+
let(:child) { post.comments.create!(title: 'Child') }
133+
134+
# creating grandchild will cascade to create the other documents
135+
let!(:grandchild) { child.comments.create!(title: 'Grandchild') }
136+
137+
let(:updated_parent_title) { 'Post Updated' }
138+
let(:updated_grandchild_title) { 'Grandchild Updated' }
139+
140+
context 'with nested attributes' do
141+
let(:attributes) do
142+
{
143+
title: updated_parent_title,
144+
comments_attributes: [
145+
{
146+
# no change for comment1
147+
_id: child.id,
148+
comments_attributes: [
149+
{
150+
_id: grandchild.id,
151+
title: updated_grandchild_title,
152+
num: updated_grandchild_num,
153+
}
154+
]
155+
}
156+
]
157+
}
158+
end
159+
160+
context 'when the grandchild is invalid' do
161+
let(:updated_grandchild_num) { -1 } # invalid value
162+
163+
it 'will not save the parent' do
164+
expect(post.update(attributes)).to be_falsey
165+
expect(post.errors).not_to be_empty
166+
expect(post.reload.title).not_to eq(updated_parent_title)
167+
expect(grandchild.reload.title).not_to eq(updated_grandchild_title)
168+
expect(grandchild.num).not_to eq(updated_grandchild_num)
169+
end
170+
end
171+
172+
context 'when the grandchild is valid' do
173+
let(:updated_grandchild_num) { 1 }
174+
175+
it 'will save the parent' do
176+
expect(post.update(attributes)).to be_truthy
177+
expect(post.errors).to be_empty
178+
expect(post.reload.title).to eq(updated_parent_title)
179+
expect(grandchild.reload.title).to eq(updated_grandchild_title)
180+
expect(grandchild.num).to eq(updated_grandchild_num)
181+
end
182+
end
183+
end
184+
end
129185
end

spec/integration/associations/has_one_spec.rb

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,7 @@
224224
end
225225

226226
context "when explicitly setting the foreign key" do
227-
let(:comment2) { HomComment.new(post_id: post.id, content: "2") }
227+
let(:comment2) { HomComment.new(container_id: post.id, container_type: post.class.name, content: "2") }
228228

229229
it "persists the new comment" do
230230
post.comment = comment1
@@ -264,10 +264,62 @@
264264

265265
it "does not overwrite the original value" do
266266
pending "MONGOID-3999"
267-
p1 = comment.post
267+
p1 = comment.container
268268
expect(p1.title).to eq("post 1")
269-
comment.post = post2
269+
comment.container = post2
270270
expect(p1.title).to eq("post 1")
271271
end
272272
end
273+
274+
context 'with deeply nested trees' do
275+
let(:post) { HomPost.create!(title: 'Post') }
276+
let(:child) { post.create_comment(content: 'Child') }
277+
278+
# creating grandchild will cascade to create the other documents
279+
let!(:grandchild) { child.create_comment(content: 'Grandchild') }
280+
281+
let(:updated_parent_title) { 'Post Updated' }
282+
let(:updated_grandchild_content) { 'Grandchild Updated' }
283+
284+
context 'with nested attributes' do
285+
let(:attributes) do
286+
{
287+
title: updated_parent_title,
288+
comment_attributes: {
289+
# no change for child
290+
_id: child.id,
291+
comment_attributes: {
292+
_id: grandchild.id,
293+
content: updated_grandchild_content,
294+
num: updated_grandchild_num,
295+
}
296+
}
297+
}
298+
end
299+
300+
context 'when the grandchild is invalid' do
301+
let(:updated_grandchild_num) { -1 } # invalid value
302+
303+
it 'will not save the parent' do
304+
expect(post.update(attributes)).to be_falsey
305+
expect(post.errors).not_to be_empty
306+
expect(post.reload.title).not_to eq(updated_parent_title)
307+
expect(grandchild.reload.content).not_to eq(updated_grandchild_content)
308+
expect(grandchild.num).not_to eq(updated_grandchild_num)
309+
end
310+
end
311+
312+
context 'when the grandchild is valid' do
313+
let(:updated_grandchild_num) { 1 }
314+
315+
it 'will save the parent' do
316+
expect(post.update(attributes)).to be_truthy
317+
expect(post.errors).to be_empty
318+
expect(post.reload.title).to eq(updated_parent_title)
319+
expect(grandchild.reload.content).to eq(updated_grandchild_content)
320+
expect(grandchild.num).to eq(updated_grandchild_num)
321+
end
322+
end
323+
end
324+
end
273325
end

spec/mongoid/association/referenced/has_many_models.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,27 @@ class HmmAnimal
9696

9797
belongs_to :trainer, class_name: 'HmmTrainer', scope: -> { where(name: 'Dave') }
9898
end
99+
100+
class HmmPost
101+
include Mongoid::Document
102+
103+
field :title, type: String
104+
105+
has_many :comments, class_name: 'HmmComment', as: :container
106+
107+
accepts_nested_attributes_for :comments, allow_destroy: true
108+
end
109+
110+
class HmmComment
111+
include Mongoid::Document
112+
113+
field :title, type: String
114+
field :num, type: Integer, default: 0
115+
116+
belongs_to :container, polymorphic: true
117+
has_many :comments, class_name: 'HmmComment', as: :container
118+
119+
accepts_nested_attributes_for :comments, allow_destroy: true
120+
121+
validates :num, numericality: { greater_than_or_equal_to: 0 }
122+
end

0 commit comments

Comments
 (0)