Merge branch 'master' of git://github.com/couchrest/couchrest_model
This commit is contained in:
commit
ccafde77ab
|
@ -2,13 +2,18 @@
|
||||||
|
|
||||||
## 1.1.0 - 2011-05-XX
|
## 1.1.0 - 2011-05-XX
|
||||||
|
|
||||||
|
* New Features
|
||||||
|
* Properties with a nil value are now no longer sent to the database.
|
||||||
|
* Now possible to build new objects via CastedArray#build
|
||||||
|
|
||||||
* Minor fixes
|
* Minor fixes
|
||||||
* #as_json now correctly uses ActiveSupports methods.
|
* #as_json now correctly uses ActiveSupports methods.
|
||||||
* nil properties are now no longer sent in the document body.
|
|
||||||
* Rails 3.1 support (Peter Williams)
|
* Rails 3.1 support (Peter Williams)
|
||||||
* Initialization blocks when creating new models (Peter Williams)
|
* Initialization blocks when creating new models (Peter Williams)
|
||||||
* Removed railties dependency (DAddYE)
|
* Removed railties dependency (DAddYE)
|
||||||
* DesignDoc cache refreshed if a database is deleted.
|
* DesignDoc cache refreshed if a database is deleted.
|
||||||
|
* Fixing dirty tracking on collection_of association.
|
||||||
|
* Uniqueness Validation views created on initialization, not on demand!
|
||||||
|
|
||||||
|
|
||||||
## 1.1.0.beta5 - 2011-04-30
|
## 1.1.0.beta5 - 2011-04-30
|
||||||
|
|
|
@ -153,7 +153,7 @@ module CouchRest
|
||||||
def #{attrib}(reload = false)
|
def #{attrib}(reload = false)
|
||||||
return @#{attrib} unless @#{attrib}.nil? or reload
|
return @#{attrib} unless @#{attrib}.nil? or reload
|
||||||
ary = self.#{options[:foreign_key]}.collect{|i| #{options[:proxy]}.get(i)}
|
ary = self.#{options[:foreign_key]}.collect{|i| #{options[:proxy]}.get(i)}
|
||||||
@#{attrib} = ::CouchRest::CollectionOfProxy.new(ary, self, '#{options[:foreign_key]}')
|
@#{attrib} = ::CouchRest::Model::CollectionOfProxy.new(ary, find_property('#{options[:foreign_key]}'), self)
|
||||||
end
|
end
|
||||||
EOS
|
EOS
|
||||||
end
|
end
|
||||||
|
@ -161,7 +161,7 @@ module CouchRest
|
||||||
def create_collection_of_setter(attrib, options)
|
def create_collection_of_setter(attrib, options)
|
||||||
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||||
def #{attrib}=(value)
|
def #{attrib}=(value)
|
||||||
@#{attrib} = ::CouchRest::CollectionOfProxy.new(value, self, '#{options[:foreign_key]}')
|
@#{attrib} = ::CouchRest::Model::CollectionOfProxy.new(value, find_property('#{options[:foreign_key]}'), self)
|
||||||
end
|
end
|
||||||
EOS
|
EOS
|
||||||
end
|
end
|
||||||
|
@ -169,67 +169,63 @@ module CouchRest
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
||||||
# Special proxy for a collection of items so that adding and removing
|
# Special proxy for a collection of items so that adding and removing
|
||||||
# to the list automatically updates the associated property.
|
# to the list automatically updates the associated property.
|
||||||
class CollectionOfProxy < Array
|
class CollectionOfProxy < CastedArray
|
||||||
attr_accessor :property
|
|
||||||
attr_accessor :casted_by
|
|
||||||
|
|
||||||
def initialize(array, casted_by, property)
|
def initialize(array, property, parent)
|
||||||
self.property = property
|
(array ||= []).compact!
|
||||||
self.casted_by = casted_by
|
super(array, property, parent)
|
||||||
(array ||= []).compact!
|
casted_by[casted_by_property.to_s] = [] # replace the original array!
|
||||||
casted_by[property.to_s] = [] # replace the original array!
|
array.compact.each do |obj|
|
||||||
array.compact.each do |obj|
|
check_obj(obj)
|
||||||
check_obj(obj)
|
casted_by[casted_by_property.to_s] << obj.id
|
||||||
casted_by[property.to_s] << obj.id
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def << obj
|
||||||
|
check_obj(obj)
|
||||||
|
casted_by[casted_by_property.to_s] << obj.id
|
||||||
|
super(obj)
|
||||||
|
end
|
||||||
|
|
||||||
|
def push(obj)
|
||||||
|
check_obj(obj)
|
||||||
|
casted_by[casted_by_property.to_s].push obj.id
|
||||||
|
super(obj)
|
||||||
|
end
|
||||||
|
|
||||||
|
def unshift(obj)
|
||||||
|
check_obj(obj)
|
||||||
|
casted_by[casted_by_property.to_s].unshift obj.id
|
||||||
|
super(obj)
|
||||||
end
|
end
|
||||||
super(array)
|
|
||||||
end
|
|
||||||
|
|
||||||
def << obj
|
|
||||||
check_obj(obj)
|
|
||||||
casted_by[property.to_s] << obj.id
|
|
||||||
super(obj)
|
|
||||||
end
|
|
||||||
|
|
||||||
def push(obj)
|
|
||||||
check_obj(obj)
|
|
||||||
casted_by[property.to_s].push obj.id
|
|
||||||
super(obj)
|
|
||||||
end
|
|
||||||
|
|
||||||
def unshift(obj)
|
|
||||||
check_obj(obj)
|
|
||||||
casted_by[property.to_s].unshift obj.id
|
|
||||||
super(obj)
|
|
||||||
end
|
|
||||||
|
|
||||||
def []= index, obj
|
def []= index, obj
|
||||||
check_obj(obj)
|
check_obj(obj)
|
||||||
casted_by[property.to_s][index] = obj.id
|
casted_by[casted_by_property.to_s][index] = obj.id
|
||||||
super(index, obj)
|
super(index, obj)
|
||||||
end
|
end
|
||||||
|
|
||||||
def pop
|
def pop
|
||||||
casted_by[property.to_s].pop
|
casted_by[casted_by_property.to_s].pop
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
def shift
|
def shift
|
||||||
casted_by[property.to_s].shift
|
casted_by[casted_by_property.to_s].shift
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
|
def check_obj(obj)
|
||||||
|
raise "Object cannot be added to #{casted_by.class.to_s}##{casted_by_property.to_s} collection unless saved" if obj.new?
|
||||||
|
end
|
||||||
|
|
||||||
def check_obj(obj)
|
|
||||||
raise "Object cannot be added to #{casted_by.class.to_s}##{property.to_s} collection unless saved" if obj.new?
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -50,6 +50,12 @@ module CouchRest::Model
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def build(*args)
|
||||||
|
obj = casted_by_property.build(*args)
|
||||||
|
self.push(obj)
|
||||||
|
obj
|
||||||
|
end
|
||||||
|
|
||||||
protected
|
protected
|
||||||
|
|
||||||
def instantiate_and_cast(obj, change = true)
|
def instantiate_and_cast(obj, change = true)
|
||||||
|
|
|
@ -64,6 +64,18 @@ module CouchRest::Model
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Initialize a new instance of a property's type ready to be
|
||||||
|
# used. If a proc is defined for the init method, it will be used instead of
|
||||||
|
# a normal call to the class.
|
||||||
|
def build(*args)
|
||||||
|
raise StandardError, "Cannot build property without a class" if @type_class.nil?
|
||||||
|
if @init_method.is_a?(Proc)
|
||||||
|
@init_method.call(*args)
|
||||||
|
else
|
||||||
|
@type_class.send(@init_method, *args)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def associate_casted_value_to_parent(parent, value)
|
def associate_casted_value_to_parent(parent, value)
|
||||||
|
|
|
@ -14,8 +14,7 @@ module CouchRest
|
||||||
elsif [String, TrueClass, Integer, Float, BigDecimal, DateTime, Time, Date, Class].include?(klass)
|
elsif [String, TrueClass, Integer, Float, BigDecimal, DateTime, Time, Date, Class].include?(klass)
|
||||||
send('typecast_to_'+klass.to_s.downcase, value)
|
send('typecast_to_'+klass.to_s.downcase, value)
|
||||||
else
|
else
|
||||||
# Allow the init_method to be defined as a Proc for advanced conversion
|
property.build(value)
|
||||||
property.init_method.is_a?(Proc) ? property.init_method.call(value) : klass.send(property.init_method, value)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
module CouchRest
|
module CouchRest
|
||||||
module Model
|
module Model
|
||||||
module Validations
|
module Validations
|
||||||
|
|
||||||
# Validates if a field is unique
|
# Validates if a field is unique
|
||||||
class UniquenessValidator < ActiveModel::EachValidator
|
class UniquenessValidator < ActiveModel::EachValidator
|
||||||
|
|
||||||
|
@ -11,29 +11,33 @@ module CouchRest
|
||||||
# or add one if necessary.
|
# or add one if necessary.
|
||||||
def setup(model)
|
def setup(model)
|
||||||
@model = model
|
@model = model
|
||||||
|
if options[:view].blank?
|
||||||
|
attributes.each do |attribute|
|
||||||
|
opts = merge_view_options(attribute)
|
||||||
|
|
||||||
|
if model.respond_to?(:has_view?) && !model.has_view?(opts[:view_name])
|
||||||
|
opts[:keys] << {:allow_nil => true}
|
||||||
|
model.view_by(*opts[:keys])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def validate_each(document, attribute, value)
|
def validate_each(document, attribute, value)
|
||||||
keys = [attribute]
|
opts = merge_view_options(attribute)
|
||||||
unless options[:scope].nil?
|
|
||||||
keys = (options[:scope].is_a?(Array) ? options[:scope] : [options[:scope]]) + keys
|
|
||||||
end
|
|
||||||
values = keys.map{|k| document.send(k)}
|
|
||||||
values = values.first if values.length == 1
|
|
||||||
|
|
||||||
view_name = options[:view].nil? ? "by_#{keys.join('_and_')}" : options[:view]
|
values = opts[:keys].map{|k| document.send(k)}
|
||||||
|
values = values.first if values.length == 1
|
||||||
|
|
||||||
model = (document.respond_to?(:model_proxy) && document.model_proxy ? document.model_proxy : @model)
|
model = (document.respond_to?(:model_proxy) && document.model_proxy ? document.model_proxy : @model)
|
||||||
# Determine the base of the search
|
# Determine the base of the search
|
||||||
base = options[:proxy].nil? ? model : document.instance_eval(options[:proxy])
|
base = opts[:proxy].nil? ? model : document.instance_eval(opts[:proxy])
|
||||||
|
|
||||||
if base.respond_to?(:has_view?) && !base.has_view?(view_name)
|
if base.respond_to?(:has_view?) && !base.has_view?(opts[:view_name])
|
||||||
raise "View #{document.class.name}.#{options[:view]} does not exist!" unless options[:view].nil?
|
raise "View #{document.class.name}.#{opts[:view_name]} does not exist for validation!"
|
||||||
keys << {:allow_nil => true}
|
|
||||||
model.view_by(*keys)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
rows = base.view(view_name, :key => values, :limit => 2, :include_docs => false)['rows']
|
rows = base.view(opts[:view_name], :key => values, :limit => 2, :include_docs => false)['rows']
|
||||||
return if rows.empty?
|
return if rows.empty?
|
||||||
|
|
||||||
unless document.new?
|
unless document.new?
|
||||||
|
@ -47,6 +51,17 @@ module CouchRest
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def merge_view_options(attr)
|
||||||
|
keys = [attr]
|
||||||
|
keys.unshift(*options[:scope]) unless options[:scope].nil?
|
||||||
|
|
||||||
|
view_name = options[:view].nil? ? "by_#{keys.join('_and_')}" : options[:view]
|
||||||
|
|
||||||
|
options.merge({:keys => keys, :view_name => view_name})
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -101,7 +101,7 @@ describe "Assocations" do
|
||||||
it "should create an associated property and collection proxy" do
|
it "should create an associated property and collection proxy" do
|
||||||
@invoice.respond_to?('entry_ids').should be_true
|
@invoice.respond_to?('entry_ids').should be_true
|
||||||
@invoice.respond_to?('entry_ids=').should be_true
|
@invoice.respond_to?('entry_ids=').should be_true
|
||||||
@invoice.entries.class.should eql(::CouchRest::CollectionOfProxy)
|
@invoice.entries.class.should eql(::CouchRest::Model::CollectionOfProxy)
|
||||||
end
|
end
|
||||||
|
|
||||||
it "should allow replacement of objects" do
|
it "should allow replacement of objects" do
|
||||||
|
@ -154,6 +154,33 @@ describe "Assocations" do
|
||||||
@invoice.entries.should be_empty
|
@invoice.entries.should be_empty
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Account for dirty tracking
|
||||||
|
describe "dirty tracking" do
|
||||||
|
it "should register changes on push" do
|
||||||
|
@invoice.changed?.should be_false
|
||||||
|
@invoice.entries << @entries[0]
|
||||||
|
@invoice.changed?.should be_true
|
||||||
|
end
|
||||||
|
it "should register changes on pop" do
|
||||||
|
@invoice.entries << @entries[0]
|
||||||
|
@invoice.save
|
||||||
|
@invoice.changed?.should be_false
|
||||||
|
@invoice.entries.pop
|
||||||
|
@invoice.changed?.should be_true
|
||||||
|
end
|
||||||
|
it "should register id changes on push" do
|
||||||
|
@invoice.entry_ids << @entries[0].id
|
||||||
|
@invoice.changed?.should be_true
|
||||||
|
end
|
||||||
|
it "should register id changes on pop" do
|
||||||
|
@invoice.entry_ids << @entries[0].id
|
||||||
|
@invoice.save
|
||||||
|
@invoice.changed?.should be_false
|
||||||
|
@invoice.entry_ids.pop
|
||||||
|
@invoice.changed?.should be_true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "proxy" do
|
describe "proxy" do
|
||||||
|
|
||||||
it "should ensure new entries to proxy are matched" do
|
it "should ensure new entries to proxy are matched" do
|
||||||
|
|
|
@ -358,6 +358,28 @@ describe "Property Class" do
|
||||||
property.init_method.should eql('parse')
|
property.init_method.should eql('parse')
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "#build" do
|
||||||
|
it "should allow instantiation of new object" do
|
||||||
|
property = CouchRest::Model::Property.new(:test, Date)
|
||||||
|
obj = property.build(2011, 05, 21)
|
||||||
|
obj.should eql(Date.new(2011, 05, 21))
|
||||||
|
end
|
||||||
|
it "should use init_method if provided" do
|
||||||
|
property = CouchRest::Model::Property.new(:test, Date, :init_method => 'parse')
|
||||||
|
obj = property.build("2011-05-21")
|
||||||
|
obj.should eql(Date.new(2011, 05, 21))
|
||||||
|
end
|
||||||
|
it "should use init_method Proc if provided" do
|
||||||
|
property = CouchRest::Model::Property.new(:test, Date, :init_method => Proc.new{|v| Date.parse(v)})
|
||||||
|
obj = property.build("2011-05-21")
|
||||||
|
obj.should eql(Date.new(2011, 05, 21))
|
||||||
|
end
|
||||||
|
it "should raise error if no class" do
|
||||||
|
property = CouchRest::Model::Property.new(:test)
|
||||||
|
lambda { property.build }.should raise_error(StandardError, /Cannot build/)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
## Property Casting method. More thoroughly tested in typecast_spec.
|
## Property Casting method. More thoroughly tested in typecast_spec.
|
||||||
|
|
||||||
describe "casting" do
|
describe "casting" do
|
||||||
|
@ -386,6 +408,18 @@ describe "Property Class" do
|
||||||
property.cast(parent, ["2010-06-01", "2010-06-02"]).class.should eql(CouchRest::Model::CastedArray)
|
property.cast(parent, ["2010-06-01", "2010-06-02"]).class.should eql(CouchRest::Model::CastedArray)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "should allow instantion of model via CastedArray#build" do
|
||||||
|
property = CouchRest::Model::Property.new(:dates, [Date])
|
||||||
|
parent = Article.new
|
||||||
|
ary = property.cast(parent, [])
|
||||||
|
obj = ary.build(2011, 05, 21)
|
||||||
|
ary.length.should eql(1)
|
||||||
|
ary.first.should eql(Date.new(2011, 05, 21))
|
||||||
|
obj = ary.build(2011, 05, 22)
|
||||||
|
ary.length.should eql(2)
|
||||||
|
ary.last.should eql(Date.new(2011, 05, 22))
|
||||||
|
end
|
||||||
|
|
||||||
it "should raise and error if value is array when type is not" do
|
it "should raise and error if value is array when type is not" do
|
||||||
property = CouchRest::Model::Property.new(:test, Date)
|
property = CouchRest::Model::Property.new(:test, Date)
|
||||||
parent = mock("FooClass")
|
parent = mock("FooClass")
|
||||||
|
|
|
@ -16,7 +16,11 @@ describe "Validations" do
|
||||||
before(:all) do
|
before(:all) do
|
||||||
@objs = ['title 1', 'title 2', 'title 3'].map{|t| WithUniqueValidation.create(:title => t)}
|
@objs = ['title 1', 'title 2', 'title 3'].map{|t| WithUniqueValidation.create(:title => t)}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "should create a new view if none defined before performing" do
|
||||||
|
WithUniqueValidation.has_view?(:by_title).should be_true
|
||||||
|
end
|
||||||
|
|
||||||
it "should validate a new unique document" do
|
it "should validate a new unique document" do
|
||||||
@obj = WithUniqueValidation.create(:title => 'title 4')
|
@obj = WithUniqueValidation.create(:title => 'title 4')
|
||||||
@obj.new?.should_not be_true
|
@obj.new?.should_not be_true
|
||||||
|
@ -35,6 +39,7 @@ describe "Validations" do
|
||||||
@obj.should be_valid
|
@obj.should be_valid
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
it "should allow own view to be specified" do
|
it "should allow own view to be specified" do
|
||||||
# validates_uniqueness_of :code, :view => 'all'
|
# validates_uniqueness_of :code, :view => 'all'
|
||||||
WithUniqueValidationView.create(:title => 'title 1', :code => '1234')
|
WithUniqueValidationView.create(:title => 'title 1', :code => '1234')
|
||||||
|
@ -50,6 +55,13 @@ describe "Validations" do
|
||||||
}.should raise_error
|
}.should raise_error
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it "should not try to create a defined view" do
|
||||||
|
WithUniqueValidationView.validates_uniqueness_of :title, :view => 'fooobar'
|
||||||
|
WithUniqueValidationView.has_view?('fooobar').should be_false
|
||||||
|
WithUniqueValidationView.has_view?('by_title').should be_false
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
it "should not try to create new view when already defined" do
|
it "should not try to create new view when already defined" do
|
||||||
@obj = @objs[1]
|
@obj = @objs[1]
|
||||||
@obj.class.should_not_receive('view_by')
|
@obj.class.should_not_receive('view_by')
|
||||||
|
@ -60,6 +72,11 @@ describe "Validations" do
|
||||||
end
|
end
|
||||||
|
|
||||||
context "with a proxy parameter" do
|
context "with a proxy parameter" do
|
||||||
|
|
||||||
|
it "should create a new view despite proxy" do
|
||||||
|
WithUniqueValidationProxy.has_view?(:by_title).should be_true
|
||||||
|
end
|
||||||
|
|
||||||
it "should be used" do
|
it "should be used" do
|
||||||
@obj = WithUniqueValidationProxy.new(:title => 'test 6')
|
@obj = WithUniqueValidationProxy.new(:title => 'test 6')
|
||||||
proxy = @obj.should_receive('proxy').and_return(@obj.class)
|
proxy = @obj.should_receive('proxy').and_return(@obj.class)
|
||||||
|
|
Loading…
Reference in a new issue