snusnu-dm-accepts_nested_attributes 0.0.2 → 0.0.3

Sign up to get free protection for your applications and to get access to all the features.
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