Merge branch 'master' of github.com:rails/rails
This commit is contained in:
commit
643862e3be
1
Gemfile
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|
|
||||
|
9
Rakefile
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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -1,6 +1,5 @@
|
||||
require "cases/helper"
|
||||
require 'cases/tests_database'
|
||||
|
||||
require 'models/person'
|
||||
|
||||
class I18nValidationTest < ActiveModel::TestCase
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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?
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
4
activemodel/test/models/track_back.rb
Normal file
4
activemodel/test/models/track_back.rb
Normal file
@ -0,0 +1,4 @@
|
||||
class Post
|
||||
class TrackBack
|
||||
end
|
||||
end
|
@ -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
|
||||
|
@ -48,6 +48,7 @@ module ActiveRecord
|
||||
autoload :Attributes
|
||||
autoload :AutosaveAssociation
|
||||
autoload :Relation
|
||||
autoload :RelationalCalculations
|
||||
autoload :Base
|
||||
autoload :Batches
|
||||
autoload :Calculations
|
||||
|
@ -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)).
|
||||
|
@ -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
|
||||
|
@ -13,6 +13,7 @@ def replace(record)
|
||||
@updated = true
|
||||
end
|
||||
|
||||
set_inverse_instance(record, @owner)
|
||||
loaded
|
||||
record
|
||||
end
|
||||
@ -22,19 +23,42 @@ def updated?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# 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
|
||||
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?
|
||||
|
||||
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
|
||||
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
|
||||
|
@ -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
|
||||
|
||||
|
@ -155,6 +155,13 @@ 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}"
|
||||
@ -162,28 +169,33 @@ def add_autosave_association_callbacks(reflection)
|
||||
|
||||
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
|
||||
|
@ -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)
|
||||
|
@ -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]
|
||||
distinct = options.has_key?(:distinct) ? options.delete(:distinct) : false
|
||||
construct_calculation_arel(options).count(options[:select], :distinct => distinct)
|
||||
else
|
||||
construct_calculation_arel.count(args[0])
|
||||
end
|
||||
when 2
|
||||
column_name, options = args
|
||||
distinct = options.has_key?(:distinct) ? options.delete(:distinct) : false
|
||||
construct_calculation_arel(options).count(column_name, :distinct => distinct)
|
||||
else
|
||||
raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}"
|
||||
end
|
||||
rescue ThrowResult
|
||||
0
|
||||
end
|
||||
|
||||
# Calculates the average value on a given column. The value is returned as
|
||||
@ -122,168 +141,63 @@ def sum(column_name, options = {})
|
||||
# Person.minimum(:age, :having => 'min(age) > 17', :group => :last_name) # Selects the minimum age for any family without any minors
|
||||
# Person.sum("2 * age")
|
||||
def calculate(operation, column_name, options = {})
|
||||
validate_calculation_options(operation, options)
|
||||
operation = operation.to_s.downcase
|
||||
|
||||
scope = scope(:find)
|
||||
|
||||
merged_includes = merge_includes(scope ? scope[:include] : [], options[:include])
|
||||
|
||||
if operation == "count"
|
||||
if merged_includes.any?
|
||||
distinct = true
|
||||
column_name = options[:select] || primary_key
|
||||
end
|
||||
|
||||
distinct = nil if column_name.to_s =~ /\s*DISTINCT\s+/i
|
||||
distinct ||= options[:distinct]
|
||||
else
|
||||
distinct = nil
|
||||
end
|
||||
|
||||
catch :invalid_query do
|
||||
relation = if merged_includes.any?
|
||||
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, merged_includes, construct_join(options[:joins], scope))
|
||||
construct_finder_arel_with_included_associations(options, join_dependency)
|
||||
else
|
||||
relation = arel_table(options[:from]).
|
||||
joins(construct_join(options[:joins], scope)).
|
||||
where(construct_conditions(options[:conditions], scope)).
|
||||
order(options[:order]).
|
||||
limit(options[:limit]).
|
||||
offset(options[:offset])
|
||||
end
|
||||
if options[:group]
|
||||
return execute_grouped_calculation(operation, column_name, options, relation)
|
||||
else
|
||||
return execute_simple_calculation(operation, column_name, options.merge(:distinct => distinct), relation)
|
||||
end
|
||||
end
|
||||
construct_calculation_arel(options).calculate(operation, column_name, options.slice(:distinct))
|
||||
rescue ThrowResult
|
||||
0
|
||||
end
|
||||
|
||||
def execute_simple_calculation(operation, column_name, options, relation) #:nodoc:
|
||||
column = if column_names.include?(column_name.to_s)
|
||||
Arel::Attribute.new(arel_table(options[:from] || table_name),
|
||||
options[:select] || column_name)
|
||||
else
|
||||
Arel::SqlLiteral.new(options[:select] ||
|
||||
(column_name == :all ? "*" : column_name.to_s))
|
||||
end
|
||||
|
||||
relation = relation.select(operation == 'count' ? column.count(options[:distinct]) : column.send(operation))
|
||||
|
||||
type_cast_calculated_value(connection.select_value(relation.to_sql), column_for(column_name), operation)
|
||||
end
|
||||
|
||||
def execute_grouped_calculation(operation, column_name, options, relation) #:nodoc:
|
||||
group_attr = options[:group].to_s
|
||||
association = reflect_on_association(group_attr.to_sym)
|
||||
associated = association && association.macro == :belongs_to # only count belongs_to associations
|
||||
group_field = associated ? association.primary_key_name : group_attr
|
||||
group_alias = column_alias_for(group_field)
|
||||
group_column = column_for group_field
|
||||
|
||||
options[:group] = connection.adapter_name == 'FrontBase' ? group_alias : group_field
|
||||
|
||||
aggregate_alias = column_alias_for(operation, column_name)
|
||||
|
||||
options[:select] = (operation == 'count' && column_name == :all) ?
|
||||
"COUNT(*) AS count_all" :
|
||||
Arel::Attribute.new(arel_table, column_name).send(operation).as(aggregate_alias).to_sql
|
||||
|
||||
options[:select] << ", #{group_field} AS #{group_alias}"
|
||||
|
||||
relation = relation.select(options[:select]).group(construct_group(options[:group], options[:having], nil))
|
||||
|
||||
calculated_data = connection.select_all(relation.to_sql)
|
||||
|
||||
if association
|
||||
key_ids = calculated_data.collect { |row| row[group_alias] }
|
||||
key_records = association.klass.base_class.find(key_ids)
|
||||
key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) }
|
||||
end
|
||||
|
||||
calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row|
|
||||
key = type_cast_calculated_value(row[group_alias], group_column)
|
||||
key = key_records[key] if associated
|
||||
value = row[aggregate_alias]
|
||||
all[key] = type_cast_calculated_value(value, column_for(column_name), operation)
|
||||
all
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
def construct_count_options_from_args(*args)
|
||||
options = {}
|
||||
column_name = :all
|
||||
|
||||
# We need to handle
|
||||
# count()
|
||||
# count(:column_name=:all)
|
||||
# count(options={})
|
||||
# count(column_name=:all, options={})
|
||||
# selects specified by scopes
|
||||
case args.size
|
||||
when 0
|
||||
column_name = scope(:find)[:select] if scope(:find)
|
||||
when 1
|
||||
if args[0].is_a?(Hash)
|
||||
column_name = scope(:find)[:select] if scope(:find)
|
||||
options = args[0]
|
||||
else
|
||||
column_name = args[0]
|
||||
end
|
||||
when 2
|
||||
column_name, options = args
|
||||
else
|
||||
raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}"
|
||||
end
|
||||
|
||||
[column_name || :all, options]
|
||||
end
|
||||
|
||||
private
|
||||
def validate_calculation_options(operation, options = {})
|
||||
def validate_calculation_options(options = {})
|
||||
options.assert_valid_keys(CALCULATIONS_OPTIONS)
|
||||
end
|
||||
|
||||
# Converts the given keys to the value that the database adapter returns as
|
||||
# a usable column name:
|
||||
#
|
||||
# column_alias_for("users.id") # => "users_id"
|
||||
# column_alias_for("sum(id)") # => "sum_id"
|
||||
# column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
|
||||
# column_alias_for("count(*)") # => "count_all"
|
||||
# column_alias_for("count", "id") # => "count_id"
|
||||
def column_alias_for(*keys)
|
||||
table_name = keys.join(' ')
|
||||
table_name.downcase!
|
||||
table_name.gsub!(/\*/, 'all')
|
||||
table_name.gsub!(/\W+/, ' ')
|
||||
table_name.strip!
|
||||
table_name.gsub!(/ +/, '_')
|
||||
def construct_calculation_arel(options = {})
|
||||
validate_calculation_options(options)
|
||||
options = options.except(:distinct)
|
||||
|
||||
connection.table_alias_for(table_name)
|
||||
end
|
||||
scope = scope(:find)
|
||||
includes = merge_includes(scope ? scope[:include] : [], options[:include])
|
||||
|
||||
def column_for(field)
|
||||
field_name = field.to_s.split('.').last
|
||||
columns.detect { |c| c.name.to_s == field_name }
|
||||
end
|
||||
|
||||
def type_cast_calculated_value(value, column, operation = nil)
|
||||
case operation
|
||||
when 'count' then value.to_i
|
||||
when 'sum' then type_cast_using_column(value || '0', column)
|
||||
when 'average' then value && (value.is_a?(Fixnum) ? value.to_f : value).to_d
|
||||
else type_cast_using_column(value, column)
|
||||
if includes.any?
|
||||
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(self, includes, construct_join(options[:joins], scope))
|
||||
construct_calculation_arel_with_included_associations(options, join_dependency)
|
||||
else
|
||||
arel_table.
|
||||
joins(construct_join(options[:joins], scope)).
|
||||
from((scope && scope[:from]) || options[:from]).
|
||||
where(construct_conditions(options[:conditions], scope)).
|
||||
order(options[:order]).
|
||||
limit(options[:limit]).
|
||||
offset(options[:offset]).
|
||||
group(options[:group]).
|
||||
having(options[:having]).
|
||||
select(options[:select] || (scope && scope[:select]) || default_select(options[:joins] || (scope && scope[:joins])))
|
||||
end
|
||||
end
|
||||
|
||||
def type_cast_using_column(value, column)
|
||||
column ? column.type_cast(value) : value
|
||||
def construct_calculation_arel_with_included_associations(options, join_dependency)
|
||||
scope = scope(:find)
|
||||
|
||||
relation = arel_table
|
||||
|
||||
for association in join_dependency.join_associations
|
||||
relation = association.join_relation(relation)
|
||||
end
|
||||
|
||||
relation = relation.joins(construct_join(options[:joins], scope)).
|
||||
select(column_aliases(join_dependency)).
|
||||
group(options[:group]).
|
||||
having(options[:having]).
|
||||
order(options[:order]).
|
||||
where(construct_conditions(options[:conditions], scope)).
|
||||
from((scope && scope[:from]) || options[:from])
|
||||
|
||||
relation = relation.where(construct_arel_limited_ids_condition(options, join_dependency)) if !using_limitable_reflections?(join_dependency.reflections) && ((scope && scope[:limit]) || options[:limit])
|
||||
relation = relation.limit(construct_limit(options[:limit], scope)) if using_limitable_reflections?(join_dependency.reflections)
|
||||
|
||||
relation
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -181,18 +181,6 @@ def commit_db_transaction() end
|
||||
# done if the transaction block raises an exception or returns false.
|
||||
def rollback_db_transaction() end
|
||||
|
||||
# Appends a locking clause to an SQL statement.
|
||||
# This method *modifies* the +sql+ parameter.
|
||||
# # SELECT * FROM suppliers FOR UPDATE
|
||||
# add_lock! 'SELECT * FROM suppliers', :lock => true
|
||||
# add_lock! 'SELECT * FROM suppliers', :lock => ' FOR UPDATE'
|
||||
def add_lock!(sql, options)
|
||||
case lock = options[:lock]
|
||||
when true; sql << ' FOR UPDATE'
|
||||
when String; sql << " #{lock}"
|
||||
end
|
||||
end
|
||||
|
||||
def default_sequence_name(table, column)
|
||||
nil
|
||||
end
|
||||
|
@ -183,12 +183,6 @@ def rollback_db_transaction #:nodoc:
|
||||
catch_schema_changes { @connection.rollback }
|
||||
end
|
||||
|
||||
# SELECT ... FOR UPDATE is redundant since the table is locked.
|
||||
def add_lock!(sql, options) #:nodoc:
|
||||
sql
|
||||
end
|
||||
|
||||
|
||||
# SCHEMA STATEMENTS ========================================
|
||||
|
||||
def tables(name = nil) #:nodoc:
|
||||
|
@ -212,6 +212,11 @@ module ClassMethods
|
||||
# nested attributes array exceeds the specified limit, NestedAttributes::TooManyRecords
|
||||
# exception is raised. If omitted, any number associations can be processed.
|
||||
# Note that the :limit option is only applicable to one-to-many associations.
|
||||
# [:update_only]
|
||||
# Allows you to specify that an existing record may only be updated.
|
||||
# A new record may only be created when there is no existing record.
|
||||
# This option only works for one-to-one associations and is ignored for
|
||||
# collection associations. This option is off by default.
|
||||
#
|
||||
# Examples:
|
||||
# # creates avatar_attributes=
|
||||
@ -221,9 +226,9 @@ module ClassMethods
|
||||
# # creates avatar_attributes= and posts_attributes=
|
||||
# accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true
|
||||
def accepts_nested_attributes_for(*attr_names)
|
||||
options = { :allow_destroy => false }
|
||||
options = { :allow_destroy => false, :update_only => false }
|
||||
options.update(attr_names.extract_options!)
|
||||
options.assert_valid_keys(:allow_destroy, :reject_if, :limit)
|
||||
options.assert_valid_keys(:allow_destroy, :reject_if, :limit, :update_only)
|
||||
|
||||
attr_names.each do |association_name|
|
||||
if reflection = reflect_on_association(association_name)
|
||||
@ -235,7 +240,7 @@ def accepts_nested_attributes_for(*attr_names)
|
||||
end
|
||||
|
||||
reflection.options[:autosave] = true
|
||||
|
||||
add_autosave_association_callbacks(reflection)
|
||||
self.nested_attributes_options[association_name.to_sym] = options
|
||||
|
||||
if options[:reject_if] == :all_blank
|
||||
@ -243,15 +248,13 @@ def accepts_nested_attributes_for(*attr_names)
|
||||
end
|
||||
|
||||
# def pirate_attributes=(attributes)
|
||||
# assign_nested_attributes_for_one_to_one_association(:pirate, attributes, false)
|
||||
# assign_nested_attributes_for_one_to_one_association(:pirate, attributes)
|
||||
# end
|
||||
class_eval %{
|
||||
def #{association_name}_attributes=(attributes)
|
||||
assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
|
||||
end
|
||||
}, __FILE__, __LINE__
|
||||
|
||||
add_autosave_association_callbacks(reflection)
|
||||
else
|
||||
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
|
||||
end
|
||||
@ -286,28 +289,29 @@ def _delete #:nodoc:
|
||||
|
||||
# Assigns the given attributes to the association.
|
||||
#
|
||||
# If the given attributes include an <tt>:id</tt> that matches the existing
|
||||
# record’s id, then the existing record will be modified. Otherwise a new
|
||||
# record will be built.
|
||||
# If update_only is false and the given attributes include an <tt>:id</tt>
|
||||
# that matches the existing record’s id, then the existing record will be
|
||||
# modified. If update_only is true, a new record is only created when no
|
||||
# object exists. Otherwise a new record will be built.
|
||||
#
|
||||
# If the given attributes include a matching <tt>:id</tt> attribute _and_ a
|
||||
# <tt>:_destroy</tt> key set to a truthy value, then the existing record
|
||||
# will be marked for destruction.
|
||||
# If the given attributes include a matching <tt>:id</tt> attribute, or
|
||||
# update_only is true, and a <tt>:_destroy</tt> key set to a truthy value,
|
||||
# then the existing record will be marked for destruction.
|
||||
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
|
||||
options = self.nested_attributes_options[association_name]
|
||||
attributes = attributes.with_indifferent_access
|
||||
check_existing_record = (options[:update_only] || !attributes['id'].blank?)
|
||||
|
||||
if attributes['id'].blank?
|
||||
unless reject_new_record?(association_name, attributes)
|
||||
method = "build_#{association_name}"
|
||||
if respond_to?(method)
|
||||
send(method, attributes.except(*UNASSIGNABLE_KEYS))
|
||||
else
|
||||
raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
|
||||
end
|
||||
if check_existing_record && (record = send(association_name)) &&
|
||||
(options[:update_only] || record.id.to_s == attributes['id'].to_s)
|
||||
assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy])
|
||||
elsif !reject_new_record?(association_name, attributes)
|
||||
method = "build_#{association_name}"
|
||||
if respond_to?(method)
|
||||
send(method, attributes.except(*UNASSIGNABLE_KEYS))
|
||||
else
|
||||
raise ArgumentError, "Cannot build association #{association_name}. Are you trying to build a polymorphic one-to-one association?"
|
||||
end
|
||||
elsif (existing_record = send(association_name)) && existing_record.id.to_s == attributes['id'].to_s
|
||||
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -214,8 +214,10 @@ def check_validity!
|
||||
end
|
||||
|
||||
def check_validity_of_inverse!
|
||||
if has_inverse? && inverse_of.nil?
|
||||
raise InverseOfAssociationNotFoundError.new(self)
|
||||
unless options[:polymorphic]
|
||||
if has_inverse? && inverse_of.nil?
|
||||
raise InverseOfAssociationNotFoundError.new(self)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -237,8 +239,16 @@ def has_inverse?
|
||||
def inverse_of
|
||||
if has_inverse?
|
||||
@inverse_of ||= klass.reflect_on_association(options[:inverse_of])
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def polymorphic_inverse_of(associated_class)
|
||||
if has_inverse?
|
||||
if inverse_relationship = associated_class.reflect_on_association(options[:inverse_of])
|
||||
inverse_relationship
|
||||
else
|
||||
raise InverseOfAssociationNotFoundError.new(self, associated_class)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1,9 +1,10 @@
|
||||
module ActiveRecord
|
||||
class Relation
|
||||
delegate :to_sql, :to => :relation
|
||||
delegate :length, :collect, :map, :each, :to => :to_a
|
||||
delegate :length, :collect, :map, :each, :all?, :to => :to_a
|
||||
attr_reader :relation, :klass, :associations_to_preload, :eager_load_associations
|
||||
|
||||
include RelationalCalculations
|
||||
def initialize(klass, relation, readonly = false, preload = [], eager_load = [])
|
||||
@klass, @relation = klass, relation
|
||||
@readonly = readonly
|
||||
@ -13,6 +14,8 @@ def initialize(klass, relation, readonly = false, preload = [], eager_load = [])
|
||||
end
|
||||
|
||||
def merge(r)
|
||||
raise ArgumentError, "Cannot merge a #{r.klass.name} relation with #{@klass.name} relation" if r.klass != @klass
|
||||
|
||||
joins(r.relation.joins(r.relation)).
|
||||
group(r.send(:group_clauses).join(', ')).
|
||||
order(r.send(:order_clauses).join(', ')).
|
||||
@ -22,7 +25,7 @@ def merge(r)
|
||||
select(r.send(:select_clauses).join(', ')).
|
||||
eager_load(r.eager_load_associations).
|
||||
preload(r.associations_to_preload).
|
||||
from(r.send(:sources).any? ? r.send(:from_clauses) : nil)
|
||||
from(r.send(:sources).present? ? r.send(:from_clauses) : nil)
|
||||
end
|
||||
|
||||
alias :& :merge
|
||||
@ -35,18 +38,35 @@ def eager_load(*associations)
|
||||
create_new_relation(@relation, @readonly, @associations_to_preload, @eager_load_associations + Array.wrap(associations))
|
||||
end
|
||||
|
||||
def readonly
|
||||
create_new_relation(@relation, true)
|
||||
def readonly(status = true)
|
||||
status.nil? ? create_new_relation : create_new_relation(@relation, status)
|
||||
end
|
||||
|
||||
def select(selects)
|
||||
selects.present? ? create_new_relation(@relation.project(selects)) : create_new_relation
|
||||
if selects.present?
|
||||
frozen = @relation.joins(relation).present? ? false : @readonly
|
||||
create_new_relation(@relation.project(selects), frozen)
|
||||
else
|
||||
create_new_relation
|
||||
end
|
||||
end
|
||||
|
||||
def from(from)
|
||||
from.present? ? create_new_relation(@relation.from(from)) : create_new_relation
|
||||
end
|
||||
|
||||
def having(*args)
|
||||
return create_new_relation if args.blank?
|
||||
|
||||
if [String, Hash, Array].include?(args.first.class)
|
||||
havings = @klass.send(:merge_conditions, args.size > 1 ? Array.wrap(args) : args.first)
|
||||
else
|
||||
havings = args.first
|
||||
end
|
||||
|
||||
create_new_relation(@relation.having(havings))
|
||||
end
|
||||
|
||||
def group(groups)
|
||||
groups.present? ? create_new_relation(@relation.group(groups)) : create_new_relation
|
||||
end
|
||||
@ -55,6 +75,17 @@ def order(orders)
|
||||
orders.present? ? create_new_relation(@relation.order(orders)) : create_new_relation
|
||||
end
|
||||
|
||||
def lock(locks = true)
|
||||
case locks
|
||||
when String
|
||||
create_new_relation(@relation.lock(locks))
|
||||
when TrueClass, NilClass
|
||||
create_new_relation(@relation.lock)
|
||||
else
|
||||
create_new_relation
|
||||
end
|
||||
end
|
||||
|
||||
def reverse_order
|
||||
relation = create_new_relation
|
||||
relation.instance_variable_set(:@orders, nil)
|
||||
@ -95,7 +126,7 @@ def joins(join, join_type = nil)
|
||||
@relation.join(join, join_type)
|
||||
end
|
||||
|
||||
create_new_relation(join_relation)
|
||||
create_new_relation(join_relation, true)
|
||||
end
|
||||
|
||||
def where(*args)
|
||||
@ -118,8 +149,8 @@ def to_a
|
||||
return @records if loaded?
|
||||
|
||||
@records = if @eager_load_associations.any?
|
||||
catch :invalid_query do
|
||||
return @klass.send(:find_with_associations, {
|
||||
begin
|
||||
@klass.send(:find_with_associations, {
|
||||
:select => @relation.send(:select_clauses).join(', '),
|
||||
:joins => @relation.joins(relation),
|
||||
:group => @relation.send(:group_clauses).join(', '),
|
||||
@ -127,11 +158,12 @@ def to_a
|
||||
:conditions => where_clause,
|
||||
:limit => @relation.taken,
|
||||
:offset => @relation.skipped,
|
||||
:from => (@relation.send(:from_clauses) if @relation.send(:sources).any?)
|
||||
:from => (@relation.send(:from_clauses) if @relation.send(:sources).present?)
|
||||
},
|
||||
ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, @eager_load_associations, nil))
|
||||
rescue ThrowResult
|
||||
[]
|
||||
end
|
||||
[]
|
||||
else
|
||||
@klass.find_by_sql(@relation.to_sql)
|
||||
end
|
||||
|
169
activerecord/lib/active_record/relational_calculations.rb
Normal file
169
activerecord/lib/active_record/relational_calculations.rb
Normal file
@ -0,0 +1,169 @@
|
||||
module ActiveRecord
|
||||
module RelationalCalculations
|
||||
|
||||
def count(*args)
|
||||
calculate(:count, *construct_count_options_from_args(*args))
|
||||
end
|
||||
|
||||
def average(column_name)
|
||||
calculate(:average, column_name)
|
||||
end
|
||||
|
||||
def minimum(column_name)
|
||||
calculate(:minimum, column_name)
|
||||
end
|
||||
|
||||
def maximum(column_name)
|
||||
calculate(:maximum, column_name)
|
||||
end
|
||||
|
||||
def sum(column_name)
|
||||
calculate(:sum, column_name)
|
||||
end
|
||||
|
||||
def calculate(operation, column_name, options = {})
|
||||
operation = operation.to_s.downcase
|
||||
|
||||
if operation == "count"
|
||||
joins = @relation.joins(relation)
|
||||
if joins.present? && joins =~ /LEFT OUTER/i
|
||||
distinct = true
|
||||
column_name = @klass.primary_key if column_name == :all
|
||||
end
|
||||
|
||||
distinct = nil if column_name.to_s =~ /\s*DISTINCT\s+/i
|
||||
distinct ||= options[:distinct]
|
||||
else
|
||||
distinct = nil
|
||||
end
|
||||
|
||||
distinct = options[:distinct] || distinct
|
||||
column_name = :all if column_name.blank? && operation == "count"
|
||||
|
||||
if @relation.send(:groupings).any?
|
||||
return execute_grouped_calculation(operation, column_name)
|
||||
else
|
||||
return execute_simple_calculation(operation, column_name, distinct)
|
||||
end
|
||||
rescue ThrowResult
|
||||
0
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def execute_simple_calculation(operation, column_name, distinct) #:nodoc:
|
||||
column = if @klass.column_names.include?(column_name.to_s)
|
||||
Arel::Attribute.new(@klass.arel_table, column_name)
|
||||
else
|
||||
Arel::SqlLiteral.new(column_name == :all ? "*" : column_name.to_s)
|
||||
end
|
||||
|
||||
relation = select(operation == 'count' ? column.count(distinct) : column.send(operation))
|
||||
type_cast_calculated_value(@klass.connection.select_value(relation.to_sql), column_for(column_name), operation)
|
||||
end
|
||||
|
||||
def execute_grouped_calculation(operation, column_name) #:nodoc:
|
||||
group_attr = @relation.send(:groupings).first.value
|
||||
association = @klass.reflect_on_association(group_attr.to_sym)
|
||||
associated = association && association.macro == :belongs_to # only count belongs_to associations
|
||||
group_field = associated ? association.primary_key_name : group_attr
|
||||
group_alias = column_alias_for(group_field)
|
||||
group_column = column_for(group_field)
|
||||
|
||||
group = @klass.connection.adapter_name == 'FrontBase' ? group_alias : group_field
|
||||
|
||||
aggregate_alias = column_alias_for(operation, column_name)
|
||||
|
||||
select_statement = if operation == 'count' && column_name == :all
|
||||
"COUNT(*) AS count_all"
|
||||
else
|
||||
Arel::Attribute.new(@klass.arel_table, column_name).send(operation).as(aggregate_alias).to_sql
|
||||
end
|
||||
|
||||
select_statement << ", #{group_field} AS #{group_alias}"
|
||||
|
||||
relation = select(select_statement).group(group)
|
||||
|
||||
calculated_data = @klass.connection.select_all(relation.to_sql)
|
||||
|
||||
if association
|
||||
key_ids = calculated_data.collect { |row| row[group_alias] }
|
||||
key_records = association.klass.base_class.find(key_ids)
|
||||
key_records = key_records.inject({}) { |hsh, r| hsh.merge(r.id => r) }
|
||||
end
|
||||
|
||||
calculated_data.inject(ActiveSupport::OrderedHash.new) do |all, row|
|
||||
key = type_cast_calculated_value(row[group_alias], group_column)
|
||||
key = key_records[key] if associated
|
||||
value = row[aggregate_alias]
|
||||
all[key] = type_cast_calculated_value(value, column_for(column_name), operation)
|
||||
all
|
||||
end
|
||||
end
|
||||
|
||||
def construct_count_options_from_args(*args)
|
||||
options = {}
|
||||
column_name = :all
|
||||
|
||||
# Handles count(), count(:column), count(:distinct => true), count(:column, :distinct => true)
|
||||
# TODO : relation.projections only works when .select() was last in the chain. Fix it!
|
||||
case args.size
|
||||
when 0
|
||||
select = @relation.send(:select_clauses).join(', ') if @relation.respond_to?(:projections) && @relation.projections.present?
|
||||
column_name = select if select !~ /(,|\*)/
|
||||
when 1
|
||||
if args[0].is_a?(Hash)
|
||||
select = @relation.send(:select_clauses).join(', ') if @relation.respond_to?(:projections) && @relation.projections.present?
|
||||
column_name = select if select !~ /(,|\*)/
|
||||
options = args[0]
|
||||
else
|
||||
column_name = args[0]
|
||||
end
|
||||
when 2
|
||||
column_name, options = args
|
||||
else
|
||||
raise ArgumentError, "Unexpected parameters passed to count(): #{args.inspect}"
|
||||
end
|
||||
|
||||
[column_name || :all, options]
|
||||
end
|
||||
|
||||
# Converts the given keys to the value that the database adapter returns as
|
||||
# a usable column name:
|
||||
#
|
||||
# column_alias_for("users.id") # => "users_id"
|
||||
# column_alias_for("sum(id)") # => "sum_id"
|
||||
# column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
|
||||
# column_alias_for("count(*)") # => "count_all"
|
||||
# column_alias_for("count", "id") # => "count_id"
|
||||
def column_alias_for(*keys)
|
||||
table_name = keys.join(' ')
|
||||
table_name.downcase!
|
||||
table_name.gsub!(/\*/, 'all')
|
||||
table_name.gsub!(/\W+/, ' ')
|
||||
table_name.strip!
|
||||
table_name.gsub!(/ +/, '_')
|
||||
|
||||
@klass.connection.table_alias_for(table_name)
|
||||
end
|
||||
|
||||
def column_for(field)
|
||||
field_name = field.to_s.split('.').last
|
||||
@klass.columns.detect { |c| c.name.to_s == field_name }
|
||||
end
|
||||
|
||||
def type_cast_calculated_value(value, column, operation = nil)
|
||||
case operation
|
||||
when 'count' then value.to_i
|
||||
when 'sum' then type_cast_using_column(value || '0', column)
|
||||
when 'average' then value && (value.is_a?(Fixnum) ? value.to_f : value).to_d
|
||||
else type_cast_using_column(value, column)
|
||||
end
|
||||
end
|
||||
|
||||
def type_cast_using_column(value, column)
|
||||
column ? column.type_cast(value) : value
|
||||
end
|
||||
|
||||
end
|
||||
end
|
@ -1,5 +1,12 @@
|
||||
module ActiveRecord
|
||||
module Validations
|
||||
class AssociatedValidator < ActiveModel::EachValidator
|
||||
def validate_each(record, attribute, value)
|
||||
return if (value.is_a?(Array) ? value : [value]).collect{ |r| r.nil? || r.valid? }.all?
|
||||
record.errors.add(attribute, :invalid, :default => options[:message], :value => value)
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Validates whether the associated object or objects are all valid themselves. Works with any kind of association.
|
||||
#
|
||||
@ -33,13 +40,8 @@ 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_associated(*attr_names)
|
||||
configuration = attr_names.extract_options!
|
||||
|
||||
validates_each(attr_names, configuration) do |record, attr_name, value|
|
||||
unless (value.is_a?(Array) ? value : [value]).collect { |r| r.nil? || r.valid? }.all?
|
||||
record.errors.add(attr_name, :invalid, :default => configuration[:message], :value => value)
|
||||
end
|
||||
end
|
||||
options = attr_names.extract_options!
|
||||
validates_with AssociatedValidator, options.merge(:attributes => attr_names)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,5 +1,77 @@
|
||||
module ActiveRecord
|
||||
module Validations
|
||||
class UniquenessValidator < ActiveModel::EachValidator
|
||||
def initialize(options)
|
||||
@klass = options.delete(:klass)
|
||||
super(options.reverse_merge(:case_sensitive => true))
|
||||
end
|
||||
|
||||
def validate_each(record, attribute, value)
|
||||
finder_class = find_finder_class_for(record)
|
||||
table_name = record.class.quoted_table_name
|
||||
sql, params = mount_sql_and_params(finder_class, table_name, attribute, value)
|
||||
|
||||
Array(options[:scope]).each do |scope_item|
|
||||
scope_value = record.send(scope_item)
|
||||
sql << " AND " << record.class.send(:attribute_condition, "#{table_name}.#{scope_item}", scope_value)
|
||||
params << scope_value
|
||||
end
|
||||
|
||||
unless record.new_record?
|
||||
sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?"
|
||||
params << record.send(:id)
|
||||
end
|
||||
|
||||
finder_class.send(:with_exclusive_scope) do
|
||||
if finder_class.exists?([sql, *params])
|
||||
record.errors.add(attribute, :taken, :default => options[:message], :value => value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# The check for an existing value should be run from a class that
|
||||
# isn't abstract. This means working down from the current class
|
||||
# (self), to the first non-abstract class. Since classes don't know
|
||||
# their subclasses, we have to build the hierarchy between self and
|
||||
# the record's class.
|
||||
def find_finder_class_for(record) #:nodoc:
|
||||
class_hierarchy = [record.class]
|
||||
|
||||
while class_hierarchy.first != @klass
|
||||
class_hierarchy.insert(0, class_hierarchy.first.superclass)
|
||||
end
|
||||
|
||||
class_hierarchy.detect { |klass| !klass.abstract_class? }
|
||||
end
|
||||
|
||||
def mount_sql_and_params(klass, table_name, attribute, value) #:nodoc:
|
||||
column = klass.columns_hash[attribute.to_s]
|
||||
|
||||
operator = if value.nil?
|
||||
"IS ?"
|
||||
elsif column.text?
|
||||
value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s
|
||||
"#{klass.connection.case_sensitive_equality_operator} ?"
|
||||
else
|
||||
"= ?"
|
||||
end
|
||||
|
||||
sql_attribute = "#{table_name}.#{klass.connection.quote_column_name(attribute)}"
|
||||
|
||||
if value.nil? || (options[:case_sensitive] || !column.text?)
|
||||
sql = "#{sql_attribute} #{operator}"
|
||||
params = [value]
|
||||
else
|
||||
sql = "LOWER(#{sql_attribute}) #{operator}"
|
||||
params = [value.mb_chars.downcase]
|
||||
end
|
||||
|
||||
[sql, params]
|
||||
end
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
# Validates whether the value of the specified attributes are unique across the system. Useful for making sure that only one user
|
||||
# can be named "davidhh".
|
||||
@ -69,6 +141,7 @@ module ClassMethods
|
||||
#
|
||||
# This could even happen if you use transactions with the 'serializable'
|
||||
# isolation level. There are several ways to get around this problem:
|
||||
#
|
||||
# - By locking the database table before validating, and unlocking it after
|
||||
# saving. However, table locking is very expensive, and thus not
|
||||
# recommended.
|
||||
@ -94,65 +167,10 @@ module ClassMethods
|
||||
# index constraint errors from other types of database errors, so you
|
||||
# will have to parse the (database-specific) exception message to detect
|
||||
# such a case.
|
||||
#
|
||||
def validates_uniqueness_of(*attr_names)
|
||||
configuration = { :case_sensitive => true }
|
||||
configuration.update(attr_names.extract_options!)
|
||||
|
||||
validates_each(attr_names,configuration) do |record, attr_name, value|
|
||||
# The check for an existing value should be run from a class that
|
||||
# isn't abstract. This means working down from the current class
|
||||
# (self), to the first non-abstract class. Since classes don't know
|
||||
# their subclasses, we have to build the hierarchy between self and
|
||||
# the record's class.
|
||||
class_hierarchy = [record.class]
|
||||
while class_hierarchy.first != self
|
||||
class_hierarchy.insert(0, class_hierarchy.first.superclass)
|
||||
end
|
||||
|
||||
# Now we can work our way down the tree to the first non-abstract
|
||||
# class (which has a database table to query from).
|
||||
finder_class = class_hierarchy.detect { |klass| !klass.abstract_class? }
|
||||
|
||||
column = finder_class.columns_hash[attr_name.to_s]
|
||||
|
||||
if value.nil?
|
||||
comparison_operator = "IS ?"
|
||||
elsif column.text?
|
||||
comparison_operator = "#{connection.case_sensitive_equality_operator} ?"
|
||||
value = column.limit ? value.to_s.mb_chars[0, column.limit] : value.to_s
|
||||
else
|
||||
comparison_operator = "= ?"
|
||||
end
|
||||
|
||||
sql_attribute = "#{record.class.quoted_table_name}.#{connection.quote_column_name(attr_name)}"
|
||||
|
||||
if value.nil? || (configuration[:case_sensitive] || !column.text?)
|
||||
condition_sql = "#{sql_attribute} #{comparison_operator}"
|
||||
condition_params = [value]
|
||||
else
|
||||
condition_sql = "LOWER(#{sql_attribute}) #{comparison_operator}"
|
||||
condition_params = [value.mb_chars.downcase]
|
||||
end
|
||||
|
||||
if scope = configuration[:scope]
|
||||
Array(scope).map do |scope_item|
|
||||
scope_value = record.send(scope_item)
|
||||
condition_sql << " AND " << attribute_condition("#{record.class.quoted_table_name}.#{scope_item}", scope_value)
|
||||
condition_params << scope_value
|
||||
end
|
||||
end
|
||||
|
||||
unless record.new_record?
|
||||
condition_sql << " AND #{record.class.quoted_table_name}.#{record.class.primary_key} <> ?"
|
||||
condition_params << record.send(:id)
|
||||
end
|
||||
|
||||
finder_class.with_exclusive_scope do
|
||||
if finder_class.exists?([condition_sql, *condition_params])
|
||||
record.errors.add(attr_name, :taken, :default => configuration[:message], :value => value)
|
||||
end
|
||||
end
|
||||
end
|
||||
options = attr_names.extract_options!
|
||||
validates_with UniquenessValidator, options.merge(:attributes => attr_names, :klass => self)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -462,7 +462,7 @@ def test_eager_with_has_many_and_limit_and_conditions_on_the_eagers
|
||||
|
||||
def test_eager_with_has_many_and_limit_and_scoped_conditions_on_the_eagers
|
||||
posts = nil
|
||||
Post.with_scope(:find => {
|
||||
Post.send(:with_scope, :find => {
|
||||
:include => :comments,
|
||||
:conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'"
|
||||
}) do
|
||||
@ -470,7 +470,7 @@ def test_eager_with_has_many_and_limit_and_scoped_conditions_on_the_eagers
|
||||
assert_equal 2, posts.size
|
||||
end
|
||||
|
||||
Post.with_scope(:find => {
|
||||
Post.send(:with_scope, :find => {
|
||||
:include => [ :comments, :author ],
|
||||
:conditions => "authors.name = 'David' AND (comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment')"
|
||||
}) do
|
||||
@ -480,7 +480,7 @@ def test_eager_with_has_many_and_limit_and_scoped_conditions_on_the_eagers
|
||||
end
|
||||
|
||||
def test_eager_with_has_many_and_limit_and_scoped_and_explicit_conditions_on_the_eagers
|
||||
Post.with_scope(:find => { :conditions => "1=1" }) do
|
||||
Post.send(:with_scope, :find => { :conditions => "1=1" }) do
|
||||
posts = authors(:david).posts.find(:all,
|
||||
:include => :comments,
|
||||
:conditions => "comments.body like 'Normal%' OR comments.#{QUOTED_TYPE}= 'SpecialComment'",
|
||||
@ -499,7 +499,7 @@ def test_eager_with_has_many_and_limit_and_scoped_and_explicit_conditions_on_the
|
||||
|
||||
def test_eager_with_scoped_order_using_association_limiting_without_explicit_scope
|
||||
posts_with_explicit_order = Post.find(:all, :conditions => 'comments.id is not null', :include => :comments, :order => 'posts.id DESC', :limit => 2)
|
||||
posts_with_scoped_order = Post.with_scope(:find => {:order => 'posts.id DESC'}) do
|
||||
posts_with_scoped_order = Post.send(:with_scope, :find => {:order => 'posts.id DESC'}) do
|
||||
Post.find(:all, :conditions => 'comments.id is not null', :include => :comments, :limit => 2)
|
||||
end
|
||||
assert_equal posts_with_explicit_order, posts_with_scoped_order
|
||||
|
@ -9,84 +9,84 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
|
||||
fixtures :authors, :posts, :comments, :categories, :categories_posts, :categorizations
|
||||
|
||||
def test_construct_finder_sql_creates_inner_joins
|
||||
sql = Author.send(:construct_finder_sql, :joins => :posts)
|
||||
sql = Author.joins(:posts).to_sql
|
||||
assert_match /INNER JOIN .?posts.? ON .?posts.?.author_id = authors.id/, sql
|
||||
end
|
||||
|
||||
def test_construct_finder_sql_cascades_inner_joins
|
||||
sql = Author.send(:construct_finder_sql, :joins => {:posts => :comments})
|
||||
sql = Author.joins(:posts => :comments).to_sql
|
||||
assert_match /INNER JOIN .?posts.? ON .?posts.?.author_id = authors.id/, sql
|
||||
assert_match /INNER JOIN .?comments.? ON .?comments.?.post_id = posts.id/, sql
|
||||
end
|
||||
|
||||
def test_construct_finder_sql_inner_joins_through_associations
|
||||
sql = Author.send(:construct_finder_sql, :joins => :categorized_posts)
|
||||
sql = Author.joins(:categorized_posts).to_sql
|
||||
assert_match /INNER JOIN .?categorizations.?.*INNER JOIN .?posts.?/, sql
|
||||
end
|
||||
|
||||
def test_construct_finder_sql_applies_association_conditions
|
||||
sql = Author.send(:construct_finder_sql, :joins => :categories_like_general, :conditions => "TERMINATING_MARKER")
|
||||
sql = Author.joins(:categories_like_general).where("TERMINATING_MARKER").to_sql
|
||||
assert_match /INNER JOIN .?categories.? ON.*AND.*.?General.?(.|\n)*TERMINATING_MARKER/, sql
|
||||
end
|
||||
|
||||
def test_construct_finder_sql_applies_aliases_tables_on_association_conditions
|
||||
result = Author.find(:all, :joins => [:thinking_posts, :welcome_posts])
|
||||
result = Author.joins(:thinking_posts, :welcome_posts).to_a
|
||||
assert_equal authors(:david), result.first
|
||||
end
|
||||
|
||||
def test_construct_finder_sql_unpacks_nested_joins
|
||||
sql = Author.send(:construct_finder_sql, :joins => {:posts => [[:comments]]})
|
||||
sql = Author.joins(:posts => [[:comments]]).to_sql
|
||||
assert_no_match /inner join.*inner join.*inner join/i, sql, "only two join clauses should be present"
|
||||
assert_match /INNER JOIN .?posts.? ON .?posts.?.author_id = authors.id/, sql
|
||||
assert_match /INNER JOIN .?comments.? ON .?comments.?.post_id = .?posts.?.id/, sql
|
||||
end
|
||||
|
||||
def test_construct_finder_sql_ignores_empty_joins_hash
|
||||
sql = Author.send(:construct_finder_sql, :joins => {})
|
||||
sql = Author.joins({}).to_sql
|
||||
assert_no_match /JOIN/i, sql
|
||||
end
|
||||
|
||||
def test_construct_finder_sql_ignores_empty_joins_array
|
||||
sql = Author.send(:construct_finder_sql, :joins => [])
|
||||
sql = Author.joins([]).to_sql
|
||||
assert_no_match /JOIN/i, sql
|
||||
end
|
||||
|
||||
def test_find_with_implicit_inner_joins_honors_readonly_without_select
|
||||
authors = Author.find(:all, :joins => :posts)
|
||||
authors = Author.joins(:posts).to_a
|
||||
assert !authors.empty?, "expected authors to be non-empty"
|
||||
assert authors.all? {|a| a.readonly? }, "expected all authors to be readonly"
|
||||
end
|
||||
|
||||
def test_find_with_implicit_inner_joins_honors_readonly_with_select
|
||||
authors = Author.find(:all, :select => 'authors.*', :joins => :posts)
|
||||
authors = Author.joins(:posts).select('authors.*').to_a
|
||||
assert !authors.empty?, "expected authors to be non-empty"
|
||||
assert authors.all? {|a| !a.readonly? }, "expected no authors to be readonly"
|
||||
end
|
||||
|
||||
def test_find_with_implicit_inner_joins_honors_readonly_false
|
||||
authors = Author.find(:all, :joins => :posts, :readonly => false)
|
||||
authors = Author.joins(:posts).readonly(false).to_a
|
||||
assert !authors.empty?, "expected authors to be non-empty"
|
||||
assert authors.all? {|a| !a.readonly? }, "expected no authors to be readonly"
|
||||
end
|
||||
|
||||
def test_find_with_implicit_inner_joins_does_not_set_associations
|
||||
authors = Author.find(:all, :select => 'authors.*', :joins => :posts)
|
||||
authors = Author.joins(:posts).select('authors.*')
|
||||
assert !authors.empty?, "expected authors to be non-empty"
|
||||
assert authors.all? {|a| !a.send(:instance_variable_names).include?("@posts")}, "expected no authors to have the @posts association loaded"
|
||||
end
|
||||
|
||||
def test_count_honors_implicit_inner_joins
|
||||
real_count = Author.find(:all).sum{|a| a.posts.count }
|
||||
real_count = Author.scoped.to_a.sum{|a| a.posts.count }
|
||||
assert_equal real_count, Author.count(:joins => :posts), "plain inner join count should match the number of referenced posts records"
|
||||
end
|
||||
|
||||
def test_calculate_honors_implicit_inner_joins
|
||||
real_count = Author.find(:all).sum{|a| a.posts.count }
|
||||
real_count = Author.scoped.to_a.sum{|a| a.posts.count }
|
||||
assert_equal real_count, Author.calculate(:count, 'authors.id', :joins => :posts), "plain inner join count should match the number of referenced posts records"
|
||||
end
|
||||
|
||||
def test_calculate_honors_implicit_inner_joins_and_distinct_and_conditions
|
||||
real_count = Author.find(:all).select {|a| a.posts.any? {|p| p.title =~ /^Welcome/} }.length
|
||||
real_count = Author.scoped.to_a.select {|a| a.posts.any? {|p| p.title =~ /^Welcome/} }.length
|
||||
authors_with_welcoming_post_titles = Author.calculate(:count, 'authors.id', :joins => :posts, :distinct => true, :conditions => "posts.title like 'Welcome%'")
|
||||
assert_equal real_count, authors_with_welcoming_post_titles, "inner join and conditions should have only returned authors posting titles starting with 'Welcome'"
|
||||
end
|
||||
|
@ -85,7 +85,7 @@ class InverseHasOneTests < ActiveRecord::TestCase
|
||||
fixtures :men, :faces
|
||||
|
||||
def test_parent_instance_should_be_shared_with_child_on_find
|
||||
m = Man.find(:first)
|
||||
m = men(:gordon)
|
||||
f = m.face
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
@ -96,7 +96,7 @@ def test_parent_instance_should_be_shared_with_child_on_find
|
||||
|
||||
|
||||
def test_parent_instance_should_be_shared_with_eager_loaded_child_on_find
|
||||
m = Man.find(:first, :include => :face)
|
||||
m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :face)
|
||||
f = m.face
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
@ -104,7 +104,7 @@ def test_parent_instance_should_be_shared_with_eager_loaded_child_on_find
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance"
|
||||
|
||||
m = Man.find(:first, :include => :face, :order => 'faces.id')
|
||||
m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :face, :order => 'faces.id')
|
||||
f = m.face
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
@ -114,7 +114,7 @@ def test_parent_instance_should_be_shared_with_eager_loaded_child_on_find
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_built_child
|
||||
m = Man.find(:first)
|
||||
m = men(:gordon)
|
||||
f = m.build_face(:description => 'haunted')
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
@ -125,7 +125,7 @@ def test_parent_instance_should_be_shared_with_newly_built_child
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_created_child
|
||||
m = Man.find(:first)
|
||||
m = men(:gordon)
|
||||
f = m.create_face(:description => 'haunted')
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
@ -135,6 +135,86 @@ def test_parent_instance_should_be_shared_with_newly_created_child
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_created_child_via_bang_method
|
||||
m = Man.find(:first)
|
||||
f = m.face.create!(:description => 'haunted')
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_built_child_when_we_dont_replace_existing
|
||||
m = Man.find(:first)
|
||||
f = m.build_face({:description => 'haunted'}, false)
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to just-built-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_created_child_when_we_dont_replace_existing
|
||||
m = Man.find(:first)
|
||||
f = m.create_face({:description => 'haunted'}, false)
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_created_child_via_bang_method_when_we_dont_replace_existing
|
||||
m = Man.find(:first)
|
||||
f = m.face.create!({:description => 'haunted'}, false)
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_replaced_via_accessor_child
|
||||
m = Man.find(:first)
|
||||
f = Face.new(:description => 'haunted')
|
||||
m.face = f
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_replaced_via_method_child
|
||||
m = Man.find(:first)
|
||||
f = Face.new(:description => 'haunted')
|
||||
m.face.replace(f)
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_replaced_via_method_child_when_we_dont_replace_existing
|
||||
m = Man.find(:first)
|
||||
f = Face.new(:description => 'haunted')
|
||||
m.face.replace(f, false)
|
||||
assert_not_nil f.man
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to parent instance"
|
||||
f.man.name = 'Mungo'
|
||||
assert_equal m.name, f.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
|
||||
end
|
||||
|
||||
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
|
||||
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.find(:first).dirty_face }
|
||||
end
|
||||
@ -144,7 +224,7 @@ class InverseHasManyTests < ActiveRecord::TestCase
|
||||
fixtures :men, :interests
|
||||
|
||||
def test_parent_instance_should_be_shared_with_every_child_on_find
|
||||
m = Man.find(:first)
|
||||
m = men(:gordon)
|
||||
is = m.interests
|
||||
is.each do |i|
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
@ -156,7 +236,7 @@ def test_parent_instance_should_be_shared_with_every_child_on_find
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_eager_loaded_children
|
||||
m = Man.find(:first, :include => :interests)
|
||||
m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :interests)
|
||||
is = m.interests
|
||||
is.each do |i|
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
@ -166,7 +246,7 @@ def test_parent_instance_should_be_shared_with_eager_loaded_children
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance"
|
||||
end
|
||||
|
||||
m = Man.find(:first, :include => :interests, :order => 'interests.id')
|
||||
m = Man.find(:first, :conditions => {:name => 'Gordon'}, :include => :interests, :order => 'interests.id')
|
||||
is = m.interests
|
||||
is.each do |i|
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
@ -175,11 +255,10 @@ def test_parent_instance_should_be_shared_with_eager_loaded_children
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance"
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_built_child
|
||||
m = Man.find(:first)
|
||||
m = men(:gordon)
|
||||
i = m.interests.build(:topic => 'Industrial Revolution Re-enactment')
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
@ -189,8 +268,20 @@ def test_parent_instance_should_be_shared_with_newly_built_child
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to just-built-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_created_child
|
||||
def test_parent_instance_should_be_shared_with_newly_block_style_built_child
|
||||
m = Man.find(:first)
|
||||
i = m.interests.build {|ii| ii.topic = 'Industrial Revolution Re-enactment'}
|
||||
assert_not_nil i.topic, "Child attributes supplied to build via blocks should be populated"
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to just-built-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_created_child
|
||||
m = men(:gordon)
|
||||
i = m.interests.create(:topic => 'Industrial Revolution Re-enactment')
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
@ -200,8 +291,31 @@ def test_parent_instance_should_be_shared_with_newly_created_child
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_poked_in_child
|
||||
def test_parent_instance_should_be_shared_with_newly_created_via_bang_method_child
|
||||
m = Man.find(:first)
|
||||
i = m.interests.create!(:topic => 'Industrial Revolution Re-enactment')
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_newly_block_style_created_child
|
||||
m = Man.find(:first)
|
||||
i = m.interests.create {|ii| ii.topic = 'Industrial Revolution Re-enactment'}
|
||||
assert_not_nil i.topic, "Child attributes supplied to create via blocks should be populated"
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_poked_in_child
|
||||
m = men(:gordon)
|
||||
i = Interest.create(:topic => 'Industrial Revolution Re-enactment')
|
||||
m.interests << i
|
||||
assert_not_nil i.man
|
||||
@ -212,6 +326,30 @@ def test_parent_instance_should_be_shared_with_poked_in_child
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_replaced_via_accessor_children
|
||||
m = Man.find(:first)
|
||||
i = Interest.new(:topic => 'Industrial Revolution Re-enactment')
|
||||
m.interests = [i]
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
|
||||
end
|
||||
|
||||
def test_parent_instance_should_be_shared_with_replaced_via_method_children
|
||||
m = Man.find(:first)
|
||||
i = Interest.new(:topic => 'Industrial Revolution Re-enactment')
|
||||
m.interests.replace([i])
|
||||
assert_not_nil i.man
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance"
|
||||
m.name = 'Bongo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to parent instance"
|
||||
i.man.name = 'Mungo'
|
||||
assert_equal m.name, i.man.name, "Name of man should be the same after changes to replaced-child-owned instance"
|
||||
end
|
||||
|
||||
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
|
||||
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.find(:first).secret_interests }
|
||||
end
|
||||
@ -221,7 +359,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase
|
||||
fixtures :men, :faces, :interests
|
||||
|
||||
def test_child_instance_should_be_shared_with_parent_on_find
|
||||
f = Face.find(:first)
|
||||
f = faces(:trusting)
|
||||
m = f.man
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
@ -231,7 +369,7 @@ def test_child_instance_should_be_shared_with_parent_on_find
|
||||
end
|
||||
|
||||
def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find
|
||||
f = Face.find(:first, :include => :man)
|
||||
f = Face.find(:first, :include => :man, :conditions => {:description => 'trusting'})
|
||||
m = f.man
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
@ -239,8 +377,7 @@ def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find
|
||||
m.face.description = 'pleasing'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance"
|
||||
|
||||
|
||||
f = Face.find(:first, :include => :man, :order => 'men.id')
|
||||
f = Face.find(:first, :include => :man, :order => 'men.id', :conditions => {:description => 'trusting'})
|
||||
m = f.man
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
@ -250,7 +387,7 @@ def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find
|
||||
end
|
||||
|
||||
def test_child_instance_should_be_shared_with_newly_built_parent
|
||||
f = Face.find(:first)
|
||||
f = faces(:trusting)
|
||||
m = f.build_man(:name => 'Charles')
|
||||
assert_not_nil m.face
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
@ -261,7 +398,7 @@ def test_child_instance_should_be_shared_with_newly_built_parent
|
||||
end
|
||||
|
||||
def test_child_instance_should_be_shared_with_newly_created_parent
|
||||
f = Face.find(:first)
|
||||
f = faces(:trusting)
|
||||
m = f.create_man(:name => 'Charles')
|
||||
assert_not_nil m.face
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
@ -272,7 +409,7 @@ def test_child_instance_should_be_shared_with_newly_created_parent
|
||||
end
|
||||
|
||||
def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many
|
||||
i = Interest.find(:first)
|
||||
i = interests(:trainspotting)
|
||||
m = i.man
|
||||
assert_not_nil m.interests
|
||||
iz = m.interests.detect {|iz| iz.id == i.id}
|
||||
@ -284,11 +421,128 @@ def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many
|
||||
assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to parent-owned instance"
|
||||
end
|
||||
|
||||
def test_child_instance_should_be_shared_with_replaced_via_accessor_parent
|
||||
f = Face.find(:first)
|
||||
m = Man.new(:name => 'Charles')
|
||||
f.man = m
|
||||
assert_not_nil m.face
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
|
||||
m.face.description = 'pleasing'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
|
||||
end
|
||||
|
||||
def test_child_instance_should_be_shared_with_replaced_via_method_parent
|
||||
f = faces(:trusting)
|
||||
assert_not_nil f.man
|
||||
m = Man.new(:name => 'Charles')
|
||||
f.man.replace(m)
|
||||
assert_not_nil m.face
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to child instance"
|
||||
m.face.description = 'pleasing'
|
||||
assert_equal f.description, m.face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
|
||||
end
|
||||
|
||||
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
|
||||
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_man }
|
||||
end
|
||||
end
|
||||
|
||||
class InversePolymorphicBelongsToTests < ActiveRecord::TestCase
|
||||
fixtures :men, :faces, :interests
|
||||
|
||||
def test_child_instance_should_be_shared_with_parent_on_find
|
||||
f = Face.find(:first, :conditions => {:description => 'confused'})
|
||||
m = f.polymorphic_man
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance"
|
||||
m.polymorphic_face.description = 'pleasing'
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance"
|
||||
end
|
||||
|
||||
def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find
|
||||
f = Face.find(:first, :conditions => {:description => 'confused'}, :include => :man)
|
||||
m = f.polymorphic_man
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance"
|
||||
m.polymorphic_face.description = 'pleasing'
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance"
|
||||
|
||||
f = Face.find(:first, :conditions => {:description => 'confused'}, :include => :man, :order => 'men.id')
|
||||
m = f.polymorphic_man
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance"
|
||||
f.description = 'gormless'
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to child instance"
|
||||
m.polymorphic_face.description = 'pleasing'
|
||||
assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance"
|
||||
end
|
||||
|
||||
def test_child_instance_should_be_shared_with_replaced_via_accessor_parent
|
||||
face = faces(:confused)
|
||||
old_man = face.polymorphic_man
|
||||
new_man = Man.new
|
||||
|
||||
assert_not_nil face.polymorphic_man
|
||||
face.polymorphic_man = new_man
|
||||
|
||||
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same before changes to parent instance"
|
||||
face.description = 'Bongo'
|
||||
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to parent instance"
|
||||
new_man.polymorphic_face.description = 'Mungo'
|
||||
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
|
||||
end
|
||||
|
||||
def test_child_instance_should_be_shared_with_replaced_via_method_parent
|
||||
face = faces(:confused)
|
||||
old_man = face.polymorphic_man
|
||||
new_man = Man.new
|
||||
|
||||
assert_not_nil face.polymorphic_man
|
||||
face.polymorphic_man.replace(new_man)
|
||||
|
||||
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same before changes to parent instance"
|
||||
face.description = 'Bongo'
|
||||
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to parent instance"
|
||||
new_man.polymorphic_face.description = 'Mungo'
|
||||
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
|
||||
end
|
||||
|
||||
def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many
|
||||
i = interests(:llama_wrangling)
|
||||
m = i.polymorphic_man
|
||||
assert_not_nil m.polymorphic_interests
|
||||
iz = m.polymorphic_interests.detect {|iz| iz.id == i.id}
|
||||
assert_not_nil iz
|
||||
assert_equal i.topic, iz.topic, "Interest topics should be the same before changes to child"
|
||||
i.topic = 'Eating cheese with a spoon'
|
||||
assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to child"
|
||||
iz.topic = 'Cow tipping'
|
||||
assert_not_equal i.topic, iz.topic, "Interest topics should not be the same after changes to parent-owned instance"
|
||||
end
|
||||
|
||||
def test_trying_to_access_inverses_that_dont_exist_shouldnt_raise_an_error
|
||||
# Ideally this would, if only for symmetry's sake with other association types
|
||||
assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_polymorphic_man }
|
||||
end
|
||||
|
||||
def test_trying_to_set_polymorphic_inverses_that_dont_exist_at_all_should_raise_an_error
|
||||
# fails because no class has the correct inverse_of for horrible_polymorphic_man
|
||||
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).horrible_polymorphic_man = Man.first }
|
||||
end
|
||||
|
||||
def test_trying_to_set_polymorphic_inverses_that_dont_exist_on_the_instance_being_set_should_raise_an_error
|
||||
# passes because Man does have the correct inverse_of
|
||||
assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).polymorphic_man = Man.first }
|
||||
# fails because Interest does have the correct inverse_of
|
||||
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.find(:first).polymorphic_man = Interest.first }
|
||||
end
|
||||
end
|
||||
|
||||
# NOTE - these tests might not be meaningful, ripped as they were from the parental_control plugin
|
||||
# which would guess the inverse rather than look for an explicit configuration option.
|
||||
class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase
|
||||
|
@ -31,11 +31,40 @@ def test_autosave_should_be_a_valid_option_for_has_and_belongs_to_many
|
||||
assert base.valid_keys_for_has_and_belongs_to_many_association.include?(:autosave)
|
||||
end
|
||||
|
||||
def test_should_not_add_the_same_callbacks_multiple_times_for_has_one
|
||||
assert_no_difference_when_adding_callbacks_twice_for Pirate, :ship
|
||||
end
|
||||
|
||||
def test_should_not_add_the_same_callbacks_multiple_times_for_belongs_to
|
||||
assert_no_difference_when_adding_callbacks_twice_for Ship, :pirate
|
||||
end
|
||||
|
||||
def test_should_not_add_the_same_callbacks_multiple_times_for_has_many
|
||||
assert_no_difference_when_adding_callbacks_twice_for Pirate, :birds
|
||||
end
|
||||
|
||||
def test_should_not_add_the_same_callbacks_multiple_times_for_has_and_belongs_to_many
|
||||
assert_no_difference_when_adding_callbacks_twice_for Pirate, :parrots
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def base
|
||||
ActiveRecord::Base
|
||||
end
|
||||
|
||||
def assert_no_difference_when_adding_callbacks_twice_for(model, association_name)
|
||||
reflection = model.reflect_on_association(association_name)
|
||||
assert_no_difference "callbacks_for_model(#{model.name}).length" do
|
||||
model.send(:add_autosave_association_callbacks, reflection)
|
||||
end
|
||||
end
|
||||
|
||||
def callbacks_for_model(model)
|
||||
model.instance_variables.grep(/_callbacks$/).map do |ivar|
|
||||
model.instance_variable_get(ivar)
|
||||
end.flatten
|
||||
end
|
||||
end
|
||||
|
||||
class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase
|
||||
|
@ -1825,7 +1825,7 @@ def test_interpolate_sql
|
||||
end
|
||||
|
||||
def test_scoped_find_conditions
|
||||
scoped_developers = Developer.with_scope(:find => { :conditions => 'salary > 90000' }) do
|
||||
scoped_developers = Developer.send(:with_scope, :find => { :conditions => 'salary > 90000' }) do
|
||||
Developer.find(:all, :conditions => 'id < 5')
|
||||
end
|
||||
assert !scoped_developers.include?(developers(:david)) # David's salary is less than 90,000
|
||||
@ -1833,7 +1833,7 @@ def test_scoped_find_conditions
|
||||
end
|
||||
|
||||
def test_scoped_find_limit_offset
|
||||
scoped_developers = Developer.with_scope(:find => { :limit => 3, :offset => 2 }) do
|
||||
scoped_developers = Developer.send(:with_scope, :find => { :limit => 3, :offset => 2 }) do
|
||||
Developer.find(:all, :order => 'id')
|
||||
end
|
||||
assert !scoped_developers.include?(developers(:david))
|
||||
@ -1847,17 +1847,17 @@ def test_scoped_find_limit_offset
|
||||
|
||||
def test_scoped_find_order
|
||||
# Test order in scope
|
||||
scoped_developers = Developer.with_scope(:find => { :limit => 1, :order => 'salary DESC' }) do
|
||||
scoped_developers = Developer.send(:with_scope, :find => { :limit => 1, :order => 'salary DESC' }) do
|
||||
Developer.find(:all)
|
||||
end
|
||||
assert_equal 'Jamis', scoped_developers.first.name
|
||||
assert scoped_developers.include?(developers(:jamis))
|
||||
# Test scope without order and order in find
|
||||
scoped_developers = Developer.with_scope(:find => { :limit => 1 }) do
|
||||
scoped_developers = Developer.send(:with_scope, :find => { :limit => 1 }) do
|
||||
Developer.find(:all, :order => 'salary DESC')
|
||||
end
|
||||
# Test scope order + find order, find has priority
|
||||
scoped_developers = Developer.with_scope(:find => { :limit => 3, :order => 'id DESC' }) do
|
||||
scoped_developers = Developer.send(:with_scope, :find => { :limit => 3, :order => 'id DESC' }) do
|
||||
Developer.find(:all, :order => 'salary ASC')
|
||||
end
|
||||
assert scoped_developers.include?(developers(:poor_jamis))
|
||||
@ -1869,7 +1869,7 @@ def test_scoped_find_order
|
||||
end
|
||||
|
||||
def test_scoped_find_limit_offset_including_has_many_association
|
||||
topics = Topic.with_scope(:find => {:limit => 1, :offset => 1, :include => :replies}) do
|
||||
topics = Topic.send(:with_scope, :find => {:limit => 1, :offset => 1, :include => :replies}) do
|
||||
Topic.find(:all, :order => "topics.id")
|
||||
end
|
||||
assert_equal 1, topics.size
|
||||
@ -1877,7 +1877,7 @@ def test_scoped_find_limit_offset_including_has_many_association
|
||||
end
|
||||
|
||||
def test_scoped_find_order_including_has_many_association
|
||||
developers = Developer.with_scope(:find => { :order => 'developers.salary DESC', :include => :projects }) do
|
||||
developers = Developer.send(:with_scope, :find => { :order => 'developers.salary DESC', :include => :projects }) do
|
||||
Developer.find(:all)
|
||||
end
|
||||
assert developers.size >= 2
|
||||
@ -1887,7 +1887,7 @@ def test_scoped_find_order_including_has_many_association
|
||||
end
|
||||
|
||||
def test_scoped_find_with_group_and_having
|
||||
developers = Developer.with_scope(:find => { :group => 'developers.salary', :having => "SUM(salary) > 10000", :select => "SUM(salary) as salary" }) do
|
||||
developers = Developer.send(:with_scope, :find => { :group => 'developers.salary', :having => "SUM(salary) > 10000", :select => "SUM(salary) as salary" }) do
|
||||
Developer.find(:all)
|
||||
end
|
||||
assert_equal 3, developers.size
|
||||
@ -1933,7 +1933,7 @@ def test_find_symbol_ordered_last
|
||||
end
|
||||
|
||||
def test_find_scoped_ordered_last
|
||||
last_developer = Developer.with_scope(:find => { :order => 'developers.salary ASC' }) do
|
||||
last_developer = Developer.send(:with_scope, :find => { :order => 'developers.salary ASC' }) do
|
||||
Developer.find(:last)
|
||||
end
|
||||
assert_equal last_developer, Developer.find(:all, :order => 'developers.salary ASC').last
|
||||
|
@ -29,8 +29,8 @@ def test_should_return_nil_as_average
|
||||
end
|
||||
|
||||
def test_type_cast_calculated_value_should_convert_db_averages_of_fixnum_class_to_decimal
|
||||
assert_equal 0, NumericData.send(:type_cast_calculated_value, 0, nil, 'avg')
|
||||
assert_equal 53.0, NumericData.send(:type_cast_calculated_value, 53, nil, 'avg')
|
||||
assert_equal 0, NumericData.scoped.send(:type_cast_calculated_value, 0, nil, 'avg')
|
||||
assert_equal 53.0, NumericData.scoped.send(:type_cast_calculated_value, 53, nil, 'avg')
|
||||
end
|
||||
|
||||
def test_should_get_maximum_of_field
|
||||
@ -42,7 +42,7 @@ def test_should_get_maximum_of_field_with_include
|
||||
end
|
||||
|
||||
def test_should_get_maximum_of_field_with_scoped_include
|
||||
Account.with_scope :find => { :include => :firm, :conditions => "companies.name != 'Summit'" } do
|
||||
Account.send :with_scope, :find => { :include => :firm, :conditions => "companies.name != 'Summit'" } do
|
||||
assert_equal 50, Account.maximum(:credit_limit)
|
||||
end
|
||||
end
|
||||
@ -248,17 +248,15 @@ def test_should_group_by_summed_field_through_association_and_having
|
||||
|
||||
def test_should_reject_invalid_options
|
||||
assert_nothing_raised do
|
||||
[:count, :sum].each do |func|
|
||||
# empty options are valid
|
||||
Company.send(:validate_calculation_options, func)
|
||||
# these options are valid for all calculations
|
||||
[:select, :conditions, :joins, :order, :group, :having, :distinct].each do |opt|
|
||||
Company.send(:validate_calculation_options, func, opt => true)
|
||||
end
|
||||
# empty options are valid
|
||||
Company.send(:validate_calculation_options)
|
||||
# these options are valid for all calculations
|
||||
[:select, :conditions, :joins, :order, :group, :having, :distinct].each do |opt|
|
||||
Company.send(:validate_calculation_options, opt => true)
|
||||
end
|
||||
|
||||
# :include is only valid on :count
|
||||
Company.send(:validate_calculation_options, :count, :include => true)
|
||||
Company.send(:validate_calculation_options, :include => true)
|
||||
end
|
||||
|
||||
assert_raise(ArgumentError) { Company.send(:validate_calculation_options, :sum, :foo => :bar) }
|
||||
|
@ -120,7 +120,7 @@ def test_exists_with_aggregate_having_three_mappings_with_one_difference
|
||||
end
|
||||
|
||||
def test_exists_with_scoped_include
|
||||
Developer.with_scope(:find => { :include => :projects, :order => "projects.name" }) do
|
||||
Developer.send(:with_scope, :find => { :include => :projects, :order => "projects.name" }) do
|
||||
assert Developer.exists?
|
||||
end
|
||||
end
|
||||
@ -1022,7 +1022,7 @@ def test_with_limiting_with_custom_select
|
||||
def test_finder_with_scoped_from
|
||||
all_topics = Topic.find(:all)
|
||||
|
||||
Topic.with_scope(:find => { :from => 'fake_topics' }) do
|
||||
Topic.send(:with_scope, :find => { :from => 'fake_topics' }) do
|
||||
assert_equal all_topics, Topic.from('topics').to_a
|
||||
end
|
||||
end
|
||||
|
@ -47,11 +47,6 @@ def execute_with_query_record(sql, name = nil, &block)
|
||||
alias_method_chain :execute, :query_record
|
||||
end
|
||||
|
||||
# Make with_scope public for tests
|
||||
class << ActiveRecord::Base
|
||||
public :with_scope, :with_exclusive_scope
|
||||
end
|
||||
|
||||
unless ENV['FIXTURE_DEBUG']
|
||||
module ActiveRecord::TestFixtures::ClassMethods
|
||||
def try_to_load_dependency_with_silence(*args)
|
||||
@ -62,9 +57,10 @@ def try_to_load_dependency_with_silence(*args)
|
||||
end
|
||||
end
|
||||
|
||||
require "cases/validations_repair_helper"
|
||||
class ActiveSupport::TestCase
|
||||
include ActiveRecord::TestFixtures
|
||||
include ActiveModel::ValidationsRepairHelper
|
||||
include ActiveRecord::ValidationsRepairHelper
|
||||
|
||||
self.fixture_path = FIXTURES_ROOT
|
||||
self.use_instantiated_fixtures = false
|
||||
|
@ -225,7 +225,7 @@ def test_sane_find_with_lock
|
||||
def test_sane_find_with_scoped_lock
|
||||
assert_nothing_raised do
|
||||
Person.transaction do
|
||||
Person.with_scope(:find => { :lock => true }) do
|
||||
Person.send(:with_scope, :find => { :lock => true }) do
|
||||
Person.find 1
|
||||
end
|
||||
end
|
||||
|
@ -10,19 +10,19 @@ class MethodScopingTest < ActiveRecord::TestCase
|
||||
fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects
|
||||
|
||||
def test_set_conditions
|
||||
Developer.with_scope(:find => { :conditions => 'just a test...' }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => 'just a test...' }) do
|
||||
assert_equal 'just a test...', Developer.send(:current_scoped_methods)[:find][:conditions]
|
||||
end
|
||||
end
|
||||
|
||||
def test_scoped_find
|
||||
Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do
|
||||
assert_nothing_raised { Developer.find(1) }
|
||||
end
|
||||
end
|
||||
|
||||
def test_scoped_find_first
|
||||
Developer.with_scope(:find => { :conditions => "salary = 100000" }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => "salary = 100000" }) do
|
||||
assert_equal Developer.find(10), Developer.find(:first, :order => 'name')
|
||||
end
|
||||
end
|
||||
@ -30,7 +30,7 @@ def test_scoped_find_first
|
||||
def test_scoped_find_last
|
||||
highest_salary = Developer.find(:first, :order => "salary DESC")
|
||||
|
||||
Developer.with_scope(:find => { :order => "salary" }) do
|
||||
Developer.send(:with_scope, :find => { :order => "salary" }) do
|
||||
assert_equal highest_salary, Developer.last
|
||||
end
|
||||
end
|
||||
@ -39,38 +39,38 @@ def test_scoped_find_last_preserves_scope
|
||||
lowest_salary = Developer.find(:first, :order => "salary ASC")
|
||||
highest_salary = Developer.find(:first, :order => "salary DESC")
|
||||
|
||||
Developer.with_scope(:find => { :order => "salary" }) do
|
||||
Developer.send(:with_scope, :find => { :order => "salary" }) do
|
||||
assert_equal highest_salary, Developer.last
|
||||
assert_equal lowest_salary, Developer.first
|
||||
end
|
||||
end
|
||||
|
||||
def test_scoped_find_combines_conditions
|
||||
Developer.with_scope(:find => { :conditions => "salary = 9000" }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => "salary = 9000" }) do
|
||||
assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => "name = 'Jamis'")
|
||||
end
|
||||
end
|
||||
|
||||
def test_scoped_find_sanitizes_conditions
|
||||
Developer.with_scope(:find => { :conditions => ['salary = ?', 9000] }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => ['salary = ?', 9000] }) do
|
||||
assert_equal developers(:poor_jamis), Developer.find(:first)
|
||||
end
|
||||
end
|
||||
|
||||
def test_scoped_find_combines_and_sanitizes_conditions
|
||||
Developer.with_scope(:find => { :conditions => ['salary = ?', 9000] }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => ['salary = ?', 9000] }) do
|
||||
assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => ['name = ?', 'Jamis'])
|
||||
end
|
||||
end
|
||||
|
||||
def test_scoped_find_all
|
||||
Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do
|
||||
assert_equal [developers(:david)], Developer.find(:all)
|
||||
end
|
||||
end
|
||||
|
||||
def test_scoped_find_select
|
||||
Developer.with_scope(:find => { :select => "id, name" }) do
|
||||
Developer.send(:with_scope, :find => { :select => "id, name" }) do
|
||||
developer = Developer.find(:first, :conditions => "name = 'David'")
|
||||
assert_equal "David", developer.name
|
||||
assert !developer.has_attribute?(:salary)
|
||||
@ -78,7 +78,7 @@ def test_scoped_find_select
|
||||
end
|
||||
|
||||
def test_options_select_replaces_scope_select
|
||||
Developer.with_scope(:find => { :select => "id, name" }) do
|
||||
Developer.send(:with_scope, :find => { :select => "id, name" }) do
|
||||
developer = Developer.find(:first, :select => 'id, salary', :conditions => "name = 'David'")
|
||||
assert_equal 80000, developer.salary
|
||||
assert !developer.has_attribute?(:name)
|
||||
@ -86,11 +86,11 @@ def test_options_select_replaces_scope_select
|
||||
end
|
||||
|
||||
def test_scoped_count
|
||||
Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do
|
||||
assert_equal 1, Developer.count
|
||||
end
|
||||
|
||||
Developer.with_scope(:find => { :conditions => 'salary = 100000' }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => 'salary = 100000' }) do
|
||||
assert_equal 8, Developer.count
|
||||
assert_equal 1, Developer.count(:conditions => "name LIKE 'fixture_1%'")
|
||||
end
|
||||
@ -98,7 +98,7 @@ def test_scoped_count
|
||||
|
||||
def test_scoped_find_include
|
||||
# with the include, will retrieve only developers for the given project
|
||||
scoped_developers = Developer.with_scope(:find => { :include => :projects }) do
|
||||
scoped_developers = Developer.send(:with_scope, :find => { :include => :projects }) do
|
||||
Developer.find(:all, :conditions => 'projects.id = 2')
|
||||
end
|
||||
assert scoped_developers.include?(developers(:david))
|
||||
@ -107,7 +107,7 @@ def test_scoped_find_include
|
||||
end
|
||||
|
||||
def test_scoped_find_joins
|
||||
scoped_developers = Developer.with_scope(:find => { :joins => 'JOIN developers_projects ON id = developer_id' } ) do
|
||||
scoped_developers = Developer.send(:with_scope, :find => { :joins => 'JOIN developers_projects ON id = developer_id' } ) do
|
||||
Developer.find(:all, :conditions => 'developers_projects.project_id = 2')
|
||||
end
|
||||
assert scoped_developers.include?(developers(:david))
|
||||
@ -117,7 +117,7 @@ def test_scoped_find_joins
|
||||
end
|
||||
|
||||
def test_scoped_find_using_new_style_joins
|
||||
scoped_developers = Developer.with_scope(:find => { :joins => :projects }) do
|
||||
scoped_developers = Developer.send(:with_scope, :find => { :joins => :projects }) do
|
||||
Developer.find(:all, :conditions => 'projects.id = 2')
|
||||
end
|
||||
assert scoped_developers.include?(developers(:david))
|
||||
@ -127,7 +127,7 @@ def test_scoped_find_using_new_style_joins
|
||||
end
|
||||
|
||||
def test_scoped_find_merges_old_style_joins
|
||||
scoped_authors = Author.with_scope(:find => { :joins => 'INNER JOIN posts ON authors.id = posts.author_id ' }) do
|
||||
scoped_authors = Author.send(:with_scope, :find => { :joins => 'INNER JOIN posts ON authors.id = posts.author_id ' }) do
|
||||
Author.find(:all, :select => 'DISTINCT authors.*', :joins => 'INNER JOIN comments ON posts.id = comments.post_id', :conditions => 'comments.id = 1')
|
||||
end
|
||||
assert scoped_authors.include?(authors(:david))
|
||||
@ -137,7 +137,7 @@ def test_scoped_find_merges_old_style_joins
|
||||
end
|
||||
|
||||
def test_scoped_find_merges_new_style_joins
|
||||
scoped_authors = Author.with_scope(:find => { :joins => :posts }) do
|
||||
scoped_authors = Author.send(:with_scope, :find => { :joins => :posts }) do
|
||||
Author.find(:all, :select => 'DISTINCT authors.*', :joins => :comments, :conditions => 'comments.id = 1')
|
||||
end
|
||||
assert scoped_authors.include?(authors(:david))
|
||||
@ -147,7 +147,7 @@ def test_scoped_find_merges_new_style_joins
|
||||
end
|
||||
|
||||
def test_scoped_find_merges_new_and_old_style_joins
|
||||
scoped_authors = Author.with_scope(:find => { :joins => :posts }) do
|
||||
scoped_authors = Author.send(:with_scope, :find => { :joins => :posts }) do
|
||||
Author.find(:all, :select => 'DISTINCT authors.*', :joins => 'JOIN comments ON posts.id = comments.post_id', :conditions => 'comments.id = 1')
|
||||
end
|
||||
assert scoped_authors.include?(authors(:david))
|
||||
@ -157,7 +157,7 @@ def test_scoped_find_merges_new_and_old_style_joins
|
||||
end
|
||||
|
||||
def test_scoped_find_merges_string_array_style_and_string_style_joins
|
||||
scoped_authors = Author.with_scope(:find => { :joins => ["INNER JOIN posts ON posts.author_id = authors.id"]}) do
|
||||
scoped_authors = Author.send(:with_scope, :find => { :joins => ["INNER JOIN posts ON posts.author_id = authors.id"]}) do
|
||||
Author.find(:all, :select => 'DISTINCT authors.*', :joins => 'INNER JOIN comments ON posts.id = comments.post_id', :conditions => 'comments.id = 1')
|
||||
end
|
||||
assert scoped_authors.include?(authors(:david))
|
||||
@ -167,7 +167,7 @@ def test_scoped_find_merges_string_array_style_and_string_style_joins
|
||||
end
|
||||
|
||||
def test_scoped_find_merges_string_array_style_and_hash_style_joins
|
||||
scoped_authors = Author.with_scope(:find => { :joins => :posts}) do
|
||||
scoped_authors = Author.send(:with_scope, :find => { :joins => :posts}) do
|
||||
Author.find(:all, :select => 'DISTINCT authors.*', :joins => ['INNER JOIN comments ON posts.id = comments.post_id'], :conditions => 'comments.id = 1')
|
||||
end
|
||||
assert scoped_authors.include?(authors(:david))
|
||||
@ -177,7 +177,7 @@ def test_scoped_find_merges_string_array_style_and_hash_style_joins
|
||||
end
|
||||
|
||||
def test_scoped_find_merges_joins_and_eliminates_duplicate_string_joins
|
||||
scoped_authors = Author.with_scope(:find => { :joins => 'INNER JOIN posts ON posts.author_id = authors.id'}) do
|
||||
scoped_authors = Author.send(:with_scope, :find => { :joins => 'INNER JOIN posts ON posts.author_id = authors.id'}) do
|
||||
Author.find(:all, :select => 'DISTINCT authors.*', :joins => ["INNER JOIN posts ON posts.author_id = authors.id", "INNER JOIN comments ON posts.id = comments.post_id"], :conditions => 'comments.id = 1')
|
||||
end
|
||||
assert scoped_authors.include?(authors(:david))
|
||||
@ -187,7 +187,7 @@ def test_scoped_find_merges_joins_and_eliminates_duplicate_string_joins
|
||||
end
|
||||
|
||||
def test_scoped_find_strips_spaces_from_string_joins_and_eliminates_duplicate_string_joins
|
||||
scoped_authors = Author.with_scope(:find => { :joins => ' INNER JOIN posts ON posts.author_id = authors.id '}) do
|
||||
scoped_authors = Author.send(:with_scope, :find => { :joins => ' INNER JOIN posts ON posts.author_id = authors.id '}) do
|
||||
Author.find(:all, :select => 'DISTINCT authors.*', :joins => ['INNER JOIN posts ON posts.author_id = authors.id'], :conditions => 'posts.id = 1')
|
||||
end
|
||||
assert scoped_authors.include?(authors(:david))
|
||||
@ -198,7 +198,7 @@ def test_scoped_find_strips_spaces_from_string_joins_and_eliminates_duplicate_st
|
||||
|
||||
def test_scoped_count_include
|
||||
# with the include, will retrieve only developers for the given project
|
||||
Developer.with_scope(:find => { :include => :projects }) do
|
||||
Developer.send(:with_scope, :find => { :include => :projects }) do
|
||||
assert_equal 1, Developer.count(:conditions => 'projects.id = 2')
|
||||
end
|
||||
end
|
||||
@ -206,7 +206,7 @@ def test_scoped_count_include
|
||||
def test_scoped_create
|
||||
new_comment = nil
|
||||
|
||||
VerySpecialComment.with_scope(:create => { :post_id => 1 }) do
|
||||
VerySpecialComment.send(:with_scope, :create => { :post_id => 1 }) do
|
||||
assert_equal({ :post_id => 1 }, VerySpecialComment.send(:current_scoped_methods)[:create])
|
||||
new_comment = VerySpecialComment.create :body => "Wonderful world"
|
||||
end
|
||||
@ -216,14 +216,14 @@ def test_scoped_create
|
||||
|
||||
def test_immutable_scope
|
||||
options = { :conditions => "name = 'David'" }
|
||||
Developer.with_scope(:find => options) do
|
||||
Developer.send(:with_scope, :find => options) do
|
||||
assert_equal %w(David), Developer.find(:all).map { |d| d.name }
|
||||
options[:conditions] = "name != 'David'"
|
||||
assert_equal %w(David), Developer.find(:all).map { |d| d.name }
|
||||
end
|
||||
|
||||
scope = { :find => { :conditions => "name = 'David'" }}
|
||||
Developer.with_scope(scope) do
|
||||
Developer.send(:with_scope, scope) do
|
||||
assert_equal %w(David), Developer.find(:all).map { |d| d.name }
|
||||
scope[:find][:conditions] = "name != 'David'"
|
||||
assert_equal %w(David), Developer.find(:all).map { |d| d.name }
|
||||
@ -232,7 +232,7 @@ def test_immutable_scope
|
||||
|
||||
def test_scoped_with_duck_typing
|
||||
scoping = Struct.new(:method_scoping).new(:find => { :conditions => ["name = ?", 'David'] })
|
||||
Developer.with_scope(scoping) do
|
||||
Developer.send(:with_scope, scoping) do
|
||||
assert_equal %w(David), Developer.find(:all).map { |d| d.name }
|
||||
end
|
||||
end
|
||||
@ -241,7 +241,7 @@ def test_ensure_that_method_scoping_is_correctly_restored
|
||||
scoped_methods = Developer.instance_eval('current_scoped_methods')
|
||||
|
||||
begin
|
||||
Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => "name = 'Jamis'" }) do
|
||||
raise "an exception"
|
||||
end
|
||||
rescue
|
||||
@ -254,8 +254,8 @@ class NestedScopingTest < ActiveRecord::TestCase
|
||||
fixtures :authors, :developers, :projects, :comments, :posts
|
||||
|
||||
def test_merge_options
|
||||
Developer.with_scope(:find => { :conditions => 'salary = 80000' }) do
|
||||
Developer.with_scope(:find => { :limit => 10 }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => 'salary = 80000' }) do
|
||||
Developer.send(:with_scope, :find => { :limit => 10 }) do
|
||||
merged_option = Developer.instance_eval('current_scoped_methods')[:find]
|
||||
assert_equal({ :conditions => 'salary = 80000', :limit => 10 }, merged_option)
|
||||
end
|
||||
@ -263,8 +263,8 @@ def test_merge_options
|
||||
end
|
||||
|
||||
def test_merge_inner_scope_has_priority
|
||||
Developer.with_scope(:find => { :limit => 5 }) do
|
||||
Developer.with_scope(:find => { :limit => 10 }) do
|
||||
Developer.send(:with_scope, :find => { :limit => 5 }) do
|
||||
Developer.send(:with_scope, :find => { :limit => 10 }) do
|
||||
merged_option = Developer.instance_eval('current_scoped_methods')[:find]
|
||||
assert_equal({ :limit => 10 }, merged_option)
|
||||
end
|
||||
@ -272,8 +272,8 @@ def test_merge_inner_scope_has_priority
|
||||
end
|
||||
|
||||
def test_replace_options
|
||||
Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
|
||||
Developer.with_exclusive_scope(:find => { :conditions => "name = 'Jamis'" }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do
|
||||
Developer.send(:with_exclusive_scope, :find => { :conditions => "name = 'Jamis'" }) do
|
||||
assert_equal({:find => { :conditions => "name = 'Jamis'" }}, Developer.instance_eval('current_scoped_methods'))
|
||||
assert_equal({:find => { :conditions => "name = 'Jamis'" }}, Developer.send(:scoped_methods)[-1])
|
||||
end
|
||||
@ -281,21 +281,21 @@ def test_replace_options
|
||||
end
|
||||
|
||||
def test_append_conditions
|
||||
Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
|
||||
Developer.with_scope(:find => { :conditions => 'salary = 80000' }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => 'salary = 80000' }) do
|
||||
appended_condition = Developer.instance_eval('current_scoped_methods')[:find][:conditions]
|
||||
assert_equal("(name = 'David') AND (salary = 80000)", appended_condition)
|
||||
assert_equal(1, Developer.count)
|
||||
end
|
||||
Developer.with_scope(:find => { :conditions => "name = 'Maiha'" }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => "name = 'Maiha'" }) do
|
||||
assert_equal(0, Developer.count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_merge_and_append_options
|
||||
Developer.with_scope(:find => { :conditions => 'salary = 80000', :limit => 10 }) do
|
||||
Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => 'salary = 80000', :limit => 10 }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do
|
||||
merged_option = Developer.instance_eval('current_scoped_methods')[:find]
|
||||
assert_equal({ :conditions => "(salary = 80000) AND (name = 'David')", :limit => 10 }, merged_option)
|
||||
end
|
||||
@ -303,8 +303,8 @@ def test_merge_and_append_options
|
||||
end
|
||||
|
||||
def test_nested_scoped_find
|
||||
Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do
|
||||
Developer.with_exclusive_scope(:find => { :conditions => "name = 'David'" }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => "name = 'Jamis'" }) do
|
||||
Developer.send(:with_exclusive_scope, :find => { :conditions => "name = 'David'" }) do
|
||||
assert_nothing_raised { Developer.find(1) }
|
||||
assert_equal('David', Developer.find(:first).name)
|
||||
end
|
||||
@ -313,8 +313,8 @@ def test_nested_scoped_find
|
||||
end
|
||||
|
||||
def test_nested_scoped_find_include
|
||||
Developer.with_scope(:find => { :include => :projects }) do
|
||||
Developer.with_scope(:find => { :conditions => "projects.id = 2" }) do
|
||||
Developer.send(:with_scope, :find => { :include => :projects }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => "projects.id = 2" }) do
|
||||
assert_nothing_raised { Developer.find(1) }
|
||||
assert_equal('David', Developer.find(:first).name)
|
||||
end
|
||||
@ -323,24 +323,24 @@ def test_nested_scoped_find_include
|
||||
|
||||
def test_nested_scoped_find_merged_include
|
||||
# :include's remain unique and don't "double up" when merging
|
||||
Developer.with_scope(:find => { :include => :projects, :conditions => "projects.id = 2" }) do
|
||||
Developer.with_scope(:find => { :include => :projects }) do
|
||||
Developer.send(:with_scope, :find => { :include => :projects, :conditions => "projects.id = 2" }) do
|
||||
Developer.send(:with_scope, :find => { :include => :projects }) do
|
||||
assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:include].length
|
||||
assert_equal('David', Developer.find(:first).name)
|
||||
end
|
||||
end
|
||||
|
||||
# the nested scope doesn't remove the first :include
|
||||
Developer.with_scope(:find => { :include => :projects, :conditions => "projects.id = 2" }) do
|
||||
Developer.with_scope(:find => { :include => [] }) do
|
||||
Developer.send(:with_scope, :find => { :include => :projects, :conditions => "projects.id = 2" }) do
|
||||
Developer.send(:with_scope, :find => { :include => [] }) do
|
||||
assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:include].length
|
||||
assert_equal('David', Developer.find(:first).name)
|
||||
end
|
||||
end
|
||||
|
||||
# mixing array and symbol include's will merge correctly
|
||||
Developer.with_scope(:find => { :include => [:projects], :conditions => "projects.id = 2" }) do
|
||||
Developer.with_scope(:find => { :include => :projects }) do
|
||||
Developer.send(:with_scope, :find => { :include => [:projects], :conditions => "projects.id = 2" }) do
|
||||
Developer.send(:with_scope, :find => { :include => :projects }) do
|
||||
assert_equal 1, Developer.instance_eval('current_scoped_methods')[:find][:include].length
|
||||
assert_equal('David', Developer.find(:first).name)
|
||||
end
|
||||
@ -348,21 +348,21 @@ def test_nested_scoped_find_merged_include
|
||||
end
|
||||
|
||||
def test_nested_scoped_find_replace_include
|
||||
Developer.with_scope(:find => { :include => :projects }) do
|
||||
Developer.with_exclusive_scope(:find => { :include => [] }) do
|
||||
Developer.send(:with_scope, :find => { :include => :projects }) do
|
||||
Developer.send(:with_exclusive_scope, :find => { :include => [] }) do
|
||||
assert_equal 0, Developer.instance_eval('current_scoped_methods')[:find][:include].length
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_three_level_nested_exclusive_scoped_find
|
||||
Developer.with_scope(:find => { :conditions => "name = 'Jamis'" }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => "name = 'Jamis'" }) do
|
||||
assert_equal('Jamis', Developer.find(:first).name)
|
||||
|
||||
Developer.with_exclusive_scope(:find => { :conditions => "name = 'David'" }) do
|
||||
Developer.send(:with_exclusive_scope, :find => { :conditions => "name = 'David'" }) do
|
||||
assert_equal('David', Developer.find(:first).name)
|
||||
|
||||
Developer.with_exclusive_scope(:find => { :conditions => "name = 'Maiha'" }) do
|
||||
Developer.send(:with_exclusive_scope, :find => { :conditions => "name = 'Maiha'" }) do
|
||||
assert_equal(nil, Developer.find(:first))
|
||||
end
|
||||
|
||||
@ -377,8 +377,8 @@ def test_three_level_nested_exclusive_scoped_find
|
||||
|
||||
def test_merged_scoped_find
|
||||
poor_jamis = developers(:poor_jamis)
|
||||
Developer.with_scope(:find => { :conditions => "salary < 100000" }) do
|
||||
Developer.with_scope(:find => { :offset => 1, :order => 'id asc' }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => "salary < 100000" }) do
|
||||
Developer.send(:with_scope, :find => { :offset => 1, :order => 'id asc' }) do
|
||||
# Oracle adapter does not generated space after asc therefore trailing space removed from regex
|
||||
assert_sql /ORDER BY id asc/ do
|
||||
assert_equal(poor_jamis, Developer.find(:first, :order => 'id asc'))
|
||||
@ -388,16 +388,16 @@ def test_merged_scoped_find
|
||||
end
|
||||
|
||||
def test_merged_scoped_find_sanitizes_conditions
|
||||
Developer.with_scope(:find => { :conditions => ["name = ?", 'David'] }) do
|
||||
Developer.with_scope(:find => { :conditions => ['salary = ?', 9000] }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => ["name = ?", 'David'] }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => ['salary = ?', 9000] }) do
|
||||
assert_raise(ActiveRecord::RecordNotFound) { developers(:poor_jamis) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def test_nested_scoped_find_combines_and_sanitizes_conditions
|
||||
Developer.with_scope(:find => { :conditions => ["name = ?", 'David'] }) do
|
||||
Developer.with_exclusive_scope(:find => { :conditions => ['salary = ?', 9000] }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => ["name = ?", 'David'] }) do
|
||||
Developer.send(:with_exclusive_scope, :find => { :conditions => ['salary = ?', 9000] }) do
|
||||
assert_equal developers(:poor_jamis), Developer.find(:first)
|
||||
assert_equal developers(:poor_jamis), Developer.find(:first, :conditions => ['name = ?', 'Jamis'])
|
||||
end
|
||||
@ -405,8 +405,8 @@ def test_nested_scoped_find_combines_and_sanitizes_conditions
|
||||
end
|
||||
|
||||
def test_merged_scoped_find_combines_and_sanitizes_conditions
|
||||
Developer.with_scope(:find => { :conditions => ["name = ?", 'David'] }) do
|
||||
Developer.with_scope(:find => { :conditions => ['salary > ?', 9000] }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => ["name = ?", 'David'] }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => ['salary > ?', 9000] }) do
|
||||
assert_equal %w(David), Developer.find(:all).map { |d| d.name }
|
||||
end
|
||||
end
|
||||
@ -414,8 +414,8 @@ def test_merged_scoped_find_combines_and_sanitizes_conditions
|
||||
|
||||
def test_nested_scoped_create
|
||||
comment = nil
|
||||
Comment.with_scope(:create => { :post_id => 1}) do
|
||||
Comment.with_scope(:create => { :post_id => 2}) do
|
||||
Comment.send(:with_scope, :create => { :post_id => 1}) do
|
||||
Comment.send(:with_scope, :create => { :post_id => 2}) do
|
||||
assert_equal({ :post_id => 2 }, Comment.send(:current_scoped_methods)[:create])
|
||||
comment = Comment.create :body => "Hey guys, nested scopes are broken. Please fix!"
|
||||
end
|
||||
@ -425,8 +425,8 @@ def test_nested_scoped_create
|
||||
|
||||
def test_nested_exclusive_scope_for_create
|
||||
comment = nil
|
||||
Comment.with_scope(:create => { :body => "Hey guys, nested scopes are broken. Please fix!" }) do
|
||||
Comment.with_exclusive_scope(:create => { :post_id => 1 }) do
|
||||
Comment.send(:with_scope, :create => { :body => "Hey guys, nested scopes are broken. Please fix!" }) do
|
||||
Comment.send(:with_exclusive_scope, :create => { :post_id => 1 }) do
|
||||
assert_equal({ :post_id => 1 }, Comment.send(:current_scoped_methods)[:create])
|
||||
comment = Comment.create :body => "Hey guys"
|
||||
end
|
||||
@ -437,8 +437,8 @@ def test_nested_exclusive_scope_for_create
|
||||
|
||||
def test_merged_scoped_find_on_blank_conditions
|
||||
[nil, " ", [], {}].each do |blank|
|
||||
Developer.with_scope(:find => {:conditions => blank}) do
|
||||
Developer.with_scope(:find => {:conditions => blank}) do
|
||||
Developer.send(:with_scope, :find => {:conditions => blank}) do
|
||||
Developer.send(:with_scope, :find => {:conditions => blank}) do
|
||||
assert_nothing_raised { Developer.find(:first) }
|
||||
end
|
||||
end
|
||||
@ -447,8 +447,8 @@ def test_merged_scoped_find_on_blank_conditions
|
||||
|
||||
def test_merged_scoped_find_on_blank_bind_conditions
|
||||
[ [""], ["",{}] ].each do |blank|
|
||||
Developer.with_scope(:find => {:conditions => blank}) do
|
||||
Developer.with_scope(:find => {:conditions => blank}) do
|
||||
Developer.send(:with_scope, :find => {:conditions => blank}) do
|
||||
Developer.send(:with_scope, :find => {:conditions => blank}) do
|
||||
assert_nothing_raised { Developer.find(:first) }
|
||||
end
|
||||
end
|
||||
@ -458,8 +458,8 @@ def test_merged_scoped_find_on_blank_bind_conditions
|
||||
def test_immutable_nested_scope
|
||||
options1 = { :conditions => "name = 'Jamis'" }
|
||||
options2 = { :conditions => "name = 'David'" }
|
||||
Developer.with_scope(:find => options1) do
|
||||
Developer.with_exclusive_scope(:find => options2) do
|
||||
Developer.send(:with_scope, :find => options1) do
|
||||
Developer.send(:with_exclusive_scope, :find => options2) do
|
||||
assert_equal %w(David), Developer.find(:all).map { |d| d.name }
|
||||
options1[:conditions] = options2[:conditions] = nil
|
||||
assert_equal %w(David), Developer.find(:all).map { |d| d.name }
|
||||
@ -470,8 +470,8 @@ def test_immutable_nested_scope
|
||||
def test_immutable_merged_scope
|
||||
options1 = { :conditions => "name = 'Jamis'" }
|
||||
options2 = { :conditions => "salary > 10000" }
|
||||
Developer.with_scope(:find => options1) do
|
||||
Developer.with_scope(:find => options2) do
|
||||
Developer.send(:with_scope, :find => options1) do
|
||||
Developer.send(:with_scope, :find => options2) do
|
||||
assert_equal %w(Jamis), Developer.find(:all).map { |d| d.name }
|
||||
options1[:conditions] = options2[:conditions] = nil
|
||||
assert_equal %w(Jamis), Developer.find(:all).map { |d| d.name }
|
||||
@ -480,10 +480,10 @@ def test_immutable_merged_scope
|
||||
end
|
||||
|
||||
def test_ensure_that_method_scoping_is_correctly_restored
|
||||
Developer.with_scope(:find => { :conditions => "name = 'David'" }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => "name = 'David'" }) do
|
||||
scoped_methods = Developer.instance_eval('current_scoped_methods')
|
||||
begin
|
||||
Developer.with_scope(:find => { :conditions => "name = 'Maiha'" }) do
|
||||
Developer.send(:with_scope, :find => { :conditions => "name = 'Maiha'" }) do
|
||||
raise "an exception"
|
||||
end
|
||||
rescue
|
||||
@ -493,8 +493,8 @@ def test_ensure_that_method_scoping_is_correctly_restored
|
||||
end
|
||||
|
||||
def test_nested_scoped_find_merges_old_style_joins
|
||||
scoped_authors = Author.with_scope(:find => { :joins => 'INNER JOIN posts ON authors.id = posts.author_id' }) do
|
||||
Author.with_scope(:find => { :joins => 'INNER JOIN comments ON posts.id = comments.post_id' }) do
|
||||
scoped_authors = Author.send(:with_scope, :find => { :joins => 'INNER JOIN posts ON authors.id = posts.author_id' }) do
|
||||
Author.send(:with_scope, :find => { :joins => 'INNER JOIN comments ON posts.id = comments.post_id' }) do
|
||||
Author.find(:all, :select => 'DISTINCT authors.*', :conditions => 'comments.id = 1')
|
||||
end
|
||||
end
|
||||
@ -505,8 +505,8 @@ def test_nested_scoped_find_merges_old_style_joins
|
||||
end
|
||||
|
||||
def test_nested_scoped_find_merges_new_style_joins
|
||||
scoped_authors = Author.with_scope(:find => { :joins => :posts }) do
|
||||
Author.with_scope(:find => { :joins => :comments }) do
|
||||
scoped_authors = Author.send(:with_scope, :find => { :joins => :posts }) do
|
||||
Author.send(:with_scope, :find => { :joins => :comments }) do
|
||||
Author.find(:all, :select => 'DISTINCT authors.*', :conditions => 'comments.id = 1')
|
||||
end
|
||||
end
|
||||
@ -517,8 +517,8 @@ def test_nested_scoped_find_merges_new_style_joins
|
||||
end
|
||||
|
||||
def test_nested_scoped_find_merges_new_and_old_style_joins
|
||||
scoped_authors = Author.with_scope(:find => { :joins => :posts }) do
|
||||
Author.with_scope(:find => { :joins => 'INNER JOIN comments ON posts.id = comments.post_id' }) do
|
||||
scoped_authors = Author.send(:with_scope, :find => { :joins => :posts }) do
|
||||
Author.send(:with_scope, :find => { :joins => 'INNER JOIN comments ON posts.id = comments.post_id' }) do
|
||||
Author.find(:all, :select => 'DISTINCT authors.*', :joins => '', :conditions => 'comments.id = 1')
|
||||
end
|
||||
end
|
||||
@ -552,7 +552,7 @@ def test_forwarding_to_dynamic_finders
|
||||
end
|
||||
|
||||
def test_nested_scope
|
||||
Comment.with_scope(:find => { :conditions => '1=1' }) do
|
||||
Comment.send(:with_scope, :find => { :conditions => '1=1' }) do
|
||||
assert_equal 'a comment...', @welcome.comments.what_are_you
|
||||
end
|
||||
end
|
||||
@ -577,7 +577,7 @@ def test_forwarding_to_dynamic_finders
|
||||
end
|
||||
|
||||
def test_nested_scope
|
||||
Category.with_scope(:find => { :conditions => '1=1' }) do
|
||||
Category.send(:with_scope, :find => { :conditions => '1=1' }) do
|
||||
assert_equal 'a comment...', @welcome.comments.what_are_you
|
||||
end
|
||||
end
|
||||
@ -633,7 +633,7 @@ def test_method_scope
|
||||
|
||||
def test_nested_scope
|
||||
expected = Developer.find(:all, :order => 'name DESC').collect { |dev| dev.salary }
|
||||
received = DeveloperOrderedBySalary.with_scope(:find => { :order => 'name DESC'}) do
|
||||
received = DeveloperOrderedBySalary.send(:with_scope, :find => { :order => 'name DESC'}) do
|
||||
DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary }
|
||||
end
|
||||
assert_equal expected, received
|
||||
@ -647,7 +647,7 @@ def test_named_scope_overwrites_default
|
||||
|
||||
def test_nested_exclusive_scope
|
||||
expected = Developer.find(:all, :limit => 100).collect { |dev| dev.salary }
|
||||
received = DeveloperOrderedBySalary.with_exclusive_scope(:find => { :limit => 100 }) do
|
||||
received = DeveloperOrderedBySalary.send(:with_exclusive_scope, :find => { :limit => 100 }) do
|
||||
DeveloperOrderedBySalary.find(:all).collect { |dev| dev.salary }
|
||||
end
|
||||
assert_equal expected, received
|
||||
|
@ -245,6 +245,27 @@ def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
|
||||
def test_should_automatically_enable_autosave_on_the_association
|
||||
assert Pirate.reflect_on_association(:ship).options[:autosave]
|
||||
end
|
||||
|
||||
def test_should_accept_update_only_option
|
||||
@pirate.update_attribute(:update_only_ship_attributes, { :id => @pirate.ship.id, :name => 'Mayflower' })
|
||||
end
|
||||
|
||||
def test_should_create_new_model_when_nothing_is_there_and_update_only_is_true
|
||||
@ship.delete
|
||||
assert_difference('Ship.count', 1) do
|
||||
@pirate.reload.update_attribute(:update_only_ship_attributes, { :name => 'Mayflower' })
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_update_existing_when_update_only_is_true_and_no_id_is_given
|
||||
@ship.delete
|
||||
@ship = @pirate.create_update_only_ship(:name => 'Nights Dirty Lightning')
|
||||
|
||||
assert_no_difference('Ship.count') do
|
||||
@pirate.update_attributes(:update_only_ship_attributes => { :name => 'Mayflower' })
|
||||
end
|
||||
assert_equal 'Mayflower', @ship.reload.name
|
||||
end
|
||||
end
|
||||
|
||||
class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
|
||||
@ -362,6 +383,27 @@ def test_should_not_destroy_the_associated_model_until_the_parent_is_saved
|
||||
def test_should_automatically_enable_autosave_on_the_association
|
||||
assert Ship.reflect_on_association(:pirate).options[:autosave]
|
||||
end
|
||||
|
||||
def test_should_accept_update_only_option
|
||||
@ship.update_attribute(:update_only_pirate_attributes, { :id => @pirate.ship.id, :catchphrase => 'Arr' })
|
||||
end
|
||||
|
||||
def test_should_create_new_model_when_nothing_is_there_and_update_only_is_true
|
||||
@pirate.delete
|
||||
assert_difference('Pirate.count', 1) do
|
||||
@ship.reload.update_attribute(:update_only_pirate_attributes, { :catchphrase => 'Arr' })
|
||||
end
|
||||
end
|
||||
|
||||
def test_should_update_existing_when_update_only_is_true_and_no_id_is_given
|
||||
@pirate.delete
|
||||
@pirate = @ship.create_update_only_pirate(:catchphrase => 'Aye')
|
||||
|
||||
assert_no_difference('Pirate.count') do
|
||||
@ship.update_attributes(:update_only_pirate_attributes => { :catchphrase => 'Arr' })
|
||||
end
|
||||
assert_equal 'Arr', @pirate.reload.catchphrase
|
||||
end
|
||||
end
|
||||
|
||||
module NestedAttributesOnACollectionAssociationTests
|
||||
@ -371,6 +413,15 @@ def test_should_define_an_attribute_writer_method_for_the_association
|
||||
assert_respond_to @pirate, association_setter
|
||||
end
|
||||
|
||||
def test_should_save_only_one_association_on_create
|
||||
pirate = Pirate.create!({
|
||||
:catchphrase => 'Arr',
|
||||
association_getter => { 'foo' => { :name => 'Grace OMalley' } }
|
||||
})
|
||||
|
||||
assert_equal 1, pirate.reload.send(@association_name).count
|
||||
end
|
||||
|
||||
def test_should_take_a_hash_with_string_keys_and_assign_the_attributes_to_the_associated_models
|
||||
@alternate_params[association_getter].stringify_keys!
|
||||
@pirate.update_attributes @alternate_params
|
||||
|
@ -33,19 +33,20 @@ def test_cant_save_readonly_record
|
||||
|
||||
def test_find_with_readonly_option
|
||||
Developer.find(:all).each { |d| assert !d.readonly? }
|
||||
Developer.find(:all, :readonly => false).each { |d| assert !d.readonly? }
|
||||
Developer.find(:all, :readonly => true).each { |d| assert d.readonly? }
|
||||
Developer.readonly(false).each { |d| assert !d.readonly? }
|
||||
Developer.readonly(true).each { |d| assert d.readonly? }
|
||||
Developer.readonly.each { |d| assert d.readonly? }
|
||||
end
|
||||
|
||||
|
||||
def test_find_with_joins_option_implies_readonly
|
||||
# Blank joins don't count.
|
||||
Developer.find(:all, :joins => ' ').each { |d| assert !d.readonly? }
|
||||
Developer.find(:all, :joins => ' ', :readonly => false).each { |d| assert !d.readonly? }
|
||||
Developer.joins(' ').each { |d| assert !d.readonly? }
|
||||
Developer.joins(' ').readonly(false).each { |d| assert !d.readonly? }
|
||||
|
||||
# Others do.
|
||||
Developer.find(:all, :joins => ', projects').each { |d| assert d.readonly? }
|
||||
Developer.find(:all, :joins => ', projects', :readonly => false).each { |d| assert !d.readonly? }
|
||||
Developer.joins(', projects').each { |d| assert d.readonly? }
|
||||
Developer.joins(', projects').readonly(false).each { |d| assert !d.readonly? }
|
||||
end
|
||||
|
||||
|
||||
@ -54,7 +55,7 @@ def test_habtm_find_readonly
|
||||
assert !dev.projects.empty?
|
||||
assert dev.projects.all?(&:readonly?)
|
||||
assert dev.projects.find(:all).all?(&:readonly?)
|
||||
assert dev.projects.find(:all, :readonly => true).all?(&:readonly?)
|
||||
assert dev.projects.readonly(true).all?(&:readonly?)
|
||||
end
|
||||
|
||||
def test_has_many_find_readonly
|
||||
@ -62,7 +63,7 @@ def test_has_many_find_readonly
|
||||
assert !post.comments.empty?
|
||||
assert !post.comments.any?(&:readonly?)
|
||||
assert !post.comments.find(:all).any?(&:readonly?)
|
||||
assert post.comments.find(:all, :readonly => true).all?(&:readonly?)
|
||||
assert post.comments.readonly(true).all?(&:readonly?)
|
||||
end
|
||||
|
||||
def test_has_many_with_through_is_not_implicitly_marked_readonly
|
||||
@ -71,32 +72,32 @@ def test_has_many_with_through_is_not_implicitly_marked_readonly
|
||||
end
|
||||
|
||||
def test_readonly_scoping
|
||||
Post.with_scope(:find => { :conditions => '1=1' }) do
|
||||
Post.send(:with_scope, :find => { :conditions => '1=1' }) do
|
||||
assert !Post.find(1).readonly?
|
||||
assert Post.find(1, :readonly => true).readonly?
|
||||
assert !Post.find(1, :readonly => false).readonly?
|
||||
assert Post.readonly(true).find(1).readonly?
|
||||
assert !Post.readonly(false).find(1).readonly?
|
||||
end
|
||||
|
||||
Post.with_scope(:find => { :joins => ' ' }) do
|
||||
Post.send(:with_scope, :find => { :joins => ' ' }) do
|
||||
assert !Post.find(1).readonly?
|
||||
assert Post.find(1, :readonly => true).readonly?
|
||||
assert !Post.find(1, :readonly => false).readonly?
|
||||
assert Post.readonly.find(1).readonly?
|
||||
assert !Post.readonly(false).find(1).readonly?
|
||||
end
|
||||
|
||||
# Oracle barfs on this because the join includes unqualified and
|
||||
# conflicting column names
|
||||
unless current_adapter?(:OracleAdapter)
|
||||
Post.with_scope(:find => { :joins => ', developers' }) do
|
||||
Post.send(:with_scope, :find => { :joins => ', developers' }) do
|
||||
assert Post.find(1).readonly?
|
||||
assert Post.find(1, :readonly => true).readonly?
|
||||
assert !Post.find(1, :readonly => false).readonly?
|
||||
assert Post.readonly.find(1).readonly?
|
||||
assert !Post.readonly(false).find(1).readonly?
|
||||
end
|
||||
end
|
||||
|
||||
Post.with_scope(:find => { :readonly => true }) do
|
||||
Post.send(:with_scope, :find => { :readonly => true }) do
|
||||
assert Post.find(1).readonly?
|
||||
assert Post.find(1, :readonly => true).readonly?
|
||||
assert !Post.find(1, :readonly => false).readonly?
|
||||
assert Post.readonly.find(1).readonly?
|
||||
assert !Post.readonly(false).find(1).readonly?
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -353,4 +353,39 @@ def test_relation_merging_with_preload
|
||||
assert_queries(2) { assert posts.first.author }
|
||||
end
|
||||
end
|
||||
|
||||
def test_invalid_merge
|
||||
assert_raises(ArgumentError) { Post.scoped & Developer.scoped }
|
||||
end
|
||||
|
||||
def test_count
|
||||
posts = Post.scoped
|
||||
|
||||
assert_equal 7, posts.count
|
||||
assert_equal 7, posts.count(:all)
|
||||
assert_equal 7, posts.count(:id)
|
||||
|
||||
assert_equal 1, posts.where('comments_count > 1').count
|
||||
assert_equal 5, posts.where(:comments_count => 0).count
|
||||
end
|
||||
|
||||
def test_count_with_distinct
|
||||
posts = Post.scoped
|
||||
|
||||
assert_equal 3, posts.count(:comments_count, :distinct => true)
|
||||
assert_equal 7, posts.count(:comments_count, :distinct => false)
|
||||
|
||||
assert_equal 3, posts.select(:comments_count).count(:distinct => true)
|
||||
assert_equal 7, posts.select(:comments_count).count(:distinct => false)
|
||||
end
|
||||
|
||||
def test_count_explicit_columns
|
||||
Post.update_all(:comments_count => nil)
|
||||
posts = Post.scoped
|
||||
|
||||
assert_equal 7, posts.select('comments_count').count('id')
|
||||
assert_equal 0, posts.select('comments_count').count
|
||||
assert_equal 0, posts.count(:comments_count)
|
||||
assert_equal 0, posts.count('comments_count')
|
||||
end
|
||||
end
|
||||
|
@ -213,7 +213,7 @@ def test_validate_uniqueness_with_non_standard_table_names
|
||||
def test_validates_uniqueness_inside_with_scope
|
||||
Topic.validates_uniqueness_of(:title)
|
||||
|
||||
Topic.with_scope(:find => { :conditions => { :author_name => "David" } }) do
|
||||
Topic.send(:with_scope, :find => { :conditions => { :author_name => "David" } }) do
|
||||
t1 = Topic.new("title" => "I'm unique!", "author_name" => "Mary")
|
||||
assert t1.save
|
||||
t2 = Topic.new("title" => "I'm unique!", "author_name" => "David")
|
||||
|
@ -1,4 +1,4 @@
|
||||
module ActiveModel
|
||||
module ActiveRecord
|
||||
module ValidationsRepairHelper
|
||||
extend ActiveSupport::Concern
|
||||
|
@ -98,14 +98,14 @@ def test_exception_on_create_bang_many_with_block
|
||||
end
|
||||
|
||||
def test_scoped_create_without_attributes
|
||||
Reply.with_scope(:create => {}) do
|
||||
Reply.send(:with_scope, :create => {}) do
|
||||
assert_raise(ActiveRecord::RecordInvalid) { Reply.create! }
|
||||
end
|
||||
end
|
||||
|
||||
def test_create_with_exceptions_using_scope_for_protected_attributes
|
||||
assert_nothing_raised do
|
||||
ProtectedPerson.with_scope( :create => { :first_name => "Mary" } ) do
|
||||
ProtectedPerson.send(:with_scope, :create => { :first_name => "Mary" } ) do
|
||||
person = ProtectedPerson.create! :addon => "Addon"
|
||||
assert_equal person.first_name, "Mary", "scope should ignore attr_protected"
|
||||
end
|
||||
@ -114,7 +114,7 @@ def test_create_with_exceptions_using_scope_for_protected_attributes
|
||||
|
||||
def test_create_with_exceptions_using_scope_and_empty_attributes
|
||||
assert_nothing_raised do
|
||||
ProtectedPerson.with_scope( :create => { :first_name => "Mary" } ) do
|
||||
ProtectedPerson.send(:with_scope, :create => { :first_name => "Mary" } ) do
|
||||
person = ProtectedPerson.create!
|
||||
assert_equal person.first_name, "Mary", "should be ok when no attributes are passed to create!"
|
||||
end
|
||||
|
4
activerecord/test/fixtures/faces.yml
vendored
4
activerecord/test/fixtures/faces.yml
vendored
@ -5,3 +5,7 @@ trusting:
|
||||
weather_beaten:
|
||||
description: weather beaten
|
||||
man: steve
|
||||
|
||||
confused:
|
||||
description: confused
|
||||
polymorphic_man: gordon (Man)
|
||||
|
6
activerecord/test/fixtures/interests.yml
vendored
6
activerecord/test/fixtures/interests.yml
vendored
@ -23,7 +23,11 @@ woodsmanship:
|
||||
zine: going_out
|
||||
man: steve
|
||||
|
||||
survial:
|
||||
survival:
|
||||
topic: Survival
|
||||
zine: going_out
|
||||
man: steve
|
||||
|
||||
llama_wrangling:
|
||||
topic: Llama Wrangling
|
||||
polymorphic_man: gordon (Man)
|
||||
|
@ -1,5 +1,7 @@
|
||||
class Face < ActiveRecord::Base
|
||||
belongs_to :man, :inverse_of => :face
|
||||
# This is a "broken" inverse_of for the purposes of testing
|
||||
belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_face
|
||||
# These is a "broken" inverse_of for the purposes of testing
|
||||
belongs_to :horrible_man, :class_name => 'Man', :inverse_of => :horrible_face
|
||||
belongs_to :horrible_polymorphic_man, :polymorphic => true, :inverse_of => :horrible_polymorphic_face
|
||||
end
|
||||
|
@ -1,4 +1,5 @@
|
||||
class Interest < ActiveRecord::Base
|
||||
belongs_to :man, :inverse_of => :interests
|
||||
belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_interests
|
||||
belongs_to :zine, :inverse_of => :interests
|
||||
end
|
||||
|
@ -1,6 +1,8 @@
|
||||
class Man < ActiveRecord::Base
|
||||
has_one :face, :inverse_of => :man
|
||||
has_one :polymorphic_face, :class_name => 'Face', :as => :polymorphic_man, :inverse_of => :polymorphic_man
|
||||
has_many :interests, :inverse_of => :man
|
||||
has_many :polymorphic_interests, :class_name => 'Interest', :as => :polymorphic_man, :inverse_of => :polymorphic_man
|
||||
# These are "broken" inverse_of associations for the purposes of testing
|
||||
has_one :dirty_face, :class_name => 'Face', :inverse_of => :dirty_man
|
||||
has_many :secret_interests, :class_name => 'Interest', :inverse_of => :secret_man
|
||||
|
@ -19,6 +19,7 @@ class Pirate < ActiveRecord::Base
|
||||
|
||||
# These both have :autosave enabled because accepts_nested_attributes_for is used on them.
|
||||
has_one :ship
|
||||
has_one :update_only_ship, :class_name => 'Ship'
|
||||
has_one :non_validated_ship, :class_name => 'Ship'
|
||||
has_many :birds
|
||||
has_many :birds_with_method_callbacks, :class_name => "Bird",
|
||||
@ -35,6 +36,7 @@ class Pirate < ActiveRecord::Base
|
||||
|
||||
accepts_nested_attributes_for :parrots, :birds, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
|
||||
accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
|
||||
accepts_nested_attributes_for :update_only_ship, :update_only => true
|
||||
accepts_nested_attributes_for :parrots_with_method_callbacks, :parrots_with_proc_callbacks,
|
||||
:birds_with_method_callbacks, :birds_with_proc_callbacks, :allow_destroy => true
|
||||
accepts_nested_attributes_for :birds_with_reject_all_blank, :reject_if => :all_blank
|
||||
|
@ -2,9 +2,11 @@ class Ship < ActiveRecord::Base
|
||||
self.record_timestamps = false
|
||||
|
||||
belongs_to :pirate
|
||||
belongs_to :update_only_pirate, :class_name => 'Pirate'
|
||||
has_many :parts, :class_name => 'ShipPart', :autosave => true
|
||||
|
||||
accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
|
||||
accepts_nested_attributes_for :update_only_pirate, :update_only => true
|
||||
|
||||
validates_presence_of :name
|
||||
end
|
||||
|
@ -520,11 +520,15 @@ def create_table(*args, &block)
|
||||
create_table :faces, :force => true do |t|
|
||||
t.string :description
|
||||
t.integer :man_id
|
||||
t.integer :polymorphic_man_id
|
||||
t.string :polymorphic_man_type
|
||||
end
|
||||
|
||||
create_table :interests, :force => true do |t|
|
||||
t.string :topic
|
||||
t.integer :man_id
|
||||
t.integer :polymorphic_man_id
|
||||
t.string :polymorphic_man_type
|
||||
t.integer :zine_id
|
||||
end
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
require "isolation/abstract_unit"
|
||||
|
||||
module InitializerTests
|
||||
class PathsTest < Test::Unit::TestCase
|
||||
class CheckRubyVersionTest < Test::Unit::TestCase
|
||||
include ActiveSupport::Testing::Isolation
|
||||
|
||||
def setup
|
||||
@ -9,52 +9,21 @@ def setup
|
||||
boot_rails
|
||||
end
|
||||
|
||||
test "rails does not initialize with ruby version 1.8.1" do
|
||||
assert_rails_does_not_boot "1.8.1"
|
||||
test "rails initializes with ruby 1.8.7 or later" do
|
||||
if RUBY_VERSION < '1.8.7'
|
||||
assert_rails_does_not_boot
|
||||
else
|
||||
assert_rails_boots
|
||||
end
|
||||
end
|
||||
|
||||
test "rails does not initialize with ruby version 1.8.2" do
|
||||
assert_rails_does_not_boot "1.8.2"
|
||||
end
|
||||
|
||||
test "rails does not initialize with ruby version 1.8.3" do
|
||||
assert_rails_does_not_boot "1.8.3"
|
||||
end
|
||||
|
||||
test "rails does not initialize with ruby version 1.8.4" do
|
||||
assert_rails_does_not_boot "1.8.4"
|
||||
end
|
||||
|
||||
test "rails does not initializes with ruby version 1.8.5" do
|
||||
assert_rails_does_not_boot "1.8.5"
|
||||
end
|
||||
|
||||
test "rails does not initialize with ruby version 1.8.6" do
|
||||
assert_rails_does_not_boot "1.8.6"
|
||||
end
|
||||
|
||||
test "rails initializes with ruby version 1.8.7" do
|
||||
assert_rails_boots "1.8.7"
|
||||
end
|
||||
|
||||
test "rails initializes with the current version of Ruby" do
|
||||
assert_rails_boots
|
||||
end
|
||||
|
||||
def set_ruby_version(version)
|
||||
$-w = nil
|
||||
Object.const_set(:RUBY_VERSION, version.freeze)
|
||||
end
|
||||
|
||||
def assert_rails_boots(version = nil)
|
||||
set_ruby_version(version) if version
|
||||
def assert_rails_boots
|
||||
assert_nothing_raised "It appears that rails does not boot" do
|
||||
require "rails"
|
||||
end
|
||||
end
|
||||
|
||||
def assert_rails_does_not_boot(version)
|
||||
set_ruby_version(version)
|
||||
def assert_rails_does_not_boot
|
||||
$stderr = File.open("/dev/null", "w")
|
||||
assert_raises(SystemExit) do
|
||||
require "rails"
|
||||
|
@ -1,101 +1,103 @@
|
||||
require "isolation/abstract_unit"
|
||||
|
||||
class PathsTest < Test::Unit::TestCase
|
||||
include ActiveSupport::Testing::Isolation
|
||||
module InitializerTests
|
||||
class PathTest < Test::Unit::TestCase
|
||||
include ActiveSupport::Testing::Isolation
|
||||
|
||||
def setup
|
||||
build_app
|
||||
boot_rails
|
||||
require "rails"
|
||||
add_to_config <<-RUBY
|
||||
config.root = "#{app_path}"
|
||||
config.frameworks = [:action_controller, :action_view, :action_mailer, :active_record]
|
||||
config.after_initialize do
|
||||
ActionController::Base.session_store = nil
|
||||
end
|
||||
RUBY
|
||||
require "#{app_path}/config/environment"
|
||||
@paths = Rails.application.config.paths
|
||||
end
|
||||
|
||||
def root(*path)
|
||||
app_path(*path).to_s
|
||||
end
|
||||
|
||||
def assert_path(paths, *dir)
|
||||
assert_equal [root(*dir)], paths.paths
|
||||
end
|
||||
|
||||
def assert_in_load_path(*path)
|
||||
assert $:.any? { |p| File.expand_path(p) == root(*path) }, "Load path does not include '#{root(*path)}'. They are:\n-----\n #{$:.join("\n")}\n-----"
|
||||
end
|
||||
|
||||
def assert_not_in_load_path(*path)
|
||||
assert !$:.any? { |p| File.expand_path(p) == root(*path) }, "Load path includes '#{root(*path)}'. They are:\n-----\n #{$:.join("\n")}\n-----"
|
||||
end
|
||||
|
||||
test "booting up Rails yields a valid paths object" do
|
||||
assert_path @paths.app, "app"
|
||||
assert_path @paths.app.metals, "app", "metal"
|
||||
assert_path @paths.app.models, "app", "models"
|
||||
assert_path @paths.app.helpers, "app", "helpers"
|
||||
assert_path @paths.app.services, "app", "services"
|
||||
assert_path @paths.lib, "lib"
|
||||
assert_path @paths.vendor, "vendor"
|
||||
assert_path @paths.vendor.plugins, "vendor", "plugins"
|
||||
assert_path @paths.tmp, "tmp"
|
||||
assert_path @paths.tmp.cache, "tmp", "cache"
|
||||
assert_path @paths.config, "config"
|
||||
assert_path @paths.config.locales, "config", "locales"
|
||||
assert_path @paths.config.environments, "config", "environments"
|
||||
|
||||
assert_equal root("app", "controllers"), @paths.app.controllers.to_a.first
|
||||
assert_equal Pathname.new(File.dirname(__FILE__)).join("..", "..", "builtin", "rails_info").expand_path,
|
||||
Pathname.new(@paths.app.controllers.to_a[1]).expand_path
|
||||
end
|
||||
|
||||
test "booting up Rails yields a list of paths that are eager" do
|
||||
assert @paths.app.models.eager_load?
|
||||
assert @paths.app.controllers.eager_load?
|
||||
assert @paths.app.helpers.eager_load?
|
||||
assert @paths.app.metals.eager_load?
|
||||
end
|
||||
|
||||
test "environments has a glob equal to the current environment" do
|
||||
assert_equal "#{RAILS_ENV}.rb", @paths.config.environments.glob
|
||||
end
|
||||
|
||||
test "load path includes each of the paths in config.paths as long as the directories exist" do
|
||||
assert_in_load_path "app"
|
||||
assert_in_load_path "app", "controllers"
|
||||
assert_in_load_path "app", "models"
|
||||
assert_in_load_path "app", "helpers"
|
||||
assert_in_load_path "lib"
|
||||
assert_in_load_path "vendor"
|
||||
|
||||
assert_not_in_load_path "app", "views"
|
||||
assert_not_in_load_path "app", "metal"
|
||||
assert_not_in_load_path "app", "services"
|
||||
assert_not_in_load_path "config"
|
||||
assert_not_in_load_path "config", "locales"
|
||||
assert_not_in_load_path "config", "environments"
|
||||
assert_not_in_load_path "tmp"
|
||||
assert_not_in_load_path "tmp", "cache"
|
||||
end
|
||||
|
||||
test "controller paths include builtin in development mode" do
|
||||
RAILS_ENV.replace "development"
|
||||
assert Rails::Configuration.new.paths.app.controllers.paths.any? { |p| p =~ /builtin/ }
|
||||
end
|
||||
|
||||
test "controller paths does not have builtin_directories in test mode" do
|
||||
RAILS_ENV.replace "test"
|
||||
assert !Rails::Configuration.new.paths.app.controllers.paths.any? { |p| p =~ /builtin/ }
|
||||
end
|
||||
|
||||
test "controller paths does not have builtin_directories in production mode" do
|
||||
RAILS_ENV.replace "production"
|
||||
assert !Rails::Configuration.new.paths.app.controllers.paths.any? { |p| p =~ /builtin/ }
|
||||
end
|
||||
|
||||
def setup
|
||||
build_app
|
||||
boot_rails
|
||||
require "rails"
|
||||
add_to_config <<-RUBY
|
||||
config.root = "#{app_path}"
|
||||
config.frameworks = [:action_controller, :action_view, :action_mailer, :active_record]
|
||||
config.after_initialize do
|
||||
ActionController::Base.session_store = nil
|
||||
end
|
||||
RUBY
|
||||
require "#{app_path}/config/environment"
|
||||
@paths = Rails.application.config.paths
|
||||
end
|
||||
|
||||
def root(*path)
|
||||
app_path(*path).to_s
|
||||
end
|
||||
|
||||
def assert_path(paths, *dir)
|
||||
assert_equal [root(*dir)], paths.paths
|
||||
end
|
||||
|
||||
def assert_in_load_path(*path)
|
||||
assert $:.any? { |p| File.expand_path(p) == root(*path) }, "Load path does not include '#{root(*path)}'. They are:\n-----\n #{$:.join("\n")}\n-----"
|
||||
end
|
||||
|
||||
def assert_not_in_load_path(*path)
|
||||
assert !$:.any? { |p| File.expand_path(p) == root(*path) }, "Load path includes '#{root(*path)}'. They are:\n-----\n #{$:.join("\n")}\n-----"
|
||||
end
|
||||
|
||||
test "booting up Rails yields a valid paths object" do
|
||||
assert_path @paths.app, "app"
|
||||
assert_path @paths.app.metals, "app", "metal"
|
||||
assert_path @paths.app.models, "app", "models"
|
||||
assert_path @paths.app.helpers, "app", "helpers"
|
||||
assert_path @paths.app.services, "app", "services"
|
||||
assert_path @paths.lib, "lib"
|
||||
assert_path @paths.vendor, "vendor"
|
||||
assert_path @paths.vendor.plugins, "vendor", "plugins"
|
||||
assert_path @paths.tmp, "tmp"
|
||||
assert_path @paths.tmp.cache, "tmp", "cache"
|
||||
assert_path @paths.config, "config"
|
||||
assert_path @paths.config.locales, "config", "locales"
|
||||
assert_path @paths.config.environments, "config", "environments"
|
||||
|
||||
assert_equal root("app", "controllers"), @paths.app.controllers.to_a.first
|
||||
assert_equal Pathname.new(File.dirname(__FILE__)).join("..", "..", "builtin", "rails_info").expand_path,
|
||||
Pathname.new(@paths.app.controllers.to_a[1]).expand_path
|
||||
end
|
||||
|
||||
test "booting up Rails yields a list of paths that are eager" do
|
||||
assert @paths.app.models.eager_load?
|
||||
assert @paths.app.controllers.eager_load?
|
||||
assert @paths.app.helpers.eager_load?
|
||||
assert @paths.app.metals.eager_load?
|
||||
end
|
||||
|
||||
test "environments has a glob equal to the current environment" do
|
||||
assert_equal "#{RAILS_ENV}.rb", @paths.config.environments.glob
|
||||
end
|
||||
|
||||
test "load path includes each of the paths in config.paths as long as the directories exist" do
|
||||
assert_in_load_path "app"
|
||||
assert_in_load_path "app", "controllers"
|
||||
assert_in_load_path "app", "models"
|
||||
assert_in_load_path "app", "helpers"
|
||||
assert_in_load_path "lib"
|
||||
assert_in_load_path "vendor"
|
||||
|
||||
assert_not_in_load_path "app", "views"
|
||||
assert_not_in_load_path "app", "metal"
|
||||
assert_not_in_load_path "app", "services"
|
||||
assert_not_in_load_path "config"
|
||||
assert_not_in_load_path "config", "locales"
|
||||
assert_not_in_load_path "config", "environments"
|
||||
assert_not_in_load_path "tmp"
|
||||
assert_not_in_load_path "tmp", "cache"
|
||||
end
|
||||
|
||||
test "controller paths include builtin in development mode" do
|
||||
RAILS_ENV.replace "development"
|
||||
assert Rails::Configuration.new.paths.app.controllers.paths.any? { |p| p =~ /builtin/ }
|
||||
end
|
||||
|
||||
test "controller paths does not have builtin_directories in test mode" do
|
||||
RAILS_ENV.replace "test"
|
||||
assert !Rails::Configuration.new.paths.app.controllers.paths.any? { |p| p =~ /builtin/ }
|
||||
end
|
||||
|
||||
test "controller paths does not have builtin_directories in production mode" do
|
||||
RAILS_ENV.replace "production"
|
||||
assert !Rails::Configuration.new.paths.app.controllers.paths.any? { |p| p =~ /builtin/ }
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
Loading…
Reference in New Issue
Block a user