snusnu-dm-accepts_nested_attributes 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +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
|