2009-01-30 03:25:45 +01:00
|
|
|
require 'mime/types'
|
|
|
|
require File.join(File.dirname(__FILE__), "property")
|
|
|
|
require File.join(File.dirname(__FILE__), '..', 'mixins', 'extended_document_mixins')
|
2009-05-28 08:59:43 +02:00
|
|
|
require "enumerator"
|
2009-01-30 03:25:45 +01:00
|
|
|
|
|
|
|
module CouchRest
|
|
|
|
|
|
|
|
# Same as CouchRest::Document but with properties and validations
|
|
|
|
class ExtendedDocument < Document
|
2009-02-06 02:06:12 +01:00
|
|
|
include CouchRest::Callbacks
|
2009-02-10 11:15:39 +01:00
|
|
|
include CouchRest::Mixins::DocumentQueries
|
2009-02-03 01:16:14 +01:00
|
|
|
include CouchRest::Mixins::Views
|
2009-01-30 03:25:45 +01:00
|
|
|
include CouchRest::Mixins::DesignDoc
|
2009-02-18 02:59:31 +01:00
|
|
|
include CouchRest::Mixins::ExtendedAttachments
|
2009-03-27 14:42:49 +01:00
|
|
|
include CouchRest::Mixins::ClassProxy
|
2009-06-04 20:43:14 +02:00
|
|
|
include CouchRest::Mixins::Collection
|
2009-09-27 01:24:26 +02:00
|
|
|
include CouchRest::Mixins::AttributeProtection
|
2009-05-28 08:59:43 +02:00
|
|
|
|
2009-06-04 20:43:14 +02:00
|
|
|
def self.subclasses
|
|
|
|
@subclasses ||= []
|
|
|
|
end
|
2009-02-04 02:33:31 +01:00
|
|
|
|
2009-02-10 11:15:39 +01:00
|
|
|
def self.inherited(subklass)
|
|
|
|
subklass.send(:include, CouchRest::Mixins::Properties)
|
2009-07-19 10:23:51 +02:00
|
|
|
subklass.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
2009-03-20 02:53:17 +01:00
|
|
|
def self.inherited(subklass)
|
|
|
|
subklass.properties = self.properties.dup
|
|
|
|
end
|
|
|
|
EOS
|
2009-05-28 19:36:25 +02:00
|
|
|
subclasses << subklass
|
2009-02-10 11:15:39 +01:00
|
|
|
end
|
|
|
|
|
2009-03-03 06:15:02 +01:00
|
|
|
# Accessors
|
|
|
|
attr_accessor :casted_by
|
|
|
|
|
2009-02-04 02:33:31 +01:00
|
|
|
# Callbacks
|
2009-06-07 23:52:23 +02:00
|
|
|
define_callbacks :create, "result == :halt"
|
|
|
|
define_callbacks :save, "result == :halt"
|
|
|
|
define_callbacks :update, "result == :halt"
|
|
|
|
define_callbacks :destroy, "result == :halt"
|
2009-01-30 03:25:45 +01:00
|
|
|
|
2009-03-03 06:15:02 +01:00
|
|
|
def initialize(passed_keys={})
|
2009-02-09 20:20:23 +01:00
|
|
|
apply_defaults # defined in CouchRest::Mixins::Properties
|
2009-11-10 03:23:55 +01:00
|
|
|
set_attributes(passed_keys) unless passed_keys.nil?
|
2009-02-18 02:59:31 +01:00
|
|
|
super
|
2009-02-09 20:20:23 +01:00
|
|
|
cast_keys # defined in CouchRest::Mixins::Properties
|
2009-02-06 03:57:11 +01:00
|
|
|
unless self['_id'] && self['_rev']
|
|
|
|
self['couchrest-type'] = self.class.to_s
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2009-07-17 09:12:33 +02:00
|
|
|
# Defines an instance and save it directly to the database
|
|
|
|
#
|
|
|
|
# ==== Returns
|
|
|
|
# returns the reloaded document
|
|
|
|
def self.create(options)
|
|
|
|
instance = new(options)
|
|
|
|
instance.create
|
|
|
|
instance
|
|
|
|
end
|
2009-05-28 03:16:50 +02:00
|
|
|
|
2009-07-17 09:12:33 +02:00
|
|
|
# Defines an instance and save it directly to the database
|
|
|
|
#
|
|
|
|
# ==== Returns
|
|
|
|
# returns the reloaded document or raises an exception
|
|
|
|
def self.create!(options)
|
|
|
|
instance = new(options)
|
|
|
|
instance.create!
|
|
|
|
instance
|
|
|
|
end
|
2009-02-06 03:57:11 +01:00
|
|
|
|
2009-01-30 03:25:45 +01:00
|
|
|
# Automatically set <tt>updated_at</tt> and <tt>created_at</tt> fields
|
|
|
|
# on the document whenever saving occurs. CouchRest uses a pretty
|
|
|
|
# decent time format by default. See Time#to_json
|
|
|
|
def self.timestamps!
|
2009-07-19 10:23:51 +02:00
|
|
|
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
2009-02-10 11:15:39 +01:00
|
|
|
property(:updated_at, :read_only => true, :cast_as => 'Time', :auto_validation => false)
|
|
|
|
property(:created_at, :read_only => true, :cast_as => 'Time', :auto_validation => false)
|
2009-02-04 02:33:31 +01:00
|
|
|
|
2009-06-07 11:57:22 +02:00
|
|
|
set_callback :save, :before do |object|
|
2009-02-04 02:33:31 +01:00
|
|
|
object['updated_at'] = Time.now
|
2009-06-05 05:44:44 +02:00
|
|
|
object['created_at'] = object['updated_at'] if object.new?
|
2009-02-04 02:33:31 +01:00
|
|
|
end
|
|
|
|
EOS
|
2009-01-30 03:25:45 +01:00
|
|
|
end
|
2009-06-10 06:02:04 +02:00
|
|
|
|
2009-01-30 03:25:45 +01:00
|
|
|
# Name a method that will be called before the document is first saved,
|
|
|
|
# which returns a string to be used for the document's <tt>_id</tt>.
|
|
|
|
# Because CouchDB enforces a constraint that each id must be unique,
|
|
|
|
# this can be used to enforce eg: uniq usernames. Note that this id
|
|
|
|
# must be globally unique across all document types which share a
|
|
|
|
# database, so if you'd like to scope uniqueness to this class, you
|
|
|
|
# should use the class name as part of the unique id.
|
|
|
|
def self.unique_id method = nil, &block
|
|
|
|
if method
|
|
|
|
define_method :set_unique_id do
|
|
|
|
self['_id'] ||= self.send(method)
|
|
|
|
end
|
|
|
|
elsif block
|
|
|
|
define_method :set_unique_id do
|
|
|
|
uniqid = block.call(self)
|
|
|
|
raise ArgumentError, "unique_id block must not return nil" if uniqid.nil?
|
|
|
|
self['_id'] ||= uniqid
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2009-02-17 09:36:11 +01:00
|
|
|
# Temp solution to make the view_by methods available
|
2009-03-27 12:27:37 +01:00
|
|
|
def self.method_missing(m, *args, &block)
|
2009-02-17 09:36:11 +01:00
|
|
|
if has_view?(m)
|
|
|
|
query = args.shift || {}
|
2009-03-27 12:27:37 +01:00
|
|
|
view(m, query, *args, &block)
|
2009-02-17 09:36:11 +01:00
|
|
|
else
|
|
|
|
super
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2009-01-30 03:25:45 +01:00
|
|
|
### instance methods
|
|
|
|
|
|
|
|
# Returns the Class properties
|
|
|
|
#
|
|
|
|
# ==== Returns
|
|
|
|
# Array:: the list of properties for the instance
|
|
|
|
def properties
|
|
|
|
self.class.properties
|
|
|
|
end
|
|
|
|
|
2009-05-29 07:42:30 +02:00
|
|
|
# Gets a reference to the actual document in the DB
|
|
|
|
# Calls up to the next document if there is one,
|
|
|
|
# Otherwise we're at the top and we return self
|
|
|
|
def base_doc
|
|
|
|
return self if base_doc?
|
|
|
|
@casted_by.base_doc
|
|
|
|
end
|
|
|
|
|
|
|
|
# Checks if we're the top document
|
|
|
|
def base_doc?
|
|
|
|
!@casted_by
|
|
|
|
end
|
|
|
|
|
2009-01-30 03:25:45 +01:00
|
|
|
# Takes a hash as argument, and applies the values by using writer methods
|
|
|
|
# for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
|
|
|
|
# missing. In case of error, no attributes are changed.
|
2009-01-30 03:45:01 +01:00
|
|
|
def update_attributes_without_saving(hash)
|
2009-06-10 06:02:04 +02:00
|
|
|
# remove attributes that cannot be updated, silently ignoring them
|
|
|
|
# which matches Rails behavior when, for instance, setting created_at.
|
|
|
|
# make a copy, we don't want to change arguments
|
|
|
|
attrs = hash.dup
|
|
|
|
%w[_id _rev created_at updated_at].each {|attr| attrs.delete(attr)}
|
2009-09-27 01:24:26 +02:00
|
|
|
check_properties_exist(attrs)
|
|
|
|
set_attributes(attrs)
|
2009-01-30 03:25:45 +01:00
|
|
|
end
|
2009-05-27 22:24:25 +02:00
|
|
|
alias :attributes= :update_attributes_without_saving
|
2009-01-30 03:25:45 +01:00
|
|
|
|
|
|
|
# Takes a hash as argument, and applies the values by using writer methods
|
|
|
|
# for each key. Raises a NoMethodError if the corresponding methods are
|
|
|
|
# missing. In case of error, no attributes are changed.
|
2009-01-30 03:45:01 +01:00
|
|
|
def update_attributes(hash)
|
2009-01-30 03:25:45 +01:00
|
|
|
update_attributes_without_saving hash
|
|
|
|
save
|
|
|
|
end
|
|
|
|
|
|
|
|
# for compatibility with old-school frameworks
|
2009-08-25 02:12:13 +02:00
|
|
|
alias :new_record? :new?
|
|
|
|
alias :new_document? :new?
|
2009-01-30 03:25:45 +01:00
|
|
|
|
2009-02-13 05:28:07 +01:00
|
|
|
# Trigger the callbacks (before, after, around)
|
|
|
|
# and create the document
|
|
|
|
# It's important to have a create callback since you can't check if a document
|
|
|
|
# was new after you saved it
|
|
|
|
#
|
|
|
|
# When creating a document, both the create and the save callbacks will be triggered.
|
|
|
|
def create(bulk = false)
|
|
|
|
caught = catch(:halt) do
|
|
|
|
_run_create_callbacks do
|
|
|
|
_run_save_callbacks do
|
|
|
|
create_without_callbacks(bulk)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
# unlike save, create returns the newly created document
|
|
|
|
def create_without_callbacks(bulk =false)
|
|
|
|
raise ArgumentError, "a document requires a database to be created to (The document or the #{self.class} default database were not set)" unless database
|
2009-08-25 02:12:13 +02:00
|
|
|
set_unique_id if new? && self.respond_to?(:set_unique_id)
|
2009-02-13 05:28:07 +01:00
|
|
|
result = database.save_doc(self, bulk)
|
|
|
|
(result["ok"] == true) ? self : false
|
|
|
|
end
|
|
|
|
|
|
|
|
# Creates the document in the db. Raises an exception
|
|
|
|
# if the document is not created properly.
|
|
|
|
def create!
|
|
|
|
raise "#{self.inspect} failed to save" unless self.create
|
|
|
|
end
|
|
|
|
|
|
|
|
# Trigger the callbacks (before, after, around)
|
|
|
|
# only if the document isn't new
|
|
|
|
def update(bulk = false)
|
|
|
|
caught = catch(:halt) do
|
2009-08-25 02:12:13 +02:00
|
|
|
if self.new?
|
2009-02-13 05:28:07 +01:00
|
|
|
save(bulk)
|
|
|
|
else
|
|
|
|
_run_update_callbacks do
|
|
|
|
_run_save_callbacks do
|
|
|
|
save_without_callbacks(bulk)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2009-02-04 02:33:31 +01:00
|
|
|
# Trigger the callbacks (before, after, around)
|
|
|
|
# and save the document
|
|
|
|
def save(bulk = false)
|
|
|
|
caught = catch(:halt) do
|
2009-08-25 02:12:13 +02:00
|
|
|
if self.new?
|
2009-02-13 05:28:07 +01:00
|
|
|
_run_save_callbacks do
|
|
|
|
save_without_callbacks(bulk)
|
|
|
|
end
|
|
|
|
else
|
|
|
|
update(bulk)
|
2009-02-04 02:33:31 +01:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2009-01-30 03:25:45 +01:00
|
|
|
# Overridden to set the unique ID.
|
|
|
|
# Returns a boolean value
|
2009-02-04 02:33:31 +01:00
|
|
|
def save_without_callbacks(bulk = false)
|
2009-02-11 01:10:35 +01:00
|
|
|
raise ArgumentError, "a document requires a database to be saved to (The document or the #{self.class} default database were not set)" unless database
|
2009-08-25 02:12:13 +02:00
|
|
|
set_unique_id if new? && self.respond_to?(:set_unique_id)
|
2009-01-30 03:25:45 +01:00
|
|
|
result = database.save_doc(self, bulk)
|
2009-08-25 01:57:58 +02:00
|
|
|
mark_as_saved
|
2009-10-12 13:55:02 +02:00
|
|
|
result["ok"] == true
|
2009-01-30 03:25:45 +01:00
|
|
|
end
|
|
|
|
|
2009-02-13 05:28:07 +01:00
|
|
|
# Saves the document to the db using save. Raises an exception
|
2009-01-30 03:25:45 +01:00
|
|
|
# if the document is not saved properly.
|
|
|
|
def save!
|
|
|
|
raise "#{self.inspect} failed to save" unless self.save
|
2009-09-03 05:27:04 +02:00
|
|
|
true
|
2009-01-30 03:25:45 +01:00
|
|
|
end
|
|
|
|
|
|
|
|
# Deletes the document from the database. Runs the :destroy callbacks.
|
|
|
|
# Removes the <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
|
|
|
|
# document to be saved to a new <tt>_id</tt>.
|
2009-02-18 02:59:31 +01:00
|
|
|
def destroy(bulk=false)
|
2009-02-04 02:33:31 +01:00
|
|
|
caught = catch(:halt) do
|
|
|
|
_run_destroy_callbacks do
|
2009-02-18 02:59:31 +01:00
|
|
|
result = database.delete_doc(self, bulk)
|
2009-02-04 02:33:31 +01:00
|
|
|
if result['ok']
|
2009-03-15 21:00:47 +01:00
|
|
|
self.delete('_rev')
|
|
|
|
self.delete('_id')
|
2009-02-04 02:33:31 +01:00
|
|
|
end
|
|
|
|
result['ok']
|
|
|
|
end
|
2009-01-30 03:25:45 +01:00
|
|
|
end
|
|
|
|
end
|
2009-02-04 02:33:31 +01:00
|
|
|
|
2009-05-29 01:09:53 +02:00
|
|
|
protected
|
|
|
|
|
|
|
|
# Set document_saved flag on all casted models to true
|
|
|
|
def mark_as_saved
|
|
|
|
self.each do |key, prop|
|
|
|
|
if prop.is_a?(Array)
|
|
|
|
prop.each do |item|
|
|
|
|
if item.respond_to?(:document_saved)
|
|
|
|
item.send(:document_saved=, true)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
elsif prop.respond_to?(:document_saved)
|
|
|
|
prop.send(:document_saved=, true)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2009-09-27 01:24:26 +02:00
|
|
|
|
|
|
|
private
|
|
|
|
|
|
|
|
def check_properties_exist(attrs)
|
2009-10-31 13:42:36 +01:00
|
|
|
attrs.each do |attribute_name, attribute_value|
|
|
|
|
raise NoMethodError, "#{attribute_name}= method not available, use property :#{attribute_name}" unless self.respond_to?("#{attribute_name}=")
|
2009-09-27 01:24:26 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
def set_attributes(hash)
|
|
|
|
attrs = remove_protected_attributes(hash)
|
2009-10-31 13:42:36 +01:00
|
|
|
attrs.each do |attribute_name, attribute_value|
|
|
|
|
if self.respond_to?("#{attribute_name}=")
|
|
|
|
self.send("#{attribute_name}=", attrs.delete(attribute_name))
|
2009-09-27 01:24:26 +02:00
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
2009-01-30 03:25:45 +01:00
|
|
|
end
|
2009-04-23 06:24:38 +02:00
|
|
|
end
|