module ViewModel::ActiveRecord::AssociationManipulation
def append_associated(association_name, subtree_hash_or_hashes, references: {}, before: nil, after: nil, deserialize_context: self.class.new_deserialize_context)
collection, the items are inserted either before `before`, after `after`, or
Create or update members of a associated collection. For an ordered
def append_associated(association_name, subtree_hash_or_hashes, references: {}, before: nil, after: nil, deserialize_context: self.class.new_deserialize_context) if self.changes.changed? raise ArgumentError.new('Invalid call to append_associated on viewmodel with pending changes') end association_data = self.class._association_data(association_name) direct_reflection = association_data.direct_reflection raise ArgumentError.new("Cannot append to single association '#{association_name}'") unless association_data.collection? ViewModel::Utils.wrap_one_or_many(subtree_hash_or_hashes) do |subtree_hashes| model_class.transaction do ViewModel::Callbacks.wrap_deserialize(self, deserialize_context: deserialize_context) do |hook_control| association_changed!(association_name) deserialize_context.run_callback(ViewModel::Callbacks::Hook::BeforeValidate, self) if association_data.through? raise ArgumentError.new('Polymorphic through relationships not supported yet') if association_data.polymorphic? direct_viewmodel_class = association_data.direct_viewmodel root_update_data, referenced_update_data = construct_indirect_append_updates(association_data, subtree_hashes, references) else raise ArgumentError.new('Polymorphic STI relationships not supported yet') if association_data.polymorphic? direct_viewmodel_class = association_data.viewmodel_class root_update_data, referenced_update_data = construct_direct_append_updates(association_data, subtree_hashes, references) end update_context = ViewModel::ActiveRecord::UpdateContext.build!(root_update_data, referenced_update_data, root_type: direct_viewmodel_class) # Set new parent new_parent = ViewModel::ActiveRecord::UpdateOperation::ParentData.new(direct_reflection.inverse_of, self) update_context.root_updates.each { |update| update.reparent_to = new_parent } # Set place in list. if association_data.ordered? new_positions = select_append_positions(association_data, direct_viewmodel_class._list_attribute_name, update_context.root_updates.count, before: before, after: after) update_context.root_updates.zip(new_positions).each do |update, new_pos| update.reposition_to = new_pos end end # Because append_associated can take from other parents, edit-check previous parents (other than this model) unless association_data.through? inverse_assoc_name = direct_reflection.inverse_of.name previous_parent_ids = Set.new update_context.root_updates.each do |update| update_model = update.viewmodel.model parent_model_id = update_model.read_attribute(update_model .association(inverse_assoc_name) .reflection.foreign_key) if parent_model_id && parent_model_id != self.id previous_parent_ids << parent_model_id end end if previous_parent_ids.present? previous_parents = self.class.find(previous_parent_ids.to_a, eager_include: false) previous_parents.each do |parent_view| ViewModel::Callbacks.wrap_deserialize(parent_view, deserialize_context: deserialize_context) do |pp_hook_control| changes = ViewModel::Changes.new(changed_associations: [association_name]) deserialize_context.run_callback(ViewModel::Callbacks::Hook::OnChange, parent_view, changes: changes) pp_hook_control.record_changes(changes) end end end end child_context = self.context_for_child(association_name, context: deserialize_context) updated_viewmodels = update_context.run!(deserialize_context: child_context) # Propagate changes and finalize the parent updated_viewmodels.each do |child| child_changes = child.previous_changes if association_data.nested? nested_children_changed! if child_changes.changed_nested_tree? referenced_children_changed! if child_changes.changed_referenced_children? elsif association_data.owned? referenced_children_changed! if child_changes.changed_owned_tree? end end final_changes = self.clear_changes! if association_data.through? updated_viewmodels.map! do |direct_vm| direct_vm._read_association(association_data.indirect_reflection.name) end end # Could happen if hooks attempted to change the parent, which aren't # valid since we're only editing children here. unless final_changes.contained_to?(associations: [association_name.to_s]) raise ViewModel::DeserializationError::InvalidParentEdit.new(final_changes, blame_reference) end deserialize_context.run_callback(ViewModel::Callbacks::Hook::OnChange, self, changes: final_changes) hook_control.record_changes(final_changes) updated_viewmodels end end end end