module ActiveRecord::NestedAttributes
def _destroy
destruction of this association.
used in conjunction with fields_for to build a form element for the
Returns ActiveRecord::AutosaveAssociation#marked_for_destruction? It's
def _destroy marked_for_destruction? end
def allow_destroy?(association_name)
def allow_destroy?(association_name) nested_attributes_options[association_name][:allow_destroy] end
def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
{ id: '2', _destroy: true }
{ name: 'John' },
{ id: '1', name: 'Peter' },
assign_nested_attributes_for_collection_association(:people, [
Also accepts an Array of attribute hashes:
for destruction.
person with the name 'John', and mark the associated Person with ID 2
Will update the name of the Person with ID 1, build a new associated
})
'3' => { id: '2', _destroy: true }
'2' => { name: 'John' },
'1' => { id: '1', name: 'Peter' },
assign_nested_attributes_for_collection_association(:people, {
For example:
matched record for destruction.
value and a :_destroy key set to a truthy value will mark the
a new record for the association. Hashes with a matching :id
will update that record. Hashes without an :id value will build
Hashes with an :id value matching an existing associated record
Assigns the given attributes to the collection association.
def assign_nested_attributes_for_collection_association(association_name, attributes_collection) options = nested_attributes_options[association_name] if attributes_collection.respond_to?(:permitted?) attributes_collection = attributes_collection.to_h end unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) raise ArgumentError, "Hash or Array expected for `#{association_name}` attributes, got #{attributes_collection.class.name}" end check_record_limit!(options[:limit], attributes_collection) if attributes_collection.is_a? Hash keys = attributes_collection.keys attributes_collection = if keys.include?("id") || keys.include?(:id) [attributes_collection] else attributes_collection.values end end association = association(association_name) existing_records = if association.loaded? association.target else attribute_ids = attributes_collection.filter_map { |a| a["id"] || a[:id] } attribute_ids.empty? ? [] : association.scope.where(association.klass.primary_key => attribute_ids) end records = attributes_collection.map do |attributes| if attributes.respond_to?(:permitted?) attributes = attributes.to_h end attributes = attributes.with_indifferent_access if attributes["id"].blank? unless reject_new_record?(association_name, attributes) association.reader.build(attributes.except(*UNASSIGNABLE_KEYS)) end elsif existing_record = find_record_by_id(association.klass, existing_records, attributes["id"]) unless call_reject_if(association_name, attributes) # Make sure we are operating on the actual object which is in the association's # proxy_target array (either by finding it, or adding it if not found) # Take into account that the proxy_target may have changed due to callbacks target_record = find_record_by_id(association.klass, association.target, attributes["id"]) if target_record existing_record = target_record else association.add_to_target(existing_record, skip_callbacks: true) end assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) existing_record end else raise_nested_attributes_record_not_found!(association_name, attributes["id"]) end end association.nested_attributes_target = records end
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
update_only is true, and a :_destroy key set to a truthy value,
If the given attributes include a matching :id attribute, or
record will be modified regardless of whether an :id is provided.
it will be replaced with a new record. If update_only is +true+ the existing
id, then the existing record will be modified. If no :id is provided
given attributes include an :id that matches the existing record's
the value of the update_only option. If update_only is +false+ and the
an associated record already exists, the method's behavior depends on
If an associated record does not yet exist, one will be instantiated. If
Assigns the given attributes to the association.
def assign_nested_attributes_for_one_to_one_association(association_name, attributes) if attributes.respond_to?(:permitted?) attributes = attributes.to_h end unless attributes.is_a?(Hash) raise ArgumentError, "Hash expected for `#{association_name}` attributes, got #{attributes.class.name}" end options = nested_attributes_options[association_name] attributes = attributes.with_indifferent_access existing_record = send(association_name) if (options[:update_only] || !attributes["id"].blank?) && existing_record && (options[:update_only] || existing_record.id.to_s == attributes["id"].to_s) assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes) elsif attributes["id"].present? raise_nested_attributes_record_not_found!(association_name, attributes["id"]) elsif !reject_new_record?(association_name, attributes) assignable_attributes = attributes.except(*UNASSIGNABLE_KEYS) if existing_record && existing_record.new_record? existing_record.assign_attributes(assignable_attributes) association(association_name).initialize_attributes(existing_record) else method = :"build_#{association_name}" if respond_to?(method) send(method, assignable_attributes) else raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?" end end end end
def assign_to_or_mark_for_destruction(record, attributes, allow_destroy)
Updates a record with the +attributes+ or marks it for destruction if
def assign_to_or_mark_for_destruction(record, attributes, allow_destroy) record.assign_attributes(attributes.except(*UNASSIGNABLE_KEYS)) record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy end
def call_reject_if(association_name, attributes)
The reject_if option is defined by +accepts_nested_attributes_for+.
rejected by calling the reject_if Symbol or Proc (if defined).
Determines if a record with the particular +attributes+ should be
def call_reject_if(association_name, attributes) return false if will_be_destroyed?(association_name, attributes) case callback = nested_attributes_options[association_name][:reject_if] when Symbol method(callback).arity == 0 ? send(callback) : send(callback, attributes) when Proc callback.call(attributes) end end
def check_record_limit!(limit, attributes_collection)
Raises TooManyRecords error if the attributes_collection is
number-like object (anything that can be compared with an integer).
records. It accepts limit in the form of symbol, proc, or
Takes in a limit and checks if the attributes_collection has too many
def check_record_limit!(limit, attributes_collection) if limit limit = \ case limit when Symbol send(limit) when Proc limit.call else limit end if limit && attributes_collection.size > limit raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead." end end end
def find_record_by_id(klass, records, id)
def find_record_by_id(klass, records, id) if klass.composite_primary_key? id = Array(id).map(&:to_s) records.find { |record| Array(record.id).map(&:to_s) == id } else records.find { |record| record.id.to_s == id.to_s } end end
def has_destroy_flag?(hash)
def has_destroy_flag?(hash) Type::Boolean.new.cast(hash["_destroy"]) end
def raise_nested_attributes_record_not_found!(association_name, record_id)
def raise_nested_attributes_record_not_found!(association_name, record_id) model = self.class._reflect_on_association(association_name).klass.name raise RecordNotFound.new("Couldn't find #{model} with ID=#{record_id} for #{self.class.name} with ID=#{id}", model, "id", record_id) end
def reject_new_record?(association_name, attributes)
has_destroy_flag? or if a :reject_if proc exists for this
Determines if a new record should be rejected by checking
def reject_new_record?(association_name, attributes) will_be_destroyed?(association_name, attributes) || call_reject_if(association_name, attributes) end
def will_be_destroyed?(association_name, attributes)
def will_be_destroyed?(association_name, attributes) allow_destroy?(association_name) && has_destroy_flag?(attributes) end