Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

Ticket #10876: extract_validations.patch

File extract_validations.patch, 49.1 kB (added by lifofifo, 2 years ago)
  • a/activerecord/lib/active_record/validations.rb

    old new  
    1414    end 
    1515  end 
    1616 
    17   # Active Record validation is reported to and from this object, which is used by Base#save to 
    18   # determine whether the object is in a valid state to be saved. See usage example in Validations. 
    19   class Errors 
    20     include Enumerable 
    21  
    22     def initialize(base) # :nodoc: 
    23       @base, @errors = base, {} 
    24     end 
    25  
    26     @@default_error_messages = { 
    27       :inclusion => "is not included in the list", 
    28       :exclusion => "is reserved", 
    29       :invalid => "is invalid", 
    30       :confirmation => "doesn't match confirmation", 
    31       :accepted  => "must be accepted", 
    32       :empty => "can't be empty", 
    33       :blank => "can't be blank", 
    34       :too_long => "is too long (maximum is %d characters)", 
    35       :too_short => "is too short (minimum is %d characters)", 
    36       :wrong_length => "is the wrong length (should be %d characters)", 
    37       :taken => "has already been taken", 
    38       :not_a_number => "is not a number", 
    39       :greater_than => "must be greater than %d", 
    40       :greater_than_or_equal_to => "must be greater than or equal to %d", 
    41       :equal_to => "must be equal to %d", 
    42       :less_than => "must be less than %d", 
    43       :less_than_or_equal_to => "must be less than or equal to %d", 
    44       :odd => "must be odd", 
    45       :even => "must be even" 
    46     } 
    47  
    48     # Holds a hash with all the default error messages that can be replaced by your own copy or localizations. 
    49     cattr_accessor :default_error_messages 
    50  
    51  
    52     # Adds an error to the base object instead of any particular attribute. This is used 
    53     # to report errors that don't tie to any specific attribute, but rather to the object 
    54     # as a whole. These error messages don't get prepended with any field name when iterating 
    55     # with each_full, so they should be complete sentences. 
    56     def add_to_base(msg) 
    57       add(:base, msg) 
    58     end 
    59  
    60     # Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt> 
    61     # for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one 
    62     # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>. 
    63     # If no +msg+ is supplied, "invalid" is assumed. 
    64     def add(attribute, msg = @@default_error_messages[:invalid]) 
    65       @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil? 
    66       @errors[attribute.to_s] << msg 
    67     end 
    68  
    69     # Will add an error message to each of the attributes in +attributes+ that is empty. 
    70     def add_on_empty(attributes, msg = @@default_error_messages[:empty]) 
    71       for attr in [attributes].flatten 
    72         value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] 
    73         is_empty = value.respond_to?("empty?") ? value.empty? : false 
    74         add(attr, msg) unless !value.nil? && !is_empty 
    75       end 
    76     end 
    77  
    78     # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?). 
    79     def add_on_blank(attributes, msg = @@default_error_messages[:blank]) 
    80       for attr in [attributes].flatten 
    81         value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] 
    82         add(attr, msg) if value.blank? 
    83       end 
    84     end 
    85  
    86     # Returns true if the specified +attribute+ has errors associated with it. 
    87     # 
    88     #   class Company < ActiveRecord::Base 
    89     #     validates_presence_of :name, :address, :email 
    90     #     validates_length_of :name, :in => 5..30 
    91     #   end 
    92     # 
    93     #   company = Company.create(:address => '123 First St.') 
    94     #   company.errors.invalid?(:name)      # => true 
    95     #   company.errors.invalid?(:address)   # => false 
    96     def invalid?(attribute) 
    97       !@errors[attribute.to_s].nil? 
    98     end 
    99  
    100     # Returns nil, if no errors are associated with the specified +attribute+. 
    101     # Returns the error message, if one error is associated with the specified +attribute+. 
    102     # Returns an array of error messages, if more than one error is associated with the specified +attribute+. 
    103     # 
    104     #   class Company < ActiveRecord::Base 
    105     #     validates_presence_of :name, :address, :email 
    106     #     validates_length_of :name, :in => 5..30 
    107     #   end 
    108     # 
    109     #   company = Company.create(:address => '123 First St.') 
    110     #   company.errors.on(:name)      # => ["is too short (minimum is 5 characters)", "can't be blank"] 
    111     #   company.errors.on(:email)     # => "can't be blank" 
    112     #   company.errors.on(:address)   # => nil 
    113     def on(attribute) 
    114       errors = @errors[attribute.to_s] 
    115       return nil if errors.nil? 
    116       errors.size == 1 ? errors.first : errors 
    117     end 
    118  
    119     alias :[] :on 
    120  
    121     # Returns errors assigned to the base object through add_to_base according to the normal rules of on(attribute). 
    122     def on_base 
    123       on(:base) 
    124     end 
    125  
    126     # Yields each attribute and associated message per error added. 
    127     # 
    128     #   class Company < ActiveRecord::Base 
    129     #     validates_presence_of :name, :address, :email 
    130     #     validates_length_of :name, :in => 5..30 
    131     #   end 
    132     # 
    133     #   company = Company.create(:address => '123 First St.') 
    134     #   company.errors.each{|attr,msg| puts "#{attr} - #{msg}" } # => 
    135     #     name - is too short (minimum is 5 characters) 
    136     #     name - can't be blank 
    137     #     address - can't be blank 
    138     def each 
    139       @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } } 
    140     end 
    141  
    142     # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned 
    143     # through iteration as "First name can't be empty". 
    144     # 
    145     #   class Company < ActiveRecord::Base 
    146     #     validates_presence_of :name, :address, :email 
    147     #     validates_length_of :name, :in => 5..30 
    148     #   end 
    149     # 
    150     #   company = Company.create(:address => '123 First St.') 
    151     #   company.errors.each_full{|msg| puts msg } # => 
    152     #     Name is too short (minimum is 5 characters) 
    153     #     Name can't be blank 
    154     #     Address can't be blank 
    155     def each_full 
    156       full_messages.each { |msg| yield msg } 
    157     end 
    158  
    159     # Returns all the full error messages in an array. 
    160     # 
    161     #   class Company < ActiveRecord::Base 
    162     #     validates_presence_of :name, :address, :email 
    163     #     validates_length_of :name, :in => 5..30 
    164     #   end 
    165     # 
    166     #   company = Company.create(:address => '123 First St.') 
    167     #   company.errors.full_messages # => 
    168     #     ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"] 
    169     def full_messages 
    170       full_messages = [] 
    171  
    172       @errors.each_key do |attr| 
    173         @errors[attr].each do |msg| 
    174           next if msg.nil? 
    175  
    176           if attr == "base" 
    177             full_messages << msg 
    178           else 
    179             full_messages << @base.class.human_attribute_name(attr) + " " + msg 
    180           end 
    181         end 
    182       end 
    183       full_messages 
    184     end 
    185  
    186     # Returns true if no errors have been added. 
    187     def empty? 
    188       @errors.empty? 
    189     end 
    190  
    191     # Removes all errors that have been added. 
    192     def clear 
    193       @errors = {} 
    194     end 
    195  
    196     # Returns the total number of errors added. Two errors added to the same attribute will be counted as such. 
    197     def size 
    198       @errors.values.inject(0) { |error_count, attribute| error_count + attribute.size } 
    199     end 
    200  
    201     alias_method :count, :size 
    202     alias_method :length, :size 
    203  
    204     # Return an XML representation of this error object. 
    205     # 
    206     #   class Company < ActiveRecord::Base 
    207     #     validates_presence_of :name, :address, :email 
    208     #     validates_length_of :name, :in => 5..30 
    209     #   end 
    210     # 
    211     #   company = Company.create(:address => '123 First St.') 
    212     #   company.errors.to_xml # => 
    213     #     <?xml version="1.0" encoding="UTF-8"?> 
    214     #     <errors> 
    215     #       <error>Name is too short (minimum is 5 characters)</error> 
    216     #       <error>Name can't be blank</error> 
    217     #       <error>Address can't be blank</error> 
    218     #     </errors> 
    219     def to_xml(options={}) 
    220       options[:root] ||= "errors" 
    221       options[:indent] ||= 2 
    222       options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent]) 
    223  
    224       options[:builder].instruct! unless options.delete(:skip_instruct) 
    225       options[:builder].errors do |e| 
    226         full_messages.each { |msg| e.error(msg) } 
    227       end 
    228     end 
    229   end 
    230  
    231  
    23217  # Active Records implement validation by overwriting Base#validate (or the variations, +validate_on_create+ and 
    23318  # +validate_on_update+). Each of these methods can inspect the state of the object, which usually means ensuring 
    23419  # that a number of attributes have a certain value (such as not empty, within a given range, matching a certain regular expression). 
     
    27055  # 
    27156  # Please do have a look at ActiveRecord::Validations::ClassMethods for a higher level of validations. 
    27257  module Validations 
    273     VALIDATIONS = %w( validate validate_on_create validate_on_update ) 
    274  
     58     
    27559    def self.included(base) # :nodoc: 
     60      base.send :include, ActiveSupport::Validations 
     61      base.default_validation_options[:on] = :save 
     62       
    27663      base.extend ClassMethods 
     64       
    27765      base.class_eval do 
    27866        alias_method_chain :save, :validation 
    27967        alias_method_chain :save!, :validation 
    28068        alias_method_chain :update_attribute, :validation_skipping 
    28169      end 
    28270 
    283       base.send :include, ActiveSupport::Callbacks 
    284  
    28571      # TODO: Use helper ActiveSupport::Callbacks#define_callbacks instead 
    286       %w( validate validate_on_create validate_on_update ).each do |validation_method| 
     72      %w( validate_on_create validate_on_update ).each do |validation_method| 
    28773        base.class_eval <<-"end_eval" 
    28874          def self.#{validation_method}(*methods, &block) 
    28975            options = methods.extract_options! 
     
    30490    # They offer a more declarative way of specifying when the model is valid and when it is not. It is recommended to use 
    30591    # these over the low-level calls to validate and validate_on_create when possible. 
    30692    module ClassMethods 
    307       DEFAULT_VALIDATION_OPTIONS = { 
    308         :on => :save, 
    309         :allow_nil => false, 
    310         :allow_blank => false, 
    311         :message => nil 
    312       }.freeze 
    313  
    314       ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze 
    315       ALL_NUMERICALITY_CHECKS = { :greater_than => '>', :greater_than_or_equal_to => '>=', 
    316                                   :equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=', 
    317                                   :odd => 'odd?', :even => 'even?' }.freeze 
    318  
    31993      # Adds a validation method or block to the class. This is useful when 
    32094      # overriding the #validate instance method becomes too unwieldly and 
    32195      # you're looking for more descriptive declaration of your validations. 
     
    363137      #   not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }).  The 
    364138      #   method, proc or string should return or evaluate to a true or false value. 
    365139      def validates_each(*attrs) 
    366         options = attrs.extract_options!.symbolize_keys 
    367         attrs   = attrs.flatten 
    368  
    369         # Declare the validation. 
    370         send(validation_method(options[:on] || :save), options) do |record| 
    371           attrs.each do |attr| 
    372             value = record.send(attr) 
    373             next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) 
    374             yield record, attr, value 
    375           end 
    376         end 
     140        super(*attrs) 
    377141      end 
    378142 
    379143      # Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example: 
     
    405169      #   not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }).  The 
    406170      #   method, proc or string should return or evaluate to a true or false value.       
    407171      def validates_confirmation_of(*attr_names) 
    408         configuration = { :message => ActiveRecord::Errors.default_error_messages[:confirmation], :on => :save } 
    409         configuration.update(attr_names.extract_options!) 
    410  
    411         attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" })) 
    412  
    413         validates_each(attr_names, configuration) do |record, attr_name, value| 
    414           record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation") 
    415         end 
     172        super(*attr_names) 
    416173      end 
    417174 
    418175      # Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example: 
     
    439196      #   not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }).  The 
    440197      #   method, proc or string should return or evaluate to a true or false value.       
    441198      def validates_acceptance_of(*attr_names) 
    442         configuration = { :message => ActiveRecord::Errors.default_error_messages[:accepted], :on => :save, :allow_nil => true, :accept => "1" } 
     199        configuration = { :message => ActiveSupport::Validations::Errors.default_error_messages[:accepted],  
     200                          :on => :save, :allow_nil => true, :accept => "1" } 
    443201        configuration.update(attr_names.extract_options!) 
    444  
     202         
    445203        db_cols = begin 
    446204          column_names 
    447205        rescue ActiveRecord::StatementInvalid 
     
    450208        names = attr_names.reject { |name| db_cols.include?(name.to_s) } 
    451209        attr_accessor(*names) 
    452210 
    453         validates_each(attr_names,configuration) do |record, attr_name, value| 
     211        validates_each(attr_names, configuration) do |record, attr_name, value| 
    454212          record.errors.add(attr_name, configuration[:message]) unless value == configuration[:accept] 
    455213        end 
    456214      end 
     
    489247      # failures on saves when both the parent object and the child object are 
    490248      # new. 
    491249      def validates_presence_of(*attr_names) 
    492         configuration = { :message => ActiveRecord::Errors.default_error_messages[:blank], :on => :save } 
    493         configuration.update(attr_names.extract_options!) 
    494  
    495         # can't use validates_each here, because it cannot cope with nonexistent attributes, 
    496         # while errors.add_on_empty can 
    497         send(validation_method(configuration[:on]), configuration) do |record| 
    498           record.errors.add_on_blank(attr_names, configuration[:message]) 
    499         end 
     250        super(*attr_names) 
    500251      end 
    501252 
    502253      # Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time: 
     
    532283      #   not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }).  The 
    533284      #   method, proc or string should return or evaluate to a true or false value.       
    534285      def validates_length_of(*attrs) 
    535         # Merge given options with defaults. 
    536         options = { 
    537           :too_long     => ActiveRecord::Errors.default_error_messages[:too_long], 
    538           :too_short    => ActiveRecord::Errors.default_error_messages[:too_short], 
    539           :wrong_length => ActiveRecord::Errors.default_error_messages[:wrong_length] 
    540         }.merge(DEFAULT_VALIDATION_OPTIONS) 
    541         options.update(attrs.extract_options!.symbolize_keys) 
    542  
    543         # Ensure that one and only one range option is specified. 
    544         range_options = ALL_RANGE_OPTIONS & options.keys 
    545         case range_options.size 
    546           when 0 
    547             raise ArgumentError, 'Range unspecified.  Specify the :within, :maximum, :minimum, or :is option.' 
    548           when 1 
    549             # Valid number of options; do nothing. 
    550           else 
    551             raise ArgumentError, 'Too many range options specified.  Choose only one.' 
    552         end 
    553  
    554         # Get range option and value. 
    555         option = range_options.first 
    556         option_value = options[range_options.first] 
    557  
    558         case option 
    559           when :within, :in 
    560             raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range) 
    561  
    562             too_short = options[:too_short] % option_value.begin 
    563             too_long  = options[:too_long]  % option_value.end 
    564  
    565             validates_each(attrs, options) do |record, attr, value| 
    566               if value.nil? or value.split(//).size < option_value.begin 
    567                 record.errors.add(attr, too_short) 
    568               elsif value.split(//).size > option_value.end 
    569                 record.errors.add(attr, too_long) 
    570               end 
    571             end 
    572           when :is, :minimum, :maximum 
    573             raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0 
    574  
    575             # Declare different validations per option. 
    576             validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" } 
    577             message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long } 
    578  
    579             message = (options[:message] || options[message_options[option]]) % option_value 
    580  
    581             validates_each(attrs, options) do |record, attr, value| 
    582               if value.kind_of?(String) 
    583                 record.errors.add(attr, message) unless !value.nil? and value.split(//).size.method(validity_checks[option])[option_value] 
    584               else 
    585                 record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value] 
    586               end 
    587             end 
    588         end 
     286        super(*attrs) 
    589287      end 
    590  
    591       alias_method :validates_size_of, :validates_length_of 
    592  
    593  
     288       
     289      def validates_size_of(*attrs) 
     290        super(*attrs) 
     291      end 
     292       
    594293      # Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user 
    595294      # can be named "davidhh". 
    596295      # 
     
    625324      #   not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }).  The 
    626325      #   method, proc or string should return or evaluate to a true or false value. 
    627326      def validates_uniqueness_of(*attr_names) 
    628         configuration = { :message => ActiveRecord::Errors.default_error_messages[:taken], :case_sensitive => true } 
     327        configuration = { :message => ActiveSupport::Validations::Errors.default_error_messages[:taken], :case_sensitive => true } 
    629328        configuration.update(attr_names.extract_options!) 
    630329 
    631         validates_each(attr_names,configuration) do |record, attr_name, value| 
     330        validates_each(attr_names, configuration) do |record, attr_name, value| 
    632331          if value.nil? || (configuration[:case_sensitive] || !columns_hash[attr_name.to_s].text?) 
    633332            condition_sql = "#{record.class.quoted_table_name}.#{attr_name} #{attribute_condition(value)}" 
    634333            condition_params = [value] 
     
    693392      #   not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }).  The 
    694393      #   method, proc or string should return or evaluate to a true or false value. 
    695394      def validates_format_of(*attr_names) 
    696         configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save, :with => nil } 
    697         configuration.update(attr_names.extract_options!) 
    698  
    699         raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") unless configuration[:with].is_a?(Regexp) 
    700  
    701         validates_each(attr_names, configuration) do |record, attr_name, value| 
    702           record.errors.add(attr_name, configuration[:message]) unless value.to_s =~ configuration[:with] 
    703         end 
     395        super(*attr_names) 
    704396      end 
    705397 
    706398      # Validates whether the value of the specified attribute is available in a particular enumerable object. 
     
    723415      #   not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }).  The 
    724416      #   method, proc or string should return or evaluate to a true or false value. 
    725417      def validates_inclusion_of(*attr_names) 
    726         configuration = { :message => ActiveRecord::Errors.default_error_messages[:inclusion], :on => :save } 
    727         configuration.update(attr_names.extract_options!) 
    728  
    729         enum = configuration[:in] || configuration[:within] 
    730  
    731         raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?") 
    732  
    733         validates_each(attr_names, configuration) do |record, attr_name, value| 
    734           record.errors.add(attr_name, configuration[:message] % value) unless enum.include?(value) 
    735         end 
     418        super(*attr_names) 
    736419      end 
    737420 
    738421      # Validates that the value of the specified attribute is not in a particular enumerable object. 
     
    755438      #   not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }).  The 
    756439      #   method, proc or string should return or evaluate to a true or false value. 
    757440      def validates_exclusion_of(*attr_names) 
    758         configuration = { :message => ActiveRecord::Errors.default_error_messages[:exclusion], :on => :save } 
    759         configuration.update(attr_names.extract_options!) 
    760  
    761         enum = configuration[:in] || configuration[:within] 
    762  
    763         raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?") 
    764  
    765         validates_each(attr_names, configuration) do |record, attr_name, value| 
    766           record.errors.add(attr_name, configuration[:message] % value) if enum.include?(value) 
    767         end 
     441        super(*attr_names) 
    768442      end 
    769443 
    770444      # Validates whether the associated object or objects are all valid themselves. Works with any kind of association. 
     
    799473      #   not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }).  The 
    800474      #   method, proc or string should return or evaluate to a true or false value. 
    801475      def validates_associated(*attr_names) 
    802         configuration = { :message => ActiveRecord::Errors.default_error_messages[:invalid], :on => :save } 
     476        configuration = { :message => ActiveSupport::Validations::Errors.default_error_messages[:invalid], :on => :save } 
    803477        configuration.update(attr_names.extract_options!) 
    804  
     478       
    805479        validates_each(attr_names, configuration) do |record, attr_name, value| 
    806480          record.errors.add(attr_name, configuration[:message]) unless 
    807481            (value.is_a?(Array) ? value : [value]).inject(true) { |v, r| (r.nil? || r.valid?) && v } 
     
    835509      #   not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }).  The 
    836510      #   method, proc or string should return or evaluate to a true or false value. 
    837511      def validates_numericality_of(*attr_names) 
    838         configuration = { :on => :save, :only_integer => false, :allow_nil => false } 
    839         configuration.update(attr_names.extract_options!) 
    840  
    841  
    842         numericality_options = ALL_NUMERICALITY_CHECKS.keys & configuration.keys 
    843  
    844         (numericality_options - [ :odd, :even ]).each do |option| 
    845           raise ArgumentError, ":#{option} must be a number" unless configuration[option].is_a?(Numeric) 
    846         end 
    847  
    848         validates_each(attr_names,configuration) do |record, attr_name, value| 
    849           raw_value = record.send("#{attr_name}_before_type_cast") || value 
    850  
    851           next if configuration[:allow_nil] and raw_value.nil? 
    852  
    853           if configuration[:only_integer] 
    854             unless raw_value.to_s =~ /\A[+-]?\d+\Z/ 
    855               record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number]) 
    856               next 
    857             end 
    858             raw_value = raw_value.to_i 
    859           else 
    860            begin 
    861               raw_value = Kernel.Float(raw_value.to_s) 
    862             rescue ArgumentError, TypeError 
    863               record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[:not_a_number]) 
    864               next 
    865             end 
    866           end 
    867  
    868           numericality_options.each do |option| 
    869             case option 
    870               when :odd, :even 
    871                 record.errors.add(attr_name, configuration[:message] || ActiveRecord::Errors.default_error_messages[option]) unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[] 
    872               else 
    873                 record.errors.add(attr_name, configuration[:message] || (ActiveRecord::Errors.default_error_messages[option] % configuration[option])) unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]] 
    874             end 
    875           end 
    876         end 
     512        super(*attr_names) 
    877513      end 
    878514 
    879515      # Creates an object just like Base.create but calls save! instead of save 
     
    889525      end 
    890526 
    891527      private 
    892         def validation_method(on
    893           case on 
     528        def validation_method(options
     529          case options[:on] || :save 
    894530            when :save   then :validate 
    895531            when :create then :validate_on_create 
    896532            when :update then :validate_on_update 
     
    929565    # Runs validate and validate_on_create or validate_on_update and returns true if no errors were added otherwise false. 
    930566    def valid? 
    931567      errors.clear 
    932  
    933568      run_callbacks(:validate) 
    934569      validate 
    935570 
     
    944579      errors.empty? 
    945580    end 
    946581 
    947     # Returns the Errors object that holds all information about attribute error messages. 
    948     def errors 
    949       @errors ||= Errors.new(self) 
    950     end 
    951  
    952582    protected 
    953       # Overwrite this method for validation checks on all saves and use Errors.add(field, msg) for invalid attributes. 
    954       def validate #:doc: 
    955       end 
     583     
     584    # Overwrite this method for validation checks on all saves and use Errors.add(field, msg) for invalid attributes. 
     585    def validate #:doc: 
     586    end 
    956587 
    957       # Overwrite this method for validation checks used only on creation. 
    958       def validate_on_create #:doc: 
    959       end 
     588    # Overwrite this method for validation checks used only on creation. 
     589    def validate_on_create #:doc: 
     590    end 
    960591 
    961       # Overwrite this method for validation checks used only on updates. 
    962       def validate_on_update # :doc: 
    963       end 
     592    # Overwrite this method for validation checks used only on updates. 
     593    def validate_on_update # :doc: 
     594    end 
    964595  end 
    965596end 
  • a/activerecord/test/cases/validations_test.rb

    old new  
    781781  end 
    782782 
    783783  def test_validates_length_with_globally_modified_error_message 
    784     ActiveRecord::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre %d' 
     784    ActiveSupport::Validations::Errors.default_error_messages[:too_short] = 'tu est trops petit hombre %d' 
    785785    Topic.validates_length_of :title, :minimum => 10 
    786786    t = Topic.create(:title => 'too short') 
    787787    assert !t.valid? 
  • a/activesupport/lib/active_support.rb

    old new  
    5050require 'active_support/multibyte' 
    5151 
    5252require 'active_support/base64' 
     53require 'active_support/validations' 
  • /dev/null

    old new  
     1module ActiveSupport 
     2  module Validations     
     3    class Errors 
     4      include Enumerable 
     5 
     6      def initialize(base) # :nodoc: 
     7        @base, @errors = base, {} 
     8      end 
     9 
     10      @@default_error_messages = { 
     11        :inclusion => "is not included in the list", 
     12        :exclusion => "is reserved", 
     13        :invalid => "is invalid", 
     14        :confirmation => "doesn't match confirmation", 
     15        :accepted  => "must be accepted", 
     16        :empty => "can't be empty", 
     17        :blank => "can't be blank", 
     18        :too_long => "is too long (maximum is %d characters)", 
     19        :too_short => "is too short (minimum is %d characters)", 
     20        :wrong_length => "is the wrong length (should be %d characters)", 
     21        :taken => "has already been taken", 
     22        :not_a_number => "is not a number", 
     23        :greater_than => "must be greater than %d", 
     24        :greater_than_or_equal_to => "must be greater than or equal to %d", 
     25        :equal_to => "must be equal to %d", 
     26        :less_than => "must be less than %d", 
     27        :less_than_or_equal_to => "must be less than or equal to %d", 
     28        :odd => "must be odd", 
     29        :even => "must be even" 
     30      } 
     31 
     32      # Holds a hash with all the default error messages that can be replaced by your own copy or localizations. 
     33      cattr_accessor :default_error_messages 
     34 
     35      # Adds an error to the base object instead of any particular attribute. This is used 
     36      # to report errors that don't tie to any specific attribute, but rather to the object 
     37      # as a whole. These error messages don't get prepended with any field name when iterating 
     38      # with each_full, so they should be complete sentences. 
     39      def add_to_base(msg) 
     40        add(:base, msg) 
     41      end 
     42 
     43      # Adds an error message (+msg+) to the +attribute+, which will be returned on a call to <tt>on(attribute)</tt> 
     44      # for the same attribute and ensure that this error object returns false when asked if <tt>empty?</tt>. More than one 
     45      # error can be added to the same +attribute+ in which case an array will be returned on a call to <tt>on(attribute)</tt>. 
     46      # If no +msg+ is supplied, "invalid" is assumed. 
     47      def add(attribute, msg = @@default_error_messages[:invalid]) 
     48        @errors[attribute.to_s] = [] if @errors[attribute.to_s].nil? 
     49        @errors[attribute.to_s] << msg 
     50      end 
     51 
     52      # Will add an error message to each of the attributes in +attributes+ that is empty. 
     53      def add_on_empty(attributes, msg = @@default_error_messages[:empty]) 
     54        for attr in [attributes].flatten 
     55          value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] 
     56          is_empty = value.respond_to?("empty?") ? value.empty? : false 
     57          add(attr, msg) unless !value.nil? && !is_empty 
     58        end 
     59      end 
     60 
     61      # Will add an error message to each of the attributes in +attributes+ that is blank (using Object#blank?). 
     62      def add_on_blank(attributes, msg = @@default_error_messages[:blank]) 
     63        for attr in [attributes].flatten 
     64          value = @base.respond_to?(attr.to_s) ? @base.send(attr.to_s) : @base[attr.to_s] 
     65          add(attr, msg) if value.blank? 
     66        end 
     67      end 
     68 
     69      # Returns true if the specified +attribute+ has errors associated with it. 
     70      # 
     71      #   class Company < ActiveRecord::Base 
     72      #     validates_presence_of :name, :address, :email 
     73      #     validates_length_of :name, :in => 5..30 
     74      #   end 
     75      # 
     76      #   company = Company.create(:address => '123 First St.') 
     77      #   company.errors.invalid?(:name)      # => true 
     78      #   company.errors.invalid?(:address)   # => false 
     79      def invalid?(attribute) 
     80        !@errors[attribute.to_s].nil? 
     81      end 
     82 
     83      # Returns nil, if no errors are associated with the specified +attribute+. 
     84      # Returns the error message, if one error is associated with the specified +attribute+. 
     85      # Returns an array of error messages, if more than one error is associated with the specified +attribute+. 
     86      # 
     87      #   class Company < ActiveRecord::Base 
     88      #     validates_presence_of :name, :address, :email 
     89      #     validates_length_of :name, :in => 5..30 
     90      #   end 
     91      # 
     92      #   company = Company.create(:address => '123 First St.') 
     93      #   company.errors.on(:name)      # => ["is too short (minimum is 5 characters)", "can't be blank"] 
     94      #   company.errors.on(:email)     # => "can't be blank" 
     95      #   company.errors.on(:address)   # => nil 
     96      def on(attribute) 
     97        errors = @errors[attribute.to_s] 
     98        return nil if errors.nil? 
     99        errors.size == 1 ? errors.first : errors 
     100      end 
     101 
     102      alias :[] :on 
     103 
     104      # Returns errors assigned to the base object through add_to_base according to the normal rules of on(attribute). 
     105      def on_base 
     106        on(:base) 
     107      end 
     108 
     109      # Yields each attribute and associated message per error added. 
     110      # 
     111      #   class Company < ActiveRecord::Base 
     112      #     validates_presence_of :name, :address, :email 
     113      #     validates_length_of :name, :in => 5..30 
     114      #   end 
     115      # 
     116      #   company = Company.create(:address => '123 First St.') 
     117      #   company.errors.each{|attr,msg| puts "#{attr} - #{msg}" } # => 
     118      #     name - is too short (minimum is 5 characters) 
     119      #     name - can't be blank 
     120      #     address - can't be blank 
     121      def each 
     122        @errors.each_key { |attr| @errors[attr].each { |msg| yield attr, msg } } 
     123      end 
     124 
     125      # Yields each full error message added. So Person.errors.add("first_name", "can't be empty") will be returned 
     126      # through iteration as "First name can't be empty". 
     127      # 
     128      #   class Company < ActiveRecord::Base 
     129      #     validates_presence_of :name, :address, :email 
     130      #     validates_length_of :name, :in => 5..30 
     131      #   end 
     132      # 
     133      #   company = Company.create(:address => '123 First St.') 
     134      #   company.errors.each_full{|msg| puts msg } # => 
     135      #     Name is too short (minimum is 5 characters) 
     136      #     Name can't be blank 
     137      #     Address can't be blank 
     138      def each_full 
     139        full_messages.each { |msg| yield msg } 
     140      end 
     141 
     142      # Returns all the full error messages in an array. 
     143      # 
     144      #   class Company < ActiveRecord::Base 
     145      #     validates_presence_of :name, :address, :email 
     146      #     validates_length_of :name, :in => 5..30 
     147      #   end 
     148      # 
     149      #   company = Company.create(:address => '123 First St.') 
     150      #   company.errors.full_messages # => 
     151      #     ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Address can't be blank"] 
     152      def full_messages 
     153        full_messages = [] 
     154 
     155        @errors.each_key do |attr| 
     156          @errors[attr].each do |msg| 
     157            next if msg.nil? 
     158 
     159            if attr == "base" 
     160              full_messages << msg 
     161            else 
     162              if @base.class.respond_to?(:human_attribute_name) 
     163                full_messages << @base.class.human_attribute_name(attr) + " " + msg 
     164              else 
     165                full_messages << attr.humanize + " " + msg 
     166              end 
     167            end 
     168          end 
     169        end 
     170        full_messages 
     171      end 
     172 
     173      # Returns true if no errors have been added. 
     174      def empty? 
     175        @errors.empty? 
     176      end 
     177 
     178      # Removes all errors that have been added. 
     179      def clear 
     180        @errors = {} 
     181      end 
     182 
     183      # Returns the total number of errors added. Two errors added to the same attribute will be counted as such. 
     184      def size 
     185        @errors.values.inject(0) { |error_count, attribute| error_count + attribute.size } 
     186      end 
     187 
     188      alias_method :count, :size 
     189      alias_method :length, :size 
     190 
     191      # Return an XML representation of this error object. 
     192      # 
     193      #   class Company < ActiveRecord::Base 
     194      #     validates_presence_of :name, :address, :email 
     195      #     validates_length_of :name, :in => 5..30 
     196      #   end 
     197      # 
     198      #   company = Company.create(:address => '123 First St.') 
     199      #   company.errors.to_xml # => 
     200      #     <?xml version="1.0" encoding="UTF-8"?> 
     201      #     <errors> 
     202      #       <error>Name is too short (minimum is 5 characters)</error> 
     203      #       <error>Name can't be blank</error> 
     204      #       <error>Address can't be blank</error> 
     205      #     </errors> 
     206      def to_xml(options={}) 
     207        options[:root] ||= "errors" 
     208        options[:indent] ||= 2 
     209        options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent]) 
     210 
     211        options[:builder].instruct! unless options.delete(:skip_instruct) 
     212        options[:builder].errors do |e| 
     213          full_messages.each { |msg| e.error(msg) } 
     214        end 
     215      end 
     216    end 
     217 
     218    def self.included(base) 
     219      base.send :include, ActiveSupport::Callbacks 
     220      base.cattr_accessor :default_validation_options 
     221      base.default_validation_options = {} 
     222       
     223      base.extend ClassMethods 
     224    end 
     225 
     226    # All of the following validations are defined in the class scope of the model that you're interested in validating. 
     227    # They offer a more declarative way of specifying when the model is valid and when it is not. It is recommended to use 
     228    # these over the low-level calls to validate and validate_on_create when possible. 
     229    module ClassMethods 
     230      ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze 
     231      ALL_NUMERICALITY_CHECKS = { :greater_than => '>', :greater_than_or_equal_to => '>=', 
     232                                  :equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=', 
     233                                  :odd => 'odd?', :even => 'even?' }.freeze 
     234                                   
     235      def validate(*methods, &block) 
     236        options = methods.extract_options! 
     237        methods << block if block_given? 
     238        methods.map! { |method| ActiveSupport::Callbacks::Callback.new(:validate, method, options) } 
     239        existing_methods = read_inheritable_attribute(:validate) || [] 
     240        write_inheritable_attribute(:validate, existing_methods | methods) 
     241      end 
     242 
     243      def validate_callback_chain 
     244        read_inheritable_attribute(:validate) || [] 
     245      end 
     246       
     247      def validates_each(*attrs) 
     248        options = attrs.extract_options!.symbolize_keys.reverse_merge(self.default_validation_options) 
     249        attrs   = attrs.flatten 
     250 
     251        # Declare the validation. 
     252        send(validation_method(options), options) do |record| 
     253          attrs.each do |attr| 
     254            value = record.send(attr) 
     255            next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank]) 
     256            yield record, attr, value 
     257          end 
     258        end 
     259      end 
     260          
     261      def validates_confirmation_of(*attr_names) 
     262        configuration = { :message => ActiveSupport::Validations::Errors.default_error_messages[:confirmation] } 
     263        configuration.update(attr_names.extract_options!).reverse_merge!(self.default_validation_options) 
     264 
     265        attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" })) 
     266 
     267        validates_each(attr_names, configuration) do |record, attr_name, value| 
     268          record.errors.add(attr_name, configuration[:message]) unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation") 
     269        end 
     270      end 
     271 
     272      def validates_acceptance_of(*attr_names) 
     273        configuration = { :message => ActiveSupport::Validations::Errors.default_error_messages[:accepted], :allow_nil => true, :accept => "1" } 
     274        configuration.update(attr_names.extract_options!).reverse_merge!(self.default_validation_options) 
     275 
     276        attr_accessor(*attr_names) 
     277         
     278        validates_each(attr_names, configuration) do |record, attr_name, value| 
     279          record.errors.add(attr_name, configuration[:message]) unless value == configuration[:accept] 
     280        end 
     281      end 
     282 
     283      def validates_presence_of(*attr_names) 
     284        configuration = { :message => ActiveSupport::Validations::Errors.default_error_messages[:blank] } 
     285        configuration.update(attr_names.extract_options!).reverse_merge!(self.default_validation_options) 
     286 
     287        # can't use validates_each here, because it cannot cope with nonexistent attributes, 
     288        # while errors.add_on_empty can 
     289        send(validation_method(configuration), configuration) do |record| 
     290          record.errors.add_on_blank(attr_names, configuration[:message]) 
     291        end 
     292      end 
     293    
     294      def validates_length_of(*attrs) 
     295        # Merge given options with defaults. 
     296        options = { 
     297          :too_long     => ActiveSupport::Validations::Errors.default_error_messages[:too_long], 
     298          :too_short    => ActiveSupport::Validations::Errors.default_error_messages[:too_short], 
     299          :wrong_length => ActiveSupport::Validations::Errors.default_error_messages[:wrong_length] 
     300        }.merge({ :allow_nil => false, :allow_blank => false, :message => nil }) 
     301         
     302        options.update(attrs.extract_options!.symbolize_keys).reverse_merge!(self.default_validation_options) 
     303 
     304        # Ensure that one and only one range option is specified. 
     305        range_options = ALL_RANGE_OPTIONS & options.keys 
     306        case range_options.size 
     307          when 0 
     308            raise ArgumentError, 'Range unspecified.  Specify the :within, :maximum, :minimum, or :is option.' 
     309          when 1 
     310            # Valid number of options; do nothing. 
     311          else 
     312            raise ArgumentError, 'Too many range options specified.  Choose only one.' 
     313        end 
     314 
     315        # Get range option and value. 
     316        option = range_options.first 
     317        option_value = options[range_options.first] 
     318 
     319        case option 
     320          when :within, :in 
     321            raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range) 
     322 
     323            too_short = options[:too_short] % option_value.begin 
     324            too_long  = options[:too_long]  % option_value.end 
     325 
     326            validates_each(attrs, options) do |record, attr, value| 
     327              if value.nil? or value.split(//).size < option_value.begin 
     328                record.errors.add(attr, too_short) 
     329              elsif value.split(//).size > option_value.end 
     330                record.errors.add(attr, too_long) 
     331              end 
     332            end 
     333          when :is, :minimum, :maximum 
     334            raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0 
     335 
     336            # Declare different validations per option. 
     337            validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" } 
     338            message_options = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long } 
     339 
     340            message = (options[:message] || options[message_options[option]]) % option_value 
     341 
     342            validates_each(attrs, options) do |record, attr, value| 
     343              if value.kind_of?(String) 
     344                record.errors.add(attr, message) unless !value.nil? and value.split(//).size.method(validity_checks[option])[option_value] 
     345              else 
     346                record.errors.add(attr, message) unless !value.nil? and value.size.method(validity_checks[option])[option_value] 
     347              end 
     348            end 
     349        end 
     350      end 
     351 
     352      def validates_size_of(*attr_names) 
     353        validates_length_of(*attr_names) 
     354      end 
     355 
     356      def validates_format_of(*attr_names) 
     357        configuration = { :message => ActiveSupport::Validations::Errors.default_error_messages[:invalid], :with => nil } 
     358        configuration.update(attr_names.extract_options!).reverse_merge!(self.default_validation_options) 
     359 
     360        unless configuration[:with].is_a?(Regexp) 
     361          raise(ArgumentError, "A regular expression must be supplied as the :with option of the configuration hash") 
     362        end 
     363 
     364        validates_each(attr_names, configuration) do |record, attr_name, value| 
     365          record.errors.add(attr_name, configuration[:message]) unless value.to_s =~ configuration[:with] 
     366        end 
     367      end 
     368 
     369      def validates_inclusion_of(*attr_names) 
     370        configuration = { :message => ActiveSupport::Validations::Errors.default_error_messages[:inclusion] } 
     371        configuration.update(attr_names.extract_options!).reverse_merge!(self.default_validation_options) 
     372 
     373        enum = configuration[:in] || configuration[:within] 
     374 
     375        raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") unless enum.respond_to?("include?") 
     376 
     377        validates_each(attr_names, configuration) do |record, attr_name, value| 
     378          record.errors.add(attr_name, configuration[:message] % value) unless enum.include?(value) 
     379        end 
     380      end 
     381 
     382      def validates_exclusion_of(*attr_names) 
     383        configuration = { :message => ActiveSupport::Validations::Errors.default_error_messages[:exclusion] } 
     384        configuration.update(attr_names.extract_options!).reverse_merge!(self.default_validation_options) 
     385 
     386        enum = configuration[:in] || configuration[:within] 
     387 
     388        unless enum.respond_to?("include?") 
     389          raise(ArgumentError, "An object with the method include? is required must be supplied as the :in option of the configuration hash") 
     390        end 
     391 
     392        validates_each(attr_names, configuration) do |record, attr_name, value| 
     393          record.errors.add(attr_name, configuration[:message] % value) if enum.include?(value) 
     394        end 
     395      end 
     396 
     397      def validates_numericality_of(*attr_names) 
     398        configuration = { :only_integer => false, :allow_nil => false } 
     399        configuration.update(attr_names.extract_options!).reverse_merge!(self.default_validation_options) 
     400 
     401        numericality_options = ALL_NUMERICALITY_CHECKS.keys & configuration.keys 
     402 
     403        (numericality_options - [ :odd, :even ]).each do |option| 
     404          raise ArgumentError, ":#{option} must be a number" unless configuration[option].is_a?(Numeric) 
     405        end 
     406 
     407        validates_each(attr_names, configuration) do |record, attr_name, value| 
     408          raw_value = nil 
     409          if record.respond_to?("#{attr_name}_before_type_cast") 
     410            raw_value = record.send("#{attr_name}_before_type_cast") 
     411          end           
     412          raw_value ||= value 
     413 
     414          next if configuration[:allow_nil] and raw_value.nil? 
     415 
     416          if configuration[:only_integer] 
     417            unless raw_value.to_s =~ /\A[+-]?\d+\Z/ 
     418              record.errors.add(attr_name, configuration[:message] || ActiveSupport::Validations::Errors.default_error_messages[:not_a_number]) 
     419              next 
     420            end 
     421            raw_value = raw_value.to_i 
     422          else 
     423           begin 
     424              raw_value = Kernel.Float(raw_value.to_s) 
     425            rescue ArgumentError, TypeError 
     426              record.errors.add(attr_name, configuration[:message] || ActiveSupport::Validations::Errors.default_error_messages[:not_a_number]) 
     427              next 
     428            end 
     429          end 
     430 
     431          numericality_options.each do |option| 
     432            case option 
     433              when :odd, :even 
     434                unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[] 
     435                  record.errors.add(attr_name, configuration[:message] || ActiveSupport::Validations::Errors.default_error_messages[option]) 
     436                end 
     437              else 
     438                unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]] 
     439                  record.errors.add(attr_name, configuration[:message] || (ActiveSupport::Validations::Errors.default_error_messages[option] % configuration[option])) 
     440                end 
     441            end 
     442          end 
     443        end 
     444      end 
     445 
     446      private 
     447        def validation_method(configuration) 
     448          :validate 
     449        end 
     450    end 
     451     
     452    # Runs validate and returns true if no errors were added otherwise false. 
     453    def is_valid? 
     454      errors.clear 
     455      run_callbacks(:validate) 
     456      validate 
     457      errors.empty? 
     458    end 
     459 
     460    # Returns the Errors object that holds all information about attribute error messages. 
     461    def errors 
     462      @errors ||= ActiveSupport::Validations::Errors.new(self) 
     463    end 
     464     
     465    protected 
     466     
     467    def validate 
     468    end 
     469     
     470  end 
     471end 
  • /dev/null

    old new  
     1require 'abstract_unit' 
     2 
     3class Car 
     4  include ActiveSupport::Validations 
     5  attr_accessor :name, :age 
     6   
     7  validates_presence_of :name 
     8end 
     9 
     10class Ferrari < Car 
     11  attr_accessor :speed 
     12   
     13  validates_numericality_of :speed 
     14  validates_presence_of :name, :message => "missing name" 
     15end 
     16 
     17class ValidationsTest < Test::Unit::TestCase 
     18   
     19  def setup 
     20    @car = Car.new     
     21    @ferrari = Ferrari.new 
     22  end 
     23   
     24  def test_validates_presence_of 
     25    assert !@car.is_valid? 
     26    assert_equal "can't be blank", @car.errors[:name] 
     27     
     28    @car.name = "Zen" 
     29    assert @car.is_valid? 
     30    assert @car.errors.empty? 
     31  end 
     32   
     33  def test_validates_numericality_of 
     34    assert !@ferrari.is_valid? 
     35     
     36    assert_equal "is not a number", @ferrari.errors[:speed] 
     37    assert_equal ["can't be blank", "missing name"], @ferrari.errors[:name] 
     38     
     39    @ferrari.name = "Vrummmm" 
     40    @ferrari.speed = 200 
     41     
     42    assert @ferrari.is_valid? 
     43  end 
     44   
     45end