module Mongoid #:nodoc: class Document include ActiveSupport::Callbacks include Commands, Observable, Validatable extend Associations attr_accessor :association_name, :parent attr_reader :attributes define_callbacks \ :after_create, :after_destroy, :after_save, :before_create, :before_destroy, :before_save class << self # Find +Documents+ given the conditions. # # Options: # # args: A +Hash+ with a conditions key and other options # # Person.all(:conditions => { :attribute => "value" }) def all(*args) find(:all, *args) end # Returns the collection associated with this +Document+. If the # document is embedded, there will be no collection associated # with it. # # Returns: Mongo::Collection def collection return nil if @embedded @collection_name = self.to_s.demodulize.tableize @collection ||= Mongoid.database.collection(@collection_name) end # Defines all the fields that are accessable on the Document # For each field that is defined, a getter and setter will be # added as an instance method to the Document. # # Options: # # name: The name of the field, as a +Symbol+. # options: A +Hash+ of options to supply to the +Field+. # # Example: # # field :score, :default => 0 def field(name, options = {}) @fields ||= HashWithIndifferentAccess.new @fields[name.to_s] = Field.new(name.to_s, options) define_method(name) { read_attribute(name) } define_method("#{name}=") { |value| write_attribute(name, value) } end # Returns all the fields for the Document as a +Hash+ with names as keys. def fields @fields end # Find a +Document+ in several different ways. # # If a +String+ is provided, it will be assumed that it is a # representation of a Mongo::ObjectID and will attempt to find a single # +Document+ based on that id. If a +Symbol+ and +Hash+ is provided then # it will attempt to find either a single +Document+ or multiples based # on the conditions provided and the first parameter. # # Person.find(:first, :conditions => { :attribute => "value" }) # # Person.find(:all, :conditions => { :attribute => "value" }) # # Person.find(Mongo::ObjectID.new.to_s) def find(*args) Criteria.translate(*args).execute(self) end # Find the first +Document+ given the conditions. # # Options: # # args: A +Hash+ with a conditions key and other options # # Person.first(:conditions => { :attribute => "value" }) def first(*args) find(:first, *args) end # Adds an index on the field specified. Options can be :unique => true or # :unique => false. It will default to the latter. def index(name, options = { :unique => false }) collection.create_index(name, options) end # Defines the field that will be used for the id of this +Document+. This # set the id of this +Document+ before save to a parameterized version of # the field that was supplied. This is good for use for readable URLS in # web applications and *MUST* be defined on documents that are embedded # in order for proper updates in has_may associations. def key(*fields) @primary_key = fields before_save :generate_key end # Returns the primary key field of the +Document+ def primary_key @primary_key end # Find all documents in paginated fashion given the supplied arguments. # If no parameters are passed just default to offset 0 and limit 20. # # Options: # # params: A +Hash+ of params to pass to the Criteria API. # # Example: # # Person.paginate(:conditions => { :field => "Test" }, :page => 1, # :per_page => 20) # # Returns paginated array of docs. def paginate(params = {}) criteria = Criteria.translate(:all, params) WillPaginate::Collection.create(criteria.page, criteria.offset, 0) do |pager| results = criteria.execute(self) pager.total_entries = results.size pager.replace(results) end end # Entry point for creating a new criteria from a Document. This will # instantiate a new +Criteria+ object with the supplied select criterion # already added to it. # # Options: # # args: A list of field names to retrict the returned fields to. # # Example: # # Person.select(:field1, :field2, :field3) # # Returns: Criteria def select(*args) Criteria.new(:all, self).select(*args) end end # Get the Mongo::Collection associated with this Document. def collection self.class.collection end # Get the fields for the Document class. def fields self.class.fields end # Get the Mongo::ObjectID associated with this object. # This is in essence the primary key. def id @attributes[:_id] end # Instantiate a new Document, setting the Document's attirbutes if given. # If no attributes are provided, they will be initialized with an empty Hash. def initialize(attributes = {}) @attributes = HashWithIndifferentAccess.new(attributes) if attributes @attributes = HashWithIndifferentAccess.new unless attributes generate_key end # Return the +Document+ primary key. def primary_key self.class.primary_key end # Returns true is the Document has not been persisted to the database, false if it has. def new_record? @attributes[:_id].nil? end # Notify observers that this Document has changed. def notify changed(true) notify_observers(self) end # Read from the attributes hash. def read_attribute(name) fields[name].value(@attributes[name]) end # Returns the id of the Document def to_param id.to_s end # Update the document based on notify from child def update(child) @attributes.insert(child.association_name, child.attributes) notify end # Write to the attributes hash. def write_attribute(name, value) @attributes[name] = value notify end # Writes all the attributes of this Document, and delegate up to # the parent. def write_attributes(attrs) @attributes = attrs notify end private def generate_key if primary_key values = primary_key.collect { |key| @attributes[key] } @attributes[:_id] = values.join(" ").parameterize.to_s end end end end