module ActiveRecord::AutosaveAssociation
def _ensure_no_duplicate_errors
def _ensure_no_duplicate_errors errors.uniq! end
def _record_changed?(reflection, record, key)
def _record_changed?(reflection, record, key) record.new_record? || association_foreign_key_changed?(reflection, record, key) || record.will_save_change_to_attribute?(reflection.foreign_key) end
def around_save_collection_association
Is used as an around_save callback to check while saving a collection
def around_save_collection_association previously_new_record_before_save = (@new_record_before_save ||= false) @new_record_before_save = !previously_new_record_before_save && new_record? yield ensure @new_record_before_save = previously_new_record_before_save end
def associated_records_to_validate_or_save(association, new_record, autosave)
or saved. If +autosave+ is +false+ only new records will be returned,
Returns the record for an association collection that should be validated
def associated_records_to_validate_or_save(association, new_record, autosave) if new_record || custom_validation_context? association && association.target elsif autosave association.target.find_all(&:changed_for_autosave?) else association.target.find_all(&:new_record?) end end
def association_foreign_key_changed?(reflection, record, key)
def association_foreign_key_changed?(reflection, record, key) return false if reflection.through_reflection? record._has_attribute?(reflection.foreign_key) && record._read_attribute(reflection.foreign_key) != key end
def association_valid?(reflection, record, index = nil)
the parent, self, if it wasn't. Skips any :autosave
Returns whether or not the association is valid and applies any errors to
def association_valid?(reflection, record, index = nil) return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?) context = validation_context if custom_validation_context? unless valid = record.valid?(context) if reflection.options[:autosave] indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveRecord.index_nested_attribute_errors) record.errors.group_by_attribute.each { |attribute, errors| attribute = normalize_reflection_attribute(indexed_attribute, reflection, index, attribute) errors.each { |error| self.errors.import( error, attribute: attribute ) } } else errors.add(reflection.name) end end valid end
def changed_for_autosave?
Returns whether or not this record has been changed in any way (including whether
def changed_for_autosave? new_record? || has_changes_to_save? || marked_for_destruction? || nested_records_changed_for_autosave? end
def custom_validation_context?
def custom_validation_context? validation_context && [:create, :update].exclude?(validation_context) end
def destroyed_by_association
Returns the association for the parent being destroyed.
def destroyed_by_association @destroyed_by_association end
def destroyed_by_association=(reflection)
Records the association that is being destroyed and destroying this
def destroyed_by_association=(reflection) @destroyed_by_association = reflection end
def mark_for_destruction
when parent.save is called.
This does _not_ actually destroy the record instantly, rather child record will be destroyed
Marks this record to be destroyed as part of the parent's save transaction.
def mark_for_destruction @marked_for_destruction = true end
def marked_for_destruction?
Returns whether or not this record will be destroyed as part of the parent's save transaction.
def marked_for_destruction? @marked_for_destruction end
def nested_records_changed_for_autosave?
any new ones), and return true if any are changed for autosave.
Go through nested autosave associations that are loaded in memory (without loading
def nested_records_changed_for_autosave? @_nested_records_changed_for_autosave_already_called ||= false return false if @_nested_records_changed_for_autosave_already_called begin @_nested_records_changed_for_autosave_already_called = true self.class._reflections.values.any? do |reflection| if reflection.options[:autosave] association = association_instance_get(reflection.name) association && Array.wrap(association.target).any?(&:changed_for_autosave?) end end ensure @_nested_records_changed_for_autosave_already_called = false end end
def normalize_reflection_attribute(indexed_attribute, reflection, index, attribute)
def normalize_reflection_attribute(indexed_attribute, reflection, index, attribute) normalized_attribute = if indexed_attribute "#{reflection.name}[#{index}]" else reflection.name end normalized_attribute = "#{normalized_attribute}.#{attribute}" if attribute != :base normalized_attribute end
def reload(options = nil)
def reload(options = nil) @marked_for_destruction = false @destroyed_by_association = nil super end
def save_belongs_to_association(reflection)
Saves the associated record if it's new or :autosave is enabled.
def save_belongs_to_association(reflection) association = association_instance_get(reflection.name) return unless association && association.loaded? && !association.stale_target? record = association.load_target if record && !record.destroyed? autosave = reflection.options[:autosave] if autosave && record.marked_for_destruction? self[reflection.foreign_key] = nil record.destroy elsif autosave != false saved = record.save(validate: !autosave) if record.new_record? || (autosave && record.changed_for_autosave?) if association.updated? association_id = record.public_send(reflection.options[:primary_key] || :id) self[reflection.foreign_key] = association_id association.loaded! end saved if autosave end end end
def save_collection_association(reflection)
This all happens inside a transaction, _if_ the Transactions module is included into
with #mark_for_destruction.
In addition, it destroys all children that were marked for destruction
:autosave is enabled on the association.
Saves any new associated records, or all loaded autosave associations if
def save_collection_association(reflection) if association = association_instance_get(reflection.name) autosave = reflection.options[:autosave] # By saving the instance variable in a local variable, # we make the whole callback re-entrant. new_record_before_save = @new_record_before_save # reconstruct the scope now that we know the owner's id association.reset_scope if records = associated_records_to_validate_or_save(association, new_record_before_save, autosave) if autosave records_to_destroy = records.select(&:marked_for_destruction?) records_to_destroy.each { |record| association.destroy(record) } records -= records_to_destroy end records.each do |record| next if record.destroyed? saved = true if autosave != false && (new_record_before_save || record.new_record?) association.set_inverse_instance(record) if autosave saved = association.insert_record(record, false) elsif !reflection.nested? association_saved = association.insert_record(record) if reflection.validate? errors.add(reflection.name) unless association_saved saved = association_saved end end elsif autosave saved = record.save(validate: false) end raise(RecordInvalid.new(association.owner)) unless saved end end end end
def save_has_one_association(reflection)
This all happens inside a transaction, _if_ the Transactions module is included into
destruction with #mark_for_destruction.
In addition, it will destroy the association if it was marked for
on the association.
Saves the associated record if it's new or :autosave is enabled
def save_has_one_association(reflection) association = association_instance_get(reflection.name) record = association && association.load_target if record && !record.destroyed? autosave = reflection.options[:autosave] if autosave && record.marked_for_destruction? record.destroy elsif autosave != false key = reflection.options[:primary_key] ? public_send(reflection.options[:primary_key]) : id if (autosave && record.changed_for_autosave?) || _record_changed?(reflection, record, key) unless reflection.through_reflection record[reflection.foreign_key] = key association.set_inverse_instance(record) end saved = record.save(validate: !autosave) raise ActiveRecord::Rollback if !saved && autosave saved end end end end
def validate_collection_association(reflection)
:autosave is turned on for the association specified by
Validate the associated records if :validate or
def validate_collection_association(reflection) if association = association_instance_get(reflection.name) if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave]) records.each_with_index { |record, index| association_valid?(reflection, record, index) } end end end
def validate_single_association(reflection)
Validate the association if :validate or :autosave is
def validate_single_association(reflection) association = association_instance_get(reflection.name) record = association && association.reader association_valid?(reflection, record) if record && (record.changed_for_autosave? || custom_validation_context?) end