Merge branch 'master' of https://github.com/2moro/couchrest_model into 2moro-dirty

Conflicts:
	.gitignore
	lib/couchrest/model/base.rb
	lib/couchrest/model/configuration.rb
	lib/couchrest_model.rb
This commit is contained in:
Sam Lown 2011-04-20 10:47:36 +02:00
commit 1bced3b207
18 changed files with 819 additions and 41 deletions

View file

@ -18,14 +18,17 @@ module CouchRest
include CouchRest::Model::Associations
include CouchRest::Model::Validations
include CouchRest::Model::Designs
include CouchRest::Model::Dirty
include CouchRest::Model::CastedBy
def self.subclasses
@subclasses ||= []
end
def self.inherited(subklass)
super
subklass.send(:include, CouchRest::Model::Properties)
subklass.class_eval <<-EOS, __FILE__, __LINE__ + 1
def self.inherited(subklass)
super
@ -36,7 +39,7 @@ module CouchRest
EOS
subclasses << subklass
end
# Accessors
attr_accessor :casted_by
@ -45,7 +48,7 @@ module CouchRest
# using the provided document hash.
#
# Options supported:
#
#
# * :directly_set_attributes: true when data comes directly from database
# * :database: provide an alternative database
#
@ -59,8 +62,8 @@ module CouchRest
end
after_initialize if respond_to?(:after_initialize)
end
# Temp solution to make the view_by methods available
def self.method_missing(m, *args, &block)
if has_view?(m)
@ -74,24 +77,17 @@ module CouchRest
end
super
end
### instance methods
# 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
# (overrides base_doc? in casted_by.rb)
def base_doc?
!@casted_by
end
## Compatibility with ActiveSupport and older frameworks
# Hack so that CouchRest::Document, which descends from Hash,
# doesn't appear to Rails routing as a Hash of options
def is_a?(klass)
@ -103,14 +99,14 @@ module CouchRest
def persisted?
!new?
end
def to_key
new? ? nil : [id]
new? ? nil : [id]
end
alias :to_param :id
alias :new_record? :new?
alias :new_document? :new?
end
end
end
end

View file

@ -5,6 +5,7 @@
module CouchRest::Model
class CastedArray < Array
include CouchRest::Model::Dirty
attr_accessor :casted_by
attr_accessor :property
@ -14,15 +15,39 @@ module CouchRest::Model
end
def << obj
couchrest_parent_will_change! if use_dirty?
super(instantiate_and_cast(obj))
end
def push(obj)
couchrest_parent_will_change! if use_dirty?
super(instantiate_and_cast(obj))
end
def pop
couchrest_parent_will_change! if use_dirty? && self.length > 0
super
end
def shift
couchrest_parent_will_change! if use_dirty? && self.length > 0
super
end
def unshift(obj)
couchrest_parent_will_change! if use_dirty?
super(instantiate_and_cast(obj))
end
def []= index, obj
super(index, instantiate_and_cast(obj))
value = instantiate_and_cast(obj)
couchrest_parent_will_change! if use_dirty? && value != self[index]
super(index, value)
end
def clear
couchrest_parent_will_change! if use_dirty? && self.length > 0
super
end
protected

View file

@ -0,0 +1,23 @@
module CouchRest::Model
module CastedBy
extend ActiveSupport::Concern
included do
self.send(:attr_accessor, :casted_by)
end
# 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 ? @casted_by.base_doc : nil
end
# Checks if we're the top document
def base_doc?
false
end
end
end

View file

@ -0,0 +1,76 @@
#
# Wrapper around Hash so that the casted_by attribute is set.
module CouchRest::Model
class CastedHash < Hash
include CouchRest::Model::Dirty
attr_accessor :casted_by
# needed for dirty
def attributes
self
end
def []= key, obj
couchrest_attribute_will_change!(key) if use_dirty? && obj != self[key]
super(key, obj)
end
def delete(key)
couchrest_attribute_will_change!(key) if use_dirty? && include?(key)
super(key)
end
def merge!(other_hash)
if use_dirty? && other_hash && other_hash.kind_of?(Hash)
other_hash.keys.each do |key|
if self[key] != other_hash[key] || !include?(key)
couchrest_attribute_will_change!(key)
end
end
end
super(other_hash)
end
def replace(other_hash)
if use_dirty? && other_hash && other_hash.kind_of?(Hash)
# new keys and changed keys
other_hash.keys.each do |key|
if self[key] != other_hash[key] || !include?(key)
couchrest_attribute_will_change!(key)
end
end
# old keys
old_keys = self.keys.reject { |key| other_hash.include?(key) }
old_keys.each { |key| couchrest_attribute_will_change!(key) }
end
super(other_hash)
end
def clear
self.keys.each { |key| couchrest_attribute_will_change!(key) } if use_dirty?
super
end
def delete_if
if use_dirty? && block_given?
self.keys.each do |key|
couchrest_attribute_will_change!(key) if yield key, self[key]
end
end
super
end
# ruby 1.9
def keep_if
if use_dirty? && block_given?
self.keys.each do |key|
couchrest_attribute_will_change!(key) if !yield key, self[key]
end
end
super
end
end
end

View file

@ -10,6 +10,7 @@ module CouchRest::Model
include CouchRest::Model::PropertyProtection
include CouchRest::Model::Associations
include CouchRest::Model::Validations
include CouchRest::Model::Dirty
attr_accessor :casted_by
end
@ -20,6 +21,7 @@ module CouchRest::Model
end
def []= key, value
couchrest_attribute_will_change!(key) if use_dirty && self[key] != value
super(key.to_s, value)
end
@ -64,5 +66,6 @@ module CouchRest::Model
end
end
alias :attributes= :update_attributes_without_saving
end
end

View file

@ -11,11 +11,13 @@ module CouchRest
add_config :model_type_key
add_config :mass_assign_any_attribute
add_config :auto_update_design_doc
add_config :use_dirty
configure do |config|
config.model_type_key = 'model' # was 'couchrest-type'
config.mass_assign_any_attribute = false
config.auto_update_design_doc = true
config.use_dirty = true
end
end
@ -49,5 +51,3 @@ module CouchRest
end
end
end

View file

@ -0,0 +1,49 @@
# encoding: utf-8
I18n.load_path << File.join(
File.dirname(__FILE__), "validations", "locale", "en.yml"
)
module CouchRest
module Model
# This applies to both Model::Base and Model::CastedModel
module Dirty
extend ActiveSupport::Concern
include CouchRest::Model::CastedBy # needed for base_doc
include ActiveModel::Dirty
included do
# internal dirty setting - overrides global setting.
# this is used to temporarily disable dirty tracking when setting
# attributes directly, for performance reasons.
self.send(:attr_accessor, :disable_dirty)
end
def use_dirty?
bdoc = base_doc
bdoc && bdoc.use_dirty && !bdoc.disable_dirty
end
def couchrest_attribute_will_change!(attr)
return if attr.nil? || !use_dirty?
attribute_will_change!(attr)
couchrest_parent_will_change!
end
def couchrest_parent_will_change!
@casted_by.couchrest_attribute_will_change!(casted_by_attribute) if @casted_by
end
private
# return the attribute name this object is referenced by in the parent
def casted_by_attribute
return @casted_by_attribute if @casted_by_attribute
attr = @casted_by.attributes
@casted_by_attribute = attr.keys.detect { |k| attr[k] == self }
end
end
end
end

View file

@ -1,6 +1,7 @@
module CouchRest
module Model
module ExtendedAttachments
extend ActiveSupport::Concern
# Add a file attachment to the current document. Expects
# :file and :name to be included in the arguments.
@ -35,7 +36,10 @@ module CouchRest
# deletes a file attachment from the current doc
def delete_attachment(attachment_name)
return unless attachments
attachments.delete attachment_name
if attachments.include?(attachment_name)
attribute_will_change!("_attachments")
attachments.delete attachment_name
end
end
# returns true if attachment_name exists
@ -66,6 +70,8 @@ module CouchRest
def set_attachment_attr(args)
content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file].path)
content_type ||= (get_mime_type(args[:name]) || 'text/plain')
attribute_will_change!("_attachments")
attachments[args[:name]] = {
'content_type' => content_type,
'data' => args[:file].read

View file

@ -12,7 +12,9 @@ module CouchRest
_run_save_callbacks do
set_unique_id if new? && self.respond_to?(:set_unique_id)
result = database.save_doc(self)
(result["ok"] == true) ? self : false
ret = (result["ok"] == true) ? self : false
@changed_attributes.clear if ret && @changed_attributes
ret
end
end
end
@ -28,10 +30,13 @@ module CouchRest
def update(options = {})
raise "Calling #{self.class.name}#update on document that has not been created!" if self.new?
return false unless perform_validations(options)
return true if use_dirty? && !self.changed?
_run_update_callbacks do
_run_save_callbacks do
result = database.save_doc(self)
result["ok"] == true
ret = result["ok"] == true
@changed_attributes.clear if ret && @changed_attributes
ret
end
end
end
@ -140,12 +145,18 @@ module CouchRest
# should use the class name as part of the unique id.
def unique_id method = nil, &block
if method
define_method :get_unique_id do
self.send(method)
end
define_method :set_unique_id do
self['_id'] ||= self.send(method)
self['_id'] ||= get_unique_id
end
elsif block
define_method :get_unique_id do
block.call(self)
end
define_method :set_unique_id do
uniqid = block.call(self)
uniqid = get_unique_id
raise ArgumentError, "unique_id block must not return nil" if uniqid.nil?
self['_id'] ||= uniqid
end

View file

@ -6,7 +6,9 @@ module CouchRest
included do
extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
extlib_inheritable_accessor(:prop_by_name) unless self.respond_to?(:prop_by_name)
self.properties ||= []
self.prop_by_name ||= {}
raise "You can only mixin Properties in a class responding to [] and []=, if you tried to mixin CastedModel, make sure your class inherits from Hash or responds to the proper methods" unless (method_defined?(:[]) && method_defined?(:[]=))
end
@ -43,6 +45,36 @@ module CouchRest
self[prop.to_s] = prop.is_a?(String) ? value : prop.cast(self, value)
end
# write property, update dirty status
def write_attribute_dirty(property, value)
prop = find_property!(property)
value = prop.is_a?(String) ? value : prop.cast(self, value)
propname = prop.to_s
attribute_will_change!(propname) if use_dirty? && self[propname] != value
self[propname] = value
end
def []=(key,value)
return super(key,value) unless use_dirty?
has_changes = self.changed?
if !has_changes && self.respond_to?(:get_unique_id)
check_id_change = true
old_id = get_unique_id
end
ret = super(key, value)
if check_id_change
# if we have set an attribute that results in the _id changing (unique_id),
# force changed? to return true so that the record can be saved
new_id = get_unique_id
changed_attributes["_id"] = new_id if old_id != new_id
end
ret
end
# 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.
@ -50,12 +82,17 @@ module CouchRest
# Remove any protected and update all the rest. Any attributes
# which do not have a property will simply be ignored.
attrs = remove_protected_attributes(hash)
directly_set_attributes(attrs)
directly_set_attributes(attrs, :dirty => true)
end
alias :attributes= :update_attributes_without_saving
# 'attributes' needed for Dirty
alias :attributes :properties_with_values
def find_property(property)
property.is_a?(Property) ? property : self.class.prop_by_name[property.to_s]
end
private
# The following methods should be accessable by the Model::Base Class, but not by anything else!
def apply_all_property_defaults
return if self.respond_to?(:new?) && (new? == false)
@ -76,15 +113,16 @@ module CouchRest
end
def find_property!(property)
prop = property.is_a?(Property) ? property : self.class.properties.detect {|p| p.to_s == property.to_s}
prop = find_property(property)
raise ArgumentError, "Missing property definition for #{property.to_s}" if prop.nil?
prop
end
# Set all the attributes and return a hash with the attributes
# that have not been accepted.
def directly_set_attributes(hash)
hash.reject do |attribute_name, attribute_value|
def directly_set_attributes(hash, options = {})
self.disable_dirty = !options[:dirty]
ret = hash.reject do |attribute_name, attribute_value|
if self.respond_to?("#{attribute_name}=")
self.send("#{attribute_name}=", attribute_value)
true
@ -95,6 +133,8 @@ module CouchRest
false
end
end
self.disable_dirty = false
ret
end
def directly_set_read_only_attributes(hash)
@ -166,13 +206,14 @@ module CouchRest
end
type = [type] # inject as an array
end
property = Property.new(name, type, options)
property = Property.new(name, type, options.merge(:use_dirty => use_dirty))
create_property_getter(property)
create_property_setter(property) unless property.read_only == true
if property.type_class.respond_to?(:validates_casted_model)
validates_casted_model property.name
end
properties << property
prop_by_name[property.to_s] = property
property
end
@ -206,7 +247,7 @@ module CouchRest
property_name = property.name
class_eval <<-EOS
def #{property_name}=(value)
write_attribute('#{property_name}', value)
write_attribute_dirty('#{property_name}', value)
end
EOS

View file

@ -4,7 +4,7 @@ module CouchRest::Model
include ::CouchRest::Model::Typecast
attr_reader :name, :type, :type_class, :read_only, :alias, :default, :casted, :init_method, :options
attr_reader :name, :type, :type_class, :read_only, :alias, :default, :casted, :init_method, :use_dirty, :options
# Attribute to define.
# All Properties are assumed casted unless the type is nil.
@ -38,8 +38,12 @@ module CouchRest::Model
end
arr = value.collect { |data| cast_value(parent, data) }
# allow casted_by calls to be passed up chain by wrapping in CastedArray
value = type_class != String ? CastedArray.new(arr, self) : arr
value = (use_dirty || type_class != String) ? CastedArray.new(arr, self) : arr
value.casted_by = parent if value.respond_to?(:casted_by)
elsif (type == Object || type == Hash) && (value.class == Hash)
# allow casted_by calls to be passed up chain by wrapping in CastedHash
value = CouchRest::Model::CastedHash[value]
value.casted_by = parent
elsif !value.nil?
value = cast_value(parent, value)
end
@ -90,6 +94,7 @@ module CouchRest::Model
@alias = options.delete(:alias) if options[:alias]
@default = options.delete(:default) unless options[:default].nil?
@init_method = options[:init_method] ? options.delete(:init_method) : 'new'
@use_dirty = options.delete(:use_dirty)
@options = options
end