snusnu-dm-accepts_nested_attributes 0.0.2 → 0.0.3

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/Manifest.txt CHANGED
@@ -6,21 +6,18 @@ README.textile
6
6
  Rakefile
7
7
  TODO
8
8
  lib/dm-accepts_nested_attributes.rb
9
- lib/dm-accepts_nested_attributes/associations.rb
9
+ lib/dm-accepts_nested_attributes/association_proxies.rb
10
+ lib/dm-accepts_nested_attributes/association_validation.rb
10
11
  lib/dm-accepts_nested_attributes/nested_attributes.rb
11
12
  lib/dm-accepts_nested_attributes/version.rb
12
13
  spec/fixtures/person.rb
13
- spec/fixtures/photo.rb
14
14
  spec/fixtures/profile.rb
15
15
  spec/fixtures/project.rb
16
16
  spec/fixtures/project_membership.rb
17
- spec/fixtures/tag.rb
18
- spec/fixtures/tagging.rb
19
17
  spec/fixtures/task.rb
20
18
  spec/integration/belongs_to_spec.rb
21
19
  spec/integration/has_1_spec.rb
22
20
  spec/integration/has_n_spec.rb
23
- spec/integration/has_n_through_renamed_spec.rb
24
21
  spec/integration/has_n_through_spec.rb
25
22
  spec/shared/rspec_tmbundle_support.rb
26
23
  spec/spec.opts
@@ -12,7 +12,8 @@ require 'dm-validations'
12
12
  # Require plugin-files
13
13
  require Pathname(__FILE__).dirname.expand_path / 'dm-accepts_nested_attributes' / 'nested_attributes'
14
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'
15
+ require Pathname(__FILE__).dirname.expand_path / 'dm-accepts_nested_attributes' / 'association_proxies'
16
16
 
17
17
  # Include the plugin in Model
18
- DataMapper::Resource.append_inclusions DataMapper::NestedAttributes
18
+ DataMapper::Model.append_extensions DataMapper::NestedAttributes::ClassMethods
19
+ DataMapper::Resource.append_inclusions DataMapper::NestedAttributes::CommonInstanceMethods
@@ -0,0 +1,91 @@
1
+ module DataMapper
2
+ module NestedAttributes
3
+
4
+ module AssociationValidation
5
+
6
+ # NOTE:
7
+ # overwriting Resource#save like this breaks the before(:save) hook stack
8
+ # this hopefully is no problem, since the current implementation doesn't rely on
9
+ # a before(:save) hook, but rather overwrites this hook with a no-op, and adds
10
+ # the desired behavior via overwriting Resource#save directly. I'd really appreciate
11
+ # any ideas for doing this differently, though. Anyways, I'm not really sure if this
12
+ # is the right approach. I don't even know if it works with custom validations,
13
+ # or maybe breaks other things. It's also really not well specced at all atm.
14
+ # Use at your own risk :-)
15
+
16
+ def save(context = :default)
17
+
18
+ # -----------------------------------------------------------------
19
+ # ORIGINAL CODE from Resource#save
20
+ # -----------------------------------------------------------------
21
+ #
22
+ # associations_saved = false
23
+ # child_associations.each { |a| associations_saved |= a.save }
24
+ #
25
+ # saved = new_record? ? create : update
26
+ #
27
+ # if saved
28
+ # original_values.clear
29
+ # end
30
+ #
31
+ # parent_associations.each { |a| associations_saved |= a.save }
32
+ #
33
+ # # We should return true if the model (or any of its associations)
34
+ # # were saved.
35
+ # (saved | associations_saved) == true
36
+ #
37
+ # -----------------------------------------------------------------
38
+
39
+ return super if context.nil? # preserve save! behavior
40
+
41
+ associations_saved = false
42
+
43
+ child_associations.each do |a|
44
+
45
+ if a.respond_to?(:valid?)
46
+ a.errors.each { |e| self.errors.add(:general, e) } unless a.valid?(context)
47
+ else
48
+ self.errors.add(:general, "child association is missing")
49
+ end
50
+
51
+ associations_saved |= a.save
52
+
53
+ end
54
+
55
+ saved = self.valid? && (new_record? ? create : update)
56
+
57
+ if saved
58
+ original_values.clear
59
+ end
60
+
61
+ parent_associations.each do |a|
62
+
63
+ if a.respond_to?(:each)
64
+ a.each do |r|
65
+ r.errors.each { |e| self.errors.add(:general, e) } unless r.valid?(context)
66
+ end
67
+ else
68
+ a.errors.each { |e| self.errors.add(:general, e) } unless a.valid?(context)
69
+ end
70
+
71
+ associations_saved |= a.save
72
+
73
+ end
74
+
75
+ (saved | associations_saved) == true
76
+
77
+ end
78
+
79
+ # everything works the same if this method isn't overwritten with a no-op
80
+ # however, i suspect that this is the case because the registered before(:save) hook
81
+ # somehow gets lost when overwriting Resource#save here in this module.
82
+ # I'll leave it in for now, to make the purpose clear
83
+
84
+ def check_validations(context = :default)
85
+ true # no-op, validations are checked inside #save
86
+ end
87
+
88
+ end
89
+
90
+ end
91
+ end
@@ -1,14 +1,13 @@
1
1
  module DataMapper
2
2
  module NestedAttributes
3
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
4
  module ClassMethods
11
5
 
6
+ def self.extended(base)
7
+ base.class_inheritable_accessor :autosave_associations
8
+ base.autosave_associations = {}
9
+ end
10
+
12
11
  # Defines an attributes reader and writer for the specified association(s).
13
12
  # If you are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>,
14
13
  # then you will need to add the attribute writer to the allowed list.
@@ -57,17 +56,37 @@ module DataMapper
57
56
 
58
57
  association_for_name(association_name)
59
58
 
59
+
60
+ # should be safe to go on
61
+
62
+ include InstanceMethods
63
+
64
+ if ::DataMapper.const_defined?('Validate')
65
+
66
+ require Pathname(__FILE__).dirname.expand_path + 'association_validation'
67
+
68
+ include AssociationValidation
69
+
70
+ end
71
+
60
72
  autosave_associations[association_name] = options
61
73
 
62
74
  type = nr_of_possible_child_instances(association_name) > 1 ? :collection : :one_to_one
63
75
 
64
76
  class_eval %{
65
77
 
78
+ def save(context = :default)
79
+ saved = false # preserve Resource#save api contract
80
+ transaction { |t| t.rollback unless saved = super }
81
+ saved
82
+ end
83
+
66
84
  def #{association_name}_attributes
67
85
  @#{association_name}_attributes
68
86
  end
69
87
 
70
88
  def #{association_name}_attributes=(attributes)
89
+ attributes = sanitize_nested_attributes(attributes)
71
90
  @#{association_name}_attributes = attributes
72
91
  assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, #{options[:allow_destroy]})
73
92
  end
@@ -151,249 +170,181 @@ module DataMapper
151
170
  end
152
171
 
153
172
 
154
- # instance methods
155
-
173
+ module InstanceMethods
156
174
 
157
- if ::DataMapper.const_defined?('Validate')
158
-
159
- # NOTE:
160
- # overwriting Resource#save like this breaks the before(:save) hook stack
161
- # this hopefully is no problem, since the current implementation doesn't rely on
162
- # a before(:save) hook, but rather overwrites this hook with a no-op, and adds
163
- # the desired behavior via overwriting Resource#save directly. I'd really appreciate
164
- # any ideas for doing this differently, though. Anyways, I'm not really sure if this
165
- # is the right approach. I don't even know if it works with custom validations,
166
- # or maybe breaks other things. It's also really not well specced at all atm.
167
- # Use at your own risk :-)
168
-
169
- def save(context = :default)
170
-
171
- # -----------------------------------------------------------------
172
- # ORIGINAL CODE from Resource#save
173
- # -----------------------------------------------------------------
174
- #
175
- # associations_saved = false
176
- # child_associations.each { |a| associations_saved |= a.save }
177
- #
178
- # saved = new_record? ? create : update
179
- #
180
- # if saved
181
- # original_values.clear
182
- # end
183
- #
184
- # parent_associations.each { |a| associations_saved |= a.save }
185
- #
186
- # # We should return true if the model (or any of its associations)
187
- # # were saved.
188
- # (saved | associations_saved) == true
189
- #
190
- # -----------------------------------------------------------------
191
-
192
-
193
- return super if context.nil? # preserve save! behavior
194
-
195
- associations_saved = false
196
-
197
- child_associations.each do |a|
198
-
199
- if a.respond_to?(:valid?)
200
- a.errors.each { |e| self.errors.add(:general, e) } unless a.valid?(context)
201
- else
202
- self.errors.add(:general, "child association is missing")
203
- end
204
-
205
- associations_saved |= a.save
206
-
207
- end
208
-
209
- saved = self.valid? && (new_record? ? create : update)
210
-
211
- if saved
212
- original_values.clear
213
- end
214
-
215
- parent_associations.each do |a|
216
-
217
- if a.respond_to?(:each)
218
- a.each do |r|
219
- r.errors.each { |e| self.errors.add(:general, e) } unless r.valid?(context)
220
- end
221
- else
222
- a.errors.each { |e| self.errors.add(:general, e) } unless a.valid?(context)
223
- end
224
-
225
- associations_saved |= a.save
226
-
227
- end
228
-
229
- (saved | associations_saved) == true
230
-
231
- end
232
-
233
- # everything works the same if this method isn't overwritten with a no-op
234
- # however, i suspect that this is the case because the registered before(:save) hook
235
- # somehow gets lost when overwriting Resource#save here in this module.
236
- # I'll leave it in for now, to make the purpose clear
237
-
238
- def check_validations(context = :default)
239
- true # no-op, validations are checked inside #save
175
+ # This method can be used to remove ambiguities from the passed attributes.
176
+ # Consider a situation with a belongs_to association where both a valid value
177
+ # for the foreign_key attribute *and* nested_attributes for a new record are
178
+ # present (i.e. item_type_id and item_type_attributes are present).
179
+ # Also see http://is.gd/sz2d on the rails-core ml for a discussion on this.
180
+ # The basic idea is, that there should be a well defined behavior for what
181
+ # exactly happens when such a situation occurs. I'm currently in favor for
182
+ # using the foreign_key if it is present, but this probably needs more thinking.
183
+ # For now, this method basically is a no-op, but at least it provides a hook where
184
+ # everyone can perform it's own sanitization (just overwrite this method)
185
+ def sanitize_nested_attributes(attrs)
186
+ attrs
240
187
  end
241
-
242
- end
243
188
 
244
- # returns nil if no resource has been associated yet
245
- def associated_instance_get(association_name, repository = :default)
246
- send(self.class.association_for_name(association_name, repository).name)
247
- end
248
-
249
- # Reloads the attributes of the object as usual and removes a mark for destruction.
250
- def reload
251
- @marked_for_destruction = false
252
- super
253
- end
254
-
255
- def marked_for_destruction?
256
- @marked_for_destruction
257
- end
258
-
259
- def mark_for_destruction
260
- @marked_for_destruction = true
261
- end
189
+ # returns nil if no resource has been associated yet
190
+ def associated_instance_get(association_name, repository = :default)
191
+ send(self.class.association_for_name(association_name, repository).name)
192
+ end
262
193
 
263
194
 
264
- private
195
+ private
265
196
 
266
- # Attribute hash keys that should not be assigned as normal attributes.
267
- # These hash keys are nested attributes implementation details.
268
- UNASSIGNABLE_KEYS = [ :id, :_delete ]
197
+ # Attribute hash keys that should not be assigned as normal attributes.
198
+ # These hash keys are nested attributes implementation details.
199
+ UNASSIGNABLE_KEYS = [ :id, :_delete ]
269
200
 
270
201
 
271
- # Assigns the given attributes to the association.
272
- #
273
- # If the given attributes include an <tt>:id</tt> that matches the existing
274
- # record’s id, then the existing record will be modified. Otherwise a new
275
- # record will be built.
276
- #
277
- # If the given attributes include a matching <tt>:id</tt> attribute _and_ a
278
- # <tt>:_delete</tt> key set to a truthy value, then the existing record
279
- # will be marked for destruction.
280
- def assign_nested_attributes_for_one_to_one_association(association_name, attributes, allow_destroy)
281
- if attributes[:id].blank?
282
- unless reject_new_record?(association_name, attributes)
283
- model = self.class.associated_model_for_name(association_name)
284
- send("#{association_name}=", model.new(attributes.except(*UNASSIGNABLE_KEYS)))
202
+ # Assigns the given attributes to the association.
203
+ #
204
+ # If the given attributes include an <tt>:id</tt> that matches the existing
205
+ # record’s id, then the existing record will be modified. Otherwise a new
206
+ # record will be built.
207
+ #
208
+ # If the given attributes include a matching <tt>:id</tt> attribute _and_ a
209
+ # <tt>:_delete</tt> key set to a truthy value, then the existing record
210
+ # will be marked for destruction.
211
+ def assign_nested_attributes_for_one_to_one_association(association_name, attributes, allow_destroy)
212
+ if attributes[:id].blank?
213
+ unless reject_new_record?(association_name, attributes)
214
+ model = self.class.associated_model_for_name(association_name)
215
+ send("#{association_name}=", model.new(attributes.except(*UNASSIGNABLE_KEYS)))
216
+ end
217
+ else (existing_record = associated_instance_get(association_name)) && existing_record.id.to_s == attributes[:id].to_s
218
+ assign_to_or_mark_for_destruction(association_name, existing_record, attributes, allow_destroy)
285
219
  end
286
- else (existing_record = associated_instance_get(association_name)) && existing_record.id.to_s == attributes[:id].to_s
287
- assign_to_or_mark_for_destruction(association_name, existing_record, attributes, allow_destroy)
288
220
  end
289
- end
290
221
 
291
- # Assigns the given attributes to the collection association.
292
- #
293
- # Hashes with an <tt>:id</tt> value matching an existing associated record
294
- # will update that record. Hashes without an <tt>:id</tt> value will build
295
- # a new record for the association. Hashes with a matching <tt>:id</tt>
296
- # value and a <tt>:_delete</tt> key set to a truthy value will mark the
297
- # matched record for destruction.
298
- #
299
- # For example:
300
- #
301
- # assign_nested_attributes_for_collection_association(:people, {
302
- # '1' => { :id => '1', :name => 'Peter' },
303
- # '2' => { :name => 'John' },
304
- # '3' => { :id => '2', :_delete => true }
305
- # })
306
- #
307
- # Will update the name of the Person with ID 1, build a new associated
308
- # person with the name `John', and mark the associatied Person with ID 2
309
- # for destruction.
310
- #
311
- # Also accepts an Array of attribute hashes:
312
- #
313
- # assign_nested_attributes_for_collection_association(:people, [
314
- # { :id => '1', :name => 'Peter' },
315
- # { :name => 'John' },
316
- # { :id => '2', :_delete => true }
317
- # ])
318
- def assign_nested_attributes_for_collection_association(association_name, attributes_collection, allow_destroy)
222
+ # Assigns the given attributes to the collection association.
223
+ #
224
+ # Hashes with an <tt>:id</tt> value matching an existing associated record
225
+ # will update that record. Hashes without an <tt>:id</tt> value will build
226
+ # a new record for the association. Hashes with a matching <tt>:id</tt>
227
+ # value and a <tt>:_delete</tt> key set to a truthy value will mark the
228
+ # matched record for destruction.
229
+ #
230
+ # For example:
231
+ #
232
+ # assign_nested_attributes_for_collection_association(:people, {
233
+ # '1' => { :id => '1', :name => 'Peter' },
234
+ # '2' => { :name => 'John' },
235
+ # '3' => { :id => '2', :_delete => true }
236
+ # })
237
+ #
238
+ # Will update the name of the Person with ID 1, build a new associated
239
+ # person with the name `John', and mark the associatied Person with ID 2
240
+ # for destruction.
241
+ #
242
+ # Also accepts an Array of attribute hashes:
243
+ #
244
+ # assign_nested_attributes_for_collection_association(:people, [
245
+ # { :id => '1', :name => 'Peter' },
246
+ # { :name => 'John' },
247
+ # { :id => '2', :_delete => true }
248
+ # ])
249
+ def assign_nested_attributes_for_collection_association(association_name, attributes_collection, allow_destroy)
319
250
 
320
- assert_kind_of 'association_name', association_name, Symbol
321
- assert_kind_of 'attributes_collection', attributes_collection, Hash, Array
251
+ assert_kind_of 'association_name', association_name, Symbol
252
+ assert_kind_of 'attributes_collection', attributes_collection, Hash, Array
322
253
 
323
- if attributes_collection.is_a? Hash
324
- attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
325
- end
254
+ if attributes_collection.is_a? Hash
255
+ attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
256
+ end
326
257
 
327
- attributes_collection.each do |attributes|
328
- if attributes[:id].blank?
329
- unless reject_new_record?(association_name, attributes)
330
- case self.class.association_type(association_name)
331
- when :one_to_many
332
- build_new_has_n_association(association_name, attributes)
333
- when :many_to_many
334
- build_new_has_n_through_association(association_name, attributes)
258
+ attributes_collection.each do |attributes|
259
+ if attributes[:id].blank?
260
+ unless reject_new_record?(association_name, attributes)
261
+ case self.class.association_type(association_name)
262
+ when :one_to_many
263
+ build_new_has_n_association(association_name, attributes)
264
+ when :many_to_many
265
+ build_new_has_n_through_association(association_name, attributes)
266
+ end
335
267
  end
268
+ elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes[:id].to_s }
269
+ assign_to_or_mark_for_destruction(association_name, existing_record, attributes, allow_destroy)
336
270
  end
337
- elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes[:id].to_s }
338
- assign_to_or_mark_for_destruction(association_name, existing_record, attributes, allow_destroy)
339
271
  end
340
- end
341
272
 
342
- end
273
+ end
343
274
 
344
- def build_new_has_n_association(association_name, attributes)
345
- send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
346
- end
275
+ def build_new_has_n_association(association_name, attributes)
276
+ send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
277
+ end
347
278
 
348
- def build_new_has_n_through_association(association_name, attributes)
349
- # fetch the association to have the information ready
350
- association = self.class.association_for_name(association_name)
279
+ def build_new_has_n_through_association(association_name, attributes)
280
+ # fetch the association to have the information ready
281
+ association = self.class.association_for_name(association_name)
351
282
 
352
- # do what's done in dm-core/specs/integration/association_through_spec.rb
283
+ # do what's done in dm-core/specs/integration/association_through_spec.rb
353
284
 
354
- # explicitly build the join entry and assign it to the join association
355
- join_entry = self.class.associated_model_for_name(association.name).new
356
- self.send(association.name) << join_entry
357
- self.save
358
- # explicitly build the child entry and assign the join entry to its join association
359
- child_entry = self.class.associated_model_for_name(association_name).new(attributes)
360
- child_entry.send(association.name) << join_entry
361
- child_entry.save
362
- end
285
+ # explicitly build the join entry and assign it to the join association
286
+ join_entry = self.class.associated_model_for_name(association.name).new
287
+ self.send(association.name) << join_entry
288
+ self.save
289
+ # explicitly build the child entry and assign the join entry to its join association
290
+ child_entry = self.class.associated_model_for_name(association_name).new(attributes)
291
+ child_entry.send(association.name) << join_entry
292
+ child_entry.save
293
+ end
363
294
 
364
- # Updates a record with the +attributes+ or marks it for destruction if
365
- # +allow_destroy+ is +true+ and has_delete_flag? returns +true+.
366
- def assign_to_or_mark_for_destruction(association_name, record, attributes, allow_destroy)
367
- if has_delete_flag?(attributes) && allow_destroy
368
- if self.class.association_type(association_name) == :many_to_many
369
- # destroy the join record
370
- record.send(self.class.association_for_name(association_name).name).destroy!
371
- # destroy the child record
372
- record.destroy
295
+ # Updates a record with the +attributes+ or marks it for destruction if
296
+ # +allow_destroy+ is +true+ and has_delete_flag? returns +true+.
297
+ def assign_to_or_mark_for_destruction(association_name, record, attributes, allow_destroy)
298
+ if has_delete_flag?(attributes) && allow_destroy
299
+ if self.class.association_type(association_name) == :many_to_many
300
+ # destroy the join record
301
+ record.send(self.class.association_for_name(association_name).name).destroy!
302
+ # destroy the child record
303
+ record.destroy
304
+ else
305
+ record.mark_for_destruction
306
+ end
373
307
  else
374
- record.mark_for_destruction
375
- end
376
- else
377
- record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
378
- if self.class.association_type(association_name) == :many_to_many
379
- record.save
308
+ record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
309
+ if self.class.association_type(association_name) == :many_to_many
310
+ record.save
311
+ end
380
312
  end
381
313
  end
382
- end
383
314
 
384
- # Determines if a hash contains a truthy _delete key.
385
- def has_delete_flag?(hash)
386
- # TODO find out if this activerecord code needs to be ported
387
- # ConnectionAdapters::Column.value_to_boolean hash['_delete']
388
- hash[:_delete]
315
+ # Determines if a hash contains a truthy _delete key.
316
+ def has_delete_flag?(hash)
317
+ # TODO find out if this activerecord code needs to be ported
318
+ # ConnectionAdapters::Column.value_to_boolean hash['_delete']
319
+ hash[:_delete]
320
+ end
321
+
322
+ # Determines if a new record should be build by checking for
323
+ # has_delete_flag? or if a <tt>:reject_if</tt> proc exists for this
324
+ # association and evaluates to +true+.
325
+ def reject_new_record?(association_name, attributes)
326
+ guard = self.class.reject_new_nested_attributes_proc_for(association_name)
327
+ has_delete_flag?(attributes) || (guard.respond_to?(:call) && guard.call(attributes))
328
+ end
329
+
389
330
  end
390
331
 
391
- # Determines if a new record should be build by checking for
392
- # has_delete_flag? or if a <tt>:reject_if</tt> proc exists for this
393
- # association and evaluates to +true+.
394
- def reject_new_record?(association_name, attributes)
395
- guard = self.class.reject_new_nested_attributes_proc_for(association_name)
396
- has_delete_flag?(attributes) || (guard.respond_to?(:call) && guard.call(attributes))
332
+ module CommonInstanceMethods
333
+
334
+ # Reloads the attributes of the object as usual and removes a mark for destruction.
335
+ def reload
336
+ @marked_for_destruction = false
337
+ super
338
+ end
339
+
340
+ def marked_for_destruction?
341
+ @marked_for_destruction
342
+ end
343
+
344
+ def mark_for_destruction
345
+ @marked_for_destruction = true
346
+ end
347
+
397
348
  end
398
349
 
399
350
  end