activerecord 2.3.4 → 2.3.5

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activerecord might be problematic. Click here for more details.

Files changed (37) hide show
  1. data/CHANGELOG +8 -0
  2. data/Rakefile +1 -1
  3. data/lib/active_record/associations.rb +10 -7
  4. data/lib/active_record/associations/association_proxy.rb +7 -8
  5. data/lib/active_record/associations/has_and_belongs_to_many_association.rb +2 -2
  6. data/lib/active_record/associations/has_one_association.rb +9 -0
  7. data/lib/active_record/autosave_association.rb +32 -23
  8. data/lib/active_record/base.rb +7 -0
  9. data/lib/active_record/connection_adapters/mysql_adapter.rb +6 -2
  10. data/lib/active_record/fixtures.rb +1 -1
  11. data/lib/active_record/locking/optimistic.rb +0 -33
  12. data/lib/active_record/locking/pessimistic.rb +0 -22
  13. data/lib/active_record/nested_attributes.rb +101 -38
  14. data/lib/active_record/validations.rb +35 -35
  15. data/lib/active_record/version.rb +1 -1
  16. data/lib/activerecord.rb +1 -0
  17. data/test/cases/associations/has_many_associations_test.rb +12 -0
  18. data/test/cases/associations/has_many_through_associations_test.rb +22 -0
  19. data/test/cases/associations/has_one_associations_test.rb +21 -0
  20. data/test/cases/autosave_association_test.rb +230 -11
  21. data/test/cases/base_test.rb +2 -0
  22. data/test/cases/connection_test_mysql.rb +8 -0
  23. data/test/cases/fixtures_test.rb +2 -2
  24. data/test/cases/locking_test.rb +0 -18
  25. data/test/cases/nested_attributes_test.rb +109 -37
  26. data/test/cases/reflection_test.rb +3 -3
  27. data/test/cases/validations_i18n_test.rb +8 -0
  28. data/test/cases/validations_test.rb +37 -9
  29. data/test/fixtures/accounts.yml +1 -0
  30. data/test/fixtures/fixture_database.sqlite3 +0 -0
  31. data/test/fixtures/fixture_database_2.sqlite3 +0 -0
  32. data/test/models/company.rb +10 -0
  33. data/test/models/pirate.rb +9 -2
  34. data/test/models/treasure.rb +2 -0
  35. data/test/schema/mysql_specific_schema.rb +12 -0
  36. data/test/schema/schema.rb +1 -0
  37. metadata +4 -4
data/CHANGELOG CHANGED
@@ -1,3 +1,11 @@
1
+ *2.3.5 (November 25, 2009)*
2
+
3
+ * Minor Bug Fixes and deprecation warnings
4
+
5
+ * 1.9 Compatibility
6
+
7
+ * Numerous fixes to the nested attributes functionality
8
+
1
9
  *2.3.4 (September 4, 2009)*
2
10
 
3
11
  * PostgreSQL: XML datatype support. #1874 [Leonardo Borges]
data/Rakefile CHANGED
@@ -192,7 +192,7 @@ spec = Gem::Specification.new do |s|
192
192
  s.files = s.files + Dir.glob( "#{dir}/**/*" ).delete_if { |item| item.include?( "\.svn" ) }
193
193
  end
194
194
 
195
- s.add_dependency('activesupport', '= 2.3.4' + PKG_BUILD)
195
+ s.add_dependency('activesupport', '= 2.3.5' + PKG_BUILD)
196
196
 
197
197
  s.files.delete FIXTURES_ROOT + "/fixture_database.sqlite"
198
198
  s.files.delete FIXTURES_ROOT + "/fixture_database_2.sqlite"
@@ -275,9 +275,10 @@ module ActiveRecord
275
275
  # You can manipulate objects and associations before they are saved to the database, but there is some special behavior you should be
276
276
  # aware of, mostly involving the saving of associated objects.
277
277
  #
278
- # Unless you enable the :autosave option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
279
- # <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association,
280
- # in which case the members are always saved.
278
+ # Unless you set the :autosave option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
279
+ # <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association. Setting it
280
+ # to +true+ will _always_ save the members, whereas setting it to +false+ will
281
+ # _never_ save the members.
281
282
  #
282
283
  # === One-to-one associations
283
284
  #
@@ -874,7 +875,9 @@ module ActiveRecord
874
875
  # if the real class name is Person, you'll have to specify it with this option.
875
876
  # [:conditions]
876
877
  # Specify the conditions that the associated object must meet in order to be included as a +WHERE+
877
- # SQL fragment, such as <tt>rank = 5</tt>.
878
+ # SQL fragment, such as <tt>rank = 5</tt>. Record creation from the association is scoped if a hash
879
+ # is used. <tt>has_one :account, :conditions => {:enabled => true}</tt> will create an enabled account with <tt>@company.create_account</tt>
880
+ # or <tt>@company.build_account</tt>.
878
881
  # [:order]
879
882
  # Specify the order in which the associated objects are returned as an <tt>ORDER BY</tt> SQL fragment,
880
883
  # such as <tt>last_name, first_name DESC</tt>.
@@ -1324,8 +1327,8 @@ module ActiveRecord
1324
1327
  end
1325
1328
 
1326
1329
  define_method("#{reflection.name.to_s.singularize}_ids=") do |new_value|
1327
- ids = (new_value || []).reject { |nid| nid.blank? }
1328
- send("#{reflection.name}=", reflection.klass.find(ids))
1330
+ ids = (new_value || []).reject { |nid| nid.blank? }.map(&:to_i)
1331
+ send("#{reflection.name}=", reflection.klass.find(ids).index_by(&:id).values_at(*ids))
1329
1332
  end
1330
1333
  end
1331
1334
  end
@@ -1408,7 +1411,7 @@ module ActiveRecord
1408
1411
  if reflection.options.include?(:dependent)
1409
1412
  # Add polymorphic type if the :as option is present
1410
1413
  dependent_conditions = []
1411
- dependent_conditions << "#{reflection.primary_key_name} = \#{record.quoted_id}"
1414
+ dependent_conditions << "#{reflection.primary_key_name} = \#{record.#{reflection.name}.send(:owner_quoted_id)}"
1412
1415
  dependent_conditions << "#{reflection.options[:as]}_type = '#{base_class.name}'" if reflection.options[:as]
1413
1416
  dependent_conditions << sanitize_sql(reflection.options[:conditions], reflection.quoted_table_name) if reflection.options[:conditions]
1414
1417
  dependent_conditions << extra_conditions if extra_conditions
@@ -210,15 +210,14 @@ module ActiveRecord
210
210
  # Forwards any missing method call to the \target.
211
211
  def method_missing(method, *args)
212
212
  if load_target
213
- unless @target.respond_to?(method)
214
- message = "undefined method `#{method.to_s}' for \"#{@target}\":#{@target.class.to_s}"
215
- raise NoMethodError, message
216
- end
217
-
218
- if block_given?
219
- @target.send(method, *args) { |*block_args| yield(*block_args) }
213
+ if @target.respond_to?(method)
214
+ if block_given?
215
+ @target.send(method, *args) { |*block_args| yield(*block_args) }
216
+ else
217
+ @target.send(method, *args)
218
+ end
220
219
  else
221
- @target.send(method, *args)
220
+ super
222
221
  end
223
222
  end
224
223
  end
@@ -24,8 +24,8 @@ module ActiveRecord
24
24
 
25
25
  def has_primary_key?
26
26
  return @has_primary_key unless @has_primary_key.nil?
27
- @has_primary_key = (ActiveRecord::Base.connection.supports_primary_key? &&
28
- ActiveRecord::Base.connection.primary_key(@reflection.options[:join_table]))
27
+ @has_primary_key = (@owner.connection.supports_primary_key? &&
28
+ @owner.connection.primary_key(@reflection.options[:join_table]))
29
29
  end
30
30
 
31
31
  protected
@@ -8,18 +8,21 @@ module ActiveRecord
8
8
 
9
9
  def create(attrs = {}, replace_existing = true)
10
10
  new_record(replace_existing) do |reflection|
11
+ attrs = merge_with_conditions(attrs)
11
12
  reflection.create_association(attrs)
12
13
  end
13
14
  end
14
15
 
15
16
  def create!(attrs = {}, replace_existing = true)
16
17
  new_record(replace_existing) do |reflection|
18
+ attrs = merge_with_conditions(attrs)
17
19
  reflection.create_association!(attrs)
18
20
  end
19
21
  end
20
22
 
21
23
  def build(attrs = {}, replace_existing = true)
22
24
  new_record(replace_existing) do |reflection|
25
+ attrs = merge_with_conditions(attrs)
23
26
  reflection.build_association(attrs)
24
27
  end
25
28
  end
@@ -119,6 +122,12 @@ module ActiveRecord
119
122
 
120
123
  record
121
124
  end
125
+
126
+ def merge_with_conditions(attrs={})
127
+ attrs ||= {}
128
+ attrs.update(@reflection.options[:conditions]) if @reflection.options[:conditions].is_a?(Hash)
129
+ attrs
130
+ end
122
131
  end
123
132
  end
124
133
  end
@@ -159,7 +159,7 @@ module ActiveRecord
159
159
  def add_autosave_association_callbacks(reflection)
160
160
  save_method = "autosave_associated_records_for_#{reflection.name}"
161
161
  validation_method = "validate_associated_records_for_#{reflection.name}"
162
- validate validation_method
162
+ force_validation = (reflection.options[:validate] == true || reflection.options[:autosave] == true)
163
163
 
164
164
  case reflection.macro
165
165
  when :has_many, :has_and_belongs_to_many
@@ -170,7 +170,10 @@ module ActiveRecord
170
170
  after_create save_method
171
171
  after_update save_method
172
172
 
173
- define_method(validation_method) { validate_collection_association(reflection) }
173
+ if force_validation || (reflection.macro == :has_many && reflection.options[:validate] != false)
174
+ define_method(validation_method) { validate_collection_association(reflection) }
175
+ validate validation_method
176
+ end
174
177
  else
175
178
  case reflection.macro
176
179
  when :has_one
@@ -180,7 +183,11 @@ module ActiveRecord
180
183
  define_method(save_method) { save_belongs_to_association(reflection) }
181
184
  before_save save_method
182
185
  end
183
- define_method(validation_method) { validate_single_association(reflection) }
186
+
187
+ if force_validation
188
+ define_method(validation_method) { validate_single_association(reflection) }
189
+ validate validation_method
190
+ end
184
191
  end
185
192
  end
186
193
  end
@@ -224,10 +231,8 @@ module ActiveRecord
224
231
  # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
225
232
  # turned on for the association specified by +reflection+.
226
233
  def validate_single_association(reflection)
227
- if reflection.options[:validate] == true || reflection.options[:autosave] == true
228
- if (association = association_instance_get(reflection.name)) && !association.target.nil?
229
- association_valid?(reflection, association)
230
- end
234
+ if (association = association_instance_get(reflection.name)) && !association.target.nil?
235
+ association_valid?(reflection, association)
231
236
  end
232
237
  end
233
238
 
@@ -235,7 +240,7 @@ module ActiveRecord
235
240
  # <tt>:autosave</tt> is turned on for the association specified by
236
241
  # +reflection+.
237
242
  def validate_collection_association(reflection)
238
- if reflection.options[:validate] != false && association = association_instance_get(reflection.name)
243
+ if association = association_instance_get(reflection.name)
239
244
  if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
240
245
  records.each { |record| association_valid?(reflection, record) }
241
246
  end
@@ -244,16 +249,15 @@ module ActiveRecord
244
249
 
245
250
  # Returns whether or not the association is valid and applies any errors to
246
251
  # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
247
- # enabled records if they're marked_for_destruction?.
252
+ # enabled records if they're marked_for_destruction? or destroyed.
248
253
  def association_valid?(reflection, association)
254
+ return true if association.destroyed? || association.marked_for_destruction?
255
+
249
256
  unless valid = association.valid?
250
257
  if reflection.options[:autosave]
251
- unless association.marked_for_destruction?
252
- association.errors.each_error do |attribute, error|
253
- error = error.dup
254
- error.attribute = "#{reflection.name}_#{attribute}"
255
- errors.add(error) unless errors.on(error.attribute)
256
- end
258
+ association.errors.each_error do |attribute, error|
259
+ attribute = "#{reflection.name}.#{attribute}"
260
+ errors.add(attribute, error.dup) unless errors.on(attribute)
257
261
  end
258
262
  else
259
263
  errors.add(reflection.name)
@@ -283,9 +287,11 @@ module ActiveRecord
283
287
 
284
288
  if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
285
289
  records.each do |record|
290
+ next if record.destroyed?
291
+
286
292
  if autosave && record.marked_for_destruction?
287
293
  association.destroy(record)
288
- elsif @new_record_before_save || record.new_record?
294
+ elsif autosave != false && (@new_record_before_save || record.new_record?)
289
295
  if autosave
290
296
  association.send(:insert_record, record, false, false)
291
297
  else
@@ -311,14 +317,17 @@ module ActiveRecord
311
317
  # This all happens inside a transaction, _if_ the Transactions module is included into
312
318
  # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
313
319
  def save_has_one_association(reflection)
314
- if (association = association_instance_get(reflection.name)) && !association.target.nil?
320
+ if (association = association_instance_get(reflection.name)) && !association.target.nil? && !association.destroyed?
315
321
  autosave = reflection.options[:autosave]
316
322
 
317
323
  if autosave && association.marked_for_destruction?
318
324
  association.destroy
319
- elsif new_record? || association.new_record? || association[reflection.primary_key_name] != id || autosave
320
- association[reflection.primary_key_name] = id
321
- association.save(!autosave)
325
+ else
326
+ key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
327
+ if autosave != false && (new_record? || association.new_record? || association[reflection.primary_key_name] != key || autosave)
328
+ association[reflection.primary_key_name] = key
329
+ association.save(!autosave)
330
+ end
322
331
  end
323
332
  end
324
333
  end
@@ -332,12 +341,12 @@ module ActiveRecord
332
341
  # This all happens inside a transaction, _if_ the Transactions module is included into
333
342
  # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
334
343
  def save_belongs_to_association(reflection)
335
- if association = association_instance_get(reflection.name)
344
+ if (association = association_instance_get(reflection.name)) && !association.destroyed?
336
345
  autosave = reflection.options[:autosave]
337
346
 
338
347
  if autosave && association.marked_for_destruction?
339
348
  association.destroy
340
- else
349
+ elsif autosave != false
341
350
  association.save(!autosave) if association.new_record? || autosave
342
351
 
343
352
  if association.updated?
@@ -352,4 +361,4 @@ module ActiveRecord
352
361
  end
353
362
  end
354
363
  end
355
- end
364
+ end
@@ -2567,6 +2567,7 @@ module ActiveRecord #:nodoc:
2567
2567
  # options, use <tt>#destroy</tt>.
2568
2568
  def delete
2569
2569
  self.class.delete(id) unless new_record?
2570
+ @destroyed = true
2570
2571
  freeze
2571
2572
  end
2572
2573
 
@@ -2581,6 +2582,7 @@ module ActiveRecord #:nodoc:
2581
2582
  )
2582
2583
  end
2583
2584
 
2585
+ @destroyed = true
2584
2586
  freeze
2585
2587
  end
2586
2588
 
@@ -2840,6 +2842,11 @@ module ActiveRecord #:nodoc:
2840
2842
  @attributes.frozen?
2841
2843
  end
2842
2844
 
2845
+ # Returns +true+ if the record has been destroyed.
2846
+ def destroyed?
2847
+ @destroyed
2848
+ end
2849
+
2843
2850
  # Returns +true+ if the record is read only. Records loaded through joins with piggy-back
2844
2851
  # attributes will be marked as read only since they cannot be saved.
2845
2852
  def readonly?
@@ -7,7 +7,8 @@ module MysqlCompat #:nodoc:
7
7
  raise 'Mysql not loaded' unless defined?(::Mysql)
8
8
 
9
9
  target = defined?(Mysql::Result) ? Mysql::Result : MysqlRes
10
- return if target.instance_methods.include?('all_hashes')
10
+ return if target.instance_methods.include?('all_hashes') ||
11
+ target.instance_methods.include?(:all_hashes)
11
12
 
12
13
  # Ruby driver has a version string and returns null values in each_hash
13
14
  # C driver >= 2.7 returns null values in each_hash
@@ -63,12 +64,15 @@ module ActiveRecord
63
64
  raise
64
65
  end
65
66
  end
67
+
66
68
  MysqlCompat.define_all_hashes_method!
67
69
 
68
70
  mysql = Mysql.init
69
71
  mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey]
70
72
 
71
- ConnectionAdapters::MysqlAdapter.new(mysql, logger, [host, username, password, database, port, socket], config)
73
+ default_flags = Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? Mysql::CLIENT_MULTI_RESULTS : 0
74
+ options = [host, username, password, database, port, socket, default_flags]
75
+ ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config)
72
76
  end
73
77
  end
74
78
 
@@ -434,7 +434,7 @@ end
434
434
  # Any fixture labeled "DEFAULTS" is safely ignored.
435
435
 
436
436
  class Fixtures < (RUBY_VERSION < '1.9' ? YAML::Omap : Hash)
437
- MAX_ID = 2 ** 31 - 1
437
+ MAX_ID = 2 ** 30 - 1
438
438
  DEFAULT_FILTER_RE = /\.ya?ml$/
439
439
 
440
440
  @@all_cached_fixtures = {}
@@ -23,16 +23,6 @@ module ActiveRecord
23
23
  # p2.first_name = "should fail"
24
24
  # p2.save # Raises a ActiveRecord::StaleObjectError
25
25
  #
26
- # Optimistic locking will also check for stale data when objects are destroyed. Example:
27
- #
28
- # p1 = Person.find(1)
29
- # p2 = Person.find(1)
30
- #
31
- # p1.first_name = "Michael"
32
- # p1.save
33
- #
34
- # p2.destroy # Raises a ActiveRecord::StaleObjectError
35
- #
36
26
  # You're then responsible for dealing with the conflict by rescuing the exception and either rolling back, merging,
37
27
  # or otherwise apply the business logic needed to resolve the conflict.
38
28
  #
@@ -49,7 +39,6 @@ module ActiveRecord
49
39
  base.lock_optimistically = true
50
40
 
51
41
  base.alias_method_chain :update, :lock
52
- base.alias_method_chain :destroy, :lock
53
42
  base.alias_method_chain :attributes_from_column_definition, :lock
54
43
 
55
44
  class << base
@@ -109,28 +98,6 @@ module ActiveRecord
109
98
  end
110
99
  end
111
100
 
112
- def destroy_with_lock #:nodoc:
113
- return destroy_without_lock unless locking_enabled?
114
-
115
- unless new_record?
116
- lock_col = self.class.locking_column
117
- previous_value = send(lock_col).to_i
118
-
119
- affected_rows = connection.delete(
120
- "DELETE FROM #{self.class.quoted_table_name} " +
121
- "WHERE #{connection.quote_column_name(self.class.primary_key)} = #{quoted_id} " +
122
- "AND #{self.class.quoted_locking_column} = #{quote_value(previous_value)}",
123
- "#{self.class.name} Destroy"
124
- )
125
-
126
- unless affected_rows == 1
127
- raise ActiveRecord::StaleObjectError, "Attempted to delete a stale object"
128
- end
129
- end
130
-
131
- freeze
132
- end
133
-
134
101
  module ClassMethods
135
102
  DEFAULT_LOCKING_COLUMN = 'lock_version'
136
103
 
@@ -1,25 +1,3 @@
1
- # Copyright (c) 2006 Shugo Maeda <shugo@ruby-lang.org>
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
9
- # to 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 NONINFRINGEMENT.
17
- # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR
18
- # ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
19
- # CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
- # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
-
22
-
23
1
  module ActiveRecord
24
2
  module Locking
25
3
  # Locking::Pessimistic provides support for row-level locking using
@@ -1,9 +1,12 @@
1
1
  module ActiveRecord
2
2
  module NestedAttributes #:nodoc:
3
+ class TooManyRecords < ActiveRecordError
4
+ end
5
+
3
6
  def self.included(base)
4
7
  base.extend(ClassMethods)
5
- base.class_inheritable_accessor :reject_new_nested_attributes_procs, :instance_writer => false
6
- base.reject_new_nested_attributes_procs = {}
8
+ base.class_inheritable_accessor :nested_attributes_options, :instance_writer => false
9
+ base.nested_attributes_options = {}
7
10
  end
8
11
 
9
12
  # == Nested Attributes
@@ -62,10 +65,10 @@ module ActiveRecord
62
65
  # accepts_nested_attributes_for :avatar, :allow_destroy => true
63
66
  # end
64
67
  #
65
- # Now, when you add the <tt>_delete</tt> key to the attributes hash, with a
68
+ # Now, when you add the <tt>_destroy</tt> key to the attributes hash, with a
66
69
  # value that evaluates to +true+, you will destroy the associated model:
67
70
  #
68
- # member.avatar_attributes = { :id => '2', :_delete => '1' }
71
+ # member.avatar_attributes = { :id => '2', :_destroy => '1' }
69
72
  # member.avatar.marked_for_destruction? # => true
70
73
  # member.save
71
74
  # member.avatar #=> nil
@@ -85,14 +88,14 @@ module ActiveRecord
85
88
  # the attribute hash.
86
89
  #
87
90
  # For each hash that does _not_ have an <tt>id</tt> key a new record will
88
- # be instantiated, unless the hash also contains a <tt>_delete</tt> key
91
+ # be instantiated, unless the hash also contains a <tt>_destroy</tt> key
89
92
  # that evaluates to +true+.
90
93
  #
91
94
  # params = { :member => {
92
95
  # :name => 'joe', :posts_attributes => [
93
96
  # { :title => 'Kari, the awesome Ruby documentation browser!' },
94
97
  # { :title => 'The egalitarian assumption of the modern citizen' },
95
- # { :title => '', :_delete => '1' } # this will be ignored
98
+ # { :title => '', :_destroy => '1' } # this will be ignored
96
99
  # ]
97
100
  # }}
98
101
  #
@@ -123,6 +126,22 @@ module ActiveRecord
123
126
  # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
124
127
  # member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
125
128
  #
129
+ # Alternatively, :reject_if also accepts a symbol for using methods:
130
+ #
131
+ # class Member < ActiveRecord::Base
132
+ # has_many :posts
133
+ # accepts_nested_attributes_for :posts, :reject_if => :new_record?
134
+ # end
135
+ #
136
+ # class Member < ActiveRecord::Base
137
+ # has_many :posts
138
+ # accepts_nested_attributes_for :posts, :reject_if => :reject_posts
139
+ #
140
+ # def reject_posts(attributed)
141
+ # attributed['title].blank?
142
+ # end
143
+ # end
144
+ #
126
145
  # If the hash contains an <tt>id</tt> key that matches an already
127
146
  # associated record, the matching record will be modified:
128
147
  #
@@ -140,7 +159,7 @@ module ActiveRecord
140
159
  # By default the associated records are protected from being destroyed. If
141
160
  # you want to destroy any of the associated records through the attributes
142
161
  # hash, you have to enable it first using the <tt>:allow_destroy</tt>
143
- # option. This will allow you to also use the <tt>_delete</tt> key to
162
+ # option. This will allow you to also use the <tt>_destroy</tt> key to
144
163
  # destroy existing records:
145
164
  #
146
165
  # class Member < ActiveRecord::Base
@@ -149,7 +168,7 @@ module ActiveRecord
149
168
  # end
150
169
  #
151
170
  # params = { :member => {
152
- # :posts_attributes => [{ :id => '2', :_delete => '1' }]
171
+ # :posts_attributes => [{ :id => '2', :_destroy => '1' }]
153
172
  # }}
154
173
  #
155
174
  # member.attributes = params['member']
@@ -172,14 +191,23 @@ module ActiveRecord
172
191
  # Supported options:
173
192
  # [:allow_destroy]
174
193
  # If true, destroys any members from the attributes hash with a
175
- # <tt>_delete</tt> key and a value that evaluates to +true+
194
+ # <tt>_destroy</tt> key and a value that evaluates to +true+
176
195
  # (eg. 1, '1', true, or 'true'). This option is off by default.
177
196
  # [:reject_if]
178
- # Allows you to specify a Proc that checks whether a record should be
179
- # built for a certain attribute hash. The hash is passed to the Proc
180
- # and the Proc should return either +true+ or +false+. When no Proc
181
- # is specified a record will be built for all attribute hashes that
182
- # do not have a <tt>_delete</tt> that evaluates to true.
197
+ # Allows you to specify a Proc or a Symbol pointing to a method
198
+ # that checks whether a record should be built for a certain attribute
199
+ # hash. The hash is passed to the supplied Proc or the method
200
+ # and it should return either +true+ or +false+. When no :reject_if
201
+ # is specified, a record will be built for all attribute hashes that
202
+ # do not have a <tt>_destroy</tt> value that evaluates to true.
203
+ # Passing <tt>:all_blank</tt> instead of a Proc will create a proc
204
+ # that will reject a record where all the attributes are blank.
205
+ # [:limit]
206
+ # Allows you to specify the maximum number of the associated records that
207
+ # can be processes with the nested attributes. If the size of the
208
+ # nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords
209
+ # exception is raised. If omitted, any number associations can be processed.
210
+ # Note that the :limit option is only applicable to one-to-many associations.
183
211
  #
184
212
  # Examples:
185
213
  # # creates avatar_attributes=
@@ -189,7 +217,7 @@ module ActiveRecord
189
217
  def accepts_nested_attributes_for(*attr_names)
190
218
  options = { :allow_destroy => false }
191
219
  options.update(attr_names.extract_options!)
192
- options.assert_valid_keys(:allow_destroy, :reject_if)
220
+ options.assert_valid_keys(:allow_destroy, :reject_if, :limit)
193
221
 
194
222
  attr_names.each do |association_name|
195
223
  if reflection = reflect_on_association(association_name)
@@ -201,16 +229,18 @@ module ActiveRecord
201
229
  end
202
230
 
203
231
  reflection.options[:autosave] = true
204
- self.reject_new_nested_attributes_procs[association_name.to_sym] = options[:reject_if]
232
+ self.nested_attributes_options[association_name.to_sym] = options
205
233
 
206
234
  # def pirate_attributes=(attributes)
207
235
  # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, false)
208
236
  # end
209
237
  class_eval %{
210
238
  def #{association_name}_attributes=(attributes)
211
- assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes, #{options[:allow_destroy]})
239
+ assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
212
240
  end
213
241
  }, __FILE__, __LINE__
242
+
243
+ add_autosave_association_callbacks(reflection)
214
244
  else
215
245
  raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
216
246
  end
@@ -223,15 +253,25 @@ module ActiveRecord
223
253
  # destruction of this association.
224
254
  #
225
255
  # See ActionView::Helpers::FormHelper::fields_for for more info.
226
- def _delete
256
+ def _destroy
227
257
  marked_for_destruction?
228
258
  end
229
259
 
260
+ # Deal with deprecated _delete.
261
+ #
262
+ def _delete #:nodoc:
263
+ ActiveSupport::Deprecation.warn "_delete is deprecated in nested attributes. Use _destroy instead."
264
+ _destroy
265
+ end
266
+
230
267
  private
231
268
 
232
269
  # Attribute hash keys that should not be assigned as normal attributes.
233
270
  # These hash keys are nested attributes implementation details.
234
- UNASSIGNABLE_KEYS = %w{ id _delete }
271
+ #
272
+ # TODO Remove _delete from UNASSIGNABLE_KEYS when deprecation warning are
273
+ # removed.
274
+ UNASSIGNABLE_KEYS = %w( id _destroy _delete )
235
275
 
236
276
  # Assigns the given attributes to the association.
237
277
  #
@@ -240,17 +280,23 @@ module ActiveRecord
240
280
  # record will be built.
241
281
  #
242
282
  # If the given attributes include a matching <tt>:id</tt> attribute _and_ a
243
- # <tt>:_delete</tt> key set to a truthy value, then the existing record
283
+ # <tt>:_destroy</tt> key set to a truthy value, then the existing record
244
284
  # will be marked for destruction.
245
- def assign_nested_attributes_for_one_to_one_association(association_name, attributes, allow_destroy)
246
- attributes = attributes.stringify_keys
285
+ def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
286
+ options = self.nested_attributes_options[association_name]
287
+ attributes = attributes.with_indifferent_access
247
288
 
248
289
  if attributes['id'].blank?
249
290
  unless reject_new_record?(association_name, attributes)
250
- send("build_#{association_name}", attributes.except(*UNASSIGNABLE_KEYS))
291
+ method = "build_#{association_name}"
292
+ if respond_to?(method)
293
+ send(method, attributes.except(*UNASSIGNABLE_KEYS))
294
+ else
295
+ raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
296
+ end
251
297
  end
252
298
  elsif (existing_record = send(association_name)) && existing_record.id.to_s == attributes['id'].to_s
253
- assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy)
299
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
254
300
  end
255
301
  end
256
302
 
@@ -259,7 +305,7 @@ module ActiveRecord
259
305
  # Hashes with an <tt>:id</tt> value matching an existing associated record
260
306
  # will update that record. Hashes without an <tt>:id</tt> value will build
261
307
  # a new record for the association. Hashes with a matching <tt>:id</tt>
262
- # value and a <tt>:_delete</tt> key set to a truthy value will mark the
308
+ # value and a <tt>:_destroy</tt> key set to a truthy value will mark the
263
309
  # matched record for destruction.
264
310
  #
265
311
  # For example:
@@ -267,7 +313,7 @@ module ActiveRecord
267
313
  # assign_nested_attributes_for_collection_association(:people, {
268
314
  # '1' => { :id => '1', :name => 'Peter' },
269
315
  # '2' => { :name => 'John' },
270
- # '3' => { :id => '2', :_delete => true }
316
+ # '3' => { :id => '2', :_destroy => true }
271
317
  # })
272
318
  #
273
319
  # Will update the name of the Person with ID 1, build a new associated
@@ -279,51 +325,68 @@ module ActiveRecord
279
325
  # assign_nested_attributes_for_collection_association(:people, [
280
326
  # { :id => '1', :name => 'Peter' },
281
327
  # { :name => 'John' },
282
- # { :id => '2', :_delete => true }
328
+ # { :id => '2', :_destroy => true }
283
329
  # ])
284
- def assign_nested_attributes_for_collection_association(association_name, attributes_collection, allow_destroy)
330
+ def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
331
+ options = self.nested_attributes_options[association_name]
332
+
285
333
  unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array)
286
334
  raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
287
335
  end
288
336
 
337
+ if options[:limit] && attributes_collection.size > options[:limit]
338
+ raise TooManyRecords, "Maximum #{options[:limit]} records are allowed. Got #{attributes_collection.size} records instead."
339
+ end
340
+
289
341
  if attributes_collection.is_a? Hash
290
342
  attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
291
343
  end
292
344
 
293
345
  attributes_collection.each do |attributes|
294
- attributes = attributes.stringify_keys
346
+ attributes = attributes.with_indifferent_access
295
347
 
296
348
  if attributes['id'].blank?
297
349
  unless reject_new_record?(association_name, attributes)
298
350
  send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS))
299
351
  end
300
352
  elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes['id'].to_s }
301
- assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy)
353
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
302
354
  end
303
355
  end
304
356
  end
305
357
 
306
358
  # Updates a record with the +attributes+ or marks it for destruction if
307
- # +allow_destroy+ is +true+ and has_delete_flag? returns +true+.
359
+ # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+.
308
360
  def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
309
- if has_delete_flag?(attributes) && allow_destroy
361
+ if has_destroy_flag?(attributes) && allow_destroy
310
362
  record.mark_for_destruction
311
363
  else
312
364
  record.attributes = attributes.except(*UNASSIGNABLE_KEYS)
313
365
  end
314
366
  end
315
367
 
316
- # Determines if a hash contains a truthy _delete key.
317
- def has_delete_flag?(hash)
318
- ConnectionAdapters::Column.value_to_boolean hash['_delete']
368
+ # Determines if a hash contains a truthy _destroy key.
369
+ def has_destroy_flag?(hash)
370
+ ConnectionAdapters::Column.value_to_boolean(hash['_destroy']) ||
371
+ ConnectionAdapters::Column.value_to_boolean(hash['_delete']) # TODO Remove after deprecation.
319
372
  end
320
373
 
321
374
  # Determines if a new record should be build by checking for
322
- # has_delete_flag? or if a <tt>:reject_if</tt> proc exists for this
375
+ # has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this
323
376
  # association and evaluates to +true+.
324
377
  def reject_new_record?(association_name, attributes)
325
- has_delete_flag?(attributes) ||
326
- self.class.reject_new_nested_attributes_procs[association_name].try(:call, attributes)
378
+ has_destroy_flag?(attributes) || call_reject_if(association_name, attributes)
379
+ end
380
+
381
+ def call_reject_if(association_name, attributes)
382
+ callback = self.nested_attributes_options[association_name][:reject_if]
383
+
384
+ case callback
385
+ when Symbol
386
+ method(callback).arity == 0 ? send(callback) : send(callback, attributes)
387
+ when Proc
388
+ callback.try(:call, attributes)
389
+ end
327
390
  end
328
391
  end
329
392
  end