snusnu-dm-accepts_nested_attributes 0.10.0 → 0.11.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/CHANGELOG +513 -0
- data/Manifest.txt +1 -2
- data/README.textile +20 -186
- data/TODO +3 -5
- data/lib/dm-accepts_nested_attributes.rb +0 -2
- data/lib/dm-accepts_nested_attributes/model.rb +46 -18
- data/lib/dm-accepts_nested_attributes/resource.rb +115 -39
- data/lib/dm-accepts_nested_attributes/transactional_save.rb +12 -1
- data/lib/dm-accepts_nested_attributes/version.rb +1 -1
- data/spec/fixtures/person.rb +15 -3
- data/spec/fixtures/profile.rb +2 -0
- data/spec/fixtures/project.rb +10 -4
- data/spec/lib/constraint_support.rb +11 -0
- data/spec/shared/belongs_to_spec.rb +18 -3
- data/spec/shared/has_1_spec.rb +41 -20
- data/spec/shared/has_n_spec.rb +32 -24
- data/spec/shared/has_n_through_spec.rb +45 -28
- data/spec/spec_helper.rb +3 -21
- metadata +5 -5
- data/lib/dm-accepts_nested_attributes/save.rb +0 -13
- data/spec/lib/rspec_tmbundle_support.rb +0 -35
data/Manifest.txt
CHANGED
@@ -10,12 +10,11 @@ lib/dm-accepts_nested_attributes.rb
|
|
10
10
|
lib/dm-accepts_nested_attributes/error_collecting.rb
|
11
11
|
lib/dm-accepts_nested_attributes/model.rb
|
12
12
|
lib/dm-accepts_nested_attributes/resource.rb
|
13
|
-
lib/dm-accepts_nested_attributes/save.rb
|
14
13
|
lib/dm-accepts_nested_attributes/transactional_save.rb
|
15
14
|
lib/dm-accepts_nested_attributes/version.rb
|
16
15
|
spec/spec.opts
|
17
16
|
spec/spec_helper.rb
|
18
|
-
spec/lib/
|
17
|
+
spec/lib/constraint_support.rb
|
19
18
|
spec/fixtures/person.rb
|
20
19
|
spec/fixtures/profile.rb
|
21
20
|
spec/fixtures/project.rb
|
data/README.textile
CHANGED
@@ -2,66 +2,7 @@ h2. dm-accepts_nested_attributes
|
|
2
2
|
|
3
3
|
A DataMapper plugin that allows nested model attribute assignment like activerecord does.
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
For more information on the progress, have a look at this README and also at
|
8
|
-
"this article":http://sick.snusnu.info/2009/04/08/dm-accepts_nested_attributes/ on my blog, where I will try to comment on the
|
9
|
-
development (problems).
|
10
|
-
|
11
|
-
h2. Current limitations
|
12
|
-
|
13
|
-
Interaction with @dm-validations@ is actually possible but not very well specced atm. I added @not null@
|
14
|
-
constraints to all spec fixtures for now, but no other custom validations. All specs still pass. However,
|
15
|
-
as long as I'm not decided on where to put the specs for interaction with @dm-validations@ (most probably
|
16
|
-
inside @dm-validations@ would be the right place for these), I won't write many more specs for these scenarios,
|
17
|
-
since it's very hard to decide where to stop, once I start writing some.
|
18
|
-
|
19
|
-
Currently, the creation of the record together with all its join models, is not handled inside a transaction.
|
20
|
-
This must definitely change! As soon as I find out why my initial experiments with transactions consistently
|
21
|
-
yielded _no such table errors_ while migrating the specsuite (see "this pastie":http://pastie.org/446060), I
|
22
|
-
will do my best to add this feature.
|
23
|
-
|
24
|
-
h2. TODO
|
25
|
-
|
26
|
-
* use transactions
|
27
|
-
* update README to include more complete usecases
|
28
|
-
* specs for custom validations (dm-validations in general)
|
29
|
-
* specs for adding errors to the parent objects
|
30
|
-
* reorganize specs and fix bugs
|
31
|
-
* Adapt to datamapper/next
|
32
|
-
|
33
|
-
h2. Implementation details
|
34
|
-
|
35
|
-
This section mainly serves as a place for me to take notes during development.
|
36
|
-
|
37
|
-
h3. Why isn't this implemented as options on association declarations?
|
38
|
-
|
39
|
-
* I somehow like the declarative style of @accepts_nested_attributes_for@ better. it jumps out immediately.
|
40
|
-
* The API for datamapper and activerecord is the same.
|
41
|
-
* association definitions can already get quite long and "unreadable". chances are you overlook it!
|
42
|
-
|
43
|
-
h3. Why doesn't accepts_nested_attributes_for take more than one association_name?
|
44
|
-
|
45
|
-
While writing the unit specs for this method, I realised that there are way too many ways to call this
|
46
|
-
method, which makes it "hard" to spec all possible calls. That's why I started to list Pros and Cons, and
|
47
|
-
decided to support only one @association_name@ per call, at least for now.
|
48
|
-
|
49
|
-
h4. Pros
|
50
|
-
|
51
|
-
* less complex code
|
52
|
-
* fewer ways to call the method (simpler to understand, easier to spec)
|
53
|
-
* easier to read (nr of calls == nr of accessible associations, this could be seen as a con also)
|
54
|
-
* easier (and more extensible) option handling
|
55
|
-
** options don't implicitly apply to _all_ associations (could be seen as a con also?)
|
56
|
-
** options can explicitly be applied to _only the desired_ associations
|
57
|
-
** reject_if option maybe often makes more sense on exactly _one_ associaton (maybe not?)
|
58
|
-
* no question what happens if the association_name is invalid (the whole call is invalid)
|
59
|
-
** with at least one _invalid_ association_name, what happens for the other _valid_ ones?
|
60
|
-
|
61
|
-
h4. Cons
|
62
|
-
|
63
|
-
* needs more method calls (overhead should be minimal)
|
64
|
-
* options that apply to more than one attribute need to be duplicated (maybe a Pro because of readability)
|
5
|
+
Current documentation can always be found at "rdoc.info":http://rdoc.info/projects/snusnu/dm-accepts_nested_attributes
|
65
6
|
|
66
7
|
h3. Examples
|
67
8
|
|
@@ -71,10 +12,6 @@ The following example illustrates the use of this plugin.
|
|
71
12
|
<code>
|
72
13
|
require "rubygems"
|
73
14
|
|
74
|
-
gem 'dm-core', '0.9.11'
|
75
|
-
gem 'dm-validations', '0.9.11'
|
76
|
-
gem 'dm-accepts_nested_attributes', '0.0.1'
|
77
|
-
|
78
15
|
require "dm-core"
|
79
16
|
require "dm-validations"
|
80
17
|
require "dm-accepts_nested_attributes"
|
@@ -89,9 +26,10 @@ class Person
|
|
89
26
|
has 1, :profile
|
90
27
|
has n, :project_memberships
|
91
28
|
has n, :projects, :through => :project_memberships
|
29
|
+
|
92
30
|
accepts_nested_attributes_for :profile
|
93
31
|
accepts_nested_attributes_for :projects
|
94
|
-
|
32
|
+
|
95
33
|
# adds the following instance methods
|
96
34
|
# #profile_attributes
|
97
35
|
# #projects_attributes
|
@@ -102,8 +40,9 @@ class Profile
|
|
102
40
|
property :id, Serial
|
103
41
|
property :person_id, Integer
|
104
42
|
belongs_to :person
|
43
|
+
|
105
44
|
accepts_nested_attributes_for :person
|
106
|
-
|
45
|
+
|
107
46
|
# adds the following instance methods
|
108
47
|
# #person_attributes
|
109
48
|
end
|
@@ -114,6 +53,7 @@ class Project
|
|
114
53
|
has n, :tasks
|
115
54
|
has n, :project_memberships
|
116
55
|
has n, :people, :through => :project_memberships
|
56
|
+
|
117
57
|
accepts_nested_attributes_for :tasks
|
118
58
|
accepts_nested_attributes_for :people
|
119
59
|
|
@@ -129,9 +69,6 @@ class ProjectMembership
|
|
129
69
|
property :project_id, Integer
|
130
70
|
belongs_to :person
|
131
71
|
belongs_to :project
|
132
|
-
|
133
|
-
# nothing added here
|
134
|
-
# code only listed to provide complete example env
|
135
72
|
end
|
136
73
|
|
137
74
|
class Task
|
@@ -139,133 +76,30 @@ class Task
|
|
139
76
|
property :id, Serial
|
140
77
|
property :project_id, Integer
|
141
78
|
belongs_to :project
|
142
|
-
|
143
|
-
# nothing added here
|
144
|
-
# code only listed to provide complete example env
|
145
79
|
end
|
146
80
|
|
147
81
|
DataMapper.auto_migrate!
|
148
82
|
</code>
|
149
83
|
</pre>
|
150
84
|
|
151
|
-
h2. Current
|
152
|
-
|
153
|
-
<pre>
|
154
|
-
<code>
|
155
|
-
DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for(:person)
|
156
|
-
- should allow to create a new person via Profile#person_attributes
|
157
|
-
- should allow to update an existing person via Profile#person_attributes
|
158
|
-
- should not allow to delete an existing person via Profile#person_attributes
|
159
|
-
|
160
|
-
DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for(:person, :allow_destroy => false)
|
161
|
-
- should allow to create a new person via Profile#person_attributes
|
162
|
-
- should allow to update an existing person via Profile#person_attributes
|
163
|
-
- should not allow to delete an existing person via Profile#person_attributes
|
164
|
-
|
165
|
-
DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for(:person, :allow_destroy = true)
|
166
|
-
- should allow to create a new person via Profile#person_attributes
|
167
|
-
- should allow to update an existing person via Profile#person_attributes
|
168
|
-
- should allow to delete an existing person via Profile#person_attributes
|
169
|
-
|
170
|
-
DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for :person, :reject_if => :foo
|
171
|
-
- should allow to create a new person via Profile#person_attributes
|
172
|
-
- should allow to update an existing person via Profile#person_attributes
|
173
|
-
- should not allow to delete an existing person via Profile#person_attributes
|
174
|
-
|
175
|
-
DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for :person, :reject_if => lambda { |attrs| true }
|
176
|
-
- should not allow to create a new person via Profile#person_attributes
|
177
|
-
- should not allow to delete an existing person via Profile#person_attributes
|
178
|
-
|
179
|
-
DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for :person, :reject_if => lambda { |attrs| false }
|
180
|
-
- should allow to create a new person via Profile#person_attributes
|
181
|
-
- should allow to update an existing person via Profile#person_attributes
|
182
|
-
- should not allow to delete an existing person via Profile#person_attributes
|
183
|
-
|
184
|
-
DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for(:profile)
|
185
|
-
- should allow to create a new profile via Person#profile_attributes
|
186
|
-
- should allow to update an existing profile via Person#profile_attributes
|
187
|
-
- should not allow to delete an existing profile via Person#profile_attributes
|
188
|
-
|
189
|
-
DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for(:profile, :allow_destroy => false)
|
190
|
-
- should allow to create a new profile via Person#profile_attributes
|
191
|
-
- should allow to update an existing profile via Person#profile_attributes
|
192
|
-
- should not allow to delete an existing profile via Person#profile_attributes
|
193
|
-
|
194
|
-
DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for(:profile, :allow_destroy => true)
|
195
|
-
- should allow to create a new profile via Person#profile_attributes
|
196
|
-
- should allow to update an existing profile via Person#profile_attributes
|
197
|
-
- should allow to delete an existing profile via Person#profile_attributes
|
198
|
-
|
199
|
-
DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for :profile, :reject_if => :foo
|
200
|
-
- should allow to create a new profile via Person#profile_attributes
|
201
|
-
- should allow to update an existing profile via Person#profile_attributes
|
202
|
-
- should not allow to delete an existing profile via Person#profile_attributes
|
203
|
-
|
204
|
-
DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for :profile, :reject_if => lambda { |attrs| true }
|
205
|
-
- should not allow to create a new profile via Person#profile_attributes
|
206
|
-
- should not allow to delete an existing profile via Person#profile_attributes
|
207
|
-
|
208
|
-
DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for :profile, :reject_if => lambda { |attrs| false }
|
209
|
-
- should allow to create a new profile via Person#profile_attributes
|
210
|
-
- should allow to update an existing profile via Person#profile_attributes
|
211
|
-
- should not allow to delete an existing profile via Person#profile_attributes
|
212
|
-
|
213
|
-
DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for(:tasks)
|
214
|
-
- should allow to create a new task via Project#tasks_attributes
|
215
|
-
- should allow to update an existing task via Project#tasks_attributes
|
216
|
-
- should not allow to delete an existing task via Profile#tasks_attributes
|
217
|
-
|
218
|
-
DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for(:tasks, :allow_destroy => false)
|
219
|
-
- should allow to create a new task via Project#tasks_attributes
|
220
|
-
- should allow to update an existing task via Project#tasks_attributes
|
221
|
-
- should not allow to delete an existing task via Profile#tasks_attributes
|
222
|
-
|
223
|
-
DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for(:tasks, :allow_destroy => true)
|
224
|
-
- should allow to create a new task via Project#tasks_attributes
|
225
|
-
- should allow to update an existing task via Project#tasks_attributes
|
226
|
-
- should allow to delete an existing task via Profile#tasks_attributes
|
227
|
-
|
228
|
-
DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for :tasks, :reject_if => :foo
|
229
|
-
- should allow to create a new task via Project#tasks_attributes
|
230
|
-
- should allow to update an existing task via Project#tasks_attributes
|
231
|
-
- should not allow to delete an existing task via Profile#tasks_attributes
|
232
|
-
|
233
|
-
DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for :tasks, :reject_if => lambda { |attrs| true }
|
234
|
-
- should not allow to create a new task via Project#tasks_attributes
|
235
|
-
- should not allow to delete an existing task via Profile#tasks_attributes
|
236
|
-
|
237
|
-
DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for :tasks, :reject_if => lambda { |attrs| false }
|
238
|
-
- should allow to create a new task via Project#tasks_attributes
|
239
|
-
- should allow to update an existing task via Project#tasks_attributes
|
240
|
-
- should not allow to delete an existing task via Profile#tasks_attributes
|
241
|
-
|
242
|
-
DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for(:projects)
|
243
|
-
- should allow to create a new project via Person#projects_attributes
|
244
|
-
- should allow to update an existing project via Person#projects_attributes
|
245
|
-
- should not allow to delete an existing project via Person#projects_attributes
|
85
|
+
h2. Current limitations
|
246
86
|
|
247
|
-
|
248
|
-
- should allow to create a new project via Person#projects_attributes
|
249
|
-
- should allow to update an existing project via Person#projects_attributes
|
250
|
-
- should not allow to delete an existing project via Person#projects_attributes
|
87
|
+
There are some minor limitations at the moment but I hope that these will be resolved some time soon.
|
251
88
|
|
252
|
-
|
253
|
-
- should allow to create a new project via Person#projects_attributes
|
254
|
-
- should allow to update an existing project via Person#projects_attributes
|
255
|
-
- should allow to delete an existing project via Person#projects_attributes
|
89
|
+
h3. marking for destruction
|
256
90
|
|
257
|
-
|
258
|
-
- should allow to create a new project via Person#projects_attributes
|
259
|
-
- should allow to update an existing project via Person#projects_attributes
|
260
|
-
- should not allow to delete an existing project via Person#projects_attributes
|
91
|
+
@many_to_one, one_to_one and one_to_many@ relationships all perform the deleting of nested models by _marking them for destruction_. This means, that as long as you don't call #save on your resource, the nested models won't get destroyed. However, this is currently _not_ the case for @many_to_many@ relationships. The @join and target resources@ both get destroyed immediately when calling the nested attributes writer.
|
261
92
|
|
262
|
-
|
263
|
-
- should not allow to create a new project via Person#projects_attributes
|
264
|
-
- should not allow to delete an existing project via Person#projects_attributes
|
93
|
+
For the above example this would mean that when you issue the code below, it would result in immediate deletion of the intermediate @ProjectMembership@ and the @Project@ resource.
|
265
94
|
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
- should not allow to delete an existing project via Person#projects_attributes
|
95
|
+
<pre>
|
96
|
+
<code>
|
97
|
+
person.projects_attributes = { :id => valid_project_id, :_delete => true }
|
270
98
|
</code>
|
271
99
|
</pre>
|
100
|
+
|
101
|
+
h2. TODO
|
102
|
+
|
103
|
+
* collect validation errors from related resources
|
104
|
+
* update README to include more complete usecases
|
105
|
+
* think about replacing :reject_if with :if and :unless
|
data/TODO
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
TODO
|
2
2
|
====
|
3
3
|
|
4
|
-
*
|
5
|
-
*
|
6
|
-
* think about
|
7
|
-
* think about :allow_destroy accepting the same parameters like :reject_if
|
8
|
-
(Symbol, String, #call)
|
4
|
+
* collect validation errors from related resources
|
5
|
+
* update README to include more complete usecases
|
6
|
+
* think about replacing :reject_if with :if and :unless
|
@@ -4,10 +4,8 @@ require 'dm-core'
|
|
4
4
|
dir = Pathname(__FILE__).dirname.expand_path / 'dm-accepts_nested_attributes'
|
5
5
|
|
6
6
|
require dir / 'model'
|
7
|
-
#require dir / 'save'
|
8
7
|
require dir / 'resource'
|
9
8
|
|
10
9
|
# Activate the plugin
|
11
10
|
DataMapper::Model.append_extensions(DataMapper::NestedAttributes::Model)
|
12
|
-
#DataMapper::Model.append_inclusions(DataMapper::NestedAttributes::Save)
|
13
11
|
DataMapper::Model.append_inclusions(DataMapper::NestedAttributes::CommonResourceSupport)
|
@@ -2,7 +2,8 @@ module DataMapper
|
|
2
2
|
module NestedAttributes
|
3
3
|
|
4
4
|
##
|
5
|
-
# raised by
|
5
|
+
# Named plugin exception that gets raised by
|
6
|
+
# @see accepts_nested_attributes_for
|
6
7
|
# if the passed options don't make sense
|
7
8
|
class InvalidOptions < ArgumentError; end
|
8
9
|
|
@@ -11,22 +12,26 @@ module DataMapper
|
|
11
12
|
##
|
12
13
|
# Allows any association to accept nested attributes.
|
13
14
|
#
|
14
|
-
# @param
|
15
|
+
# @param [Symbol, String] association_name
|
15
16
|
# The name of the association that should accept nested attributes
|
16
17
|
#
|
17
|
-
# @param
|
18
|
+
# @param [Hash, nil] options
|
18
19
|
# List of resources to initialize the Collection with
|
19
20
|
#
|
20
|
-
# @option
|
21
|
+
# @option [Symbol, String, #call] :reject_if
|
21
22
|
# An instance method name or an object that respond_to?(:call), which
|
22
23
|
# stops a new record from being created, if it evaluates to true.
|
23
24
|
#
|
24
|
-
# @option
|
25
|
+
# @option [true, false] :allow_destroy
|
25
26
|
# If true, allow destroying the association via the generated writer
|
26
27
|
# If false, prevent destroying the association via the generated writer
|
27
28
|
# defaults to false
|
28
29
|
#
|
30
|
+
# @raise [DataMapper::NestedAttributes::InvalidOptions]
|
31
|
+
# A named exception class indicating invalid options
|
32
|
+
#
|
29
33
|
# @return nil
|
34
|
+
#
|
30
35
|
def accepts_nested_attributes_for(association_name, options = {})
|
31
36
|
|
32
37
|
# ----------------------------------------------------------------------------------
|
@@ -51,8 +56,6 @@ module DataMapper
|
|
51
56
|
|
52
57
|
include ::DataMapper::NestedAttributes::Resource
|
53
58
|
|
54
|
-
add_save_behavior
|
55
|
-
|
56
59
|
# TODO i wonder if this is the best place here?
|
57
60
|
# the transactional save behavior is definitely not needed for all resources,
|
58
61
|
# but it's necessary for resources that accept nested attributes
|
@@ -78,33 +81,58 @@ module DataMapper
|
|
78
81
|
end
|
79
82
|
|
80
83
|
##
|
81
|
-
# The options given to the accepts_nested_attributes method
|
82
|
-
#
|
84
|
+
# The options given to the accepts_nested_attributes method.
|
85
|
+
#
|
86
|
+
# @return [Hash]
|
87
|
+
# The options given to the accepts_nested_attributes method
|
83
88
|
#
|
84
|
-
# @return [Hash] The options given to the accepts_nested_attributes method
|
85
|
-
# @see accepts_nested_attributes
|
86
89
|
def options_for_nested_attributes
|
87
90
|
@options_for_nested_attributes ||= {}
|
88
91
|
end
|
89
92
|
|
90
93
|
private
|
91
94
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
95
|
+
##
|
96
|
+
# Provides a hook to include or disable customized transactional save behavior.
|
97
|
+
# Override this method to customize the implementation or disable it altogether.
|
98
|
+
# The current implementation in @see DataMapper::NestedAttributes::TransactionalSave
|
99
|
+
# simply wraps the saving of the complete object tree inside a transaction
|
100
|
+
# and rolls back in case any exceptions are raised, or any of the calls to
|
101
|
+
# @see DataMapper::Resource#save returned false
|
102
|
+
#
|
103
|
+
# @return Not specified
|
104
|
+
#
|
97
105
|
def add_transactional_save_behavior
|
98
106
|
require Pathname(__FILE__).dirname.expand_path + 'transactional_save'
|
99
107
|
include ::DataMapper::NestedAttributes::TransactionalSave
|
100
108
|
end
|
101
109
|
|
110
|
+
##
|
111
|
+
# Provides a hook to include or disable customized error collecting behavior.
|
112
|
+
# Overwrite this method to customize the implementation or disable it altogether.
|
113
|
+
# The current implementation in @see DataMapper::NestedAttributes::ValidationErrorCollecting
|
114
|
+
# simply attaches all errors of related resources to the object that was initially saved.
|
115
|
+
#
|
116
|
+
# @return Not specified
|
117
|
+
#
|
102
118
|
def add_error_collection_behavior
|
103
119
|
require Pathname(__FILE__).dirname.expand_path + 'error_collecting'
|
104
120
|
include ::DataMapper::NestedAttributes::ValidationErrorCollecting
|
105
121
|
end
|
106
|
-
|
107
|
-
|
122
|
+
|
123
|
+
##
|
124
|
+
# Checks options passed to @see accepts_nested_attributes_for
|
125
|
+
# If any of the given options is invalid, this method will raise
|
126
|
+
# @see DataMapper::NestedAttributes::InvalidOptions
|
127
|
+
#
|
128
|
+
# @param [Hash, nil] options
|
129
|
+
# The options passed to @see accepts_nested_attributes_for
|
130
|
+
#
|
131
|
+
# @raise [DataMapper::NestedAttributes::InvalidOptions]
|
132
|
+
# A named exception class indicating invalid options
|
133
|
+
#
|
134
|
+
# @return [nil]
|
135
|
+
#
|
108
136
|
def assert_valid_options_for_nested_attributes(options)
|
109
137
|
|
110
138
|
assert_kind_of 'options', options, Hash
|
@@ -1,6 +1,14 @@
|
|
1
1
|
module DataMapper
|
2
2
|
module NestedAttributes
|
3
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.
|
4
12
|
module Resource
|
5
13
|
|
6
14
|
##
|
@@ -15,16 +23,27 @@ module DataMapper
|
|
15
23
|
# For now, this method basically is a no-op, but at least it provides a hook where
|
16
24
|
# everyone can perform it's own sanitization by overwriting this method.
|
17
25
|
#
|
18
|
-
# @
|
26
|
+
# @param attributes [Hash]
|
27
|
+
# The attributes to sanitize
|
28
|
+
#
|
29
|
+
# @return [Hash]
|
30
|
+
# The sanitized attributes
|
31
|
+
#
|
19
32
|
def sanitize_nested_attributes(attributes)
|
20
33
|
attributes # noop
|
21
34
|
end
|
22
35
|
|
23
36
|
private
|
24
37
|
|
38
|
+
##
|
25
39
|
# Attribute hash keys that should not be assigned as normal attributes.
|
26
|
-
#
|
27
|
-
|
40
|
+
#
|
41
|
+
# @return [#each]
|
42
|
+
# The model key and :_delete, the latter being a special value
|
43
|
+
# used to mark a resource for destruction
|
44
|
+
def unassignable_keys
|
45
|
+
model.key.to_a << :_delete
|
46
|
+
end
|
28
47
|
|
29
48
|
|
30
49
|
##
|
@@ -50,12 +69,12 @@ module DataMapper
|
|
50
69
|
def assign_nested_attributes_for_related_resource(relationship, attributes)
|
51
70
|
if attributes[:id].blank?
|
52
71
|
return if reject_new_record?(relationship, attributes)
|
53
|
-
new_record = relationship.target_model.new(attributes.except(*
|
72
|
+
new_record = relationship.target_model.new(attributes.except(*unassignable_keys))
|
54
73
|
relationship.set(self, new_record)
|
55
74
|
else
|
56
75
|
existing_record = relationship.get(self)
|
57
76
|
if existing_record && existing_record.id.to_s == attributes[:id].to_s
|
58
|
-
|
77
|
+
assign_or_mark_for_destruction(relationship, existing_record, attributes)
|
59
78
|
end
|
60
79
|
end
|
61
80
|
end
|
@@ -104,11 +123,11 @@ module DataMapper
|
|
104
123
|
|
105
124
|
if attributes[:id].blank?
|
106
125
|
next if reject_new_record?(relationship, attributes)
|
107
|
-
relationship.get(self).new(attributes.except(*
|
126
|
+
relationship.get(self).new(attributes.except(*unassignable_keys))
|
108
127
|
else
|
109
128
|
collection = relationship.get(self)
|
110
|
-
if existing_record = collection.
|
111
|
-
|
129
|
+
if existing_record = collection.get(attributes[:id])
|
130
|
+
assign_or_mark_for_destruction(relationship, existing_record, attributes)
|
112
131
|
end
|
113
132
|
end
|
114
133
|
|
@@ -129,54 +148,82 @@ module DataMapper
|
|
129
148
|
# All attributes except @see UNASSIGNABLE_KEYS will be assigned
|
130
149
|
#
|
131
150
|
# @return nil
|
132
|
-
def
|
151
|
+
def assign_or_mark_for_destruction(relationship, resource, attributes)
|
133
152
|
allow_destroy = self.class.options_for_nested_attributes[relationship][:allow_destroy]
|
134
153
|
if has_delete_flag?(attributes) && allow_destroy
|
135
|
-
|
154
|
+
if relationship.is_a?(DataMapper::Associations::ManyToMany::Relationship)
|
155
|
+
|
156
|
+
target_query = relationship.target_key.zip(resource.key).to_hash
|
157
|
+
target_collection = relationship.get(self, target_query)
|
158
|
+
|
159
|
+
unless target_collection.empty?
|
160
|
+
|
161
|
+
target_collection.send(:intermediaries, target_collection).each do |intermediary|
|
162
|
+
intermediary.mark_for_destruction
|
163
|
+
end
|
164
|
+
|
165
|
+
target_collection.each { |r| r.mark_for_destruction }
|
166
|
+
|
167
|
+
end
|
168
|
+
|
169
|
+
else
|
170
|
+
resource.mark_for_destruction
|
171
|
+
end
|
136
172
|
else
|
137
|
-
resource.
|
173
|
+
resource.attributes = attributes.except(*unassignable_keys)
|
174
|
+
resource.save
|
138
175
|
end
|
139
176
|
end
|
140
177
|
|
141
178
|
##
|
142
|
-
# Determines if
|
179
|
+
# Determines if the given attributes hash contains a truthy :_delete key.
|
143
180
|
#
|
144
|
-
# @param
|
181
|
+
# @param attributes [Hash] The attributes to test
|
145
182
|
#
|
146
|
-
# @return [
|
147
|
-
# true, if
|
148
|
-
|
149
|
-
|
150
|
-
# TODO find out if this activerecord code needs to be ported
|
151
|
-
# ConnectionAdapters::Column.value_to_boolean hash['_delete']
|
152
|
-
hash[:_delete]
|
183
|
+
# @return [TrueClass, FalseClass]
|
184
|
+
# true, if attributes contains a truthy :_delete key
|
185
|
+
def has_delete_flag?(attributes)
|
186
|
+
!!attributes[:_delete]
|
153
187
|
end
|
154
188
|
|
155
189
|
##
|
156
|
-
# Determines if a new record should be
|
157
|
-
#
|
158
|
-
#
|
190
|
+
# Determines if a new record should be built with the given attributes.
|
191
|
+
# Rejects a new record if @see has_delete_flag? returns true for the given attributes,
|
192
|
+
# or if a :reject_if guard exists for the passed relationship that evaluates to +true+.
|
159
193
|
#
|
160
194
|
# @param relationship [DataMapper::Associations::Relationship]
|
161
|
-
# The relationship backing the association.
|
162
|
-
# Assignment will happen on the target end of the relationship
|
195
|
+
# The relationship backing the association.
|
163
196
|
#
|
164
197
|
# @param attributes [Hash]
|
165
|
-
# The attributes to
|
166
|
-
# All attributes except @see UNASSIGNABLE_KEYS will be assigned
|
198
|
+
# The attributes to test with @see has_delete_flag?
|
167
199
|
#
|
168
|
-
# @return [
|
169
|
-
# true, if the given attributes
|
170
|
-
# false, otherwise
|
200
|
+
# @return [TrueClass, FalseClass]
|
201
|
+
# true, if the given attributes will be rejected
|
171
202
|
def reject_new_record?(relationship, attributes)
|
172
203
|
guard = self.class.options_for_nested_attributes[relationship][:reject_if]
|
173
204
|
return false if guard.nil? # if relationship guard is nil, nothing will be rejected
|
174
205
|
has_delete_flag?(attributes) || evaluate_reject_new_record_guard(guard, attributes)
|
175
206
|
end
|
176
207
|
|
208
|
+
##
|
209
|
+
# Evaluates the given guard by calling it with the given attributes
|
210
|
+
#
|
211
|
+
# @param [Symbol, String, #call] guard
|
212
|
+
# An instance method name or an object that respond_to?(:call), which
|
213
|
+
# would stop a new record from being created, if it evaluates to true.
|
214
|
+
#
|
215
|
+
# @param [Hash] attributes
|
216
|
+
# The attributes to pass to the guard for evaluating if it should reject
|
217
|
+
# the creation of a new resource
|
218
|
+
#
|
219
|
+
# @raise ArgumentError
|
220
|
+
# If the given guard doesn't match [Symbol, String, #call]
|
221
|
+
#
|
222
|
+
# @return [true, false]
|
223
|
+
# The value returned by evaluating the guard
|
177
224
|
def evaluate_reject_new_record_guard(guard, attributes)
|
178
225
|
if guard.is_a?(Symbol) || guard.is_a?(String)
|
179
|
-
send(guard)
|
226
|
+
send(guard, attributes)
|
180
227
|
elsif guard.respond_to?(:call)
|
181
228
|
guard.call(attributes)
|
182
229
|
else
|
@@ -185,18 +232,48 @@ module DataMapper
|
|
185
232
|
end
|
186
233
|
end
|
187
234
|
|
188
|
-
|
189
|
-
|
190
|
-
|
235
|
+
##
|
236
|
+
# Make sure to return a collection of attribute hashes.
|
237
|
+
# If passed an attributes hash, map it to its attributes
|
238
|
+
#
|
239
|
+
# @param attributes [Hash, #each]
|
240
|
+
# An attributes hash or a collection of attribute hashes
|
241
|
+
#
|
242
|
+
# @return [#each]
|
243
|
+
# A collection of attribute hashes
|
244
|
+
def normalize_attributes_collection(attributes)
|
245
|
+
if attributes.is_a?(Hash)
|
246
|
+
attributes.map { |_, attributes| attributes }
|
191
247
|
else
|
192
|
-
|
248
|
+
attributes
|
193
249
|
end
|
194
250
|
end
|
195
251
|
|
196
252
|
end
|
197
253
|
|
254
|
+
##
|
255
|
+
# This module provides basic support for accepting nested attributes,
|
256
|
+
# that every @see DataMapper::Resource must include. It includes methods
|
257
|
+
# that allow a resource to be marked for destruction and it provides an
|
258
|
+
# overwritten version of @see DataMapper::Resource#save_self that either
|
259
|
+
# destroys a resource if it's @see marked_for_destruction? or performs
|
260
|
+
# an ordinary save by delegating to super
|
261
|
+
#
|
198
262
|
module CommonResourceSupport
|
199
263
|
|
264
|
+
##
|
265
|
+
# If self is marked for destruction, destroy self
|
266
|
+
# else, save self by delegating to super method.
|
267
|
+
#
|
268
|
+
# @return The same value that super returns
|
269
|
+
def save_self
|
270
|
+
if marked_for_destruction?
|
271
|
+
saved? ? destroy : true
|
272
|
+
else
|
273
|
+
super
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
200
277
|
##
|
201
278
|
# remove mark for destruction if present
|
202
279
|
# before delegating reload behavior to super
|
@@ -210,11 +287,10 @@ module DataMapper
|
|
210
287
|
##
|
211
288
|
# Test if this resource is marked for destruction
|
212
289
|
#
|
213
|
-
# @return [
|
214
|
-
# true
|
215
|
-
# false, otherwise
|
290
|
+
# @return [true, false]
|
291
|
+
# true if this resource is marked for destruction
|
216
292
|
def marked_for_destruction?
|
217
|
-
|
293
|
+
!!@marked_for_destruction
|
218
294
|
end
|
219
295
|
|
220
296
|
##
|