snusnu-dm-accepts_nested_attributes 0.10.0 → 0.11.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
##
|