Permalink
Browse files

now using ActiveModel::Dirty. only writes to database if model.changed?

  • Loading branch information...
1 parent 53b052f commit 4dbf694e5112952ed6e4abd6016e3b50da620694 @sobakasu sobakasu committed Feb 28, 2011
View
@@ -6,4 +6,4 @@ pkg
.bundle
couchdb.std*
*.*~
-
+spec.out
@@ -16,6 +16,7 @@ class Base < Document
include CouchRest::Model::PropertyProtection
include CouchRest::Model::Associations
include CouchRest::Model::Validations
+ include CouchRest::Model::Dirty
def self.subclasses
@subclasses ||= []
@@ -55,7 +56,7 @@ def initialize(doc = {}, options = {})
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)
@@ -5,6 +5,7 @@
module CouchRest::Model
class CastedArray < Array
+ include CouchRest::Model::Dirty
attr_accessor :casted_by
attr_accessor :property
@@ -14,15 +15,34 @@ def initialize(array, property)
end
def << obj
+ couchrest_parent_will_change!
super(instantiate_and_cast(obj))
end
def push(obj)
+ couchrest_parent_will_change!
super(instantiate_and_cast(obj))
end
+
+ def pop
+ couchrest_parent_will_change!
+ super
+ end
+
+ def shift
+ couchrest_parent_will_change!
+ super
+ end
+
+ def unshift(obj)
+ couchrest_parent_will_change!
+ super(obj)
+ end
def []= index, obj
- super(index, instantiate_and_cast(obj))
+ value = instantiate_and_cast(obj)
+ couchrest_parent_will_change! if value != self[index]
+ super(index, value)
end
protected
@@ -0,0 +1,20 @@
+#
+# 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
+
+ def []= index, obj
+ couchrest_parent_will_change! if obj != self[index]
+ super(index, obj)
+ end
+
+ # needed for dirty
+ def attributes
+ self
+ end
+
+ end
+end
@@ -10,6 +10,7 @@ module CastedModel
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 @@ def initialize(keys = {})
end
def []= key, value
+ couchrest_attribute_will_change!(key) unless self[key] == value
super(key.to_s, value)
end
@@ -64,5 +66,6 @@ def update_attributes_without_saving(hash)
end
end
alias :attributes= :update_attributes_without_saving
+
end
end
@@ -0,0 +1,41 @@
+# 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
+
+ included do
+ include ActiveModel::Dirty
+ end
+
+ def couchrest_attribute_will_change!(attr)
+ return if attr.nil?
+ self.send("#{attr}_will_change!")
+ if pkey = casted_by_attribute
+ @casted_by.couchrest_attribute_will_change!(pkey)
+ end
+ 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 nil unless @casted_by
+ attr = @casted_by.attributes
+ attr.keys.detect { |k| attr[k] == self }
+ end
+
+ end
+ end
+end
@@ -1,6 +1,12 @@
module CouchRest
module Model
module ExtendedAttachments
+ extend ActiveSupport::Concern
+ include ActiveModel::Dirty
+ included do
+ # for _attachments_will_change!
+ define_attribute_methods [:_attachments]
+ end
# Add a file attachment to the current document. Expects
# :file and :name to be included in the arguments.
@@ -35,6 +41,7 @@ def update_attachment(args={})
# deletes a file attachment from the current doc
def delete_attachment(attachment_name)
return unless attachments
+ _attachments_will_change! if attachments.include?(attachment_name)
attachments.delete attachment_name
end
@@ -66,6 +73,8 @@ def get_mime_type(path)
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')
+
+ _attachments_will_change!
attachments[args[:name]] = {
'content_type' => content_type,
'data' => args[:file].read
@@ -12,7 +12,9 @@ def create(options = {})
_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 @@ def create!
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 unless 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 @@ def create!(attributes = {})
# 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
@@ -3,6 +3,7 @@ module CouchRest
module Model
module Properties
extend ActiveSupport::Concern
+ include ActiveModel::Dirty
included do
extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
@@ -38,24 +39,57 @@ def read_attribute(property)
# Store a casted value in the current instance of an attribute defined
# with a property.
+ # TODO: mixin dirty functionality into value (?)
def write_attribute(property, value)
prop = find_property!(property)
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)
+ self.send("#{prop}_will_change!") unless self[prop.to_s] == value
+ write_attribute(property, value)
+ end
+
+ def []=(key,value)
+ old_id = get_unique_id if self.respond_to?(:get_unique_id)
+
+ super(key, value)
+
+ if self.respond_to?(:get_unique_id)
+ # 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
+ 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.
def update_attributes_without_saving(hash)
# 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
+ # needed for Dirty
+ def attributes
+ ret = {}
+ self.class.properties.each do |property|
+ ret[property.name] = read_attribute(property)
+ end
+ ret
+ end
+
+ def find_property(property)
+ property.is_a?(Property) ? property : self.class.properties.detect {|p| p.to_s == 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,17 +110,26 @@ def prepare_all_attributes(doc = {}, options = {})
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)
+ def directly_set_attributes(hash, options = {})
hash.reject do |attribute_name, attribute_value|
if self.respond_to?("#{attribute_name}=")
- self.send("#{attribute_name}=", attribute_value)
+ if find_property(attribute_name)
+ if options[:dirty]
+ self.write_attribute_dirty(attribute_name, attribute_value)
+ else
+ # set attribute without updating dirty status
+ self.write_attribute(attribute_name, attribute_value)
+ end
+ else
+ self.send("#{attribute_name}=", attribute_value)
+ end
true
elsif mass_assign_any_attribute # config option
self[attribute_name] = attribute_value
@@ -126,7 +169,7 @@ def property(name, *options, &block)
end
existing_property = self.properties.find{|p| p.name == name.to_s}
if existing_property.nil? || (existing_property.default != opts[:default])
- define_property(name, opts, &block)
+ define_property(name, opts, &block)
end
end
@@ -139,8 +182,8 @@ def timestamps!
property(:created_at, Time, :read_only => true, :protected => true, :auto_validation => false)
set_callback :save, :before do |object|
- write_attribute('updated_at', Time.now)
- write_attribute('created_at', Time.now) if object.new?
+ write_attribute_dirty('updated_at', Time.now)
+ write_attribute_dirty('created_at', Time.now) if object.new?
end
EOS
end
@@ -203,7 +246,7 @@ def create_property_setter(property)
property_name = property.name
class_eval <<-EOS
def #{property_name}=(value)
- write_attribute('#{property_name}', value)
+ write_attribute_dirty('#{property_name}', value)
end
EOS
@@ -40,6 +40,10 @@ def cast(parent, value)
# allow casted_by calls to be passed up chain by wrapping in CastedArray
value = 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
@@ -8,6 +8,7 @@
require "active_model/translation"
require "active_model/validator"
require "active_model/validations"
+require "active_model/dirty"
require 'active_support/core_ext'
require 'active_support/json'
@@ -28,8 +29,11 @@
require "couchrest/model/typecast"
require "couchrest/model/property"
require "couchrest/model/property_protection"
-require "couchrest/model/casted_array"
require "couchrest/model/properties"
+require "couchrest/model/dirty"
+require "couchrest/model/casted_array"
+require "couchrest/model/casted_hash"
+require "couchrest/model/casted_model"
require "couchrest/model/validations"
require "couchrest/model/callbacks"
require "couchrest/model/document_queries"
@@ -372,6 +372,7 @@
foundart.created_at.should == foundart.updated_at
end
it "should set the time on update" do
+ @art.title = "new title" # only saved if @art.changed? == true
@art.save
@art.created_at.should < @art.updated_at
end
Oops, something went wrong.

0 comments on commit 4dbf694

Please sign in to comment.