# encoding: utf-8 module Mongoid #:nodoc: module Associations #:nodoc: # Represents embedding many documents within a parent document, which will # be an array as the underlying storage mechanism. class EmbedsMany < Proxy attr_accessor :association_name, :klass # Appends the object to the +Array+, setting its parent in # the process. def <<(*documents) documents.flatten.each do |doc| doc.parentize(@parent, @association_name) @target << doc doc._index = @target.size - 1 doc.notify end end alias :concat :<< alias :push :<< # Builds a new Document and adds it to the association collection. The # document created will be of the same class as the others in the # association, and the attributes will be passed into the constructor. # # Returns: # # The newly created Document. def build(attrs = {}, type = nil) document = type ? type.instantiate : @klass.instantiate document.parentize(@parent, @association_name) document.write_attributes(attrs) @target << document document._index = @target.size - 1 document end # Clears the association, and notifies the parents of the removal. def clear unless @target.empty? document = @target.first document._parent.update_child(document, true) if (document._parent) @target.clear end end # Returns a count of the number of documents in the association that have # actually been persisted to the database. # # Use #size if you want the total number of documents. # # Returns: # # The total number of persisted embedded docs, as flagged by the # #persisted? method. def count @target.select(&:persisted?).size end # Creates a new Document and adds it to the association collection. The # document created will be of the same class as the others in the # association, and the attributes will be passed into the constructor and # the new object will then be saved. # # Returns: # # The newly created Document. def create(attrs = {}, type = nil) build(attrs, type).tap(&:save) end # Creates a new Document and adds it to the association collection. The # document created will be of the same class as the others in the # association, and the attributes will be passed into the constructor and # the new object will then be saved. If validation fails an error will # get raised. # # Returns: # # The newly created Document. def create!(attrs = {}, type = nil) document = create(attrs, type) raise Errors::Validations.new(document) unless document.errors.empty? document end # Delete all the documents in the association without running callbacks. # # Example: # # addresses.delete_all # # Returns: # # The number of documents deleted. def delete_all(conditions = {}) remove(:delete, conditions) end # Delete all the documents in the association and run destroy callbacks. # # Example: # # addresses.destroy_all # # Returns: # # The number of documents destroyed. def destroy_all(conditions = {}) remove(:destroy, conditions) end # Finds a document in this association. # # If :all is passed, returns all the documents # # If an id is passed, will return the document for that id. # # Returns: # # Array or single Document. def find(param) return @target if param == :all criteria.id(param).first end # Creates the new association by finding the attributes in # the parent document with its name, and instantiating a # new document for each one found. These will then be put in an # internal array. # # This then delegated all methods to the array class since this is # essentially a proxy to an array itself. # # Options: # # parent: The parent document to the association. # options: The association options. def initialize(parent, options, target_array = nil) @parent, @association_name = parent, options.name @klass, @options = options.klass, options if target_array build_children_from_target_array(target_array) else build_children_from_attributes(parent.raw_attributes[@association_name]) end extends(options) end # If the target array does not respond to the supplied method then try to # find a named scope or criteria on the class and send the call there. # # If the method exists on the array, use the default proxy behavior. def method_missing(name, *args, &block) unless @target.respond_to?(name) object = @klass.send(name, *args) object.documents = @target return object end super end # Used for setting associations via a nested attributes setter from the # parent +Document+. # # Options: # # attributes: A +Hash+ of integer keys and +Hash+ values. # # Returns: # # The newly build target Document. def nested_build(attributes, options = {}) @parent.instance_variable_set(:@building_nested, true) attributes.each do |index, attrs| if document = detect { |document| document._index == index.to_i } if options && options[:allow_destroy] && Boolean.set(attrs['_destroy']) @target.delete(document) document.destroy else document.write_attributes(attrs) end else build(attrs) end end @target.each_with_index { |document, index| document._index = index } @parent.instance_variable_set(:@building_nested, false) self end # Paginate the association. Will create a new criteria, set the documents # on it and execute in an enumerable context. # # Options: # # options: A +Hash+ of pagination options. # # Returns: # # A +WillPaginate::Collection+. def paginate(options) criteria = Mongoid::Criteria.translate(@klass, options) criteria.documents = @target criteria.paginate(options) end protected # Initializes each of the attributes in the hash. def build_children_from_attributes(attributes) @target = [] if attributes attributes.each_with_index do |attrs, index| klass = attrs.klass child = klass ? klass.instantiate(attrs) : @klass.instantiate(attrs) child.parentize(@parent, @association_name) child._index = index @target << child end end end # Initializes the target array from an existing array of documents. def build_children_from_target_array(target_array) @target = target_array @target.each_with_index do |child, index| child._index = index end end # Removes documents based on a method. def remove(method, conditions) criteria = @klass.find(conditions || {}) criteria.documents = @target count = criteria.size criteria.each do |doc| @target.delete(doc); doc.send(method) end; count end class << self # Preferred method of creating a new +EmbedsMany+ association. It will # delegate to new. # # Options: # # document: The parent +Document+ # options: The association options def instantiate(document, options, target_array = nil) new(document, options, target_array) end # Returns the macro used to create the association. def macro :embeds_many end # Perform an update of the relationship of the parent and child. This # is initialized by setting the has_many to the supplied +Enumerable+ # and setting up the parentization. def update(children, parent, options) parent.raw_attributes.delete(options.name) children.assimilate(parent, options) if children && children.first.is_a?(Mongoid::Document) instantiate(parent, options, children) else instantiate(parent, options) end end end end end end