From 5024d10d4be017b6a4514385e0d82edce83c63a8 Mon Sep 17 00:00:00 2001 From: Geremia Taglialatela Date: Thu, 28 Feb 2019 15:38:43 +0100 Subject: [PATCH 1/3] Add support to Rails 6.0.0.beta2 [ci skip] --- .travis.yml | 8 ++++---- Appraisals | 4 ++-- client_side_validations.gemspec | 2 +- ...ails_6.0.0.beta1.gemfile => rails_6.0.0.beta2.gemfile} | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) rename gemfiles/{rails_6.0.0.beta1.gemfile => rails_6.0.0.beta2.gemfile} (80%) diff --git a/.travis.yml b/.travis.yml index 6fd811c22..e8802398f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,7 @@ gemfile: - gemfiles/rails_5.0.gemfile - gemfiles/rails_5.1.gemfile - gemfiles/rails_5.2.gemfile - - gemfiles/rails_6.0.0.beta1.gemfile + - gemfiles/rails_6.0.0.beta2.gemfile - gemfiles/rails_edge.gemfile matrix: allow_failures: @@ -19,15 +19,15 @@ matrix: - gemfile: gemfiles/rails_edge.gemfile exclude: - rvm: 2.2.10 - gemfile: gemfiles/rails_6.0.0.beta1.gemfile + gemfile: gemfiles/rails_6.0.0.beta2.gemfile - rvm: 2.2.10 gemfile: gemfiles/rails_edge.gemfile - rvm: 2.3.8 - gemfile: gemfiles/rails_6.0.0.beta1.gemfile + gemfile: gemfiles/rails_6.0.0.beta2.gemfile - rvm: 2.3.8 gemfile: gemfiles/rails_edge.gemfile - rvm: 2.4.5 - gemfile: gemfiles/rails_6.0.0.beta1.gemfile + gemfile: gemfiles/rails_6.0.0.beta2.gemfile - rvm: 2.4.5 gemfile: gemfiles/rails_edge.gemfile fast_finish: true diff --git a/Appraisals b/Appraisals index b75196bde..0d2deb3f9 100644 --- a/Appraisals +++ b/Appraisals @@ -12,8 +12,8 @@ appraise 'rails-5.2' do gem 'rails', '~> 5.2.0' end -appraise 'rails-6.0.0.beta1' do - gem 'rails', '~> 6.0.0.beta1' +appraise 'rails-6.0.0.beta2' do + gem 'rails', '~> 6.0.0.beta2' end appraise 'rails-edge' do diff --git a/client_side_validations.gemspec b/client_side_validations.gemspec index 40373884c..95d3e7f38 100644 --- a/client_side_validations.gemspec +++ b/client_side_validations.gemspec @@ -20,7 +20,7 @@ Gem::Specification.new do |spec| spec.files = `git ls-files -z -- {CHANGELOG.md,LICENSE.md,README.md,lib,vendor}`.split("\x0") spec.require_paths = ['lib'] - spec.add_dependency 'rails', '>= 5.0.0.1', '<= 6.0.0.beta1' + spec.add_dependency 'rails', '>= 5.0.0.1', '<= 6.0.0.beta2' spec.add_dependency 'jquery-rails', '~> 4.3' spec.add_dependency 'js_regex', '~> 3.1' diff --git a/gemfiles/rails_6.0.0.beta1.gemfile b/gemfiles/rails_6.0.0.beta2.gemfile similarity index 80% rename from gemfiles/rails_6.0.0.beta1.gemfile rename to gemfiles/rails_6.0.0.beta2.gemfile index 8e589dd73..aed864127 100644 --- a/gemfiles/rails_6.0.0.beta1.gemfile +++ b/gemfiles/rails_6.0.0.beta2.gemfile @@ -4,6 +4,6 @@ source 'https://rubygems.org' -gem 'rails', '~> 6.0.0.beta1' +gem 'rails', '~> 6.0.0.beta2' gemspec path: '../' From d219af22e9163478940723594515aab1ea3b6f16 Mon Sep 17 00:00:00 2001 From: Geremia Taglialatela Date: Thu, 28 Feb 2019 20:29:25 +0100 Subject: [PATCH 2/3] Fix uniqueness validator behavior In MySQL, the default collation is case insensitive. Since the uniqueness validator enforces case sensitive comparison by default, it frequently causes mismatched collation issues (performance, weird behavior, etc) to MySQL users. Ref: rails/rails@6cb176c Close: #753 --- .../active_record/uniqueness.rb | 2 +- .../cases/test_uniqueness_validator.rb | 38 ++++++++++++++----- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/lib/client_side_validations/active_record/uniqueness.rb b/lib/client_side_validations/active_record/uniqueness.rb index 8d4115506..aeb359485 100644 --- a/lib/client_side_validations/active_record/uniqueness.rb +++ b/lib/client_side_validations/active_record/uniqueness.rb @@ -6,7 +6,7 @@ module Uniqueness def client_side_hash(model, attribute, _force = nil) hash = {} hash[:message] = model.errors.generate_message(attribute, message_type, options.except(:scope)) - hash[:case_sensitive] = options[:case_sensitive] + hash[:case_sensitive] = true if options[:case_sensitive] hash[:id] = model.id unless model.new_record? hash[:allow_blank] = true if options[:allow_nil] || options[:allow_blank] diff --git a/test/active_record/cases/test_uniqueness_validator.rb b/test/active_record/cases/test_uniqueness_validator.rb index 29b66e88d..cd44226a7 100644 --- a/test/active_record/cases/test_uniqueness_validator.rb +++ b/test/active_record/cases/test_uniqueness_validator.rb @@ -4,37 +4,55 @@ module ActiveRecord class UniquenessValidatorTest < ClientSideValidations::ActiveRecordTestBase + def uniqueness_validator_options(hash) + if defined?(Rails.version) && Gem::Version.new(Rails.version) < Gem::Version.new('6.0.0.beta2') + { case_sensitive: true }.merge(hash) + else + hash + end + end + def test_uniqueness_client_side_hash - expected_hash = { message: 'has already been taken', case_sensitive: true } + expected_hash = uniqueness_validator_options(message: 'has already been taken') assert_equal expected_hash, UniquenessValidator.new(attributes: [:name]).client_side_hash(@user, :name) end def test_uniqueness_client_side_hash_allowing_blank - expected_hash = { message: 'has already been taken', case_sensitive: true, allow_blank: true } + expected_hash = uniqueness_validator_options(message: 'has already been taken', allow_blank: true) assert_equal expected_hash, UniquenessValidator.new(attributes: [:name], allow_blank: true).client_side_hash(@user, :name) end def test_uniqueness_client_side_hash_allowing_nil - expected_hash = { message: 'has already been taken', case_sensitive: true, allow_blank: true } + expected_hash = uniqueness_validator_options(message: 'has already been taken', allow_blank: true) assert_equal expected_hash, UniquenessValidator.new(attributes: [:name], allow_nil: true).client_side_hash(@user, :name) end + def test_uniqueness_client_side_hash_case_insensitive + expected_hash = { message: 'has already been taken' } + assert_equal expected_hash, UniquenessValidator.new(attributes: [:name], case_sensitive: false).client_side_hash(@user, :name) + end + + def test_uniqueness_client_side_hash_case_sensitive + expected_hash = uniqueness_validator_options(message: 'has already been taken', case_sensitive: true) + assert_equal expected_hash, UniquenessValidator.new(attributes: [:name], case_sensitive: true).client_side_hash(@user, :name) + end + def test_uniqueness_client_side_hash_with_custom_message - expected_hash = { message: 'is not available', case_sensitive: true } + expected_hash = uniqueness_validator_options(message: 'is not available') assert_equal expected_hash, UniquenessValidator.new(attributes: [:name], message: 'is not available').client_side_hash(@user, :name) end def test_uniqueness_client_side_hash_with_existing_record @user.stubs(:new_record?).returns(false) @user.stubs(:id).returns(1) - expected_hash = { message: 'has already been taken', case_sensitive: true, id: 1 } + expected_hash = uniqueness_validator_options(message: 'has already been taken', id: 1) assert_equal expected_hash, UniquenessValidator.new(attributes: [:name]).client_side_hash(@user, :name) end def test_uniqueness_client_side_hash_with_single_scope_item @user.stubs(:age).returns(30) @user.stubs(:title).returns('test title') - expected_hash = { message: 'has already been taken', case_sensitive: true, scope: { title: 'test title' } } + expected_hash = uniqueness_validator_options(message: 'has already been taken', scope: { title: 'test title' }) result_hash = UniquenessValidator.new(attributes: [:name], scope: :title).client_side_hash(@user, :name) assert_equal expected_hash, result_hash end @@ -42,26 +60,26 @@ def test_uniqueness_client_side_hash_with_single_scope_item def test_uniqueness_client_side_hash_with_multiple_scope_items @user.stubs(:age).returns(30) @user.stubs(:title).returns('test title') - expected_hash = { message: 'has already been taken', case_sensitive: true, scope: { age: 30, title: 'test title' } } + expected_hash = uniqueness_validator_options(message: 'has already been taken', scope: { age: 30, title: 'test title' }) result_hash = UniquenessValidator.new(attributes: [:name], scope: %i[age title]).client_side_hash(@user, :name) assert_equal expected_hash, result_hash end def test_uniqueness_client_side_hash_with_empty_scope_array - expected_hash = { message: 'has already been taken', case_sensitive: true } + expected_hash = uniqueness_validator_options(message: 'has already been taken') result_hash = UniquenessValidator.new(attributes: [:name], scope: []).client_side_hash(@user, :name) assert_equal expected_hash, result_hash end def test_uniqueness_client_side_hash_when_nested_module @user = ActiveRecordTestModule::User2.new - expected_hash = { message: 'has already been taken', case_sensitive: true, class: 'active_record_test_module/user2' } + expected_hash = uniqueness_validator_options(message: 'has already been taken', class: 'active_record_test_module/user2') assert_equal expected_hash, UniquenessValidator.new(attributes: [:name]).client_side_hash(@user, :name) end def test_uniqueness_client_side_hash_with_class_from_options @user = UserForm.new - expected_hash = { message: 'has already been taken', case_sensitive: true, class: 'user' } + expected_hash = uniqueness_validator_options(message: 'has already been taken', class: 'user') assert_equal expected_hash, UniquenessValidator.new(attributes: [:name], client_validations: { class: 'User' }).client_side_hash(@user, :name) end end From 645b8b090e4d78ca7007a6bf4249a01bffd6b125 Mon Sep 17 00:00:00 2001 From: Geremia Taglialatela Date: Thu, 28 Feb 2019 21:24:55 +0100 Subject: [PATCH 3/3] Support javascript case-sensitive uniqueness --- coffeescript/rails.validations.coffee | 8 +++- .../public/test/validators/uniqueness.js | 43 ++++++++++++++++++- .../assets/javascripts/rails.validations.js | 8 +++- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/coffeescript/rails.validations.coffee b/coffeescript/rails.validations.coffee index 644a33d36..a5d495297 100644 --- a/coffeescript/rails.validations.coffee +++ b/coffeescript/rails.validations.coffee @@ -413,8 +413,14 @@ ClientSideValidations = valid = true form.find(":input[name^=\"#{name_prefix}\"][name$=\"#{name_suffix}\"]").each -> + other_value = $(@).val() + + unless options.case_sensitive + value = value.toLowerCase() + other_value = other_value.toLowerCase() + if $(@).attr('name') != name - if $(@).val() == value + if other_value == value valid = false $(@).data('notLocallyUnique', true) else diff --git a/test/javascript/public/test/validators/uniqueness.js b/test/javascript/public/test/validators/uniqueness.js index 20ecd1d45..7a93c70bc 100644 --- a/test/javascript/public/test/validators/uniqueness.js +++ b/test/javascript/public/test/validators/uniqueness.js @@ -33,7 +33,7 @@ QUnit.module('Uniqueness options', { } }); -QUnit.test('when matching local uniqueness for nested has-many resources', function(assert) { +QUnit.test('when matching local case-insensitive uniqueness for nested has-many resources', function(assert) { dataCsv = { html_settings: { type: 'ActionView::Helpers::FormBuilder', @@ -67,7 +67,46 @@ QUnit.test('when matching local uniqueness for nested has-many resources', funct options = { 'message': "must be unique" }; user_0_email.val('not-locally-unique'); - user_1_email.val('not-locally-unique'); + user_1_email.val('Not-Locally-Unique'); assert.equal(ClientSideValidations.validators.local.uniqueness(user_1_email, options), "must be unique"); }); + +QUnit.test('when matching case-sensitive local uniqueness for nested has-many resources', function(assert) { + dataCsv = { + html_settings: { + type: 'ActionView::Helpers::FormBuilder', + input_tag: '
', + label_tag: '
' + }, + validators: { 'user[email]':{"uniqueness":[{"message": "must be unique"}]}} + } + + $('#qunit-fixture') + .append($('
', { + action: '/users', + 'data-client-side-validations': JSON.stringify(dataCsv), + method: 'post', + id: 'new_user_3' + })) + .find('form') + .append($('', { + name: 'profile[user_attributes][0][email]', + id: 'user_0_email', + })) + .append($('', { + name: 'profile[user_attributes][1][email]', + id: 'user_1_email', + })); + + $('form#new_user_3').validate(); + + var user_0_email = $('#user_0_email'), + user_1_email = $('#user_1_email'), + options = { 'message': "must be unique", "case_sensitive": true }; + + user_0_email.val('locally-unique'); + user_1_email.val('Locally-Unique'); + + assert.equal(ClientSideValidations.validators.local.uniqueness(user_1_email, options), undefined); +}); diff --git a/vendor/assets/javascripts/rails.validations.js b/vendor/assets/javascripts/rails.validations.js index 37351e3e4..b84a5c643 100644 --- a/vendor/assets/javascripts/rails.validations.js +++ b/vendor/assets/javascripts/rails.validations.js @@ -535,8 +535,14 @@ form = element.closest('form'); valid = true; form.find(":input[name^=\"" + name_prefix + "\"][name$=\"" + name_suffix + "\"]").each(function() { + var other_value; + other_value = $(this).val(); + if (!options.case_sensitive) { + value = value.toLowerCase(); + other_value = other_value.toLowerCase(); + } if ($(this).attr('name') !== name) { - if ($(this).val() === value) { + if (other_value === value) { valid = false; return $(this).data('notLocallyUnique', true); } else {