dm-accepts_nested_attributes 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +26 -0
- data/CHANGELOG +970 -0
- data/LICENSE +20 -0
- data/README.textile +94 -0
- data/Rakefile +35 -0
- data/TODO +6 -0
- data/dm-accepts_nested_attributes.gemspec +90 -0
- data/lib/dm-accepts_nested_attributes.rb +7 -0
- data/lib/dm-accepts_nested_attributes/error_collecting.rb +35 -0
- data/lib/dm-accepts_nested_attributes/model.rb +155 -0
- data/lib/dm-accepts_nested_attributes/resource.rb +259 -0
- data/lib/dm-accepts_nested_attributes/transactional_save.rb +24 -0
- data/lib/dm-accepts_nested_attributes/version.rb +7 -0
- data/spec/accepts_nested_attributes_for_spec.rb +408 -0
- data/spec/many_to_many_spec.rb +129 -0
- data/spec/many_to_one_spec.rb +101 -0
- data/spec/one_to_many_spec.rb +100 -0
- data/spec/one_to_one_spec.rb +115 -0
- data/spec/rcov.opts +6 -0
- data/spec/shared/many_to_many_spec.rb +162 -0
- data/spec/shared/many_to_one_spec.rb +150 -0
- data/spec/shared/one_to_many_spec.rb +128 -0
- data/spec/shared/one_to_one_spec.rb +130 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +53 -0
- data/tasks/changelog.rake +20 -0
- data/tasks/ci.rake +1 -0
- data/tasks/metrics.rake +36 -0
- data/tasks/spec.rake +25 -0
- data/tasks/yard.rake +9 -0
- data/tasks/yardstick.rake +19 -0
- metadata +125 -0
@@ -0,0 +1,259 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module NestedAttributes
|
3
|
+
|
4
|
+
##
|
5
|
+
# Extensions and customizations for @see DataMapper::Resource
|
6
|
+
# that are needed if the @see DataMapper::Resource wants to
|
7
|
+
# accept nested attributes for any given relationship.
|
8
|
+
# Basically, this module provides functionality that allows
|
9
|
+
# either assignment or marking for destruction of related parent
|
10
|
+
# and child associations, based on the given attributes and what
|
11
|
+
# kind of relationship should be altered.
|
12
|
+
module Resource
|
13
|
+
|
14
|
+
##
|
15
|
+
# Can be used to remove ambiguities from the passed attributes.
|
16
|
+
# Consider a situation with a belongs_to association where both a valid value
|
17
|
+
# for the foreign_key attribute *and* nested_attributes for a new record are
|
18
|
+
# present (i.e. item_type_id and item_type_attributes are present).
|
19
|
+
# Also see http://is.gd/sz2d on the rails-core ml for a discussion on this.
|
20
|
+
# The basic idea is, that there should be a well defined behavior for what
|
21
|
+
# exactly happens when such a situation occurs. I'm currently in favor for
|
22
|
+
# using the foreign_key if it is present, but this probably needs more thinking.
|
23
|
+
# For now, this method basically is a no-op, but at least it provides a hook where
|
24
|
+
# everyone can perform it's own sanitization by overwriting this method.
|
25
|
+
#
|
26
|
+
# @param attributes [Hash]
|
27
|
+
# The attributes to sanitize
|
28
|
+
#
|
29
|
+
# @return [Hash]
|
30
|
+
# The sanitized attributes
|
31
|
+
#
|
32
|
+
def sanitize_nested_attributes(attributes)
|
33
|
+
attributes # noop
|
34
|
+
end
|
35
|
+
|
36
|
+
##
|
37
|
+
# Destroy intermediate resources that possibly need to be cleaned up
|
38
|
+
def save(*)
|
39
|
+
saved = super
|
40
|
+
remove_destroyables
|
41
|
+
saved
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
##
|
47
|
+
# Attribute hash keys that should not be assigned as normal attributes.
|
48
|
+
#
|
49
|
+
# @return [#each]
|
50
|
+
# The model key and :_delete, the latter being a special value
|
51
|
+
# used to mark a resource for destruction
|
52
|
+
def unassignable_keys
|
53
|
+
model.key.to_a << :_delete
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
##
|
58
|
+
# Assigns the given attributes to the resource association.
|
59
|
+
#
|
60
|
+
# If the given attributes include an <tt>:id</tt> that matches the existing
|
61
|
+
# record’s id, then the existing record will be modified. Otherwise a new
|
62
|
+
# record will be built.
|
63
|
+
#
|
64
|
+
# If the given attributes include a matching <tt>:id</tt> attribute _and_ a
|
65
|
+
# <tt>:_delete</tt> key set to a truthy value, then the existing record
|
66
|
+
# will be marked for destruction.
|
67
|
+
#
|
68
|
+
# @param relationship [DataMapper::Associations::Relationship]
|
69
|
+
# The relationship backing the association.
|
70
|
+
# Assignment will happen on the target end of the relationship
|
71
|
+
#
|
72
|
+
# @param attributes [Hash]
|
73
|
+
# The attributes to assign to the relationship's target end
|
74
|
+
# All attributes except @see UNASSIGNABLE_KEYS will be assigned
|
75
|
+
#
|
76
|
+
# @return nil
|
77
|
+
def assign_nested_attributes_for_related_resource(relationship, attributes)
|
78
|
+
if attributes[:id].blank?
|
79
|
+
return if reject_new_record?(relationship, attributes)
|
80
|
+
new_record = relationship.target_model.new(attributes.except(*unassignable_keys))
|
81
|
+
relationship.set(self, new_record)
|
82
|
+
else
|
83
|
+
existing_record = relationship.get(self)
|
84
|
+
if existing_record && existing_record.id.to_s == attributes[:id].to_s
|
85
|
+
update_or_mark_as_destroyable(relationship, existing_record, attributes)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
##
|
91
|
+
# Assigns the given attributes to the collection association.
|
92
|
+
#
|
93
|
+
# Hashes with an <tt>:id</tt> value matching an existing associated record
|
94
|
+
# will update that record. Hashes without an <tt>:id</tt> value will build
|
95
|
+
# a new record for the association. Hashes with a matching <tt>:id</tt>
|
96
|
+
# value and a <tt>:_delete</tt> key set to a truthy value will mark the
|
97
|
+
# matched record for destruction.
|
98
|
+
#
|
99
|
+
# For example:
|
100
|
+
#
|
101
|
+
# assign_nested_attributes_for_collection_association(:people, {
|
102
|
+
# '1' => { :id => '1', :name => 'Peter' },
|
103
|
+
# '2' => { :name => 'John' },
|
104
|
+
# '3' => { :id => '2', :_delete => true }
|
105
|
+
# })
|
106
|
+
#
|
107
|
+
# Will update the name of the Person with ID 1, build a new associated
|
108
|
+
# person with the name `John', and mark the associatied Person with ID 2
|
109
|
+
# for destruction.
|
110
|
+
#
|
111
|
+
# Also accepts an Array of attribute hashes:
|
112
|
+
#
|
113
|
+
# assign_nested_attributes_for_collection_association(:people, [
|
114
|
+
# { :id => '1', :name => 'Peter' },
|
115
|
+
# { :name => 'John' },
|
116
|
+
# { :id => '2', :_delete => true }
|
117
|
+
# ])
|
118
|
+
#
|
119
|
+
# @param relationship [DataMapper::Associations::Relationship]
|
120
|
+
# The relationship backing the association.
|
121
|
+
# Assignment will happen on the target end of the relationship
|
122
|
+
#
|
123
|
+
# @param attributes [Hash]
|
124
|
+
# The attributes to assign to the relationship's target end
|
125
|
+
# All attributes except @see UNASSIGNABLE_KEYS will be assigned
|
126
|
+
#
|
127
|
+
# @return nil
|
128
|
+
def assign_nested_attributes_for_related_collection(relationship, attributes_collection)
|
129
|
+
|
130
|
+
normalize_attributes_collection(attributes_collection).each do |attributes|
|
131
|
+
|
132
|
+
if attributes[:id].blank?
|
133
|
+
next if reject_new_record?(relationship, attributes)
|
134
|
+
relationship.get(self).new(attributes.except(*unassignable_keys))
|
135
|
+
else
|
136
|
+
collection = relationship.get(self)
|
137
|
+
if existing_record = collection.get(attributes[:id])
|
138
|
+
update_or_mark_as_destroyable(relationship, existing_record, attributes)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
##
|
147
|
+
# Updates a record with the +attributes+ or marks it for destruction if
|
148
|
+
# +allow_destroy+ is +true+ and has_delete_flag? returns +true+.
|
149
|
+
#
|
150
|
+
# @param relationship [DataMapper::Associations::Relationship]
|
151
|
+
# The relationship backing the association.
|
152
|
+
# Assignment will happen on the target end of the relationship
|
153
|
+
#
|
154
|
+
# @param attributes [Hash]
|
155
|
+
# The attributes to assign to the relationship's target end
|
156
|
+
# All attributes except @see UNASSIGNABLE_KEYS will be assigned
|
157
|
+
#
|
158
|
+
# @return nil
|
159
|
+
def update_or_mark_as_destroyable(relationship, resource, attributes)
|
160
|
+
allow_destroy = self.class.options_for_nested_attributes[relationship][:allow_destroy]
|
161
|
+
if has_delete_flag?(attributes) && allow_destroy
|
162
|
+
if relationship.is_a?(DataMapper::Associations::ManyToMany::Relationship)
|
163
|
+
intermediaries = relationship.through.get(self).all(relationship.via => resource)
|
164
|
+
intermediaries.each { |intermediate| destroyables << intermediate }
|
165
|
+
end
|
166
|
+
destroyables << resource
|
167
|
+
else
|
168
|
+
resource.update(attributes.except(*unassignable_keys))
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
##
|
173
|
+
# Determines if the given attributes hash contains a truthy :_delete key.
|
174
|
+
#
|
175
|
+
# @param attributes [Hash] The attributes to test
|
176
|
+
#
|
177
|
+
# @return [TrueClass, FalseClass]
|
178
|
+
# true, if attributes contains a truthy :_delete key
|
179
|
+
def has_delete_flag?(attributes)
|
180
|
+
!!attributes[:_delete]
|
181
|
+
end
|
182
|
+
|
183
|
+
##
|
184
|
+
# Determines if a new record should be built with the given attributes.
|
185
|
+
# Rejects a new record if @see has_delete_flag? returns true for the given attributes,
|
186
|
+
# or if a :reject_if guard exists for the passed relationship that evaluates to +true+.
|
187
|
+
#
|
188
|
+
# @param relationship [DataMapper::Associations::Relationship]
|
189
|
+
# The relationship backing the association.
|
190
|
+
#
|
191
|
+
# @param attributes [Hash]
|
192
|
+
# The attributes to test with @see has_delete_flag?
|
193
|
+
#
|
194
|
+
# @return [TrueClass, FalseClass]
|
195
|
+
# true, if the given attributes will be rejected
|
196
|
+
def reject_new_record?(relationship, attributes)
|
197
|
+
guard = self.class.options_for_nested_attributes[relationship][:reject_if]
|
198
|
+
return false if guard.nil? # if relationship guard is nil, nothing will be rejected
|
199
|
+
has_delete_flag?(attributes) || evaluate_reject_new_record_guard(guard, attributes)
|
200
|
+
end
|
201
|
+
|
202
|
+
##
|
203
|
+
# Evaluates the given guard by calling it with the given attributes
|
204
|
+
#
|
205
|
+
# @param [Symbol, String, #call] guard
|
206
|
+
# An instance method name or an object that respond_to?(:call), which
|
207
|
+
# would stop a new record from being created, if it evaluates to true.
|
208
|
+
#
|
209
|
+
# @param [Hash] attributes
|
210
|
+
# The attributes to pass to the guard for evaluating if it should reject
|
211
|
+
# the creation of a new resource
|
212
|
+
#
|
213
|
+
# @raise [ArgumentError]
|
214
|
+
# If the given guard doesn't match [Symbol, String, #call]
|
215
|
+
#
|
216
|
+
# @return [true, false]
|
217
|
+
# The value returned by evaluating the guard
|
218
|
+
def evaluate_reject_new_record_guard(guard, attributes)
|
219
|
+
if guard.is_a?(Symbol) || guard.is_a?(String)
|
220
|
+
send(guard, attributes)
|
221
|
+
elsif guard.respond_to?(:call)
|
222
|
+
guard.call(attributes)
|
223
|
+
else
|
224
|
+
# never reached when called from inside the plugin
|
225
|
+
raise ArgumentError, "guard must be a Symbol, a String, or respond_to?(:call)"
|
226
|
+
end
|
227
|
+
end
|
228
|
+
|
229
|
+
##
|
230
|
+
# Make sure to return a collection of attribute hashes.
|
231
|
+
# If passed an attributes hash, map it to its attributes
|
232
|
+
#
|
233
|
+
# @param attributes [Hash, #each]
|
234
|
+
# An attributes hash or a collection of attribute hashes
|
235
|
+
#
|
236
|
+
# @return [#each]
|
237
|
+
# A collection of attribute hashes
|
238
|
+
def normalize_attributes_collection(attributes)
|
239
|
+
if attributes.is_a?(Hash)
|
240
|
+
attributes.map { |_, attributes| attributes }
|
241
|
+
else
|
242
|
+
attributes
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
|
247
|
+
def destroyables
|
248
|
+
@destroyables ||= []
|
249
|
+
end
|
250
|
+
|
251
|
+
def remove_destroyables
|
252
|
+
destroyables.each { |r| r.destroy if r.saved? }
|
253
|
+
@destroyables.clear
|
254
|
+
end
|
255
|
+
|
256
|
+
end
|
257
|
+
|
258
|
+
end
|
259
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module NestedAttributes
|
3
|
+
|
4
|
+
module TransactionalSave
|
5
|
+
|
6
|
+
##
|
7
|
+
# Overrides @see DataMapper::Resource#save to perform inside a transaction.
|
8
|
+
# The current implementation simply wraps the saving of the complete object tree
|
9
|
+
# inside a transaction and rolls back in case any exceptions are raised,
|
10
|
+
# or any of the calls to
|
11
|
+
#
|
12
|
+
# @see DataMapper::Resource#save
|
13
|
+
#
|
14
|
+
# @return [Boolean]
|
15
|
+
# true if all related resources were saved properly
|
16
|
+
#
|
17
|
+
def save
|
18
|
+
transaction { |t| super || t.rollback && false }
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,408 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "DataMapper::Model.accepts_nested_attributes_for" do
|
4
|
+
|
5
|
+
FIXTURES = <<-RUBY
|
6
|
+
|
7
|
+
class Branch
|
8
|
+
include DataMapper::Resource
|
9
|
+
property :id, Serial
|
10
|
+
has 1, :shop
|
11
|
+
has n, :items
|
12
|
+
has n, :bookings, :through => :items
|
13
|
+
end
|
14
|
+
|
15
|
+
class Shop
|
16
|
+
include DataMapper::Resource
|
17
|
+
property :id, Serial
|
18
|
+
belongs_to :branch
|
19
|
+
end
|
20
|
+
|
21
|
+
class Item
|
22
|
+
include DataMapper::Resource
|
23
|
+
property :id, Serial
|
24
|
+
belongs_to :branch
|
25
|
+
has n, :bookings
|
26
|
+
end
|
27
|
+
|
28
|
+
class Booking
|
29
|
+
include DataMapper::Resource
|
30
|
+
property :id, Serial
|
31
|
+
belongs_to :item
|
32
|
+
end
|
33
|
+
|
34
|
+
RUBY
|
35
|
+
|
36
|
+
before(:all) do
|
37
|
+
eval FIXTURES
|
38
|
+
DataMapper.auto_migrate!
|
39
|
+
end
|
40
|
+
|
41
|
+
before(:each) do
|
42
|
+
|
43
|
+
Object.send(:remove_const, 'Branch') if Object.const_defined?('Branch')
|
44
|
+
Object.send(:remove_const, 'Shop') if Object.const_defined?('Shop')
|
45
|
+
Object.send(:remove_const, 'Item') if Object.const_defined?('Item')
|
46
|
+
Object.send(:remove_const, 'Booking') if Object.const_defined?('Booking')
|
47
|
+
|
48
|
+
eval FIXTURES # neither class_eval nor instance_eval work here
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
|
53
|
+
describe "when called with" do
|
54
|
+
|
55
|
+
describe "no association_name" do
|
56
|
+
|
57
|
+
it "should raise" do
|
58
|
+
lambda { Branch.accepts_nested_attributes_for }.should raise_error(ArgumentError)
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
|
63
|
+
describe "nil as association_name" do
|
64
|
+
|
65
|
+
describe "and no options" do
|
66
|
+
|
67
|
+
it "should raise" do
|
68
|
+
lambda { Branch.accepts_nested_attributes_for(nil) }.should raise_error(ArgumentError)
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "and empty options" do
|
74
|
+
|
75
|
+
it "should raise" do
|
76
|
+
lambda { Branch.accepts_nested_attributes_for(nil, {}) }.should raise_error(ArgumentError)
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
describe "and invalid options" do
|
82
|
+
|
83
|
+
it "should raise" do
|
84
|
+
lambda { Branch.accepts_nested_attributes_for(nil, { :foo => :bar }) }.should raise_error(ArgumentError)
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "and valid options" do
|
90
|
+
|
91
|
+
it "should raise" do
|
92
|
+
lambda { Branch.accepts_nested_attributes_for(nil, { :allow_destroy => true }) }.should raise_error(ArgumentError)
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
end
|
98
|
+
|
99
|
+
describe "an invalid association_name" do
|
100
|
+
|
101
|
+
describe "and no options" do
|
102
|
+
|
103
|
+
it "should raise" do
|
104
|
+
lambda { Branch.accepts_nested_attributes_for(:foo) }.should raise_error(ArgumentError)
|
105
|
+
end
|
106
|
+
|
107
|
+
end
|
108
|
+
|
109
|
+
describe "and empty options" do
|
110
|
+
|
111
|
+
it "should raise" do
|
112
|
+
lambda { Branch.accepts_nested_attributes_for(:foo, {}) }.should raise_error(ArgumentError)
|
113
|
+
end
|
114
|
+
|
115
|
+
end
|
116
|
+
|
117
|
+
describe "and invalid options" do
|
118
|
+
|
119
|
+
it "should raise" do
|
120
|
+
lambda { Branch.accepts_nested_attributes_for(:foo, { :foo => :bar }) }.should raise_error(ArgumentError)
|
121
|
+
end
|
122
|
+
|
123
|
+
end
|
124
|
+
|
125
|
+
describe "and valid options" do
|
126
|
+
|
127
|
+
it "should raise" do
|
128
|
+
lambda { Branch.accepts_nested_attributes_for(:foo, { :allow_destroy => true }) }.should raise_error(ArgumentError)
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
|
135
|
+
|
136
|
+
describe "a valid association_name", :shared => true do
|
137
|
+
|
138
|
+
|
139
|
+
describe "and no options" do
|
140
|
+
|
141
|
+
it "should not raise" do
|
142
|
+
lambda { @model.accepts_nested_attributes_for @association }.should_not raise_error
|
143
|
+
end
|
144
|
+
|
145
|
+
it "should store the accessible association in .options_for_nested_attributes" do
|
146
|
+
@model.options_for_nested_attributes[@association].should be_nil
|
147
|
+
@model.accepts_nested_attributes_for @association
|
148
|
+
relationship = @model.relationships[@association]
|
149
|
+
@model.options_for_nested_attributes[relationship].should_not be_nil
|
150
|
+
end
|
151
|
+
|
152
|
+
it "should store the default options under the association_name in .options_for_nested_attributes" do
|
153
|
+
@model.options_for_nested_attributes[@association].should be_nil
|
154
|
+
@model.accepts_nested_attributes_for @association
|
155
|
+
relationship = @model.relationships[@association]
|
156
|
+
@model.options_for_nested_attributes[relationship].should == { :allow_destroy => false }
|
157
|
+
end
|
158
|
+
|
159
|
+
it "should create a \#{association_name}_attributes instance reader" do
|
160
|
+
p = @model.new
|
161
|
+
p.respond_to?("#{@association}_attributes").should be_false
|
162
|
+
@model.accepts_nested_attributes_for @association
|
163
|
+
p = @model.new
|
164
|
+
p.respond_to?("#{@association}_attributes").should be_true
|
165
|
+
end
|
166
|
+
|
167
|
+
it "should create a \#{association_name}_attributes instance writer" do
|
168
|
+
p = @model.new
|
169
|
+
p.respond_to?("#{@association}_attributes=").should be_false
|
170
|
+
@model.accepts_nested_attributes_for @association
|
171
|
+
p = @model.new
|
172
|
+
p.respond_to?("#{@association}_attributes=").should be_true
|
173
|
+
end
|
174
|
+
|
175
|
+
end
|
176
|
+
|
177
|
+
describe "and empty options" do
|
178
|
+
|
179
|
+
it "should not raise" do
|
180
|
+
lambda { @model.accepts_nested_attributes_for @association, {} }.should_not raise_error
|
181
|
+
end
|
182
|
+
|
183
|
+
it "should store the accessible association in .autosave_associations" do
|
184
|
+
@model.options_for_nested_attributes[@association].should be_nil
|
185
|
+
@model.accepts_nested_attributes_for @association, {}
|
186
|
+
relationship = @model.relationships[@association]
|
187
|
+
@model.options_for_nested_attributes[relationship].should_not be_nil
|
188
|
+
end
|
189
|
+
|
190
|
+
it "should store the default options under the association_name in .autosave_associations" do
|
191
|
+
@model.options_for_nested_attributes[@association].should be_nil
|
192
|
+
@model.accepts_nested_attributes_for @association, {}
|
193
|
+
relationship = @model.relationships[@association]
|
194
|
+
@model.options_for_nested_attributes[relationship].should == { :allow_destroy => false }
|
195
|
+
end
|
196
|
+
|
197
|
+
it "should create a \#{association_name}_attributes instance reader" do
|
198
|
+
p = @model.new
|
199
|
+
p.respond_to?("#{@association}_attributes").should be_false
|
200
|
+
@model.accepts_nested_attributes_for @association, {}
|
201
|
+
p = @model.new
|
202
|
+
p.respond_to?("#{@association}_attributes").should be_true
|
203
|
+
end
|
204
|
+
|
205
|
+
it "should create a \#{association_name}_attributes instance writer" do
|
206
|
+
p = @model.new
|
207
|
+
p.respond_to?("#{@association}_attributes=").should be_false
|
208
|
+
@model.accepts_nested_attributes_for @association, {}
|
209
|
+
p = @model.new
|
210
|
+
p.respond_to?("#{@association}_attributes=").should be_true
|
211
|
+
end
|
212
|
+
|
213
|
+
end
|
214
|
+
|
215
|
+
describe "and invalid options" do
|
216
|
+
|
217
|
+
it "should raise" do
|
218
|
+
lambda { @model.accepts_nested_attributes_for @association, { :foo => :bar } }.should raise_error(DataMapper::NestedAttributes::InvalidOptions)
|
219
|
+
end
|
220
|
+
|
221
|
+
it "should not store the accessible association in .autosave_associations" do
|
222
|
+
@model.options_for_nested_attributes[@association].should be_nil
|
223
|
+
lambda { @model.accepts_nested_attributes_for @association, { :foo => :bar } }.should raise_error(DataMapper::NestedAttributes::InvalidOptions)
|
224
|
+
@model.options_for_nested_attributes[@association].should be_nil
|
225
|
+
end
|
226
|
+
|
227
|
+
it "should not create a \#{association_name}_attributes instance reader" do
|
228
|
+
p = @model.new
|
229
|
+
p.respond_to?("#{@association}_attributes").should be_false
|
230
|
+
lambda { @model.accepts_nested_attributes_for @association, { :foo => :bar } }.should raise_error(DataMapper::NestedAttributes::InvalidOptions)
|
231
|
+
p = @model.new
|
232
|
+
p.respond_to?("#{@association}_attributes").should be_false
|
233
|
+
end
|
234
|
+
|
235
|
+
it "should not create a \#{association_name}_attributes instance writer" do
|
236
|
+
p = @model.new
|
237
|
+
p.respond_to?("#{@association}_attributes=").should be_false
|
238
|
+
lambda { @model.accepts_nested_attributes_for @association, { :foo => :bar } }.should raise_error(DataMapper::NestedAttributes::InvalidOptions)
|
239
|
+
p = @model.new
|
240
|
+
p.respond_to?("#{@association}_attributes=").should be_false
|
241
|
+
end
|
242
|
+
|
243
|
+
end
|
244
|
+
|
245
|
+
describe "and valid options" do
|
246
|
+
|
247
|
+
it "should not raise" do
|
248
|
+
lambda { @model.accepts_nested_attributes_for @association, :allow_destroy => true }.should_not raise_error
|
249
|
+
end
|
250
|
+
|
251
|
+
it "should store the accessible association in .autosave_associations" do
|
252
|
+
@model.options_for_nested_attributes[@association].should be_nil
|
253
|
+
@model.accepts_nested_attributes_for @association, :allow_destroy => true
|
254
|
+
relationship = @model.relationships[@association]
|
255
|
+
@model.options_for_nested_attributes[relationship].should_not be_nil
|
256
|
+
end
|
257
|
+
|
258
|
+
it "should accept :allow_destroy as the only option (and thus overwrite the default option)" do
|
259
|
+
@model.options_for_nested_attributes[@association].should be_nil
|
260
|
+
@model.accepts_nested_attributes_for @association, :allow_destroy => true
|
261
|
+
relationship = @model.relationships[@association]
|
262
|
+
@model.options_for_nested_attributes[relationship].should == { :allow_destroy => true }
|
263
|
+
end
|
264
|
+
|
265
|
+
it "should accept :reject_if as the only option (and add :allow_destroy => false)" do
|
266
|
+
@model.options_for_nested_attributes[@association].should be_nil
|
267
|
+
@model.accepts_nested_attributes_for @association, :reject_if => lambda { |attributes| nil }
|
268
|
+
relationship = @model.relationships[@association]
|
269
|
+
@model.options_for_nested_attributes[relationship].should_not be_nil
|
270
|
+
@model.options_for_nested_attributes[relationship][:allow_destroy].should be_false
|
271
|
+
@model.options_for_nested_attributes[relationship][:reject_if].should be_kind_of(Proc)
|
272
|
+
end
|
273
|
+
|
274
|
+
it "should accept both :allow_destroy and :reject_if as options" do
|
275
|
+
@model.options_for_nested_attributes[@association].should be_nil
|
276
|
+
@model.accepts_nested_attributes_for @association, :allow_destroy => true, :reject_if => lambda { |attributes| nil }
|
277
|
+
relationship = @model.relationships[@association]
|
278
|
+
@model.options_for_nested_attributes[relationship].should_not be_nil
|
279
|
+
@model.options_for_nested_attributes[relationship][:allow_destroy].should be_true
|
280
|
+
@model.options_for_nested_attributes[relationship][:reject_if].should be_kind_of(Proc)
|
281
|
+
end
|
282
|
+
|
283
|
+
it "should create a \#{association_name}_attributes instance reader" do
|
284
|
+
p = @model.new
|
285
|
+
p.respond_to?("#{@association}_attributes").should be_false
|
286
|
+
@model.accepts_nested_attributes_for @association, :allow_destroy => true
|
287
|
+
p = @model.new
|
288
|
+
p.respond_to?("#{@association}_attributes=").should be_true
|
289
|
+
end
|
290
|
+
|
291
|
+
it "should create a \#{association_name}_attributes instance writer" do
|
292
|
+
p = @model.new
|
293
|
+
p.respond_to?("#{@association}_attributes").should be_false
|
294
|
+
@model.accepts_nested_attributes_for @association, :allow_destroy => true
|
295
|
+
p = @model.new
|
296
|
+
p.respond_to?("#{@association}_attributes=").should be_true
|
297
|
+
end
|
298
|
+
|
299
|
+
end
|
300
|
+
|
301
|
+
end
|
302
|
+
|
303
|
+
describe "a valid association_name pointing to multiple resources", :shared => true do
|
304
|
+
|
305
|
+
describe "and no options" do
|
306
|
+
|
307
|
+
it "should not create a get_\#{association_name} instance reader" do
|
308
|
+
p = @model.new
|
309
|
+
p.respond_to?("get_#{@association}").should be_false
|
310
|
+
lambda { @model.accepts_nested_attributes_for @association }.should_not raise_error
|
311
|
+
p = @model.new
|
312
|
+
p.respond_to?("get_#{@association}").should be_false
|
313
|
+
end
|
314
|
+
|
315
|
+
end
|
316
|
+
|
317
|
+
describe "and empty options" do
|
318
|
+
|
319
|
+
it "should not create a get_\#{association_name} instance reader" do
|
320
|
+
p = @model.new
|
321
|
+
p.respond_to?("get_#{@association}").should be_false
|
322
|
+
lambda { @model.accepts_nested_attributes_for @association, {} }.should_not raise_error
|
323
|
+
p = @model.new
|
324
|
+
p.respond_to?("get_#{@association}").should be_false
|
325
|
+
end
|
326
|
+
|
327
|
+
end
|
328
|
+
|
329
|
+
describe "and invalid options" do
|
330
|
+
|
331
|
+
it "should not create a get_\#{association_name} instance reader" do
|
332
|
+
p = @model.new
|
333
|
+
p.respond_to?("get_#{@association}").should be_false
|
334
|
+
lambda { @model.accepts_nested_attributes_for @association, { :foo => :bar } }.should raise_error(DataMapper::NestedAttributes::InvalidOptions)
|
335
|
+
p = @model.new
|
336
|
+
p.respond_to?("get_#{@association}").should be_false
|
337
|
+
end
|
338
|
+
|
339
|
+
end
|
340
|
+
|
341
|
+
describe "and valid options" do
|
342
|
+
|
343
|
+
it "should not create a get_\#{association_name} instance reader" do
|
344
|
+
p = @model.new
|
345
|
+
p.respond_to?("get_#{@association}").should be_false
|
346
|
+
lambda { @model.accepts_nested_attributes_for @association, { :allow_destroy => :true } }.should_not raise_error
|
347
|
+
p = @model.new
|
348
|
+
p.respond_to?("get_#{@association}").should be_false
|
349
|
+
end
|
350
|
+
|
351
|
+
end
|
352
|
+
|
353
|
+
end
|
354
|
+
|
355
|
+
# ----------------------------------------------------------------------------------------
|
356
|
+
# ----------------------------------------------------------------------------------------
|
357
|
+
# ----------------------------------------------------------------------------------------
|
358
|
+
|
359
|
+
|
360
|
+
describe "a valid belongs_to association_name" do
|
361
|
+
|
362
|
+
before(:each) do
|
363
|
+
@model = Item
|
364
|
+
@association = :branch
|
365
|
+
end
|
366
|
+
|
367
|
+
it_should_behave_like "a valid association_name"
|
368
|
+
|
369
|
+
end
|
370
|
+
|
371
|
+
describe "a valid has(1) association_name" do
|
372
|
+
|
373
|
+
before(:each) do
|
374
|
+
@model = Branch
|
375
|
+
@association = :shop
|
376
|
+
end
|
377
|
+
|
378
|
+
it_should_behave_like "a valid association_name"
|
379
|
+
|
380
|
+
end
|
381
|
+
|
382
|
+
describe "a valid has(n) association_name" do
|
383
|
+
|
384
|
+
before(:each) do
|
385
|
+
@model = Branch
|
386
|
+
@association = :items
|
387
|
+
end
|
388
|
+
|
389
|
+
it_should_behave_like "a valid association_name"
|
390
|
+
it_should_behave_like "a valid association_name pointing to multiple resources"
|
391
|
+
|
392
|
+
end
|
393
|
+
|
394
|
+
describe "a valid has(n, :through) association_name" do
|
395
|
+
|
396
|
+
before(:each) do
|
397
|
+
@model = Branch
|
398
|
+
@association = :bookings
|
399
|
+
end
|
400
|
+
|
401
|
+
it_should_behave_like "a valid association_name"
|
402
|
+
it_should_behave_like "a valid association_name pointing to multiple resources"
|
403
|
+
|
404
|
+
end
|
405
|
+
|
406
|
+
end
|
407
|
+
|
408
|
+
end
|