snusnu-dm-accepts_nested_attributes 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +23 -0
- data/History.txt +1 -0
- data/LICENSE +20 -0
- data/Manifest.txt +29 -0
- data/README.textile +250 -0
- data/Rakefile +32 -0
- data/TODO +3 -0
- data/lib/dm-accepts_nested_attributes.rb +18 -0
- data/lib/dm-accepts_nested_attributes/associations.rb +55 -0
- data/lib/dm-accepts_nested_attributes/nested_attributes.rb +300 -0
- data/lib/dm-accepts_nested_attributes/version.rb +7 -0
- data/spec/fixtures/person.rb +8 -0
- data/spec/fixtures/profile.rb +7 -0
- data/spec/fixtures/project.rb +8 -0
- data/spec/fixtures/project_membership.rb +8 -0
- data/spec/fixtures/task.rb +7 -0
- data/spec/integration/belongs_to_spec.rb +204 -0
- data/spec/integration/has_1_spec.rb +193 -0
- data/spec/integration/has_n_spec.rb +181 -0
- data/spec/integration/has_n_through_spec.rb +200 -0
- data/spec/shared/rspec_tmbundle_support.rb +35 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +58 -0
- data/spec/unit/accepts_nested_attributes_for_spec.rb +454 -0
- data/spec/unit/resource_spec.rb +174 -0
- data/tasks/gemspec.rb +10 -0
- data/tasks/hoe.rb +46 -0
- data/tasks/install.rb +15 -0
- data/tasks/spec.rb +25 -0
- metadata +103 -0
data/.gitignore
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
test_log
|
2
|
+
pkg
|
3
|
+
pkg/*
|
4
|
+
*/pkg/*
|
5
|
+
bundle
|
6
|
+
bundle/*
|
7
|
+
doc
|
8
|
+
*.log
|
9
|
+
log
|
10
|
+
!log*.rb
|
11
|
+
*/log
|
12
|
+
log/*
|
13
|
+
*/log/*
|
14
|
+
coverage
|
15
|
+
*/coverage
|
16
|
+
lib/dm-more.rb
|
17
|
+
*.db
|
18
|
+
nbproject
|
19
|
+
.DS_Store
|
20
|
+
rspec_report.html
|
21
|
+
*.swp
|
22
|
+
_Yardoc
|
23
|
+
*/ri
|
data/History.txt
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
|
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2008 Martin Gamsjäger
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Manifest.txt
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
.gitignore
|
2
|
+
History.txt
|
3
|
+
LICENSE
|
4
|
+
Manifest.txt
|
5
|
+
README.textile
|
6
|
+
Rakefile
|
7
|
+
TODO
|
8
|
+
lib/dm-accepts_nested_attributes.rb
|
9
|
+
lib/dm-accepts_nested_attributes/associations.rb
|
10
|
+
lib/dm-accepts_nested_attributes/nested_attributes.rb
|
11
|
+
lib/dm-accepts_nested_attributes/version.rb
|
12
|
+
spec/fixtures/person.rb
|
13
|
+
spec/fixtures/profile.rb
|
14
|
+
spec/fixtures/project.rb
|
15
|
+
spec/fixtures/project_membership.rb
|
16
|
+
spec/fixtures/task.rb
|
17
|
+
spec/integration/belongs_to_spec.rb
|
18
|
+
spec/integration/has_1_spec.rb
|
19
|
+
spec/integration/has_n_spec.rb
|
20
|
+
spec/integration/has_n_through_spec.rb
|
21
|
+
spec/shared/rspec_tmbundle_support.rb
|
22
|
+
spec/spec.opts
|
23
|
+
spec/spec_helper.rb
|
24
|
+
spec/unit/accepts_nested_attributes_for_spec.rb
|
25
|
+
spec/unit/resource_spec.rb
|
26
|
+
tasks/gemspec.rb
|
27
|
+
tasks/hoe.rb
|
28
|
+
tasks/install.rb
|
29
|
+
tasks/spec.rb
|
data/README.textile
ADDED
@@ -0,0 +1,250 @@
|
|
1
|
+
h2. dm-accepts_nested_attributes
|
2
|
+
|
3
|
+
A DataMapper plugin that allows nested model attribute assignment like activerecord does.
|
4
|
+
|
5
|
+
At the end of this file, you can see a list of all current integration specs.
|
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
|
+
h3. Why isn't this implemented as options on association declarations?
|
12
|
+
|
13
|
+
* I somehow like the declarative style of @accepts_nested_attributes_for@ better. it jumps out immediately.
|
14
|
+
* The API for datamapper and activerecord is the same.
|
15
|
+
* association definitions can already get quite long and "unreadable". chances are you overlook it!
|
16
|
+
|
17
|
+
h3. Why doesn't accepts_nested_attributes_for take more than one association_name?
|
18
|
+
|
19
|
+
While writing the unit specs for this method, I realised that there are way too many ways to call this
|
20
|
+
method, which makes it "hard" to spec all possible calls. That's why I started to list Pros and Cons, and
|
21
|
+
decided to support only one @association_name@ per call, at least for now.
|
22
|
+
|
23
|
+
h4. Pros
|
24
|
+
|
25
|
+
* less complex code
|
26
|
+
* fewer ways to call the method (simpler to understand, easier to spec)
|
27
|
+
* easier to read (nr of calls == nr of accessible associations, this could be seen as a con also)
|
28
|
+
* easier (and more extensible) option handling
|
29
|
+
** options don't implicitly apply to _all_ associations (could be seen as a con also?)
|
30
|
+
** options can explicitly be applied to _only the desired_ associations
|
31
|
+
** reject_if option maybe often makes more sense on exactly _one_ associaton (maybe not?)
|
32
|
+
* no question what happens if the association_name is invalid (the whole call is invalid)
|
33
|
+
** with at least one _invalid_ association_name, what happens for the other _valid_ ones?
|
34
|
+
|
35
|
+
h4. Cons
|
36
|
+
|
37
|
+
* needs more method calls (overhead should be minimal)
|
38
|
+
* options that apply to more than one attribute need to be duplicated (maybe a Pro because of readability)
|
39
|
+
|
40
|
+
h3. Examples
|
41
|
+
|
42
|
+
The following example illustrates the use of this plugin.
|
43
|
+
|
44
|
+
<pre>
|
45
|
+
<code>
|
46
|
+
require "rubygems"
|
47
|
+
|
48
|
+
gem 'dm-core', '0.9.11'
|
49
|
+
gem 'dm-validations', '0.9.11'
|
50
|
+
gem 'dm-accepts_nested_attributes', '0.0.1'
|
51
|
+
|
52
|
+
require "dm-core"
|
53
|
+
require "dm-validations"
|
54
|
+
require "dm-accepts_nested_attributes"
|
55
|
+
|
56
|
+
DataMapper::Logger.new(STDOUT, :debug)
|
57
|
+
DataMapper.setup(:default, 'sqlite3::memory:')
|
58
|
+
|
59
|
+
class Person
|
60
|
+
include DataMapper::Resource
|
61
|
+
property :id, Serial
|
62
|
+
property :name, String
|
63
|
+
has 1, :profile
|
64
|
+
has n, :project_memberships
|
65
|
+
has n, :projects, :through => :project_memberships
|
66
|
+
accepts_nested_attributes_for :profile
|
67
|
+
accepts_nested_attributes_for :projects
|
68
|
+
|
69
|
+
# adds the following instance methods
|
70
|
+
# #profile_attributes
|
71
|
+
# #projects_attributes
|
72
|
+
end
|
73
|
+
|
74
|
+
class Profile
|
75
|
+
include DataMapper::Resource
|
76
|
+
property :id, Serial
|
77
|
+
property :person_id, Integer
|
78
|
+
belongs_to :person
|
79
|
+
accepts_nested_attributes_for :person
|
80
|
+
|
81
|
+
# adds the following instance methods
|
82
|
+
# #person_attributes
|
83
|
+
end
|
84
|
+
|
85
|
+
class Project
|
86
|
+
include DataMapper::Resource
|
87
|
+
property :id, Serial
|
88
|
+
has n, :tasks
|
89
|
+
has n, :project_memberships
|
90
|
+
has n, :people, :through => :project_memberships
|
91
|
+
accepts_nested_attributes_for :tasks
|
92
|
+
accepts_nested_attributes_for :people
|
93
|
+
|
94
|
+
# adds the following instance methods
|
95
|
+
# #tasks_attributes
|
96
|
+
# #people_attributes
|
97
|
+
end
|
98
|
+
|
99
|
+
class ProjectMembership
|
100
|
+
include DataMapper::Resource
|
101
|
+
property :id, Serial
|
102
|
+
property :person_id, Integer
|
103
|
+
property :project_id, Integer
|
104
|
+
belongs_to :person
|
105
|
+
belongs_to :project
|
106
|
+
|
107
|
+
# nothing added here
|
108
|
+
# code only listed to provide complete example env
|
109
|
+
end
|
110
|
+
|
111
|
+
class Task
|
112
|
+
include DataMapper::Resource
|
113
|
+
property :id, Serial
|
114
|
+
property :project_id, Integer
|
115
|
+
belongs_to :project
|
116
|
+
|
117
|
+
# nothing added here
|
118
|
+
# code only listed to provide complete example env
|
119
|
+
end
|
120
|
+
|
121
|
+
DataMapper.auto_migrate!
|
122
|
+
</code>
|
123
|
+
</pre>
|
124
|
+
|
125
|
+
h2. Current Integration Specs
|
126
|
+
|
127
|
+
<pre>
|
128
|
+
<code>
|
129
|
+
DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for(:person)
|
130
|
+
- should allow to create a new person via Profile#person_attributes
|
131
|
+
- should allow to update an existing person via Profile#person_attributes
|
132
|
+
- should not allow to delete an existing person via Profile#person_attributes
|
133
|
+
|
134
|
+
DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for(:person, :allow_destroy => false)
|
135
|
+
- should allow to create a new person via Profile#person_attributes
|
136
|
+
- should allow to update an existing person via Profile#person_attributes
|
137
|
+
- should not allow to delete an existing person via Profile#person_attributes
|
138
|
+
|
139
|
+
DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for(:person, :allow_destroy = true)
|
140
|
+
- should allow to create a new person via Profile#person_attributes
|
141
|
+
- should allow to update an existing person via Profile#person_attributes
|
142
|
+
- should allow to delete an existing person via Profile#person_attributes
|
143
|
+
|
144
|
+
DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for :person, :reject_if => :foo
|
145
|
+
- should allow to create a new person via Profile#person_attributes
|
146
|
+
- should allow to update an existing person via Profile#person_attributes
|
147
|
+
- should not allow to delete an existing person via Profile#person_attributes
|
148
|
+
|
149
|
+
DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for :person, :reject_if => lambda { |attrs| true }
|
150
|
+
- should not allow to create a new person via Profile#person_attributes
|
151
|
+
- should not allow to delete an existing person via Profile#person_attributes
|
152
|
+
|
153
|
+
DataMapper::NestedAttributes Profile.belongs_to(:person) accepts_nested_attributes_for :person, :reject_if => lambda { |attrs| false }
|
154
|
+
- should allow to create a new person via Profile#person_attributes
|
155
|
+
- should allow to update an existing person via Profile#person_attributes
|
156
|
+
- should not allow to delete an existing person via Profile#person_attributes
|
157
|
+
|
158
|
+
DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for(:profile)
|
159
|
+
- should allow to create a new profile via Person#profile_attributes
|
160
|
+
- should allow to update an existing profile via Person#profile_attributes
|
161
|
+
- should not allow to delete an existing profile via Person#profile_attributes
|
162
|
+
|
163
|
+
DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for(:profile, :allow_destroy => false)
|
164
|
+
- should allow to create a new profile via Person#profile_attributes
|
165
|
+
- should allow to update an existing profile via Person#profile_attributes
|
166
|
+
- should not allow to delete an existing profile via Person#profile_attributes
|
167
|
+
|
168
|
+
DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for(:profile, :allow_destroy => true)
|
169
|
+
- should allow to create a new profile via Person#profile_attributes
|
170
|
+
- should allow to update an existing profile via Person#profile_attributes
|
171
|
+
- should allow to delete an existing profile via Person#profile_attributes
|
172
|
+
|
173
|
+
DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for :profile, :reject_if => :foo
|
174
|
+
- should allow to create a new profile via Person#profile_attributes
|
175
|
+
- should allow to update an existing profile via Person#profile_attributes
|
176
|
+
- should not allow to delete an existing profile via Person#profile_attributes
|
177
|
+
|
178
|
+
DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for :profile, :reject_if => lambda { |attrs| true }
|
179
|
+
- should not allow to create a new profile via Person#profile_attributes
|
180
|
+
- should not allow to delete an existing profile via Person#profile_attributes
|
181
|
+
|
182
|
+
DataMapper::NestedAttributes Person.has(1, :profile) accepts_nested_attributes_for :profile, :reject_if => lambda { |attrs| false }
|
183
|
+
- should allow to create a new profile via Person#profile_attributes
|
184
|
+
- should allow to update an existing profile via Person#profile_attributes
|
185
|
+
- should not allow to delete an existing profile via Person#profile_attributes
|
186
|
+
|
187
|
+
DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for(:tasks)
|
188
|
+
- should allow to create a new task via Project#tasks_attributes
|
189
|
+
- should allow to update an existing task via Project#tasks_attributes
|
190
|
+
- should not allow to delete an existing task via Profile#tasks_attributes
|
191
|
+
|
192
|
+
DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for(:tasks, :allow_destroy => false)
|
193
|
+
- should allow to create a new task via Project#tasks_attributes
|
194
|
+
- should allow to update an existing task via Project#tasks_attributes
|
195
|
+
- should not allow to delete an existing task via Profile#tasks_attributes
|
196
|
+
|
197
|
+
DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for(:tasks, :allow_destroy => true)
|
198
|
+
- should allow to create a new task via Project#tasks_attributes
|
199
|
+
- should allow to update an existing task via Project#tasks_attributes
|
200
|
+
- should allow to delete an existing task via Profile#tasks_attributes
|
201
|
+
|
202
|
+
DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for :tasks, :reject_if => :foo
|
203
|
+
- should allow to create a new task via Project#tasks_attributes
|
204
|
+
- should allow to update an existing task via Project#tasks_attributes
|
205
|
+
- should not allow to delete an existing task via Profile#tasks_attributes
|
206
|
+
|
207
|
+
DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for :tasks, :reject_if => lambda { |attrs| true }
|
208
|
+
- should not allow to create a new task via Project#tasks_attributes
|
209
|
+
- should not allow to delete an existing task via Profile#tasks_attributes
|
210
|
+
|
211
|
+
DataMapper::NestedAttributes Project.has(n, :tasks) accepts_nested_attributes_for :tasks, :reject_if => lambda { |attrs| false }
|
212
|
+
- should allow to create a new task via Project#tasks_attributes
|
213
|
+
- should allow to update an existing task via Project#tasks_attributes
|
214
|
+
- should not allow to delete an existing task via Profile#tasks_attributes
|
215
|
+
|
216
|
+
DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for(:projects)
|
217
|
+
- should allow to create a new project via Person#projects_attributes
|
218
|
+
- should allow to update an existing project via Person#projects_attributes
|
219
|
+
- should not allow to delete an existing project via Person#projects_attributes
|
220
|
+
|
221
|
+
DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for(:projects, :allow_destroy => false)
|
222
|
+
- should allow to create a new project via Person#projects_attributes
|
223
|
+
- should allow to update an existing project via Person#projects_attributes
|
224
|
+
- should not allow to delete an existing project via Person#projects_attributes
|
225
|
+
|
226
|
+
DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for(:projects, :allow_destroy = true)
|
227
|
+
- should allow to create a new project via Person#projects_attributes
|
228
|
+
- should allow to update an existing project via Person#projects_attributes
|
229
|
+
- should allow to delete an existing project via Person#projects_attributes
|
230
|
+
|
231
|
+
DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for :projects, :reject_if => :foo
|
232
|
+
- should allow to create a new project via Person#projects_attributes
|
233
|
+
- should allow to update an existing project via Person#projects_attributes
|
234
|
+
- should not allow to delete an existing project via Person#projects_attributes
|
235
|
+
|
236
|
+
DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for :projects, :reject_if => lambda { |attrs| true }
|
237
|
+
- should not allow to create a new project via Person#projects_attributes
|
238
|
+
- should not allow to delete an existing project via Person#projects_attributes
|
239
|
+
|
240
|
+
DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for :projects, :reject_if => lambda { |attrs| false }
|
241
|
+
- should allow to create a new project via Person#projects_attributes
|
242
|
+
- should allow to update an existing project via Person#projects_attributes
|
243
|
+
- should not allow to delete an existing project via Person#projects_attributes
|
244
|
+
</code>
|
245
|
+
</pre>
|
246
|
+
|
247
|
+
h2. TODO
|
248
|
+
|
249
|
+
* add more specs and fix bugs
|
250
|
+
* Adapt to datamapper/next
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'rubygems'
|
3
|
+
require 'rake'
|
4
|
+
require 'rake/rdoctask'
|
5
|
+
|
6
|
+
ROOT = Pathname(__FILE__).dirname.expand_path
|
7
|
+
JRUBY = RUBY_PLATFORM =~ /java/
|
8
|
+
WINDOWS = Gem.win_platform?
|
9
|
+
SUDO = (WINDOWS || JRUBY) ? '' : ('sudo' unless ENV['SUDOLESS'])
|
10
|
+
|
11
|
+
require ROOT + 'lib/dm-accepts_nested_attributes/version'
|
12
|
+
|
13
|
+
AUTHOR = "Martin Gamsjäger"
|
14
|
+
EMAIL = "gamsnjaga [a] gmail [d] com"
|
15
|
+
GEM_NAME = "dm-accepts_nested_attributes"
|
16
|
+
GEM_VERSION = DataMapper::NestedAttributes::VERSION
|
17
|
+
|
18
|
+
GEM_DEPENDENCIES = [
|
19
|
+
["dm-core", '>=0.9.11'],
|
20
|
+
['addressable', '~>2.0.2' ]
|
21
|
+
]
|
22
|
+
|
23
|
+
GEM_CLEAN = %w[ log pkg coverage ]
|
24
|
+
GEM_EXTRAS = { :has_rdoc => true, :extra_rdoc_files => %w[ README.textile LICENSE TODO History.txt ] }
|
25
|
+
|
26
|
+
PROJECT_NAME = "dm-accepts_nested_attributes"
|
27
|
+
PROJECT_URL = "http://github.com/snusnu/dm-accepts_nested_attributes/tree/master"
|
28
|
+
PROJECT_DESCRIPTION = PROJECT_SUMMARY = %{
|
29
|
+
A DataMapper plugin that adds the possibility to perform nested model attribute assignment
|
30
|
+
}
|
31
|
+
|
32
|
+
Pathname.glob(ROOT.join('tasks/**/*.rb').to_s).each { |f| require f }
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Needed to import datamapper and other gems
|
2
|
+
require 'rubygems'
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
# Add all external dependencies for the plugin here
|
6
|
+
gem 'dm-core', '>=0.9.11'
|
7
|
+
gem 'dm-validations', '>=0.9.11'
|
8
|
+
|
9
|
+
require 'dm-core'
|
10
|
+
require 'dm-validations'
|
11
|
+
|
12
|
+
# Require plugin-files
|
13
|
+
require Pathname(__FILE__).dirname.expand_path / 'dm-accepts_nested_attributes' / 'nested_attributes'
|
14
|
+
# monkeypatches for dm-core/associations/(many_to_one.rb and one_to_many.rb)
|
15
|
+
require Pathname(__FILE__).dirname.expand_path / 'dm-accepts_nested_attributes' / 'associations'
|
16
|
+
|
17
|
+
# Include the plugin in Model
|
18
|
+
DataMapper::Resource.append_inclusions DataMapper::NestedAttributes
|
@@ -0,0 +1,55 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Associations
|
3
|
+
|
4
|
+
module ManyToOne
|
5
|
+
|
6
|
+
class Proxy
|
7
|
+
|
8
|
+
def save
|
9
|
+
|
10
|
+
return false if @parent.nil?
|
11
|
+
|
12
|
+
# original dm-core-0.9.11 code:
|
13
|
+
# return true unless parent.new_record?
|
14
|
+
|
15
|
+
# and the backwards compatible extension to it (allows update of belongs_to model)
|
16
|
+
if !parent.new_record? && !@relationship.child_model.autosave_associations.key?(@relationship.name)
|
17
|
+
return true
|
18
|
+
end
|
19
|
+
|
20
|
+
@relationship.with_repository(parent) do
|
21
|
+
result = parent.marked_for_destruction? ? parent.destroy : parent.save
|
22
|
+
@relationship.child_key.set(@child, @relationship.parent_key.get(parent)) if result
|
23
|
+
result
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
module OneToMany
|
34
|
+
|
35
|
+
class Proxy
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def save_resource(resource, parent = @parent)
|
40
|
+
@relationship.with_repository(resource) do |r|
|
41
|
+
if parent.nil? && resource.model.respond_to?(:many_to_many)
|
42
|
+
resource.destroy
|
43
|
+
else
|
44
|
+
@relationship.attach_parent(resource, parent)
|
45
|
+
resource.marked_for_destruction? ? resource.destroy : resource.save
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,300 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module NestedAttributes
|
3
|
+
|
4
|
+
def self.included(base)
|
5
|
+
base.extend(ClassMethods)
|
6
|
+
base.class_inheritable_accessor :autosave_associations
|
7
|
+
base.autosave_associations = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
module ClassMethods
|
11
|
+
|
12
|
+
# Defines an attributes writer for the specified association(s). If you
|
13
|
+
# are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you
|
14
|
+
# will need to add the attribute writer to the allowed list.
|
15
|
+
#
|
16
|
+
# Supported options:
|
17
|
+
# [:allow_destroy]
|
18
|
+
# If true, destroys any members from the attributes hash with a
|
19
|
+
# <tt>_delete</tt> key and a value that evaluates to +true+
|
20
|
+
# (eg. 1, '1', true, or 'true'). This option is off by default.
|
21
|
+
# [:reject_if]
|
22
|
+
# Allows you to specify a Proc that checks whether a record should be
|
23
|
+
# built for a certain attribute hash. The hash is passed to the Proc
|
24
|
+
# and the Proc should return either +true+ or +false+. When no Proc
|
25
|
+
# is specified a record will be built for all attribute hashes that
|
26
|
+
# do not have a <tt>_delete</tt> that evaluates to true.
|
27
|
+
#
|
28
|
+
# Examples:
|
29
|
+
# # creates avatar_attributes=
|
30
|
+
# accepts_nested_attributes_for :avatar, :reject_if => proc { |attributes| attributes['name'].blank? }
|
31
|
+
# # creates avatar_attributes= and posts_attributes=
|
32
|
+
# accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true
|
33
|
+
def accepts_nested_attributes_for(association_name, options = {})
|
34
|
+
|
35
|
+
assert_kind_of 'association_name', association_name, Symbol, String
|
36
|
+
assert_kind_of 'options', options, Hash
|
37
|
+
|
38
|
+
options = { :allow_destroy => false }.update(options)
|
39
|
+
|
40
|
+
# raises if the specified option keys aren't valid
|
41
|
+
assert_valid_autosave_options(options)
|
42
|
+
|
43
|
+
# raises if the specified association doesn't exist
|
44
|
+
# we don't need the return value here, just the check
|
45
|
+
# ------------------------------------------------------
|
46
|
+
# also, when using the return value from this call to
|
47
|
+
# replace association_name with association.name,
|
48
|
+
# has(1, :through) are broken, because they seem to have
|
49
|
+
# a different name
|
50
|
+
|
51
|
+
association_for_name(association_name)
|
52
|
+
|
53
|
+
autosave_associations[association_name] = options
|
54
|
+
|
55
|
+
type = nr_of_possible_child_instances(association_name) > 1 ? :collection : :one_to_one
|
56
|
+
|
57
|
+
class_eval %{
|
58
|
+
|
59
|
+
def #{association_name}_attributes=(attributes)
|
60
|
+
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, #{options[:allow_destroy]})
|
61
|
+
end
|
62
|
+
|
63
|
+
if association_type(:#{association_name}) == :many_to_one || association_type(:#{association_name}) == :one_to_one
|
64
|
+
|
65
|
+
def get_#{association_name}
|
66
|
+
#{association_name.to_s} || self.class.associated_model_for_name(:#{association_name}).new
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
|
71
|
+
}, __FILE__, __LINE__ + 1
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
def reject_new_nested_attributes_proc_for(association_name)
|
76
|
+
autosave_associations[association_name] ? autosave_associations[association_name][:reject_if] : nil
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
# utility methods
|
81
|
+
|
82
|
+
def nr_of_possible_child_instances(association_name, repository = :default)
|
83
|
+
# belongs_to seems to generate no options[:max]
|
84
|
+
association_for_name(association_name, repository).options[:max] || 1
|
85
|
+
end
|
86
|
+
|
87
|
+
# i have the feeling this should be refactored
|
88
|
+
def associated_model_for_name(association_name, repository = :default)
|
89
|
+
a = association_for_name(association_name, repository)
|
90
|
+
case association_type(association_name)
|
91
|
+
when :many_to_one
|
92
|
+
a.parent_model
|
93
|
+
when :one_to_one
|
94
|
+
a.child_model
|
95
|
+
when :one_to_many
|
96
|
+
a.child_model
|
97
|
+
when :many_to_many
|
98
|
+
Object.full_const_get(a.options[:child_model])
|
99
|
+
else
|
100
|
+
raise ArgumentError, "Unknown association type #{a.inspect}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# maybe this should be provided by dm-core somehow
|
105
|
+
# DataMapper::Association::Relationship would be a place maybe?
|
106
|
+
def association_type(association_name)
|
107
|
+
a = association_for_name(association_name)
|
108
|
+
if a.options[:max].nil? # belongs_to
|
109
|
+
:many_to_one
|
110
|
+
elsif a.options[:max] == 1 # has(1)
|
111
|
+
:one_to_one
|
112
|
+
elsif a.options[:max] > 1 && !a.is_a?(DataMapper::Associations::RelationshipChain) # has(n)
|
113
|
+
:one_to_many
|
114
|
+
elsif a.is_a?(DataMapper::Associations::RelationshipChain) # has(n, :through) MUST be checked after has(n) here
|
115
|
+
:many_to_many
|
116
|
+
else
|
117
|
+
raise ArgumentError, "Unknown association type #{a.inspect}"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
# avoid nil access by always going through this
|
122
|
+
# this method raises if the association named name is not established in this model
|
123
|
+
def association_for_name(name, repository = :default)
|
124
|
+
association = self.relationships(repository)[name]
|
125
|
+
# TODO think about using a specific Error class like UnknownAssociationError
|
126
|
+
raise(ArgumentError, "Relationship #{name.inspect} does not exist in \#{model}") unless association
|
127
|
+
association
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
# think about storing valid options in a classlevel constant
|
133
|
+
def assert_valid_autosave_options(options)
|
134
|
+
unless options.all? { |k,v| [ :allow_destroy, :reject_if ].include?(k) }
|
135
|
+
raise ArgumentError, 'accepts_nested_attributes_for only takes :allow_destroy and :reject_if as options'
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
|
142
|
+
# instance methods
|
143
|
+
|
144
|
+
# returns nil if no resource has been associated yet
|
145
|
+
def associated_instance_get(association_name, repository = :default)
|
146
|
+
send(self.class.association_for_name(association_name, repository).name)
|
147
|
+
end
|
148
|
+
|
149
|
+
# Reloads the attributes of the object as usual and removes a mark for destruction.
|
150
|
+
def reload
|
151
|
+
@marked_for_destruction = false
|
152
|
+
super
|
153
|
+
end
|
154
|
+
|
155
|
+
def marked_for_destruction?
|
156
|
+
@marked_for_destruction
|
157
|
+
end
|
158
|
+
|
159
|
+
def mark_for_destruction
|
160
|
+
@marked_for_destruction = true
|
161
|
+
end
|
162
|
+
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
# Attribute hash keys that should not be assigned as normal attributes.
|
167
|
+
# These hash keys are nested attributes implementation details.
|
168
|
+
UNASSIGNABLE_KEYS = [ :id, :_delete ]
|
169
|
+
|
170
|
+
|
171
|
+
# Assigns the given attributes to the association.
|
172
|
+
#
|
173
|
+
# If the given attributes include an <tt>:id</tt> that matches the existing
|
174
|
+
# record’s id, then the existing record will be modified. Otherwise a new
|
175
|
+
# record will be built.
|
176
|
+
#
|
177
|
+
# If the given attributes include a matching <tt>:id</tt> attribute _and_ a
|
178
|
+
# <tt>:_delete</tt> key set to a truthy value, then the existing record
|
179
|
+
# will be marked for destruction.
|
180
|
+
def assign_nested_attributes_for_one_to_one_association(association_name, attributes, allow_destroy)
|
181
|
+
if attributes[:id].blank?
|
182
|
+
unless reject_new_record?(association_name, attributes)
|
183
|
+
model = self.class.associated_model_for_name(association_name)
|
184
|
+
send("#{association_name}=", model.new(attributes.except(*UNASSIGNABLE_KEYS)))
|
185
|
+
end
|
186
|
+
else (existing_record = associated_instance_get(association_name)) && existing_record.id.to_s == attributes[:id].to_s
|
187
|
+
assign_to_or_mark_for_destruction(association_name, existing_record, attributes, allow_destroy)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Assigns the given attributes to the collection association.
|
192
|
+
#
|
193
|
+
# Hashes with an <tt>:id</tt> value matching an existing associated record
|
194
|
+
# will update that record. Hashes without an <tt>:id</tt> value will build
|
195
|
+
# a new record for the association. Hashes with a matching <tt>:id</tt>
|
196
|
+
# value and a <tt>:_delete</tt> key set to a truthy value will mark the
|
197
|
+
# matched record for destruction.
|
198
|
+
#
|
199
|
+
# For example:
|
200
|
+
#
|
201
|
+
# assign_nested_attributes_for_collection_association(:people, {
|
202
|
+
# '1' => { :id => '1', :name => 'Peter' },
|
203
|
+
# '2' => { :name => 'John' },
|
204
|
+
# '3' => { :id => '2', :_delete => true }
|
205
|
+
# })
|
206
|
+
#
|
207
|
+
# Will update the name of the Person with ID 1, build a new associated
|
208
|
+
# person with the name `John', and mark the associatied Person with ID 2
|
209
|
+
# for destruction.
|
210
|
+
#
|
211
|
+
# Also accepts an Array of attribute hashes:
|
212
|
+
#
|
213
|
+
# assign_nested_attributes_for_collection_association(:people, [
|
214
|
+
# { :id => '1', :name => 'Peter' },
|
215
|
+
# { :name => 'John' },
|
216
|
+
# { :id => '2', :_delete => true }
|
217
|
+
# ])
|
218
|
+
def assign_nested_attributes_for_collection_association(association_name, attributes_collection, allow_destroy)
|
219
|
+
|
220
|
+
assert_kind_of 'association_name', association_name, Symbol
|
221
|
+
assert_kind_of 'attributes_collection', attributes_collection, Hash, Array
|
222
|
+
|
223
|
+
if attributes_collection.is_a? Hash
|
224
|
+
attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
|
225
|
+
end
|
226
|
+
|
227
|
+
attributes_collection.each do |attributes|
|
228
|
+
if attributes[:id].blank?
|
229
|
+
unless reject_new_record?(association_name, attributes)
|
230
|
+
case self.class.association_type(association_name)
|
231
|
+
when :one_to_many
|
232
|
+
build_new_has_n_association(association_name, attributes)
|
233
|
+
when :many_to_many
|
234
|
+
build_new_has_n_through_association(association_name, attributes)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes[:id].to_s }
|
238
|
+
assign_to_or_mark_for_destruction(association_name, existing_record, attributes, allow_destroy)
|
239
|
+
end
|
240
|
+
end
|
241
|
+
|
242
|
+
end
|
243
|
+
|
244
|
+
def build_new_has_n_association(association_name, attributes)
|
245
|
+
send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
|
246
|
+
end
|
247
|
+
|
248
|
+
def build_new_has_n_through_association(association_name, attributes)
|
249
|
+
# fetch the association to have the information ready
|
250
|
+
association = self.class.association_for_name(association_name)
|
251
|
+
|
252
|
+
# do what's done in dm-core/specs/integration/association_through_spec.rb
|
253
|
+
|
254
|
+
# explicitly build the join entry and assign it to the join association
|
255
|
+
join_entry = Extlib::Inflection.constantize(Extlib::Inflection.classify(association.name)).new
|
256
|
+
self.send(association.name) << join_entry
|
257
|
+
self.save
|
258
|
+
# explicitly build the child entry and assign the join entry to its join association
|
259
|
+
child_entry = self.class.associated_model_for_name(association_name).new(attributes)
|
260
|
+
child_entry.send(association.name) << join_entry
|
261
|
+
child_entry.save
|
262
|
+
end
|
263
|
+
|
264
|
+
# Updates a record with the +attributes+ or marks it for destruction if
|
265
|
+
# +allow_destroy+ is +true+ and has_delete_flag? returns +true+.
|
266
|
+
def assign_to_or_mark_for_destruction(association_name, record, attributes, allow_destroy)
|
267
|
+
if has_delete_flag?(attributes) && allow_destroy
|
268
|
+
if self.class.association_type(association_name) == :many_to_many
|
269
|
+
# destroy the join record
|
270
|
+
record.send(self.class.association_for_name(association_name).name).destroy!
|
271
|
+
# destroy the child record
|
272
|
+
record.destroy
|
273
|
+
else
|
274
|
+
record.mark_for_destruction
|
275
|
+
end
|
276
|
+
else
|
277
|
+
record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
|
278
|
+
if self.class.association_type(association_name) == :many_to_many
|
279
|
+
record.save
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
283
|
+
|
284
|
+
# Determines if a hash contains a truthy _delete key.
|
285
|
+
def has_delete_flag?(hash)
|
286
|
+
# TODO find out if this activerecord code needs to be ported
|
287
|
+
# ConnectionAdapters::Column.value_to_boolean hash['_delete']
|
288
|
+
hash[:_delete]
|
289
|
+
end
|
290
|
+
|
291
|
+
# Determines if a new record should be build by checking for
|
292
|
+
# has_delete_flag? or if a <tt>:reject_if</tt> proc exists for this
|
293
|
+
# association and evaluates to +true+.
|
294
|
+
def reject_new_record?(association_name, attributes)
|
295
|
+
guard = self.class.reject_new_nested_attributes_proc_for(association_name)
|
296
|
+
has_delete_flag?(attributes) || (guard.respond_to?(:call) && guard.call(attributes))
|
297
|
+
end
|
298
|
+
|
299
|
+
end
|
300
|
+
end
|