diff --git a/Gemfile.lock b/Gemfile.lock index d0a19df..25cd3a7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,9 +1,9 @@ PATH remote: . specs: - couchrest_model (1.1.0.beta) + couchrest_model (1.1.0.beta2) activemodel (~> 3.0.0) - couchrest (~> 1.0.2) + couchrest (~> 1.1.0.pre) mime-types (~> 1.15) railties (~> 3.0.0) tzinfo (~> 0.3.22) @@ -28,7 +28,7 @@ GEM i18n (~> 0.4) activesupport (3.0.5) builder (2.1.2) - couchrest (1.0.2) + couchrest (1.1.0.pre) json (~> 1.5.1) mime-types (~> 1.15) rest-client (~> 1.6.1) @@ -66,6 +66,11 @@ PLATFORMS ruby DEPENDENCIES + activemodel (~> 3.0.0) + couchrest (~> 1.1.0.pre) couchrest_model! + mime-types (~> 1.15) rack-test (>= 0.5.7) + railties (~> 3.0.0) rspec (>= 2.0.0) + tzinfo (~> 0.3.22) diff --git a/couchrest_model.gemspec b/couchrest_model.gemspec index 76a1fba..4290bf1 100644 --- a/couchrest_model.gemspec +++ b/couchrest_model.gemspec @@ -23,7 +23,7 @@ Gem::Specification.new do |s| s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } s.require_paths = ["lib"] - s.add_dependency(%q, "~> 1.0.2") + s.add_dependency(%q, "~> 1.1.0.pre") s.add_dependency(%q, "~> 1.15") s.add_dependency(%q, "~> 3.0.0") s.add_dependency(%q, "~> 0.3.22") diff --git a/history.txt b/history.txt index 7d4d1b5..823a8f1 100644 --- a/history.txt +++ b/history.txt @@ -3,6 +3,8 @@ * Minor enhancements: * Time handling improved in accordance with CouchRest 1.0.3. Always set to UTC. * Refinements to associations and uniqueness validation for proxy (based on issue found by Gleb Kanterov) + * Added :allow_nil and :allow_blank options when creating a new view + * Unique Validation now supports scopes! == 1.1.0.beta diff --git a/lib/couchrest/model/designs/view.rb b/lib/couchrest/model/designs/view.rb index a1f4055..766da3c 100644 --- a/lib/couchrest/model/designs/view.rb +++ b/lib/couchrest/model/designs/view.rb @@ -412,6 +412,16 @@ module CouchRest # The view name is the same, but three keys would be used in the # subsecuent index. # + # By default, a check is made on each of the view's keys to ensure they + # do not contain a nil value ('null' in javascript). This is probably what + # you want in most cases but sometimes in can be useful to create an + # index where nil is permited. Set the :allow_nil option to true to + # remove this check. + # + # Conversely, keys are not checked to see if they are empty or blank. If you'd + # like to enable this, set the :allow_blank option to false. The default + # is true, empty strings are permited in the indexes. + # def create(model, name, opts = {}) unless opts[:map] @@ -421,12 +431,14 @@ module CouchRest raise "View cannot be created without recognised name, :map or :by options" if opts[:by].nil? + opts[:allow_blank] = opts[:allow_blank].nil? ? true : opts[:allow_blank] opts[:guards] ||= [] opts[:guards].push "(doc['#{model.model_type_key}'] == '#{model.to_s}')" keys = opts[:by].map{|o| "doc['#{o}']"} emit = keys.length == 1 ? keys.first : "[#{keys.join(', ')}]" - opts[:guards] += keys.map{|k| "(#{k} != null)"} + opts[:guards] += keys.map{|k| "(#{k} != null)"} unless opts[:allow_nil] + opts[:guards] += keys.map{|k| "(#{k} != '')"} unless opts[:allow_blank] opts[:map] = <<-EOF function(doc) { if (#{opts[:guards].join(' && ')}) { diff --git a/lib/couchrest/model/validations/uniqueness.rb b/lib/couchrest/model/validations/uniqueness.rb index 4cf6b90..d76ed09 100644 --- a/lib/couchrest/model/validations/uniqueness.rb +++ b/lib/couchrest/model/validations/uniqueness.rb @@ -14,7 +14,14 @@ module CouchRest end def validate_each(document, attribute, value) - view_name = options[:view].nil? ? "by_#{attribute}" : options[:view] + keys = [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] model = (document.respond_to?(:model_proxy) && document.model_proxy ? document.model_proxy : @model) # Determine the base of the search @@ -22,18 +29,20 @@ module CouchRest if base.respond_to?(:has_view?) && !base.has_view?(view_name) raise "View #{document.class.name}.#{options[:view]} does not exist!" unless options[:view].nil? - model.view_by attribute + model.view_by *keys, :allow_nil => true end - docs = base.view(view_name, :key => value, :limit => 2, :include_docs => false)['rows'] - return if docs.empty? + rows = base.view(view_name, :key => values, :limit => 2, :include_docs => false)['rows'] + return if rows.empty? unless document.new? - return if docs.find{|doc| doc['id'] == document.id} + return if rows.find{|row| row['id'] == document.id} end - if docs.length > 0 - document.errors.add(attribute, :taken, options.merge(:value => value)) + if rows.length > 0 + opts = options.merge(:value => value) + opts.delete(:scope) # Has meaning with I18n! + document.errors.add(attribute, :taken, opts) end end diff --git a/spec/couchrest/validations_spec.rb b/spec/couchrest/validations_spec.rb index 85f20c7..f8e2f4b 100644 --- a/spec/couchrest/validations_spec.rb +++ b/spec/couchrest/validations_spec.rb @@ -12,45 +12,45 @@ describe "Validations" do describe "Uniqueness" do - before(:all) do - @objs = ['title 1', 'title 2', 'title 3'].map{|t| WithUniqueValidation.create(:title => t)} - end - - it "should validate a new unique document" do - @obj = WithUniqueValidation.create(:title => 'title 4') - @obj.new?.should_not be_true - @obj.should be_valid - end + context "basic" do + before(:all) do + @objs = ['title 1', 'title 2', 'title 3'].map{|t| WithUniqueValidation.create(:title => t)} + end + + it "should validate a new unique document" do + @obj = WithUniqueValidation.create(:title => 'title 4') + @obj.new?.should_not be_true + @obj.should be_valid + end - it "should not validate a non-unique document" do - @obj = WithUniqueValidation.create(:title => 'title 1') - @obj.should_not be_valid - @obj.errors[:title].should == ["has already been taken"] - end + it "should not validate a non-unique document" do + @obj = WithUniqueValidation.create(:title => 'title 1') + @obj.should_not be_valid + @obj.errors[:title].should == ["has already been taken"] + end - it "should save already created document" do - @obj = @objs.first - @obj.save.should_not be_false - @obj.should be_valid - end + it "should save already created document" do + @obj = @objs.first + @obj.save.should_not be_false + @obj.should be_valid + end - it "should allow own view to be specified" do - # validates_uniqueness_of :code, :view => 'all' - WithUniqueValidationView.create(:title => 'title 1', :code => '1234') - @obj = WithUniqueValidationView.new(:title => 'title 5', :code => '1234') - @obj.should_not be_valid - end + it "should allow own view to be specified" do + # validates_uniqueness_of :code, :view => 'all' + WithUniqueValidationView.create(:title => 'title 1', :code => '1234') + @obj = WithUniqueValidationView.new(:title => 'title 5', :code => '1234') + @obj.should_not be_valid + end - it "should raise an error if specified view does not exist" do - WithUniqueValidationView.validates_uniqueness_of :title, :view => 'fooobar' - @obj = WithUniqueValidationView.new(:title => 'title 2', :code => '12345') - lambda { - @obj.valid? - }.should raise_error - end + it "should raise an error if specified view does not exist" do + WithUniqueValidationView.validates_uniqueness_of :title, :view => 'fooobar' + @obj = WithUniqueValidationView.new(:title => 'title 2', :code => '12345') + lambda { + @obj.valid? + }.should raise_error + end - context "with a pre-defined view" do - it "should not try to create new view" do + it "should not try to create new view when already defined" do @obj = @objs[1] @obj.class.should_not_receive('view_by') @obj.class.should_receive('has_view?').and_return(true) @@ -58,7 +58,7 @@ describe "Validations" do @obj.valid? end end - + context "with a proxy parameter" do it "should be used" do @obj = WithUniqueValidationProxy.new(:title => 'test 6') @@ -85,6 +85,44 @@ describe "Validations" do @obj.stub!(:model_proxy).twice.and_return(mp) @obj.valid? end + end + + context "with a scope" do + before(:all) do + @objs = [['title 1', 1], ['title 2', 1], ['title 3', 1]].map{|t| WithScopedUniqueValidation.create(:title => t[0], :parent_id => t[1])} + @objs_nil = [['title 1', nil], ['title 2', nil], ['title 3', nil]].map{|t| WithScopedUniqueValidation.create(:title => t[0], :parent_id => t[1])} + end + + it "should create the view" do + @objs.first.class.has_view?('by_parent_id_and_title') + end + + it "should validate unique document" do + @obj = WithScopedUniqueValidation.create(:title => 'title 4', :parent_id => 1) + @obj.should be_valid + end + + it "should validate unique document outside of scope" do + @obj = WithScopedUniqueValidation.create(:title => 'title 1', :parent_id => 2) + @obj.should be_valid + end + + it "should validate non-unique document" do + @obj = WithScopedUniqueValidation.create(:title => 'title 1', :parent_id => 1) + @obj.should_not be_valid + @obj.errors[:title].should == ["has already been taken"] + end + + it "should validate unique document will nil scope" do + @obj = WithScopedUniqueValidation.create(:title => 'title 4', :parent_id => nil) + @obj.should be_valid + end + + it "should validate non-unique document with nil scope" do + @obj = WithScopedUniqueValidation.create(:title => 'title 1', :parent_id => nil) + @obj.should_not be_valid + @obj.errors[:title].should == ["has already been taken"] + end end diff --git a/spec/fixtures/base.rb b/spec/fixtures/base.rb index 34f6778..aac9b2b 100644 --- a/spec/fixtures/base.rb +++ b/spec/fixtures/base.rb @@ -138,4 +138,13 @@ class WithUniqueValidationView < CouchRest::Model::Base validates_uniqueness_of :code, :view => 'all' end +class WithScopedUniqueValidation < CouchRest::Model::Base + use_database DB + + property :parent_id + property :title + + validates_uniqueness_of :title, :scope => :parent_id +end + diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 3a865b5..415a2c7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -32,7 +32,7 @@ RSpec.configure do |config| cr = TEST_SERVER test_dbs = cr.databases.select { |db| db =~ /^#{TESTDB}/ } test_dbs.each do |db| - cr.database(db).delete! rescue nil + #cr.database(db).delete! rescue nil end end end