diff options
author | Gonçalo Silva <goncalossilva@gmail.com> | 2011-04-17 17:08:49 +0100 |
---|---|---|
committer | Gonçalo Silva <goncalossilva@gmail.com> | 2011-04-17 17:08:49 +0100 |
commit | 1c2b2233c3a7ec76c0a0eddf5b8be45c489be133 (patch) | |
tree | 56f2b767c3a4f1f14c51606bf2cbb714a98c5f89 /activemodel | |
parent | 8d558cb1b069410c8f693295c9c4e2ffc9661e06 (diff) | |
parent | b6843f22ac42b503f6b8ac00105ca0679049be7d (diff) | |
download | rails-1c2b2233c3a7ec76c0a0eddf5b8be45c489be133.tar.gz rails-1c2b2233c3a7ec76c0a0eddf5b8be45c489be133.tar.bz2 rails-1c2b2233c3a7ec76c0a0eddf5b8be45c489be133.zip |
Merge branch 'master' of https://github.com/rails/rails into performance_test
Diffstat (limited to 'activemodel')
15 files changed, 166 insertions, 38 deletions
diff --git a/activemodel/CHANGELOG b/activemodel/CHANGELOG index 3082c7186a..03287fac7a 100644 --- a/activemodel/CHANGELOG +++ b/activemodel/CHANGELOG @@ -1,5 +1,14 @@ *Rails 3.1.0 (unreleased)* +* Add support for proc or lambda as an option for InclusionValidator, + ExclusionValidator, and FormatValidator [Prem Sichanugrist] + + You can now supply Proc, lambda, or anything that respond to #call in those + validations, and it will be called with current record as an argument. + That given proc or lambda must returns an object which respond to #include? for + InclusionValidator and ExclusionValidator, and returns a regular expression + object for FormatValidator. + * Added ActiveModel::SecurePassword to encapsulate dead-simple password usage with BCrypt encryption and salting [DHH] * ActiveModel::AttributeMethods allows attributes to be defined on demand [Alexander Uvarov] diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index a479795d51..5ede78617a 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -8,7 +8,7 @@ module ActiveModel # Provides a way to track changes in your object in the same way as # Active Record does. # - # The requirements to implement ActiveModel::Dirty are to: + # The requirements for implementing ActiveModel::Dirty are: # # * <tt>include ActiveModel::Dirty</tt> in your object # * Call <tt>define_attribute_methods</tt> passing each method you want to diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 5e3cf510b0..22ca3efa2b 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -278,19 +278,18 @@ module ActiveModel # When using inheritance in your models, it will check all the inherited # models too, but only if the model itself hasn't been found. Say you have # <tt>class Admin < User; end</tt> and you wanted the translation for - # the <tt>:blank</tt> error +message+ for the <tt>title</tt> +attribute+, + # the <tt>:blank</tt> error message for the <tt>title</tt> attribute, # it looks for these translations: # - # <ol> - # <li><tt>activemodel.errors.models.admin.attributes.title.blank</tt></li> - # <li><tt>activemodel.errors.models.admin.blank</tt></li> - # <li><tt>activemodel.errors.models.user.attributes.title.blank</tt></li> - # <li><tt>activemodel.errors.models.user.blank</tt></li> - # <li>any default you provided through the +options+ hash (in the activemodel.errors scope)</li> - # <li><tt>activemodel.errors.messages.blank</tt></li> - # <li><tt>errors.attributes.title.blank</tt></li> - # <li><tt>errors.messages.blank</tt></li> - # </ol> + # * <tt>activemodel.errors.models.admin.attributes.title.blank</tt> + # * <tt>activemodel.errors.models.admin.blank</tt> + # * <tt>activemodel.errors.models.user.attributes.title.blank</tt> + # * <tt>activemodel.errors.models.user.blank</tt> + # * any default you provided through the +options+ hash (in the <tt>activemodel.errors</tt> scope) + # * <tt>activemodel.errors.messages.blank</tt> + # * <tt>errors.attributes.title.blank</tt> + # * <tt>errors.messages.blank</tt> + # def generate_message(attribute, type = :invalid, options = {}) type = options.delete(:message) if options[:message].is_a?(Symbol) diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 957d0ddaaa..ee94ad66cf 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -31,11 +31,10 @@ module ActiveModel # User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user def has_secure_password attr_reader :password - attr_accessor :password_confirmation validates_confirmation_of :password validates_presence_of :password_digest - + include InstanceMethodsOnActivation if respond_to?(:attributes_protected_by_default) @@ -59,7 +58,9 @@ module ActiveModel # Encrypts the password into the password_digest attribute. def password=(unencrypted_password) @password = unencrypted_password - self.password_digest = BCrypt::Password.create(unencrypted_password) + unless unencrypted_password.blank? + self.password_digest = BCrypt::Password.create(unencrypted_password) + end end end end diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb index e38e565d09..a85c23f725 100644 --- a/activemodel/lib/active_model/validations/exclusion.rb +++ b/activemodel/lib/active_model/validations/exclusion.rb @@ -1,18 +1,35 @@ +require 'active_support/core_ext/range.rb' + module ActiveModel # == Active Model Exclusion Validator module Validations class ExclusionValidator < EachValidator + ERROR_MESSAGE = "An object with the method #include? or a proc or lambda is required, " << + "and must be supplied as the :in option of the configuration hash" + def check_validity! - raise ArgumentError, "An object with the method include? is required must be supplied as the " << - ":in option of the configuration hash" unless options[:in].respond_to?(:include?) + unless [:include?, :call].any? { |method| options[:in].respond_to?(method) } + raise ArgumentError, ERROR_MESSAGE + end end def validate_each(record, attribute, value) - if options[:in].include?(value) + delimiter = options[:in] + exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter + if exclusions.send(inclusion_method(exclusions), value) record.errors.add(attribute, :exclusion, options.except(:in).merge!(:value => value)) end end + + private + + # In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible values in the + # range for equality, so it may be slow for large ranges. The new <tt>Range#cover?</tt> + # uses the previous logic of comparing a value with the range endpoints. + def inclusion_method(enumerable) + enumerable.is_a?(Range) ? :cover? : :include? + end end module HelperMethods @@ -22,10 +39,14 @@ module ActiveModel # validates_exclusion_of :username, :in => %w( admin superuser ), :message => "You don't belong here" # validates_exclusion_of :age, :in => 30..60, :message => "This site is only for under 30 and over 60" # validates_exclusion_of :format, :in => %w( mov avi ), :message => "extension %{value} is not allowed" + # validates_exclusion_of :password, :in => lambda { |p| [p.username, p.first_name] }, :message => "should not be the same as your username or first name" # end # # Configuration options: # * <tt>:in</tt> - An enumerable object of items that the value shouldn't be part of. + # This can be supplied as a proc or lambda which returns an enumerable. If the enumerable + # is a range the test is performed with <tt>Range#cover?</tt> + # (backported in Active Support for 1.8), otherwise with <tt>include?</tt>. # * <tt>:message</tt> - Specifies a custom error message (default is: "is reserved"). # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb index 541f53a834..6f23d492eb 100644 --- a/activemodel/lib/active_model/validations/format.rb +++ b/activemodel/lib/active_model/validations/format.rb @@ -4,10 +4,12 @@ module ActiveModel module Validations class FormatValidator < EachValidator def validate_each(record, attribute, value) - if options[:with] && value.to_s !~ options[:with] - record.errors.add(attribute, :invalid, options.except(:with).merge!(:value => value)) - elsif options[:without] && value.to_s =~ options[:without] - record.errors.add(attribute, :invalid, options.except(:without).merge!(:value => value)) + if options[:with] + regexp = option_call(record, :with) + record_error(record, attribute, :with, value) if value.to_s !~ regexp + elsif options[:without] + regexp = option_call(record, :without) + record_error(record, attribute, :without, value) if value.to_s =~ regexp end end @@ -16,12 +18,25 @@ module ActiveModel raise ArgumentError, "Either :with or :without must be supplied (but not both)" end - if options[:with] && !options[:with].is_a?(Regexp) - raise ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash" - end + check_options_validity(options, :with) + check_options_validity(options, :without) + end + + private - if options[:without] && !options[:without].is_a?(Regexp) - raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash" + def option_call(record, name) + option = options[name] + option.respond_to?(:call) ? option.call(record) : option + end + + def record_error(record, attribute, name, value) + record.errors.add(attribute, :invalid, options.except(name).merge!(:value => value)) + end + + def check_options_validity(options, name) + option = options[name] + if option && !option.is_a?(Regexp) && !option.respond_to?(:call) + raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}" end end end @@ -40,17 +55,26 @@ module ActiveModel # validates_format_of :email, :without => /NOSPAM/ # end # + # You can also provide a proc or lambda which will determine the regular expression that will be used to validate the attribute + # + # class Person < ActiveRecord::Base + # # Admin can have number as a first letter in their screen name + # validates_format_of :screen_name, :with => lambda{ |person| person.admin? ? /\A[a-z0-9][a-z0-9_\-]*\Z/i : /\A[a-z][a-z0-9_\-]*\Z/i } + # end + # # Note: use <tt>\A</tt> and <tt>\Z</tt> to match the start and end of the string, <tt>^</tt> and <tt>$</tt> match the start/end of a line. # - # You must pass either <tt>:with</tt> or <tt>:without</tt> as an option. In addition, both must be a regular expression, - # or else an exception will be raised. + # You must pass either <tt>:with</tt> or <tt>:without</tt> as an option. In addition, both must be a regular expression + # or a proc or lambda, or else an exception will be raised. # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "is invalid"). # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). # * <tt>:allow_blank</tt> - If set to true, skips this validation if the attribute is blank (default is +false+). # * <tt>:with</tt> - Regular expression that if the attribute matches will result in a successful validation. + # This can be provided as a proc or lambda returning regular expression which will be called at runtime. # * <tt>:without</tt> - Regular expression that if the attribute does not match will result in a successful validation. + # This can be provided as a proc or lambda returning regular expression which will be called at runtime. # * <tt>:on</tt> - Specifies when this validation is active. Runs in all # validation contexts by default (+nil+), other options are <tt>:create</tt> # and <tt>:update</tt>. diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 92ac940f36..d32aebeb88 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -5,20 +5,30 @@ module ActiveModel # == Active Model Inclusion Validator module Validations class InclusionValidator < EachValidator + ERROR_MESSAGE = "An object with the method #include? or a proc or lambda is required, " << + "and must be supplied as the :in option of the configuration hash" + def check_validity! - raise ArgumentError, "An object with the method include? is required must be supplied as the " << - ":in option of the configuration hash" unless options[:in].respond_to?(:include?) + unless [:include?, :call].any?{ |method| options[:in].respond_to?(method) } + raise ArgumentError, ERROR_MESSAGE + end end def validate_each(record, attribute, value) - record.errors.add(attribute, :inclusion, options.except(:in).merge!(:value => value)) unless options[:in].send(include?, value) + delimiter = options[:in] + exclusions = delimiter.respond_to?(:call) ? delimiter.call(record) : delimiter + unless exclusions.send(inclusion_method(exclusions), value) + record.errors.add(attribute, :inclusion, options.except(:in).merge!(:value => value)) + end end + private + # In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible values in the # range for equality, so it may be slow for large ranges. The new <tt>Range#cover?</tt> # uses the previous logic of comparing a value with the range endpoints. - def include? - options[:in].is_a?(Range) ? :cover? : :include? + def inclusion_method(enumerable) + enumerable.is_a?(Range) ? :cover? : :include? end end @@ -29,11 +39,13 @@ module ActiveModel # validates_inclusion_of :gender, :in => %w( m f ) # validates_inclusion_of :age, :in => 0..99 # validates_inclusion_of :format, :in => %w( jpg gif png ), :message => "extension %{value} is not included in the list" + # validates_inclusion_of :states, :in => lambda{ |person| STATES[person.country] } # end # # Configuration options: - # * <tt>:in</tt> - An enumerable object of available items. - # If the enumerable is a range the test is performed with <tt>Range#cover?</tt> + # * <tt>:in</tt> - An enumerable object of available items. This can be + # supplied as a proc or lambda which returns an enumerable. If the enumerable + # is a range the test is performed with <tt>Range#cover?</tt> # (backported in Active Support for 1.8), otherwise with <tt>include?</tt>. # * <tt>:message</tt> - Specifies a custom error message (default is: "is not included in the list"). # * <tt>:allow_nil</tt> - If set to true, skips this validation if the attribute is +nil+ (default is +false+). diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb index c5ed8d22d3..5304743389 100644 --- a/activemodel/lib/active_model/validator.rb +++ b/activemodel/lib/active_model/validator.rb @@ -1,6 +1,7 @@ require 'active_support/core_ext/array/wrap' require "active_support/core_ext/module/anonymous" require 'active_support/core_ext/object/blank' +require 'active_support/core_ext/object/inclusion' module ActiveModel #:nodoc: @@ -67,7 +68,7 @@ module ActiveModel #:nodoc: # # class TitleValidator < ActiveModel::EachValidator # def validate_each(record, attribute, value) - # record.errors[attribute] << 'must be Mr. Mrs. or Dr.' unless ['Mr.', 'Mrs.', 'Dr.'].include?(value) + # record.errors[attribute] << 'must be Mr. Mrs. or Dr.' unless value.in?(['Mr.', 'Mrs.', 'Dr.']) # end # end # diff --git a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb index 015153ec7c..9a73a5ad91 100644 --- a/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb +++ b/activemodel/test/cases/mass_assignment_security/sanitizer_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require 'logger' +require 'active_support/core_ext/object/inclusion' class SanitizerTest < ActiveModel::TestCase @@ -9,7 +10,7 @@ class SanitizerTest < ActiveModel::TestCase attr_accessor :logger def deny?(key) - [ 'admin' ].include?(key) + key.in?(['admin']) end end diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index 4a47a7a226..c455cf57b3 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -9,6 +9,18 @@ class SecurePasswordTest < ActiveModel::TestCase @user = User.new end + test "blank password" do + user = User.new + user.password = '' + assert !user.valid?, 'user should be invalid' + end + + test "nil password" do + user = User.new + user.password = nil + assert !user.valid?, 'user should be invalid' + end + test "password must be present" do assert !@user.valid? assert_equal 1, @user.errors.size diff --git a/activemodel/test/cases/serializeration/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb index 500a5c575f..500a5c575f 100644 --- a/activemodel/test/cases/serializeration/json_serialization_test.rb +++ b/activemodel/test/cases/serializers/json_serialization_test.rb diff --git a/activemodel/test/cases/serializeration/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb index b6a2f88667..b6a2f88667 100644 --- a/activemodel/test/cases/serializeration/xml_serialization_test.rb +++ b/activemodel/test/cases/serializers/xml_serialization_test.rb diff --git a/activemodel/test/cases/validations/exclusion_validation_test.rb b/activemodel/test/cases/validations/exclusion_validation_test.rb index be9d98d644..72a383f128 100644 --- a/activemodel/test/cases/validations/exclusion_validation_test.rb +++ b/activemodel/test/cases/validations/exclusion_validation_test.rb @@ -42,4 +42,16 @@ class ExclusionValidationTest < ActiveModel::TestCase ensure Person.reset_callbacks(:validate) end + + def test_validates_exclusion_of_with_lambda + Topic.validates_exclusion_of :title, :in => lambda{ |topic| topic.author_name == "sikachu" ? %w( monkey elephant ) : %w( abe wasabi ) } + + p = Topic.new + p.title = "elephant" + p.author_name = "sikachu" + assert p.invalid? + + p.title = "wasabi" + assert p.valid? + end end diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb index 6c4fb36d52..73647efea5 100644 --- a/activemodel/test/cases/validations/format_validation_test.rb +++ b/activemodel/test/cases/validations/format_validation_test.rb @@ -98,6 +98,30 @@ class PresenceValidationTest < ActiveModel::TestCase assert_raise(ArgumentError) { Topic.validates_format_of(:title, :without => "clearly not a regexp") } end + def test_validates_format_of_with_lambda + Topic.validates_format_of :content, :with => lambda{ |topic| topic.title == "digit" ? /\A\d+\Z/ : /\A\S+\Z/ } + + p = Topic.new + p.title = "digit" + p.content = "Pixies" + assert p.invalid? + + p.content = "1234" + assert p.valid? + end + + def test_validates_format_of_without_lambda + Topic.validates_format_of :content, :without => lambda{ |topic| topic.title == "characters" ? /\A\d+\Z/ : /\A\S+\Z/ } + + p = Topic.new + p.title = "characters" + p.content = "1234" + assert p.invalid? + + p.content = "Pixies" + assert p.valid? + end + def test_validates_format_of_for_ruby_class Person.validates_format_of :karma, :with => /\A\d+\Z/ diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb index 62f2ec785d..92c473de0e 100644 --- a/activemodel/test/cases/validations/inclusion_validation_test.rb +++ b/activemodel/test/cases/validations/inclusion_validation_test.rb @@ -74,4 +74,16 @@ class InclusionValidationTest < ActiveModel::TestCase ensure Person.reset_callbacks(:validate) end + + def test_validates_inclusion_of_with_lambda + Topic.validates_inclusion_of :title, :in => lambda{ |topic| topic.author_name == "sikachu" ? %w( monkey elephant ) : %w( abe wasabi ) } + + p = Topic.new + p.title = "wasabi" + p.author_name = "sikachu" + assert p.invalid? + + p.title = "elephant" + assert p.valid? + end end |