diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 05c7e9713f6335df3edaca560d4e9f3ed9c0df17..edcf547e01a12678d1e1b3f586ea6e677e4a50bb 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -3,6 +3,9 @@
module ActiveRecord
module NestedAttributes #:nodoc:
+ class TooManyRecords < ActiveRecordError
+ end
+
extend ActiveSupport::Concern
included do
@@ -203,6 +206,12 @@ module ClassMethods
# do not have a _destroy value that evaluates to true.
# Passing :all_blank instead of a Proc will create a proc
# that will reject a record where all the attributes are blank.
+ # [:limit]
+ # Allows you to specify the maximum number of the associated records that
+ # can be processes with the nested attributes. If the size of the
+ # nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords
+ # exception is raised. If omitted, any number associations can be processed.
+ # Note that the :limit option is only applicable to one-to-many associations.
#
# Examples:
# # creates avatar_attributes=
@@ -214,7 +223,7 @@ module ClassMethods
def accepts_nested_attributes_for(*attr_names)
options = { :allow_destroy => false }
options.update(attr_names.extract_options!)
- options.assert_valid_keys(:allow_destroy, :reject_if)
+ options.assert_valid_keys(:allow_destroy, :reject_if, :limit)
attr_names.each do |association_name|
if reflection = reflect_on_association(association_name)
@@ -334,6 +343,10 @@ def assign_nested_attributes_for_collection_association(association_name, attrib
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
end
+ if options[:limit] && attributes_collection.size > options[:limit]
+ raise TooManyRecords, "Maximum #{options[:limit]} records are allowed. Got #{attributes_collection.size} records instead."
+ end
+
if attributes_collection.is_a? Hash
attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes }
end
diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb
index 2a2a19fd4cd661a6dfc4233d979b7256b99c9430..53fd168e1b0145d2d47ead1ebe006a9804ea3967 100644
--- a/activerecord/test/cases/nested_attributes_test.rb
+++ b/activerecord/test/cases/nested_attributes_test.rb
@@ -603,3 +603,33 @@ def setup
include NestedAttributesOnACollectionAssociationTests
end
+
+class TestNestedAttributesLimit < ActiveRecord::TestCase
+ def setup
+ Pirate.accepts_nested_attributes_for :parrots, :limit => 2
+
+ @pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
+ end
+
+ def teardown
+ Pirate.accepts_nested_attributes_for :parrots, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
+ end
+
+ def test_limit_with_less_records
+ @pirate.attributes = { :parrots_attributes => { 'foo' => { :name => 'Big Big Love' } } }
+ assert_difference('Parrot.count') { @pirate.save! }
+ end
+
+ def test_limit_with_number_exact_records
+ @pirate.attributes = { :parrots_attributes => { 'foo' => { :name => 'Lovely Day' }, 'bar' => { :name => 'Blown Away' } } }
+ assert_difference('Parrot.count', 2) { @pirate.save! }
+ end
+
+ def test_limit_with_exceeding_records
+ assert_raises(ActiveRecord::NestedAttributes::TooManyRecords) do
+ @pirate.attributes = { :parrots_attributes => { 'foo' => { :name => 'Lovely Day' },
+ 'bar' => { :name => 'Blown Away' },
+ 'car' => { :name => 'The Happening' }} }
+ end
+ end
+end