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:
commit
1bced3b207
18 changed files with 819 additions and 41 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
23
lib/couchrest/model/casted_by.rb
Normal file
23
lib/couchrest/model/casted_by.rb
Normal 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
|
76
lib/couchrest/model/casted_hash.rb
Normal file
76
lib/couchrest/model/casted_hash.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
49
lib/couchrest/model/dirty.rb
Normal file
49
lib/couchrest/model/dirty.rb
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue