Permalink
Browse files

Merge branch 'master' of github.com:rails/rails

  • Loading branch information...
2 parents 9abbe9f + 078ea0d commit 643862e3be1bbe004e2c1a00286b12c5bdc9849a Yehuda Katz committed Dec 29, 2009
Showing with 1,652 additions and 1,126 deletions.
  1. +1 −0 Gemfile
  2. +8 −1 Rakefile
  3. +4 −3 activemodel/lib/active_model.rb
  4. +22 −4 activemodel/lib/active_model/naming.rb
  5. +1 −21 activemodel/lib/active_model/translation.rb
  6. +23 −33 activemodel/lib/active_model/validations.rb
  7. +14 −7 activemodel/lib/active_model/validations/acceptance.rb
  8. +11 −9 activemodel/lib/active_model/validations/confirmation.rb
  9. +15 −11 activemodel/lib/active_model/validations/exclusion.rb
  10. +15 −13 activemodel/lib/active_model/validations/format.rb
  11. +15 −11 activemodel/lib/active_model/validations/inclusion.rb
  12. +72 −57 activemodel/lib/active_model/validations/length.rb
  13. +64 −58 activemodel/lib/active_model/validations/numericality.rb
  14. +8 −7 activemodel/lib/active_model/validations/presence.rb
  15. +3 −8 activemodel/lib/active_model/validations/with.rb
  16. +48 −7 activemodel/lib/active_model/validator.rb
  17. +2 −1 activemodel/test/cases/naming_test.rb
  18. +15 −21 activemodel/test/cases/translation_test.rb
  19. +12 −21 activemodel/test/cases/validations/acceptance_validation_test.rb
  20. +3 −2 activemodel/test/cases/validations/conditional_validation_test.rb
  21. +12 −22 activemodel/test/cases/validations/confirmation_validation_test.rb
  22. +12 −11 activemodel/test/cases/validations/exclusion_validation_test.rb
  23. +12 −21 activemodel/test/cases/validations/format_validation_test.rb
  24. +0 −1 activemodel/test/cases/validations/i18n_validation_test.rb
  25. +12 −21 activemodel/test/cases/validations/inclusion_validation_test.rb
  26. +12 −41 activemodel/test/cases/validations/length_validation_test.rb
  27. +12 −29 activemodel/test/cases/validations/numericality_validation_test.rb
  28. +26 −28 activemodel/test/cases/validations/presence_validation_test.rb
  29. +10 −9 activemodel/test/cases/validations/with_validation_test.rb
  30. +3 −2 activemodel/test/cases/validations_test.rb
  31. +5 −1 activemodel/test/models/person.rb
  32. +4 −0 activemodel/test/models/track_back.rb
  33. +23 −0 activerecord/CHANGELOG
  34. +1 −0 activerecord/lib/active_record.rb
  35. +11 −10 activerecord/lib/active_record/associations.rb
  36. +2 −2 activerecord/lib/active_record/associations/association_collection.rb
  37. +34 −10 activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
  38. +2 −2 activerecord/lib/active_record/associations/has_one_association.rb
  39. +26 −14 activerecord/lib/active_record/autosave_association.rb
  40. +10 −27 activerecord/lib/active_record/base.rb
  41. +57 −143 activerecord/lib/active_record/calculations.rb
  42. +0 −12 activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
  43. +0 −6 activerecord/lib/active_record/connection_adapters/sqlite_adapter.rb
  44. +26 −22 activerecord/lib/active_record/nested_attributes.rb
  45. +14 −4 activerecord/lib/active_record/reflection.rb
  46. +42 −10 activerecord/lib/active_record/relation.rb
  47. +169 −0 activerecord/lib/active_record/relational_calculations.rb
  48. +9 −7 activerecord/lib/active_record/validations/associated.rb
  49. +76 −58 activerecord/lib/active_record/validations/uniqueness.rb
  50. +4 −4 activerecord/test/cases/associations/eager_test.rb
  51. +15 −15 activerecord/test/cases/associations/inner_join_association_test.rb
  52. +273 −19 activerecord/test/cases/associations/inverse_associations_test.rb
  53. +29 −0 activerecord/test/cases/autosave_association_test.rb
  54. +9 −9 activerecord/test/cases/base_test.rb
  55. +9 −11 activerecord/test/cases/calculations_test.rb
  56. +2 −2 activerecord/test/cases/finder_test.rb
  57. +2 −6 activerecord/test/cases/helper.rb
  58. +1 −1 activerecord/test/cases/locking_test.rb
  59. +87 −87 activerecord/test/cases/method_scoping_test.rb
  60. +51 −0 activerecord/test/cases/nested_attributes_test.rb
  61. +21 −20 activerecord/test/cases/readonly_test.rb
  62. +35 −0 activerecord/test/cases/relations_test.rb
  63. +1 −1 activerecord/test/cases/validations/uniqueness_validation_test.rb
  64. +1 −1 {activemodel/lib/active_model → activerecord/test/cases}/validations_repair_helper.rb
  65. +3 −3 activerecord/test/cases/validations_test.rb
  66. +4 −0 activerecord/test/fixtures/faces.yml
  67. +5 −1 activerecord/test/fixtures/interests.yml
  68. +3 −1 activerecord/test/models/face.rb
  69. +1 −0 activerecord/test/models/interest.rb
  70. +2 −0 activerecord/test/models/man.rb
  71. +2 −0 activerecord/test/models/pirate.rb
  72. +2 −0 activerecord/test/models/ship.rb
  73. +4 −0 activerecord/test/schema/schema.rb
  74. +9 −40 railties/test/initializer/check_ruby_version_test.rb
  75. +99 −97 railties/test/initializer/path_test.rb
View
1 Gemfile
@@ -1,5 +1,6 @@
gem "rake", ">= 0.8.7"
gem "mocha", ">= 0.9.8"
+gem "ruby-debug", ">= 0.10.3" if RUBY_VERSION < '1.9'
gem "rails", "3.0.pre", :path => "railties"
%w(activesupport activemodel actionpack actionmailer activerecord activeresource).each do |lib|
View
9 Rakefile
@@ -24,8 +24,15 @@ task :default => %w(test test:isolated)
end
end
-spec = eval(File.read('rails.gemspec'))
+desc "Smoke-test all projects"
+task :smoke do
+ (PROJECTS - %w(activerecord)).each do |project|
+ system %(cd #{project} && #{env} #{$0} test:isolated)
+ end
+ system %(cd activerecord && #{env} #{$0} sqlite3:isolated_test)
+end
+spec = eval(File.read('rails.gemspec'))
Rake::GemPackageTask.new(spec) do |pkg|
pkg.gem_spec = spec
end
View
7 activemodel/lib/active_model.rb
@@ -35,16 +35,17 @@ module ActiveModel
autoload :Dirty
autoload :Errors
autoload :Lint
- autoload :Name, 'active_model/naming'
+ autoload :Name, 'active_model/naming'
autoload :Naming
- autoload :Observer, 'active_model/observing'
+ autoload :Observer, 'active_model/observing'
autoload :Observing
autoload :Serialization
autoload :StateMachine
autoload :Translation
autoload :Validations
- autoload :ValidationsRepairHelper
autoload :Validator
+ autoload :EachValidator, 'active_model/validator'
+ autoload :BlockValidator, 'active_model/validator'
autoload :VERSION
module Serializers
View
26 activemodel/lib/active_model/naming.rb
@@ -2,11 +2,11 @@
module ActiveModel
class Name < String
- attr_reader :singular, :plural, :element, :collection, :partial_path, :human
+ attr_reader :singular, :plural, :element, :collection, :partial_path
alias_method :cache_key, :collection
- def initialize(klass, name)
- super(name)
+ def initialize(klass)
+ super(klass.name)
@klass = klass
@singular = ActiveSupport::Inflector.underscore(self).tr('/', '_').freeze
@plural = ActiveSupport::Inflector.pluralize(@singular).freeze
@@ -15,13 +15,31 @@ def initialize(klass, name)
@collection = ActiveSupport::Inflector.tableize(self).freeze
@partial_path = "#{@collection}/#{@element}".freeze
end
+
+ # Transform the model name into a more humane format, using I18n. By default,
+ # it will underscore then humanize the class name (BlogPost.model_name.human #=> "Blog post").
+ # Specify +options+ with additional translating options.
+ def human(options={})
+ return @human unless @klass.respond_to?(:lookup_ancestors) &&
+ @klass.respond_to?(:i18n_scope)
+
+ defaults = @klass.lookup_ancestors.map do |klass|
+ klass.model_name.underscore.to_sym
+ end
+
+ defaults << options.delete(:default) if options[:default]
+ defaults << @human
+
+ options.reverse_merge! :scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults
+ I18n.translate(defaults.shift, options)
+ end
end
module Naming
# Returns an ActiveModel::Name object for module. It can be
# used to retrieve all kinds of naming-related information.
def model_name
- @_model_name ||= ActiveModel::Name.new(self, name)
+ @_model_name ||= ActiveModel::Name.new(self)
end
end
end
View
22 activemodel/lib/active_model/translation.rb
@@ -37,28 +37,8 @@ def human_attribute_name(attribute, options = {})
# Model.human_name is deprecated. Use Model.model_name.human instead.
def human_name(*args)
- ActiveSupport::Deprecation.warn("human_name has been deprecated, please use model_name.human instead", caller[0,1])
+ ActiveSupport::Deprecation.warn("human_name has been deprecated, please use model_name.human instead", caller[0,5])
model_name.human(*args)
end
end
-
- class Name < String
- # Transform the model name into a more humane format, using I18n. By default,
- # it will underscore then humanize the class name (BlogPost.human_name #=> "Blog post").
- # Specify +options+ with additional translating options.
- def human(options={})
- return @human unless @klass.respond_to?(:lookup_ancestors) &&
- @klass.respond_to?(:i18n_scope)
-
- defaults = @klass.lookup_ancestors.map do |klass|
- klass.model_name.underscore.to_sym
- end
-
- defaults << options.delete(:default) if options[:default]
- defaults << @human
-
- options.reverse_merge! :scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults
- I18n.translate(defaults.shift, options)
- end
- end
end
View
56 activemodel/lib/active_model/validations.rb
@@ -13,6 +13,29 @@ module Validations
end
module ClassMethods
+ # Validates each attribute against a block.
+ #
+ # class Person < ActiveRecord::Base
+ # validates_each :first_name, :last_name do |record, attr, value|
+ # record.errors.add attr, 'starts with z.' if value[0] == ?z
+ # end
+ # end
+ #
+ # Options:
+ # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
+ # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
+ # * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
+ # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
+ # method, proc or string should return or evaluate to a true or false value.
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
+ # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
+ # method, proc or string should return or evaluate to a true or false value.
+ def validates_each(*attr_names, &block)
+ options = attr_names.extract_options!.symbolize_keys
+ validates_with BlockValidator, options.merge(:attributes => attr_names.flatten), &block
+ end
+
# Adds a validation method or block to the class. This is useful when
# overriding the +validate+ instance method becomes too unwieldly and
# you're looking for more descriptive declaration of your validations.
@@ -40,39 +63,6 @@ module ClassMethods
# end
#
# This usage applies to +validate_on_create+ and +validate_on_update as well+.
-
- # Validates each attribute against a block.
- #
- # class Person < ActiveRecord::Base
- # validates_each :first_name, :last_name do |record, attr, value|
- # record.errors.add attr, 'starts with z.' if value[0] == ?z
- # end
- # end
- #
- # Options:
- # * <tt>:on</tt> - Specifies when this validation is active (default is <tt>:save</tt>, other options <tt>:create</tt>, <tt>:update</tt>).
- # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
- # * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
- # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if the validation should
- # occur (e.g. <tt>:if => :allow_validation</tt>, or <tt>:if => Proc.new { |user| user.signup_step > 2 }</tt>). The
- # method, proc or string should return or evaluate to a true or false value.
- # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine if the validation should
- # not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
- # method, proc or string should return or evaluate to a true or false value.
- def validates_each(*attrs)
- options = attrs.extract_options!.symbolize_keys
- attrs = attrs.flatten
-
- # Declare the validation.
- validate options do |record|
- attrs.each do |attr|
- value = record.send(:read_attribute_for_validation, attr)
- next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
- yield record, attr, value
- end
- end
- end
-
def validate(*args, &block)
options = args.last
if options.is_a?(Hash) && options.key?(:on)
View
21 activemodel/lib/active_model/validations/acceptance.rb
@@ -1,5 +1,17 @@
module ActiveModel
module Validations
+ class AcceptanceValidator < EachValidator
+ def initialize(options)
+ super(options.reverse_merge(:allow_nil => true, :accept => "1"))
+ end
+
+ def validate_each(record, attribute, value)
+ unless value == options[:accept]
+ record.errors.add(attribute, :accepted, :default => options[:message])
+ end
+ end
+ end
+
module ClassMethods
# Encapsulates the pattern of wanting to validate the acceptance of a terms of service check box (or similar agreement). Example:
#
@@ -25,8 +37,7 @@ module ClassMethods
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_acceptance_of(*attr_names)
- configuration = { :allow_nil => true, :accept => "1" }
- configuration.update(attr_names.extract_options!)
+ options = attr_names.extract_options!
db_cols = begin
column_names
@@ -37,11 +48,7 @@ def validates_acceptance_of(*attr_names)
names = attr_names.reject { |name| db_cols.include?(name.to_s) }
attr_accessor(*names)
- validates_each(attr_names,configuration) do |record, attr_name, value|
- unless value == configuration[:accept]
- record.errors.add(attr_name, :accepted, :default => configuration[:message])
- end
- end
+ validates_with AcceptanceValidator, options.merge(:attributes => attr_names)
end
end
end
View
20 activemodel/lib/active_model/validations/confirmation.rb
@@ -1,5 +1,13 @@
module ActiveModel
module Validations
+ class ConfirmationValidator < EachValidator
+ def validate_each(record, attribute, value)
+ confirmed = record.send(:"#{attribute}_confirmation")
+ return if confirmed.nil? || value == confirmed
+ record.errors.add(attribute, :confirmation, :default => options[:message])
+ end
+ end
+
module ClassMethods
# Encapsulates the pattern of wanting to validate a password or email address field with a confirmation. Example:
#
@@ -30,15 +38,9 @@ module ClassMethods
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_confirmation_of(*attr_names)
- configuration = attr_names.extract_options!
-
- attr_accessor(*(attr_names.map { |n| "#{n}_confirmation" }))
-
- validates_each(attr_names, configuration) do |record, attr_name, value|
- unless record.send("#{attr_name}_confirmation").nil? or value == record.send("#{attr_name}_confirmation")
- record.errors.add(attr_name, :confirmation, :default => configuration[:message])
- end
- end
+ options = attr_names.extract_options!
+ attr_accessor(*(attr_names.map { |n| :"#{n}_confirmation" }))
+ validates_with ConfirmationValidator, options.merge(:attributes => attr_names)
end
end
end
View
26 activemodel/lib/active_model/validations/exclusion.rb
@@ -1,5 +1,17 @@
module ActiveModel
module Validations
+ class ExclusionValidator < EachValidator
+ 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?)
+ end
+
+ def validate_each(record, attribute, value)
+ return unless options[:in].include?(value)
+ record.errors.add(attribute, :exclusion, :default => options[:message], :value => value)
+ end
+ end
+
module ClassMethods
# Validates that the value of the specified attribute is not in a particular enumerable object.
#
@@ -21,17 +33,9 @@ module ClassMethods
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_exclusion_of(*attr_names)
- configuration = attr_names.extract_options!
-
- enum = configuration[:in] || configuration[:within]
-
- 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?)
-
- validates_each(attr_names, configuration) do |record, attr_name, value|
- if enum.include?(value)
- record.errors.add(attr_name, :exclusion, :default => configuration[:message], :value => value)
- end
- end
+ options = attr_names.extract_options!
+ options[:in] ||= options.delete(:within)
+ validates_with ExclusionValidator, options.merge(:attributes => attr_names)
end
end
end
View
28 activemodel/lib/active_model/validations/format.rb
@@ -1,5 +1,15 @@
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, :default => options[:message], :value => value)
+ elsif options[:without] && value.to_s =~ options[:without]
+ record.errors.add(attribute, :invalid, :default => options[:message], :value => value)
+ end
+ end
+ end
+
module ClassMethods
# Validates whether the value of the specified attribute is of the correct form, going by the regular expression provided.
# You can require that the attribute matches the regular expression:
@@ -33,29 +43,21 @@ module ClassMethods
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_format_of(*attr_names)
- configuration = attr_names.extract_options!
+ options = attr_names.extract_options!
- unless configuration.include?(:with) ^ configuration.include?(:without) # ^ == xor, or "exclusive or"
+ unless options.include?(:with) ^ options.include?(:without) # ^ == xor, or "exclusive or"
raise ArgumentError, "Either :with or :without must be supplied (but not both)"
end
- if configuration[:with] && !configuration[:with].is_a?(Regexp)
+ 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
- if configuration[:without] && !configuration[:without].is_a?(Regexp)
+ if options[:without] && !options[:without].is_a?(Regexp)
raise ArgumentError, "A regular expression must be supplied as the :without option of the configuration hash"
end
- if configuration[:with]
- validates_each(attr_names, configuration) do |record, attr_name, value|
- record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) if value.to_s !~ configuration[:with]
- end
- elsif configuration[:without]
- validates_each(attr_names, configuration) do |record, attr_name, value|
- record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value) if value.to_s =~ configuration[:without]
- end
- end
+ validates_with FormatValidator, options.merge(:attributes => attr_names)
end
end
end
View
26 activemodel/lib/active_model/validations/inclusion.rb
@@ -1,5 +1,17 @@
module ActiveModel
module Validations
+ class InclusionValidator < EachValidator
+ 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?)
+ end
+
+ def validate_each(record, attribute, value)
+ return if options[:in].include?(value)
+ record.errors.add(attribute, :inclusion, :default => options[:message], :value => value)
+ end
+ end
+
module ClassMethods
# Validates whether the value of the specified attribute is available in a particular enumerable object.
#
@@ -21,17 +33,9 @@ module ClassMethods
# not occur (e.g. <tt>:unless => :skip_validation</tt>, or <tt>:unless => Proc.new { |user| user.signup_step <= 2 }</tt>). The
# method, proc or string should return or evaluate to a true or false value.
def validates_inclusion_of(*attr_names)
- configuration = attr_names.extract_options!
-
- enum = configuration[:in] || configuration[:within]
-
- 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?)
-
- validates_each(attr_names, configuration) do |record, attr_name, value|
- unless enum.include?(value)
- record.errors.add(attr_name, :inclusion, :default => configuration[:message], :value => value)
- end
- end
+ options = attr_names.extract_options!
+ options[:in] ||= options.delete(:within)
+ validates_with InclusionValidator, options.merge(:attributes => attr_names)
end
end
end
View
129 activemodel/lib/active_model/validations/length.rb
@@ -1,7 +1,75 @@
module ActiveModel
module Validations
+ class LengthValidator < EachValidator
+ OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze
+ MESSAGES = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }.freeze
+ CHECKS = { :is => :==, :minimum => :>=, :maximum => :<= }.freeze
+
+ DEFAULT_TOKENIZER = lambda { |value| value.split(//) }
+ attr_reader :type
+
+ def initialize(options)
+ @type = (OPTIONS & options.keys).first
+ super(options.reverse_merge(:tokenizer => DEFAULT_TOKENIZER))
+ end
+
+ def check_validity!
+ ensure_one_range_option!
+ ensure_argument_types!
+ end
+
+ def validate_each(record, attribute, value)
+ checks = options.slice(:minimum, :maximum, :is)
+ value = options[:tokenizer].call(value) if value.kind_of?(String)
+
+ if [:within, :in].include?(type)
+ range = options[type]
+ checks[:minimum], checks[:maximum] = range.begin, range.end
+ checks[:maximum] -= 1 if range.exclude_end?
+ end
+
+ checks.each do |key, check_value|
+ custom_message = options[:message] || options[MESSAGES[key]]
+ validity_check = CHECKS[key]
+
+ valid_value = if key == :maximum
+ value.nil? || value.size.send(validity_check, check_value)
+ else
+ value && value.size.send(validity_check, check_value)
+ end
+
+ record.errors.add(attribute, MESSAGES[key], :default => custom_message, :count => check_value) unless valid_value
+ end
+ end
+
+ protected
+
+ def ensure_one_range_option! #:nodoc:
+ range_options = OPTIONS & options.keys
+
+ case range_options.size
+ when 0
+ raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.'
+ when 1
+ # Valid number of options; do nothing.
+ else
+ raise ArgumentError, 'Too many range options specified. Choose only one.'
+ end
+ end
+
+ def ensure_argument_types! #:nodoc:
+ value = options[type]
+
+ case type
+ when :within, :in
+ raise ArgumentError, ":#{type} must be a Range" unless value.is_a?(Range)
+ when :is, :minimum, :maximum
+ raise ArgumentError, ":#{type} must be a nonnegative Integer" unless value.is_a?(Integer) && value >= 0
+ end
+ end
+ end
+
module ClassMethods
- ALL_RANGE_OPTIONS = [ :is, :within, :in, :minimum, :maximum ].freeze
# Validates that the specified attribute matches the length restrictions supplied. Only one option can be used at a time:
#
@@ -38,62 +106,9 @@ module ClassMethods
# * <tt>:tokenizer</tt> - Specifies how to split up the attribute string. (e.g. <tt>:tokenizer => lambda {|str| str.scan(/\w+/)}</tt> to
# count words as in above example.)
# Defaults to <tt>lambda{ |value| value.split(//) }</tt> which counts individual characters.
- def validates_length_of(*attrs)
- # Merge given options with defaults.
- options = { :tokenizer => lambda {|value| value.split(//)} }
- options.update(attrs.extract_options!.symbolize_keys)
-
- # Ensure that one and only one range option is specified.
- range_options = ALL_RANGE_OPTIONS & options.keys
- case range_options.size
- when 0
- raise ArgumentError, 'Range unspecified. Specify the :within, :maximum, :minimum, or :is option.'
- when 1
- # Valid number of options; do nothing.
- else
- raise ArgumentError, 'Too many range options specified. Choose only one.'
- end
-
- # Get range option and value.
- option = range_options.first
- option_value = options[range_options.first]
- key = {:is => :wrong_length, :minimum => :too_short, :maximum => :too_long}[option]
- custom_message = options[:message] || options[key]
-
- case option
- when :within, :in
- raise ArgumentError, ":#{option} must be a Range" unless option_value.is_a?(Range)
-
- validates_each(attrs, options) do |record, attr, value|
- value = options[:tokenizer].call(value) if value.kind_of?(String)
-
- min, max = option_value.begin, option_value.end
- max = max - 1 if option_value.exclude_end?
-
- if value.nil? || value.size < min
- record.errors.add(attr, :too_short, :default => custom_message || options[:too_short], :count => min)
- elsif value.size > max
- record.errors.add(attr, :too_long, :default => custom_message || options[:too_long], :count => max)
- end
- end
- when :is, :minimum, :maximum
- raise ArgumentError, ":#{option} must be a nonnegative Integer" unless option_value.is_a?(Integer) and option_value >= 0
-
- # Declare different validations per option.
- validity_checks = { :is => "==", :minimum => ">=", :maximum => "<=" }
-
- validates_each(attrs, options) do |record, attr, value|
- value = options[:tokenizer].call(value) if value.kind_of?(String)
-
- valid_value = if option == :maximum
- value.nil? || value.size.send(validity_checks[option], option_value)
- else
- value && value.size.send(validity_checks[option], option_value)
- end
-
- record.errors.add(attr, key, :default => custom_message, :count => option_value) unless valid_value
- end
- end
+ def validates_length_of(*attr_names)
+ options = attr_names.extract_options!
+ validates_with LengthValidator, options.merge(:attributes => attr_names)
end
alias_method :validates_size_of, :validates_length_of
View
122 activemodel/lib/active_model/validations/numericality.rb
@@ -1,10 +1,68 @@
module ActiveModel
module Validations
- module ClassMethods
- ALL_NUMERICALITY_CHECKS = { :greater_than => '>', :greater_than_or_equal_to => '>=',
- :equal_to => '==', :less_than => '<', :less_than_or_equal_to => '<=',
- :odd => 'odd?', :even => 'even?' }.freeze
+ class NumericalityValidator < EachValidator
+ CHECKS = { :greater_than => :>, :greater_than_or_equal_to => :>=,
+ :equal_to => :==, :less_than => :<, :less_than_or_equal_to => :<=,
+ :odd => :odd?, :even => :even? }.freeze
+
+ def initialize(options)
+ super(options.reverse_merge(:only_integer => false, :allow_nil => false))
+ end
+
+ def check_validity!
+ options.slice(*CHECKS.keys) do |option, value|
+ next if [:odd, :even].include?(option)
+ raise ArgumentError, ":#{option} must be a number, a symbol or a proc" unless value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol)
+ end
+ end
+
+ def validate_each(record, attr_name, value)
+ before_type_cast = "#{attr_name}_before_type_cast"
+
+ raw_value = record.send("#{attr_name}_before_type_cast") if record.respond_to?(before_type_cast.to_sym)
+ raw_value ||= value
+
+ return if options[:allow_nil] && raw_value.nil?
+
+ unless value = parse_raw_value(raw_value, options)
+ record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => options[:message])
+ return
+ end
+
+ options.slice(*CHECKS.keys).each do |option, option_value|
+ case option
+ when :odd, :even
+ unless value.to_i.send(CHECKS[option])
+ record.errors.add(attr_name, option, :value => value, :default => options[:message])
+ end
+ else
+ option_value = option_value.call(record) if option_value.is_a?(Proc)
+ option_value = record.send(option_value) if option_value.is_a?(Symbol)
+
+ unless value.send(CHECKS[option], option_value)
+ record.errors.add(attr_name, option, :default => options[:message], :value => value, :count => option_value)
+ end
+ end
+ end
+ end
+
+ protected
+
+ def parse_raw_value(raw_value, options)
+ if options[:only_integer]
+ raw_value.to_i if raw_value.to_s =~ /\A[+-]?\d+\Z/
+ else
+ begin
+ Kernel.Float(raw_value)
+ rescue ArgumentError, TypeError
+ nil
+ end
+ end
+ end
+ end
+
+ module ClassMethods
# Validates whether the value of the specified attribute is numeric by trying to convert it to
# a float with Kernel.Float (if <tt>only_integer</tt> is false) or applying it to the regular expression
# <tt>/\A[\+\-]?\d+\Z/</tt> (if <tt>only_integer</tt> is set to true).
@@ -44,61 +102,9 @@ module ClassMethods
# validates_numericality_of :width, :greater_than => :minimum_weight
# end
#
- #
-
def validates_numericality_of(*attr_names)
- configuration = { :only_integer => false, :allow_nil => false }
- configuration.update(attr_names.extract_options!)
-
- numericality_options = ALL_NUMERICALITY_CHECKS.keys & configuration.keys
-
- (numericality_options - [ :odd, :even ]).each do |option|
- value = configuration[option]
- raise ArgumentError, ":#{option} must be a number, a symbol or a proc" unless value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol)
- end
-
- validates_each(attr_names,configuration) do |record, attr_name, value|
- before_type_cast = "#{attr_name}_before_type_cast"
-
- if record.respond_to?(before_type_cast.to_sym)
- raw_value = record.send("#{attr_name}_before_type_cast") || value
- else
- raw_value = value
- end
-
- next if configuration[:allow_nil] and raw_value.nil?
-
- if configuration[:only_integer]
- unless raw_value.to_s =~ /\A[+-]?\d+\Z/
- record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
- next
- end
- raw_value = raw_value.to_i
- else
- begin
- raw_value = Kernel.Float(raw_value)
- rescue ArgumentError, TypeError
- record.errors.add(attr_name, :not_a_number, :value => raw_value, :default => configuration[:message])
- next
- end
- end
-
- numericality_options.each do |option|
- case option
- when :odd, :even
- unless raw_value.to_i.method(ALL_NUMERICALITY_CHECKS[option])[]
- record.errors.add(attr_name, option, :value => raw_value, :default => configuration[:message])
- end
- else
- configuration[option] = configuration[option].call(record) if configuration[option].is_a? Proc
- configuration[option] = record.method(configuration[option]).call if configuration[option].is_a? Symbol
-
- unless raw_value.method(ALL_NUMERICALITY_CHECKS[option])[configuration[option]]
- record.errors.add(attr_name, option, :default => configuration[:message], :value => raw_value, :count => configuration[option])
- end
- end
- end
- end
+ options = attr_names.extract_options!
+ validates_with NumericalityValidator, options.merge(:attributes => attr_names)
end
end
end
View
15 activemodel/lib/active_model/validations/presence.rb
@@ -2,6 +2,12 @@
module ActiveModel
module Validations
+ class PresenceValidator < EachValidator
+ def validate(record)
+ record.errors.add_on_blank(attributes, options[:message])
+ end
+ end
+
module ClassMethods
# Validates that the specified attributes are not blank (as defined by Object#blank?). Happens by default on save. Example:
#
@@ -28,13 +34,8 @@ module ClassMethods
# The method, proc or string should return or evaluate to a true or false value.
#
def validates_presence_of(*attr_names)
- configuration = attr_names.extract_options!
-
- # can't use validates_each here, because it cannot cope with nonexistent attributes,
- # while errors.add_on_empty can
- validate configuration do |record|
- record.errors.add_on_blank(attr_names, configuration[:message])
- end
+ options = attr_names.extract_options!
+ validates_with PresenceValidator, options.merge(:attributes => attr_names)
end
end
end
View
11 activemodel/lib/active_model/validations/with.rb
@@ -48,14 +48,9 @@ module ClassMethods
# end
# end
#
- def validates_with(*args)
- configuration = args.extract_options!
-
- validate configuration do |record|
- args.each do |klass|
- klass.new(record, configuration.except(:on, :if, :unless)).validate
- end
- end
+ def validates_with(*args, &block)
+ options = args.extract_options!
+ args.each { |klass| validate(klass.new(options, &block), options) }
end
end
end
View
55 activemodel/lib/active_model/validator.rb
@@ -1,5 +1,4 @@
module ActiveModel #:nodoc:
-
# A simple base class that can be used along with ActiveModel::Base.validates_with
#
# class Person < ActiveModel::Base
@@ -52,17 +51,59 @@ module ActiveModel #:nodoc:
# @my_custom_field = options[:field_name] || :first_name
# end
# end
- #
class Validator
- attr_reader :record, :options
+ attr_reader :options
- def initialize(record, options)
- @record = record
+ def initialize(options)
@options = options
end
- def validate
- raise "You must override this method"
+ def validate(record)
+ raise NotImplementedError
+ end
+ end
+
+ # EachValidator is a validator which iterates through the attributes given
+ # in the options hash invoking the validate_each method passing in the
+ # record, attribute and value.
+ #
+ # All ActiveModel validations are built on top of this Validator.
+ class EachValidator < Validator
+ attr_reader :attributes
+
+ def initialize(options)
+ @attributes = options.delete(:attributes)
+ super
+ check_validity!
+ end
+
+ def validate(record)
+ attributes.each do |attribute|
+ value = record.send(:read_attribute_for_validation, attribute)
+ next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
+ validate_each(record, attribute, value)
+ end
+ end
+
+ def validate_each(record, attribute, value)
+ raise NotImplementedError
+ end
+
+ def check_validity!
+ end
+ end
+
+ # BlockValidator is a special EachValidator which receives a block on initialization
+ # and call this block for each attribute being validated. +validates_each+ uses this
+ # Validator.
+ class BlockValidator < EachValidator
+ def initialize(options, &block)
+ @block = block
+ super
+ end
+
+ def validate_each(record, attribute, value)
+ @block.call(record, attribute, value)
end
end
end
View
3 activemodel/test/cases/naming_test.rb
@@ -1,8 +1,9 @@
require 'cases/helper'
+require 'models/track_back'
class NamingTest < ActiveModel::TestCase
def setup
- @model_name = ActiveModel::Name.new(self, 'Post::TrackBack')
+ @model_name = ActiveModel::Name.new(Post::TrackBack)
end
def test_singular
View
36 activemodel/test/cases/translation_test.rb
@@ -1,11 +1,5 @@
require 'cases/helper'
-
-class SuperUser
- extend ActiveModel::Translation
-end
-
-class User < SuperUser
-end
+require 'models/person'
class ActiveModelI18nTests < ActiveModel::TestCase
@@ -14,38 +8,38 @@ def setup
end
def test_translated_model_attributes
- I18n.backend.store_translations 'en', :activemodel => {:attributes => {:super_user => {:name => 'super_user name attribute'} } }
- assert_equal 'super_user name attribute', SuperUser.human_attribute_name('name')
+ I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:name => 'person name attribute'} } }
+ assert_equal 'person name attribute', Person.human_attribute_name('name')
end
def test_translated_model_attributes_with_symbols
- I18n.backend.store_translations 'en', :activemodel => {:attributes => {:super_user => {:name => 'super_user name attribute'} } }
- assert_equal 'super_user name attribute', SuperUser.human_attribute_name(:name)
+ I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:name => 'person name attribute'} } }
+ assert_equal 'person name attribute', Person.human_attribute_name(:name)
end
def test_translated_model_attributes_with_ancestor
- I18n.backend.store_translations 'en', :activemodel => {:attributes => {:user => {:name => 'user name attribute'} } }
- assert_equal 'user name attribute', User.human_attribute_name('name')
+ I18n.backend.store_translations 'en', :activemodel => {:attributes => {:child => {:name => 'child name attribute'} } }
+ assert_equal 'child name attribute', Child.human_attribute_name('name')
end
def test_translated_model_attributes_with_ancestors_fallback
- I18n.backend.store_translations 'en', :activemodel => {:attributes => {:super_user => {:name => 'super_user name attribute'} } }
- assert_equal 'super_user name attribute', User.human_attribute_name('name')
+ I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:name => 'person name attribute'} } }
+ assert_equal 'person name attribute', Child.human_attribute_name('name')
end
def test_translated_model_names
- I18n.backend.store_translations 'en', :activemodel => {:models => {:super_user => 'super_user model'} }
- assert_equal 'super_user model', SuperUser.model_name.human
+ I18n.backend.store_translations 'en', :activemodel => {:models => {:person => 'person model'} }
+ assert_equal 'person model', Person.model_name.human
end
def test_translated_model_names_with_sti
- I18n.backend.store_translations 'en', :activemodel => {:models => {:user => 'user model'} }
- assert_equal 'user model', User.model_name.human
+ I18n.backend.store_translations 'en', :activemodel => {:models => {:child => 'child model'} }
+ assert_equal 'child model', Child.model_name.human
end
def test_translated_model_names_with_ancestors_fallback
- I18n.backend.store_translations 'en', :activemodel => {:models => {:super_user => 'super_user model'} }
- assert_equal 'super_user model', User.model_name.human
+ I18n.backend.store_translations 'en', :activemodel => {:models => {:person => 'person model'} }
+ assert_equal 'person model', Child.model_name.human
end
end
View
33 activemodel/test/cases/validations/acceptance_validation_test.rb
@@ -9,9 +9,10 @@
class AcceptanceValidationTest < ActiveModel::TestCase
include ActiveModel::TestsDatabase
- include ActiveModel::ValidationsRepairHelper
- repair_validations(Topic)
+ def teardown
+ Topic.reset_callbacks(:validate)
+ end
def test_terms_of_service_agreement_no_acceptance
Topic.validates_acceptance_of(:terms_of_service, :on => :create)
@@ -53,28 +54,18 @@ def test_terms_of_service_agreement_with_accept_value
assert t.save
end
- def test_validates_acceptance_of_with_custom_error_using_quotes
- repair_validations(Developer) do
- Developer.validates_acceptance_of :salary, :message=> "This string contains 'single' and \"double\" quotes"
- d = Developer.new
- d.salary = "0"
- assert !d.valid?
- assert_equal "This string contains 'single' and \"double\" quotes", d.errors[:salary].last
- end
- end
-
def test_validates_acceptance_of_for_ruby_class
- repair_validations(Person) do
- Person.validates_acceptance_of :karma
+ Person.validates_acceptance_of :karma
- p = Person.new
- p.karma = ""
+ p = Person.new
+ p.karma = ""
- assert p.invalid?
- assert_equal ["must be accepted"], p.errors[:karma]
+ assert p.invalid?
+ assert_equal ["must be accepted"], p.errors[:karma]
- p.karma = "1"
- assert p.valid?
- end
+ p.karma = "1"
+ assert p.valid?
+ ensure
+ Person.reset_callbacks(:validate)
end
end
View
5 activemodel/test/cases/validations/conditional_validation_test.rb
@@ -6,9 +6,10 @@
class ConditionalValidationTest < ActiveModel::TestCase
include ActiveModel::TestsDatabase
- include ActiveModel::ValidationsRepairHelper
- repair_validations(Topic)
+ def teardown
+ Topic.reset_callbacks(:validate)
+ end
def test_if_validation_using_method_true
# When the method returns true
View
34 activemodel/test/cases/validations/confirmation_validation_test.rb
@@ -8,9 +8,10 @@
class ConfirmationValidationTest < ActiveModel::TestCase
include ActiveModel::TestsDatabase
- include ActiveModel::ValidationsRepairHelper
- repair_validations(Topic)
+ def teardown
+ Topic.reset_callbacks(:validate)
+ end
def test_no_title_confirmation
Topic.validates_confirmation_of(:title)
@@ -39,30 +40,19 @@ def test_title_confirmation
assert t.save
end
- def test_validates_confirmation_of_with_custom_error_using_quotes
- repair_validations(Developer) do
- Developer.validates_confirmation_of :name, :message=> "confirm 'single' and \"double\" quotes"
- d = Developer.new
- d.name = "John"
- d.name_confirmation = "Johnny"
- assert !d.valid?
- assert_equal ["confirm 'single' and \"double\" quotes"], d.errors[:name]
- end
- end
-
def test_validates_confirmation_of_for_ruby_class
- repair_validations(Person) do
- Person.validates_confirmation_of :karma
+ Person.validates_confirmation_of :karma
- p = Person.new
- p.karma_confirmation = "None"
- assert p.invalid?
+ p = Person.new
+ p.karma_confirmation = "None"
+ assert p.invalid?
- assert_equal ["doesn't match confirmation"], p.errors[:karma]
+ assert_equal ["doesn't match confirmation"], p.errors[:karma]
- p.karma = "None"
- assert p.valid?
- end
+ p.karma = "None"
+ assert p.valid?
+ ensure
+ Person.reset_callbacks(:validate)
end
end
View
23 activemodel/test/cases/validations/exclusion_validation_test.rb
@@ -7,9 +7,10 @@
class ExclusionValidationTest < ActiveModel::TestCase
include ActiveModel::TestsDatabase
- include ActiveModel::ValidationsRepairHelper
- repair_validations(Topic)
+ def teardown
+ Topic.reset_callbacks(:validate)
+ end
def test_validates_exclusion_of
Topic.validates_exclusion_of( :title, :in => %w( abe monkey ) )
@@ -30,17 +31,17 @@ def test_validates_exclusion_of_with_formatted_message
end
def test_validates_exclusion_of_for_ruby_class
- repair_validations(Person) do
- Person.validates_exclusion_of :karma, :in => %w( abe monkey )
+ Person.validates_exclusion_of :karma, :in => %w( abe monkey )
- p = Person.new
- p.karma = "abe"
- assert p.invalid?
+ p = Person.new
+ p.karma = "abe"
+ assert p.invalid?
- assert_equal ["is reserved"], p.errors[:karma]
+ assert_equal ["is reserved"], p.errors[:karma]
- p.karma = "Lifo"
- assert p.valid?
- end
+ p.karma = "Lifo"
+ assert p.valid?
+ ensure
+ Person.reset_callbacks(:validate)
end
end
View
33 activemodel/test/cases/validations/format_validation_test.rb
@@ -8,9 +8,10 @@
class PresenceValidationTest < ActiveModel::TestCase
include ActiveModel::TestsDatabase
- include ActiveModel::ValidationsRepairHelper
- repair_validations(Topic)
+ def teardown
+ Topic.reset_callbacks(:validate)
+ end
def test_validate_format
Topic.validates_format_of(:title, :content, :with => /^Validation\smacros \w+!$/, :message => "is bad data")
@@ -100,28 +101,18 @@ def test_validates_format_of_when_not_isnt_a_regexp_should_raise_error
assert_raise(ArgumentError) { Topic.validates_format_of(:title, :without => "clearly not a regexp") }
end
- def test_validates_format_of_with_custom_error_using_quotes
- repair_validations(Developer) do
- Developer.validates_format_of :name, :with => /^(A-Z*)$/, :message=> "format 'single' and \"double\" quotes"
- d = Developer.new
- d.name = d.name_confirmation = "John 32"
- assert !d.valid?
- assert_equal ["format 'single' and \"double\" quotes"], d.errors[:name]
- end
- end
-
def test_validates_format_of_for_ruby_class
- repair_validations(Person) do
- Person.validates_format_of :karma, :with => /\A\d+\Z/
+ Person.validates_format_of :karma, :with => /\A\d+\Z/
- p = Person.new
- p.karma = "Pixies"
- assert p.invalid?
+ p = Person.new
+ p.karma = "Pixies"
+ assert p.invalid?
- assert_equal ["is invalid"], p.errors[:karma]
+ assert_equal ["is invalid"], p.errors[:karma]
- p.karma = "1234"
- assert p.valid?
- end
+ p.karma = "1234"
+ assert p.valid?
+ ensure
+ Person.reset_callbacks(:validate)
end
end
View
1 activemodel/test/cases/validations/i18n_validation_test.rb
@@ -1,6 +1,5 @@
require "cases/helper"
require 'cases/tests_database'
-
require 'models/person'
class I18nValidationTest < ActiveModel::TestCase
View
33 activemodel/test/cases/validations/inclusion_validation_test.rb
@@ -8,9 +8,10 @@
class InclusionValidationTest < ActiveModel::TestCase
include ActiveModel::TestsDatabase
- include ActiveModel::ValidationsRepairHelper
- repair_validations(Topic)
+ def teardown
+ Topic.reset_callbacks(:validate)
+ end
def test_validates_inclusion_of
Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ) )
@@ -53,28 +54,18 @@ def test_validates_inclusion_of_with_formatted_message
assert_equal ["option uhoh is not in the list"], t.errors[:title]
end
- def test_validates_inclusion_of_with_custom_error_using_quotes
- repair_validations(Developer) do
- Developer.validates_inclusion_of :salary, :in => 1000..80000, :message=> "This string contains 'single' and \"double\" quotes"
- d = Developer.new
- d.salary = "90,000"
- assert !d.valid?
- assert_equal "This string contains 'single' and \"double\" quotes", d.errors[:salary].last
- end
- end
-
def test_validates_inclusion_of_for_ruby_class
- repair_validations(Person) do
- Person.validates_inclusion_of :karma, :in => %w( abe monkey )
+ Person.validates_inclusion_of :karma, :in => %w( abe monkey )
- p = Person.new
- p.karma = "Lifo"
- assert p.invalid?
+ p = Person.new
+ p.karma = "Lifo"
+ assert p.invalid?
- assert_equal ["is not included in the list"], p.errors[:karma]
+ assert_equal ["is not included in the list"], p.errors[:karma]
- p.karma = "monkey"
- assert p.valid?
- end
+ p.karma = "monkey"
+ assert p.valid?
+ ensure
+ Person.reset_callbacks(:validate)
end
end
View
53 activemodel/test/cases/validations/length_validation_test.rb
@@ -8,9 +8,10 @@
class LengthValidationTest < ActiveModel::TestCase
include ActiveModel::TestsDatabase
- include ActiveModel::ValidationsRepairHelper
- repair_validations(Topic)
+ def teardown
+ Topic.reset_callbacks(:validate)
+ end
def test_validates_length_of_with_allow_nil
Topic.validates_length_of( :title, :is => 5, :allow_nil=>true )
@@ -419,48 +420,18 @@ def test_validates_length_of_with_block
assert_equal ["Your essay must be at least 5 words."], t.errors[:content]
end
- def test_validates_length_of_with_custom_too_long_using_quotes
- repair_validations(Developer) do
- Developer.validates_length_of :name, :maximum => 4, :too_long=> "This string contains 'single' and \"double\" quotes"
- d = Developer.new
- d.name = "Jeffrey"
- assert !d.valid?
- assert_equal ["This string contains 'single' and \"double\" quotes"], d.errors[:name]
- end
- end
-
- def test_validates_length_of_with_custom_too_short_using_quotes
- repair_validations(Developer) do
- Developer.validates_length_of :name, :minimum => 4, :too_short=> "This string contains 'single' and \"double\" quotes"
- d = Developer.new
- d.name = "Joe"
- assert !d.valid?
- assert_equal ["This string contains 'single' and \"double\" quotes"], d.errors[:name]
- end
- end
-
- def test_validates_length_of_with_custom_message_using_quotes
- repair_validations(Developer) do
- Developer.validates_length_of :name, :minimum => 4, :message=> "This string contains 'single' and \"double\" quotes"
- d = Developer.new
- d.name = "Joe"
- assert !d.valid?
- assert_equal ["This string contains 'single' and \"double\" quotes"], d.errors[:name]
- end
- end
-
def test_validates_length_of_for_ruby_class
- repair_validations(Person) do
- Person.validates_length_of :karma, :minimum => 5
+ Person.validates_length_of :karma, :minimum => 5
- p = Person.new
- p.karma = "Pix"
- assert p.invalid?
+ p = Person.new
+ p.karma = "Pix"
+ assert p.invalid?
- assert_equal ["is too short (minimum is 5 characters)"], p.errors[:karma]
+ assert_equal ["is too short (minimum is 5 characters)"], p.errors[:karma]
- p.karma = "The Smiths"
- assert p.valid?
- end
+ p.karma = "The Smiths"
+ assert p.valid?
+ ensure
+ Person.reset_callbacks(:validate)
end
end
View
41 activemodel/test/cases/validations/numericality_validation_test.rb
@@ -8,9 +8,10 @@
class NumericalityValidationTest < ActiveModel::TestCase
include ActiveModel::TestsDatabase
- include ActiveModel::ValidationsRepairHelper
- repair_validations(Topic)
+ def teardown
+ Topic.reset_callbacks(:validate)
+ end
NIL = [nil]
BLANK = ["", " ", " \t \r \n"]
@@ -138,37 +139,19 @@ def test_validates_numericality_with_numeric_message
assert_equal ["greater than 4"], topic.errors[:approved]
end
- def test_numericality_with_getter_method
- repair_validations(Developer) do
- Developer.validates_numericality_of( :salary )
- developer = Developer.new("name" => "michael", "salary" => nil)
- developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end")
- assert developer.valid?
- end
- end
-
- def test_numericality_with_allow_nil_and_getter_method
- repair_validations(Developer) do
- Developer.validates_numericality_of( :salary, :allow_nil => true)
- developer = Developer.new("name" => "michael", "salary" => nil)
- developer.instance_eval("def salary; read_attribute('salary') ? read_attribute('salary') : 100000; end")
- assert developer.valid?
- end
- end
-
def test_validates_numericality_of_for_ruby_class
- repair_validations(Person) do
- Person.validates_numericality_of :karma, :allow_nil => false
+ Person.validates_numericality_of :karma, :allow_nil => false
- p = Person.new
- p.karma = "Pix"
- assert p.invalid?
+ p = Person.new
+ p.karma = "Pix"
+ assert p.invalid?
- assert_equal ["is not a number"], p.errors[:karma]
+ assert_equal ["is not a number"], p.errors[:karma]
- p.karma = "1234"
- assert p.valid?
- end
+ p.karma = "1234"
+ assert p.valid?
+ ensure
+ Person.reset_callbacks(:validate)
end
private
View
54 activemodel/test/cases/validations/presence_validation_test.rb
@@ -9,9 +9,6 @@
class PresenceValidationTest < ActiveModel::TestCase
include ActiveModel::TestsDatabase
- include ActiveModel::ValidationsRepairHelper
-
- repair_validations(Topic)
def test_validate_presences
Topic.validates_presence_of(:title, :content)
@@ -30,43 +27,44 @@ def test_validate_presences
t.content = "like stuff"
assert t.save
+ ensure
+ Topic.reset_callbacks(:validate)
end
- # def test_validates_presence_of_with_custom_message_using_quotes
- # repair_validations(Developer) do
- # Developer.validates_presence_of :non_existent, :message=> "This string contains 'single' and \"double\" quotes"
- # d = Developer.new
- # d.name = "Joe"
- # assert !d.valid?
- # assert_equal ["This string contains 'single' and \"double\" quotes"], d.errors[:non_existent]
- # end
- # end
+ def test_validates_acceptance_of_with_custom_error_using_quotes
+ Person.validates_presence_of :karma, :message=> "This string contains 'single' and \"double\" quotes"
+ p = Person.new
+ assert !p.valid?
+ assert_equal "This string contains 'single' and \"double\" quotes", p.errors[:karma].last
+ ensure
+ Person.reset_callbacks(:validate)
+ end
def test_validates_presence_of_for_ruby_class
- repair_validations(Person) do
- Person.validates_presence_of :karma
+ Person.validates_presence_of :karma
- p = Person.new
- assert p.invalid?
+ p = Person.new
+ assert p.invalid?
- assert_equal ["can't be blank"], p.errors[:karma]
+ assert_equal ["can't be blank"], p.errors[:karma]
- p.karma = "Cold"
- assert p.valid?
- end
+ p.karma = "Cold"
+ assert p.valid?
+ ensure
+ Person.reset_callbacks(:validate)
end
def test_validates_presence_of_for_ruby_class_with_custom_reader
- repair_validations(Person) do
- CustomReader.validates_presence_of :karma
+ CustomReader.validates_presence_of :karma
- p = CustomReader.new
- assert p.invalid?
+ p = CustomReader.new
+ assert p.invalid?
- assert_equal ["can't be blank"], p.errors[:karma]
+ assert_equal ["can't be blank"], p.errors[:karma]
- p[:karma] = "Cold"
- assert p.valid?
- end
+ p[:karma] = "Cold"
+ assert p.valid?
+ ensure
+ CustomReader.reset_callbacks(:validate)
end
end
View
19 activemodel/test/cases/validations/with_validation_test.rb
@@ -6,32 +6,33 @@
class ValidatesWithTest < ActiveRecord::TestCase
include ActiveModel::TestsDatabase
- include ActiveModel::ValidationsRepairHelper
- repair_validations(Topic)
+ def teardown
+ Topic.reset_callbacks(:validate)
+ end
ERROR_MESSAGE = "Validation error from validator"
OTHER_ERROR_MESSAGE = "Validation error from other validator"
class ValidatorThatAddsErrors < ActiveModel::Validator
- def validate()
+ def validate(record)
record.errors[:base] << ERROR_MESSAGE
end
end
class OtherValidatorThatAddsErrors < ActiveModel::Validator
- def validate()
+ def validate(record)
record.errors[:base] << OTHER_ERROR_MESSAGE
end
end
class ValidatorThatDoesNotAddErrors < ActiveModel::Validator
- def validate()
+ def validate(record)
end
end
class ValidatorThatValidatesOptions < ActiveModel::Validator
- def validate()
+ def validate(record)
if options[:field] == :first_name
record.errors[:base] << ERROR_MESSAGE
end
@@ -98,11 +99,11 @@ def validate()
assert topic.errors[:base].include?(ERROR_MESSAGE)
end
- test "passes all non-standard configuration options to the validator class" do
+ test "passes all configuration options to the validator class" do
topic = Topic.new
validator = mock()
- validator.expects(:new).with(topic, {:foo => :bar}).returns(validator)
- validator.expects(:validate)
+ validator.expects(:new).with(:foo => :bar, :if => "1 == 1").returns(validator)
+ validator.expects(:validate).with(topic)
Topic.validates_with(validator, :if => "1 == 1", :foo => :bar)
assert topic.valid?
View
5 activemodel/test/cases/validations_test.rb
@@ -9,11 +9,12 @@
class ValidationsTest < ActiveModel::TestCase
include ActiveModel::TestsDatabase
- include ActiveModel::ValidationsRepairHelper
# Most of the tests mess with the validations of Topic, so lets repair it all the time.
# Other classes we mess with will be dealt with in the specific tests
- repair_validations(Topic)
+ def teardown
+ Topic.reset_callbacks(:validate)
+ end
def test_single_field_validation
r = Reply.new
View
6 activemodel/test/models/person.rb
@@ -1,5 +1,9 @@
class Person
include ActiveModel::Validations
+ extend ActiveModel::Translation
- attr_accessor :title, :karma
+ attr_accessor :title, :karma, :salary
+end
+
+class Child < Person
end
View
4 activemodel/test/models/track_back.rb
@@ -0,0 +1,4 @@
+class Post
+ class TrackBack
+ end
+end
View
23 activerecord/CHANGELOG
@@ -1,5 +1,28 @@
*Edge*
+* Add Model.having and Relation#having. [Pratik Naik]
+
+ Developer.group("salary").having("sum(salary) > 10000").select("salary")
+
+* Add Relation#count. [Pratik Naik]
+
+ legends = People.where("age > 100")
+ legends.count
+ legends.count(:age, :distinct => true)
+ legends.select('id').count
+
+* Add Model.readonly and association_collection#readonly finder method. [Pratik Naik]
+
+ Post.readonly.to_a # Load all posts in readonly mode
+ @user.items.readonly(false).to_a # Load all the user items in writable mode
+
+* Add .lock finder method [Pratik Naik]
+
+ User.lock.where(:name => 'lifo').to_a
+
+ old_items = Item.where("age > 100")
+ old_items.lock.each {|i| .. }
+
* Add Model.from and association_collection#from finder methods [Pratik Naik]
user = User.scoped
View
1 activerecord/lib/active_record.rb
@@ -48,6 +48,7 @@ module ActiveRecord
autoload :Attributes
autoload :AutosaveAssociation
autoload :Relation
+ autoload :RelationalCalculations
autoload :Base
autoload :Batches
autoload :Calculations
View
21 activerecord/lib/active_record/associations.rb
@@ -3,8 +3,8 @@
module ActiveRecord
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
- def initialize(reflection)
- super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{reflection.class_name})")
+ def initialize(reflection, associated_class = nil)
+ super("Could not find the inverse association for #{reflection.name} (#{reflection.options[:inverse_of].inspect} in #{associated_class.nil? ? reflection.class_name : associated_class.name})")
end
end
@@ -1466,11 +1466,10 @@ def add_touch_callbacks(reflection, touch_attribute)
end
def find_with_associations(options = {}, join_dependency = nil)
- catch :invalid_query do
- join_dependency ||= JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
- rows = select_all_rows(options, join_dependency)
- return join_dependency.instantiate(rows)
- end
+ join_dependency ||= JoinDependency.new(self, merge_includes(scope(:find, :include), options[:include]), options[:joins])
+ rows = select_all_rows(options, join_dependency)
+ join_dependency.instantiate(rows)
+ rescue ThrowResult
[]
end
@@ -1715,7 +1714,8 @@ def construct_finder_arel_with_included_associations(options, join_dependency)
relation = relation.joins(construct_join(options[:joins], scope)).
select(column_aliases(join_dependency)).
- group(construct_group(options[:group], options[:having], scope)).
+ group(options[:group] || (scope && scope[:group])).
+ having(options[:having] || (scope && scope[:having])).
order(construct_order(options[:order], scope)).
where(construct_conditions(options[:conditions], scope)).
from((scope && scope[:from]) || options[:from])
@@ -1732,7 +1732,7 @@ def construct_finder_sql_with_included_associations(options, join_dependency)
def construct_arel_limited_ids_condition(options, join_dependency)
if (ids_array = select_limited_ids_array(options, join_dependency)).empty?
- throw :invalid_query
+ raise ThrowResult
else
Arel::Predicates::In.new(
Arel::SqlLiteral.new("#{connection.quote_table_name table_name}.#{primary_key}"),
@@ -1759,7 +1759,8 @@ def construct_finder_sql_for_association_limiting(options, join_dependency)
relation = relation.joins(construct_join(options[:joins], scope)).
where(construct_conditions(options[:conditions], scope)).
- group(construct_group(options[:group], options[:having], scope)).
+ group(options[:group] || (scope && scope[:group])).
+ having(options[:having] || (scope && scope[:having])).
order(construct_order(options[:order], scope)).
limit(construct_limit(options[:limit], scope)).
offset(construct_limit(options[:offset], scope)).
View
4 activerecord/lib/active_record/associations/association_collection.rb
@@ -21,7 +21,7 @@ def initialize(owner, reflection)
construct_sql
end
- delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :to => :scoped
+ delegate :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :lock, :readonly, :having, :to => :scoped
def select(select = nil, &block)
if block_given?
@@ -177,7 +177,7 @@ def count(*args)
if @reflection.options[:counter_sql]
@reflection.klass.count_by_sql(@counter_sql)
else
- column_name, options = @reflection.klass.send(:construct_count_options_from_args, *args)
+ column_name, options = @reflection.klass.scoped.send(:construct_count_options_from_args, *args)
if @reflection.options[:uniq]
# This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
column_name = "#{@reflection.quoted_table_name}.#{@reflection.klass.primary_key}" if column_name == :all
View
44 activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -13,6 +13,7 @@ def replace(record)
@updated = true
end
+ set_inverse_instance(record, @owner)
loaded
record
end
@@ -22,21 +23,44 @@ def updated?
end
private
- def find_target
- return nil if association_class.nil?
- if @reflection.options[:conditions]
- association_class.find(
- @owner[@reflection.primary_key_name],
- :select => @reflection.options[:select],
- :conditions => conditions,
- :include => @reflection.options[:include]
- )
+ # NOTE - for now, we're only supporting inverse setting from belongs_to back onto
+ # has_one associations.
+ def we_can_set_the_inverse_on_this?(record)
+ if @reflection.has_inverse?
+ inverse_association = @reflection.polymorphic_inverse_of(record.class)
+ inverse_association && inverse_association.macro == :has_one
else
- association_class.find(@owner[@reflection.primary_key_name], :select => @reflection.options[:select], :include => @reflection.options[:include])
+ false
+ end
+ end
+
+ def set_inverse_instance(record, instance)
+ return if record.nil? || !we_can_set_the_inverse_on_this?(record)
+ inverse_relationship = @reflection.polymorphic_inverse_of(record.class)
+ unless inverse_relationship.nil?
+ record.send(:"set_#{inverse_relationship.name}_target", instance)
end
end
+ def find_target
+ return nil if association_class.nil?
+
+ target =
+ if @reflection.options[:conditions]
+ association_class.find(
+ @owner[@reflection.primary_key_name],
+ :select => @reflection.options[:select],
+ :conditions => conditions,
+ :include => @reflection.options[:include]
+ )
+ else
+ association_class.find(@owner[@reflection.primary_key_name], :select => @reflection.options[:select], :include => @reflection.options[:include])
+ end
+ set_inverse_instance(target, @owner)
+ target
+ end
+
def foreign_key_present
!@owner[@reflection.primary_key_name].nil?
end
View
4 activerecord/lib/active_record/associations/has_one_association.rb
@@ -57,6 +57,7 @@ def replace(obj, dont_save = false)
@target = (AssociationProxy === obj ? obj.target : obj)
end
+ set_inverse_instance(obj, @owner)
@loaded = true
unless @owner.new_record? or obj.nil? or dont_save
@@ -120,10 +121,9 @@ def new_record(replace_existing)
else
record[@reflection.primary_key_name] = @owner.id unless @owner.new_record?
self.target = record
+ set_inverse_instance(record, @owner)
end
- set_inverse_instance(record, @owner)
-
record
end
View
40 activerecord/lib/active_record/autosave_association.rb
@@ -155,35 +155,47 @@ def #{type}(name, options = {})
# Adds a validate and save callback for the association as specified by
# the +reflection+.
+ #
+ # For performance reasons, we don't check whether to validate at runtime,
+ # but instead only define the method and callback when needed. However,
+ # this can change, for instance, when using nested attributes. Since we
+ # don't want the callbacks to get defined multiple times, there are
+ # guards that check if the save or validation methods have already been
+ # defined before actually defining them.
def add_autosave_association_callbacks(reflection)
save_method = "autosave_associated_records_for_#{reflection.name}"
validation_method = "validate_associated_records_for_#{reflection.name}"
force_validation = (reflection.options[:validate] == true || reflection.options[:autosave] == true)
case reflection.macro
when :has_many, :has_and_belongs_to_many
- before_save :before_save_collection_association
+ unless method_defined?(save_method)
+ before_save :before_save_collection_association
- define_method(save_method) { save_collection_association(reflection) }
- # Doesn't use after_save as that would save associations added in after_create/after_update twice
- after_create save_method
- after_update save_method
+ define_method(save_method) { save_collection_association(reflection) }
+ # Doesn't use after_save as that would save associations added in after_create/after_update twice
+ after_create save_method
+ after_update save_method
+ end
- if force_validation || (reflection.macro == :has_many && reflection.options[:validate] != false)
+ if !method_defined?(validation_method) &&
+ (force_validation || (reflection.macro == :has_many && reflection.options[:validate] != false))
define_method(validation_method) { validate_collection_association(reflection) }
validate validation_method
end
else
- case reflection.macro
- when :has_one
- define_method(save_method) { save_has_one_association(reflection) }
- after_save save_method
- when :belongs_to
- define_method(save_method) { save_belongs_to_association(reflection) }
- before_save save_method
+ unless method_defined?(save_method)
+ case reflection.macro
+ when :has_one
+ define_method(save_method) { save_has_one_association(reflection) }
+ after_save save_method
+ when :belongs_to
+ define_method(save_method) { save_belongs_to_association(reflection) }
+ before_save save_method
+ end
end
- if force_validation
+ if !method_defined?(validation_method) && force_validation
define_method(validation_method) { validate_single_association(reflection) }
validate validation_method
end
View
37 activerecord/lib/active_record/base.rb
@@ -69,6 +69,10 @@ class RecordNotSaved < ActiveRecordError
class StatementInvalid < ActiveRecordError
end
+ # Raised when SQL statement is invalid and the application gets a blank result.
+ class ThrowResult < ActiveRecordError
+ end
+
# Parent class for all specific exceptions which wrap database driver exceptions
# provides access to the original exception also.
class WrappedDatabaseException < StatementInvalid
@@ -652,7 +656,7 @@ def find(*args)
end
end
- delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :to => :scoped
+ delegate :select, :group, :order, :limit, :joins, :where, :preload, :eager_load, :from, :lock, :readonly, :having, :to => :scoped
# A convenience wrapper for <tt>find(:first, *args)</tt>. You can pass in all the
# same arguments to this method as you can to <tt>find(:first)</tt>.
@@ -1560,19 +1564,22 @@ def default_select(qualified)
end
def construct_finder_arel(options = {}, scope = scope(:find))
- # TODO add lock to Arel
validate_find_options(options)
relation = arel_table.
joins(construct_join(options[:joins], scope)).
where(construct_conditions(options[:conditions], scope)).
select(options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins]))).
- group(construct_group(options[:group], options[:having], scope)).
+ group(options[:group] || (scope && scope[:group])).
+ having(options[:having] || (scope && scope[:having])).
order(construct_order(options[:order], scope)).
limit(construct_limit(options[:limit], scope)).
offset(construct_offset(options[:offset], scope)).
from(options[:from])
+ lock = (scope && scope[:lock]) || options[:lock]
+ relation = relation.lock if lock.present?
+
relation = relation.readonly if options[:readonly]
relation
@@ -1593,10 +1600,6 @@ def construct_finder_arel_with_includes(options = {})
relation
end
- def construct_finder_sql(options, scope = scope(:find))
- construct_finder_arel(options, scope).to_sql
- end
-
def construct_join(joins, scope)
merged_joins = scope && scope[:joins] && joins ? merge_joins(scope[:joins], joins) : (joins || scope && scope[:joins])
case merged_joins
@@ -1613,18 +1616,6 @@ def construct_join(joins, scope)
end
end
- def construct_group(group, having, scope)
- sql = ''
- if group
- sql << group.to_s
- sql << " HAVING #{sanitize_sql_for_conditions(having)}" if having
- elsif scope && (scoped_group = scope[:group])
- sql << scoped_group.to_s
- sql << " HAVING #{sanitize_sql_for_conditions(scope[:having])}" if scope[:having]
- end
- sql
- end
-
def construct_order(order, scope)
orders = []
@@ -1703,14 +1694,6 @@ def array_of_strings?(o)
o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)}
end
- # The optional scope argument is for the current <tt>:find</tt> scope.
- # The <tt>:lock</tt> option has precedence over a scoped <tt>:lock</tt>.
- def add_lock!(sql, options, scope = :auto)
- scope = scope(:find) if :auto == scope
- options = options.reverse_merge(:lock => scope[:lock]) if scope
- connection.add_lock!(sql, options)
- end
-
def type_condition(table_alias=nil)
quoted_table_alias = self.connection.quote_table_name(table_alias || table_name)
quoted_inheritance_column = connection.quote_column_name(inheritance_column)
View
200 activerecord/lib/active_record/calculations.rb
@@ -44,7 +44,26 @@ module ClassMethods
#
# Note: <tt>Person.count(:all)</tt> will not work because it will use <tt>:all</tt> as the condition. Use Person.count instead.
def count(*args)
- calculate(:count, *construct_count_options_from_args(*args))
+ case args.size
+ when 0
+ construct_calculation_arel.count
+ when 1
+ if args[0].is_a?(Hash)
+ options = args[0]