snusnu-dm-accepts_nested_attributes 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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/rspec_tmbundle_support.rb
17
+ spec/lib/constraint_support.rb
19
18
  spec/fixtures/person.rb
20
19
  spec/fixtures/profile.rb
21
20
  spec/fixtures/project.rb
@@ -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
- 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
- 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 Integration Specs
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
- DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for(:projects, :allow_destroy => false)
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
- DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for(:projects, :allow_destroy = true)
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
- DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for :projects, :reject_if => :foo
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
- DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for :projects, :reject_if => lambda { |attrs| true }
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
- DataMapper::NestedAttributes Person.has(n, :projects, :through => :project_memberships) accepts_nested_attributes_for :projects, :reject_if => lambda { |attrs| false }
267
- - should allow to create a new project via Person#projects_attributes
268
- - should allow to update an existing project via Person#projects_attributes
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
- * add specs for :reject_if => :foo option, but think about _where_ first!
5
- * think about supporting :reject_unless in addition to :reject_if
6
- * think about generalizing :reject_if to not only work for new? resources
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 accepts_nested_attributes_for
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 association_name [Symbol, String]
15
+ # @param [Symbol, String] association_name
15
16
  # The name of the association that should accept nested attributes
16
17
  #
17
- # @param options [Hash, nil]
18
+ # @param [Hash, nil] options
18
19
  # List of resources to initialize the Collection with
19
20
  #
20
- # @option :reject_if [Symbol, String, #call]
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 :allow_destroy [true, false]
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
- # They are guaranteed to be valid if they made it this far.
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
- def add_save_behavior
93
- require Pathname(__FILE__).dirname.expand_path + 'save'
94
- include ::DataMapper::NestedAttributes::Save
95
- end
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
- # @return [Hash] The sanitized attributes
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
- # These hash keys are nested attributes implementation details.
27
- UNASSIGNABLE_KEYS = [ :id, :_delete ]
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(*UNASSIGNABLE_KEYS))
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
- assign_to_or_mark_for_destruction(relationship, existing_record, attributes)
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(*UNASSIGNABLE_KEYS))
126
+ relationship.get(self).new(attributes.except(*unassignable_keys))
108
127
  else
109
128
  collection = relationship.get(self)
110
- if existing_record = collection.detect { |record| record.id.to_s == attributes[:id].to_s }
111
- assign_to_or_mark_for_destruction(relationship, existing_record, attributes)
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 assign_to_or_mark_for_destruction(relationship, resource, attributes)
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
- resource.mark_for_destruction
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.update(attributes.except(*UNASSIGNABLE_KEYS))
173
+ resource.attributes = attributes.except(*unassignable_keys)
174
+ resource.save
138
175
  end
139
176
  end
140
177
 
141
178
  ##
142
- # Determines if a hash contains a truthy _delete key.
179
+ # Determines if the given attributes hash contains a truthy :_delete key.
143
180
  #
144
- # @param hash [Hash] The hash to test
181
+ # @param attributes [Hash] The attributes to test
145
182
  #
146
- # @return [Boolean]
147
- # true, if hash containts a truthy _delete key
148
- # false, otherwise
149
- def has_delete_flag?(hash)
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 build by checking for
157
- # has_delete_flag? or if a <tt>:reject_if</tt> proc exists for this
158
- # association and evaluates to +true+.
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 assign to the relationship's target end
166
- # All attributes except @see UNASSIGNABLE_KEYS will be assigned
198
+ # The attributes to test with @see has_delete_flag?
167
199
  #
168
- # @return [Boolean]
169
- # true, if the given attributes won't be rejected
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
- def normalize_attributes_collection(attributes_collection)
189
- if attributes_collection.is_a?(Hash)
190
- attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
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
- attributes_collection
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 [Boolean]
214
- # true, if this resource is marked for destruction
215
- # false, otherwise
290
+ # @return [true, false]
291
+ # true if this resource is marked for destruction
216
292
  def marked_for_destruction?
217
- @marked_for_destruction
293
+ !!@marked_for_destruction
218
294
  end
219
295
 
220
296
  ##