# encoding: utf-8
module Mongoid #:nodoc:
module Document
extend ActiveSupport::Concern
included do
include Mongoid::Components
attr_accessor :association_name
attr_reader :new_record
unless self.instance_of?(Class) and self.name == ""
(@@descendants ||= {})[self] = :seen
end
end
class << self
# Returns all classes that have included Mongoid::Document.
#
# This will not get subclasses of the top level models, for those we will
# use Class.descendents in the rake task for indexes.
def descendants
(@@descendants ||= {}).keys
end
end
module ClassMethods #:nodoc:
# Perform default behavior but mark the hierarchy as being hereditary.
#
# This method must remain in the +Document+ module, even though its
# behavior affects items in the Hierarchy module.
def inherited(subclass)
super(subclass)
self.hereditary = true
end
# Instantiate a new object, only when loaded from the database or when
# the attributes have already been typecast.
#
# Example:
#
# Person.instantiate(:title => "Sir", :age => 30)
def instantiate(attrs = nil, allocating = false)
attributes = attrs || {}
if attributes["_id"] || allocating
document = allocate
document.instance_variable_set(:@attributes, attributes)
document.setup_modifications
return document
else
return new(attrs)
end
end
# Returns all types to query for when using this class as the base.
# *subclasses* is from activesupport. Note that a bug in *subclasses*
# causes the first call to only return direct children, hence
# the double call and unique.
def _types
@_type ||= [descendants + [self]].flatten.uniq.map(&:to_s)
end
end
# Performs equality checking on the document ids. For more robust
# equality checking please override this method.
def ==(other)
return false unless other.is_a?(Document)
id == other.id
end
# Delegates to ==
def eql?(comparison_object)
self == (comparison_object)
end
# Delegates to id in order to allow two records of the same type and id to
# work with something like:
# [ Person.find(1),
# Person.find(2),
# Person.find(3) ] &
# [ Person.find(1),
# Person.find(4) ] # => [ Person.find(1) ]
def hash
id.hash
end
# Introduces a child object into the +Document+ object graph. This will
# set up the relationships between the parent and child and update the
# attributes of the parent +Document+.
#
# Options:
#
# parent: The +Document+ to assimilate with.
# options: The association +Options+ for the child.
def assimilate(parent, options)
parentize(parent, options.name); notify; self
end
# Return the attributes hash with indifferent access.
def attributes
@attributes.with_indifferent_access
end
# Clone the current +Document+. This will return all attributes with the
# exception of the document's id and versions.
def clone
self.class.instantiate(@attributes.except("_id").except("versions").dup, true)
end
# Generate an id for this +Document+.
def identify
Identity.new(self).create
end
# Instantiate a new +Document+, setting the Document's attributes if
# given. If no attributes are provided, they will be initialized with
# an empty +Hash+.
#
# If a primary key is defined, the document's id will be set to that key,
# otherwise it will be set to a fresh +BSON::ObjectID+ string.
#
# Options:
#
# attrs: The attributes +Hash+ to set up the document with.
def initialize(attrs = nil)
@attributes = default_attributes
process(attrs)
@new_record = true
document = yield self if block_given?
identify
end
# Returns the class name plus its attributes.
def inspect
attrs = fields.map { |name, field| "#{name}: #{@attributes[name].inspect}" }
if Mongoid.allow_dynamic_fields
dynamic_keys = @attributes.keys - fields.keys - associations.keys - ["_id", "_type"]
attrs += dynamic_keys.map { |name| "#{name}: #{@attributes[name].inspect}" }
end
"#<#{self.class.name} _id: #{id}, #{attrs * ', '}>"
end
# Notify parent of an update.
#
# Example:
#
# person.notify
def notify
_parent.update_child(self) if _parent
end
# Return the attributes hash.
def raw_attributes
@attributes
end
# Reloads the +Document+ attributes from the database.
def reload
reloaded = collection.find_one(:_id => id)
if Mongoid.raise_not_found_error
raise Errors::DocumentNotFound.new(self.class, id) if reloaded.nil?
end
@attributes = {}.merge(reloaded || {})
self.associations.keys.each { |association_name| unmemoize(association_name) }; self
end
# Remove a child document from this parent +Document+. Will reset the
# memoized association and notify the parent of the change.
def remove(child)
name = child.association_name
if @building_nested
@attributes.remove(name, child.raw_attributes)
else
reset(name) do
@attributes.remove(name, child.raw_attributes)
@attributes[name]
end
notify
end
end
# def remove_without_reset
# name = child.association_name
# @attributes.remove(name, child.raw_attributes)
# notify
# end
# Return an array with this +Document+ only in it.
def to_a
[ self ]
end
# Recieve a notify call from a child +Document+. This will either update
# existing attributes on the +Document+ or clear them out for the child if
# the clear boolean is provided.
#
# Options:
#
# child: The child +Document+ that sent the notification.
# clear: Will clear out the child's attributes if set to true.
def update_child(child, clear = false)
name = child.association_name
attrs = child.instance_variable_get(:@attributes)
if clear
@attributes.delete(name)
else
# check good for array only
@attributes.insert(name, attrs) unless @attributes[name] && @attributes[name].include?(attrs)
end
end
end
end