diff options
author | Pratik Naik <pratiknaik@gmail.com> | 2009-02-19 21:20:15 +0100 |
---|---|---|
committer | Pratik Naik <pratiknaik@gmail.com> | 2009-02-19 21:20:15 +0100 |
commit | d8f1ee4b41352b870f617b01099b2877f754d32c (patch) | |
tree | dd6bdf1d1ded6aab7bb926109a24e942fc740b73 /activerecord/lib | |
parent | 8ba1fc18e13c03966d411947180022c1730e81ff (diff) | |
parent | 7c0e008973e594ebf53607362c1dfbe34b693600 (diff) | |
download | rails-d8f1ee4b41352b870f617b01099b2877f754d32c.tar.gz rails-d8f1ee4b41352b870f617b01099b2877f754d32c.tar.bz2 rails-d8f1ee4b41352b870f617b01099b2877f754d32c.zip |
Merge commit 'mainstream/master'
Diffstat (limited to 'activerecord/lib')
-rw-r--r-- | activerecord/lib/active_record/migration.rb | 6 | ||||
-rw-r--r-- | activerecord/lib/active_record/nested_attributes.rb | 246 |
2 files changed, 151 insertions, 101 deletions
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 15350cf1e1..657acd6dc0 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -338,6 +338,10 @@ module ActiveRecord self.verbose = save end + def connection + ActiveRecord::Base.connection + end + def method_missing(method, *arguments, &block) arg_list = arguments.map(&:inspect) * ', ' @@ -345,7 +349,7 @@ module ActiveRecord unless arguments.empty? || method == :execute arguments[0] = Migrator.proper_table_name(arguments.first) end - ActiveRecord::Base.connection.send(method, *arguments, &block) + connection.send(method, *arguments, &block) end end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 5778223c74..e3122d195a 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -41,15 +41,16 @@ module ActiveRecord # Enabling nested attributes on a one-to-one association allows you to # create the member and avatar in one go: # - # params = { 'member' => { 'name' => 'Jack', 'avatar_attributes' => { 'icon' => 'smiling' } } } + # params = { :member => { :name => 'Jack', :avatar_attributes => { :icon => 'smiling' } } } # member = Member.create(params) - # member.avatar.icon #=> 'smiling' + # member.avatar.id # => 2 + # member.avatar.icon # => 'smiling' # # It also allows you to update the avatar through the member: # - # params = { 'member' => { 'avatar_attributes' => { 'icon' => 'sad' } } } + # params = { :member' => { :avatar_attributes => { :id => '2', :icon => 'sad' } } } # member.update_attributes params['member'] - # member.avatar.icon #=> 'sad' + # member.avatar.icon # => 'sad' # # By default you will only be able to set and update attributes on the # associated model. If you want to destroy the associated model through the @@ -64,7 +65,7 @@ module ActiveRecord # Now, when you add the <tt>_delete</tt> key to the attributes hash, with a # value that evaluates to +true+, you will destroy the associated model: # - # member.avatar_attributes = { '_delete' => '1' } + # member.avatar_attributes = { :id => '2', :_delete => '1' } # member.avatar.marked_for_destruction? # => true # member.save # member.avatar #=> nil @@ -77,59 +78,80 @@ module ActiveRecord # # class Member < ActiveRecord::Base # has_many :posts - # accepts_nested_attributes_for :posts, :reject_if => proc { |attributes| attributes['title'].blank? } + # accepts_nested_attributes_for :posts # end # # You can now set or update attributes on an associated post model through # the attribute hash. # - # For each key in the hash that starts with the string 'new' a new model - # will be instantiated. When the proc given with the <tt>:reject_if</tt> - # option evaluates to +false+ for a certain attribute hash no record will - # be built for that hash. (Rejecting new records can alternatively be done - # by utilizing the <tt>'_delete'</tt> key. Scroll down for more info.) - # - # params = { 'member' => { - # 'name' => 'joe', 'posts_attributes' => { - # 'new_12345' => { 'title' => 'Kari, the awesome Ruby documentation browser!' }, - # 'new_54321' => { 'title' => 'The egalitarian assumption of the modern citizen' }, - # 'new_67890' => { 'title' => '' } # This one matches the :reject_if proc and will not be instantiated. - # } + # For each hash that does _not_ have an <tt>id</tt> key a new record will + # be instantiated, unless the hash also contains a <tt>_delete</tt> key + # that evaluates to +true+. + # + # params = { :member => { + # :name => 'joe', :posts_attributes => [ + # { :title => 'Kari, the awesome Ruby documentation browser!' }, + # { :title => 'The egalitarian assumption of the modern citizen' }, + # { :title => '', :_delete => '1' } # this will be ignored + # ] # }} # # member = Member.create(params['member']) - # member.posts.length #=> 2 - # member.posts.first.title #=> 'Kari, the awesome Ruby documentation browser!' - # member.posts.second.title #=> 'The egalitarian assumption of the modern citizen' + # member.posts.length # => 2 + # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!' + # member.posts.second.title # => 'The egalitarian assumption of the modern citizen' + # + # You may also set a :reject_if proc to silently ignore any new record + # hashes if they fail to pass your criteria. For example, the previous + # example could be rewritten as: + # + # class Member < ActiveRecord::Base + # has_many :posts + # accepts_nested_attributes_for :posts, :reject_if => proc { |attributes| attributes['title'].blank? } + # end + # + # params = { :member => { + # :name => 'joe', :posts_attributes => [ + # { :title => 'Kari, the awesome Ruby documentation browser!' }, + # { :title => 'The egalitarian assumption of the modern citizen' }, + # { :title => '' } # this will be ignored because of the :reject_if proc + # ] + # }} + # + # member = Member.create(params['member']) + # member.posts.length # => 2 + # member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!' + # member.posts.second.title # => 'The egalitarian assumption of the modern citizen' # - # When the key for post attributes is an integer, the associated post with - # that ID will be updated: + # If the hash contains an <tt>id</tt> key that matches an already + # associated record, the matching record will be modified: # # member.attributes = { - # 'name' => 'Joe', - # 'posts_attributes' => { - # '1' => { 'title' => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' }, - # '2' => { 'title' => '[UPDATED] other post' } - # } + # :name => 'Joe', + # :posts_attributes => [ + # { :id => 1, :title => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' }, + # { :id => 2, :title => '[UPDATED] other post' } + # ] # } # - # By default the associated models are protected from being destroyed. If - # you want to destroy any of the associated models through the attributes - # hash, you have to enable it first using the <tt>:allow_destroy</tt> - # option. + # member.posts.first.title # => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' + # member.posts.second.title # => '[UPDATED] other post' # - # This will allow you to specify which models to destroy in the attributes - # hash by setting the '_delete' attribute to a value that evaluates to - # +true+: + # By default the associated records are protected from being destroyed. If + # you want to destroy any of the associated records through the attributes + # hash, you have to enable it first using the <tt>:allow_destroy</tt> + # option. This will allow you to also use the <tt>_delete</tt> key to + # destroy existing records: # # class Member < ActiveRecord::Base # has_many :posts # accepts_nested_attributes_for :posts, :allow_destroy => true # end # - # params = {'member' => { 'name' => 'joe', 'posts_attributes' => { - # '2' => { '_delete' => '1' } - # }}} + # params = { :member => { + # :posts_attributes => [{ :id => '2', :_delete => '1' }] + # }} + # # member.attributes = params['member'] # member.posts.detect { |p| p.id == 2 }.marked_for_destruction? # => true # member.posts.length #=> 2 @@ -143,24 +165,27 @@ module ActiveRecord # the parent model is saved. This happens inside the transaction initiated # by the parents save method. See ActiveRecord::AutosaveAssociation. module ClassMethods - # Defines an attributes writer for the specified association(s). + # Defines an attributes writer for the specified association(s). If you + # are using <tt>attr_protected</tt> or <tt>attr_accessible</tt>, then you + # will need to add the attribute writer to the allowed list. # # Supported options: # [:allow_destroy] # If true, destroys any members from the attributes hash with a - # <tt>_delete</tt> key and a value that converts to +true+ + # <tt>_delete</tt> key and a value that evaluates to +true+ # (eg. 1, '1', true, or 'true'). This option is off by default. # [:reject_if] # Allows you to specify a Proc that checks whether a record should be # built for a certain attribute hash. The hash is passed to the Proc # and the Proc should return either +true+ or +false+. When no Proc - # is specified a record will be built for all attribute hashes. + # is specified a record will be built for all attribute hashes that + # do not have a <tt>_delete</tt> that evaluates to true. # # Examples: - # accepts_nested_attributes_for :avatar - # accepts_nested_attributes_for :avatar, :allow_destroy => true - # accepts_nested_attributes_for :avatar, :reject_if => proc { ... } - # accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true, :reject_if => proc { ... } + # # creates avatar_attributes= + # accepts_nested_attributes_for :avatar, :reject_if => proc { |attributes| attributes['name'].blank? } + # # creates avatar_attributes= and posts_attributes= + # accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true def accepts_nested_attributes_for(*attr_names) options = { :allow_destroy => false } options.update(attr_names.extract_options!) @@ -193,9 +218,9 @@ module ActiveRecord end end - # Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? - # It's used in conjunction with fields_for to build a form element - # for the destruction of this association. + # Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's + # used in conjunction with fields_for to build a form element for the + # destruction of this association. # # See ActionView::Helpers::FormHelper::fields_for for more info. def _delete @@ -204,80 +229,101 @@ module ActiveRecord private - # Assigns the given attributes to the association. An association will be - # build if it doesn't exist yet. + # Attribute hash keys that should not be assigned as normal attributes. + # These hash keys are nested attributes implementation details. + UNASSIGNABLE_KEYS = %w{ id _delete } + + # Assigns the given attributes to the association. + # + # If the given attributes include an <tt>:id</tt> that matches the existing + # record’s id, then the existing record will be modified. Otherwise a new + # record will be built. + # + # If the given attributes include a matching <tt>:id</tt> attribute _and_ a + # <tt>:_delete</tt> key set to a truthy value, then the existing record + # will be marked for destruction. def assign_nested_attributes_for_one_to_one_association(association_name, attributes, allow_destroy) - if should_destroy_nested_attributes_record?(allow_destroy, attributes) - send(association_name).mark_for_destruction - else - (send(association_name) || send("build_#{association_name}")).attributes = attributes + attributes = attributes.stringify_keys + + if attributes['id'].blank? + unless reject_new_record?(association_name, attributes) + send("build_#{association_name}", attributes.except(*UNASSIGNABLE_KEYS)) + end + elsif (existing_record = send(association_name)) && existing_record.id.to_s == attributes['id'].to_s + assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy) end end # Assigns the given attributes to the collection association. # - # Keys containing an ID for an associated record will update that record. - # Keys starting with <tt>new</tt> will instantiate a new record for that - # association. + # Hashes with an <tt>:id</tt> value matching an existing associated record + # will update that record. Hashes without an <tt>:id</tt> value will build + # a new record for the association. Hashes with a matching <tt>:id</tt> + # value and a <tt>:_delete</tt> key set to a truthy value will mark the + # matched record for destruction. # # For example: # # assign_nested_attributes_for_collection_association(:people, { - # '1' => { 'name' => 'Peter' }, - # 'new_43' => { 'name' => 'John' } + # '1' => { :id => '1', :name => 'Peter' }, + # '2' => { :name => 'John' }, + # '3' => { :id => '2', :_delete => true } # }) # - # Will update the name of the Person with ID 1 and create a new associated - # person with the name 'John'. - def assign_nested_attributes_for_collection_association(association_name, attributes, allow_destroy) - unless attributes.is_a?(Hash) - raise ArgumentError, "Hash expected, got #{attributes.class.name} (#{attributes.inspect})" + # Will update the name of the Person with ID 1, build a new associated + # person with the name `John', and mark the associatied Person with ID 2 + # for destruction. + # + # Also accepts an Array of attribute hashes: + # + # assign_nested_attributes_for_collection_association(:people, [ + # { :id => '1', :name => 'Peter' }, + # { :name => 'John' }, + # { :id => '2', :_delete => true } + # ]) + def assign_nested_attributes_for_collection_association(association_name, attributes_collection, allow_destroy) + unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) + raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" end - # Make sure any new records sorted by their id before they're build. - sorted_by_id = attributes.sort_by { |id, _| id.is_a?(String) ? id.sub(/^new_/, '').to_i : id } - - sorted_by_id.each do |id, record_attributes| - if id.acts_like?(:string) && id.starts_with?('new_') - build_new_nested_attributes_record(association_name, record_attributes) - else - assign_to_or_destroy_nested_attributes_record(association_name, id, record_attributes, allow_destroy) - end + if attributes_collection.is_a? Hash + attributes_collection = attributes_collection.sort_by { |index, _| index.to_i }.map { |_, attributes| attributes } end - end - # Returns +true+ if <tt>allow_destroy</tt> is enabled and the attributes - # contains a truthy value for the key <tt>'_delete'</tt>. - # - # It will _always_ remove the <tt>'_delete'</tt> key, if present. - def should_destroy_nested_attributes_record?(allow_destroy, attributes) - ConnectionAdapters::Column.value_to_boolean(attributes.delete('_delete')) && allow_destroy - end + attributes_collection.each do |attributes| + attributes = attributes.stringify_keys - # Builds a new record with the given attributes. - # - # If a <tt>:reject_if</tt> proc exists for this association, it will be - # called with the attributes as its argument. If the proc returns a truthy - # value, the record is _not_ build. - # - # Alternatively, you can specify the <tt>'_delete'</tt> key to _not_ build - # a record. See should_destroy_nested_attributes_record? for more info. - def build_new_nested_attributes_record(association_name, attributes) - if reject_proc = self.class.reject_new_nested_attributes_procs[association_name] - return if reject_proc.call(attributes) + if attributes['id'].blank? + unless reject_new_record?(association_name, attributes) + send(association_name).build(attributes.except(*UNASSIGNABLE_KEYS)) + end + elsif existing_record = send(association_name).detect { |record| record.id.to_s == attributes['id'].to_s } + assign_to_or_mark_for_destruction(existing_record, attributes, allow_destroy) + end end - send(association_name).build(attributes) unless should_destroy_nested_attributes_record?(true, attributes) end - # Assigns the attributes to the record specified by +id+. Or marks it for - # destruction if #should_destroy_nested_attributes_record? returns +true+. - def assign_to_or_destroy_nested_attributes_record(association_name, id, attributes, allow_destroy) - record = send(association_name).detect { |record| record.id == id.to_i } - if should_destroy_nested_attributes_record?(allow_destroy, attributes) + # Updates a record with the +attributes+ or marks it for destruction if + # +allow_destroy+ is +true+ and has_delete_flag? returns +true+. + def assign_to_or_mark_for_destruction(record, attributes, allow_destroy) + if has_delete_flag?(attributes) && allow_destroy record.mark_for_destruction else - record.attributes = attributes + record.attributes = attributes.except(*UNASSIGNABLE_KEYS) end end + + # Determines if a hash contains a truthy _delete key. + def has_delete_flag?(hash) + ConnectionAdapters::Column.value_to_boolean hash['_delete'] + end + + # Determines if a new record should be build by checking for + # has_delete_flag? or if a <tt>:reject_if</tt> proc exists for this + # association and evaluates to +true+. + def reject_new_record?(association_name, attributes) + has_delete_flag?(attributes) || + self.class.reject_new_nested_attributes_procs[association_name].try(:call, attributes) + end end -end
\ No newline at end of file +end |