merges rails

This commit is contained in:
Xavier Noria
2011-03-05 12:06:43 +01:00
98 changed files with 2116 additions and 1336 deletions

1
.gitignore vendored
View File

@@ -10,6 +10,7 @@ activerecord/doc
actionpack/doc
actionmailer/doc
activesupport/doc
activesupport/test/tmp
activemodel/test/fixtures/fixture_database.sqlite3
actionpack/test/tmp
activesupport/test/fixtures/isolation_test

View File

@@ -29,7 +29,7 @@ class AssetHostTest < Test::Unit::TestCase
assert_equal %Q{<img alt="Somelogo" src="http://www.example.com/images/somelogo.png" />}, mail.body.to_s.strip
end
def test_asset_host_as_one_arguement_proc
def test_asset_host_as_one_argument_proc
AssetHostMailer.config.asset_host = Proc.new { |source|
if source.starts_with?('/images')
"http://images.example.com"
@@ -41,7 +41,7 @@ class AssetHostTest < Test::Unit::TestCase
assert_equal %Q{<img alt="Somelogo" src="http://images.example.com/images/somelogo.png" />}, mail.body.to_s.strip
end
def test_asset_host_as_two_arguement_proc
def test_asset_host_as_two_argument_proc
ActionController::Base.config.asset_host = Proc.new {|source,request|
if request && request.ssl?
"https://www.example.com"

View File

@@ -32,21 +32,21 @@ class BaseTest < ActiveSupport::TestCase
end
test "mail() with bcc, cc, content_type, charset, mime_version, reply_to and date" do
@time = Time.now.beginning_of_day.to_datetime
time = Time.now.beginning_of_day.to_datetime
email = BaseMailer.welcome(:bcc => 'bcc@test.lindsaar.net',
:cc => 'cc@test.lindsaar.net',
:content_type => 'multipart/mixed',
:charset => 'iso-8559-1',
:mime_version => '2.0',
:reply_to => 'reply-to@test.lindsaar.net',
:date => @time)
:date => time)
assert_equal(['bcc@test.lindsaar.net'], email.bcc)
assert_equal(['cc@test.lindsaar.net'], email.cc)
assert_equal('multipart/mixed; charset=iso-8559-1', email.content_type)
assert_equal('iso-8559-1', email.charset)
assert_equal('2.0', email.mime_version)
assert_equal(['reply-to@test.lindsaar.net'], email.reply_to)
assert_equal(@time, email.date)
assert_equal(time, email.date)
end
test "mail() renders the template using the method being processed" do

View File

@@ -1,5 +1,9 @@
*Rails 3.1.0 (unreleased)*
* ActionDispatch::MiddlewareStack now uses composition over inheritance. It is
no longer an array which means there may be methods missing that were not
tested.
* Add an :authenticity_token option to form_tag for custom handling or to omit the token (pass :authenticity_token => false). [Jakub Kuźma, Igor Wiedler]
* HTML5 button_tag helper. [Rizwan Reza]

View File

@@ -103,12 +103,14 @@ module ActionController #:nodoc:
end
def _save_fragment(name, options)
return unless caching_allowed?
content = response_body
content = content.join if content.is_a?(Array)
write_fragment(name, content, options)
if caching_allowed?
write_fragment(name, content, options)
else
content
end
end
protected

View File

@@ -36,7 +36,7 @@ module ActionController
action = action.to_s
raise "MiddlewareStack#build requires an app" unless app
reverse.inject(app) do |a, middleware|
middlewares.reverse.inject(app) do |a, middleware|
middleware.valid?(action) ?
middleware.build(a) : a
end

View File

@@ -2,17 +2,26 @@ require "active_support/inflector/methods"
require "active_support/dependencies"
module ActionDispatch
class MiddlewareStack < Array
class MiddlewareStack
class Middleware
attr_reader :args, :block
attr_reader :args, :block, :name, :classcache
def initialize(klass_or_name, *args, &block)
@ref = ActiveSupport::Dependencies::Reference.new(klass_or_name)
@klass = nil
if klass_or_name.respond_to?(:name)
@klass = klass_or_name
@name = @klass.name
else
@name = klass_or_name.to_s
end
@classcache = ActiveSupport::Dependencies::Reference
@args, @block = args, block
end
def klass
@ref.get
@klass || classcache[@name]
end
def ==(middleware)
@@ -22,7 +31,7 @@ module ActionDispatch
when Class
klass == middleware
else
normalize(@ref.name) == normalize(middleware)
normalize(@name) == normalize(middleware)
end
end
@@ -41,18 +50,39 @@ module ActionDispatch
end
end
# Use this instead of super to work around a warning.
alias :array_initialize :initialize
include Enumerable
attr_accessor :middlewares
def initialize(*args)
array_initialize(*args)
@middlewares = []
yield(self) if block_given?
end
def each
@middlewares.each { |x| yield x }
end
def size
middlewares.size
end
def last
middlewares.last
end
def [](i)
middlewares[i]
end
def initialize_copy(other)
self.middlewares = other.middlewares.dup
end
def insert(index, *args, &block)
index = assert_index(index, :before)
middleware = self.class::Middleware.new(*args, &block)
super(index, middleware)
middlewares.insert(index, middleware)
end
alias_method :insert_before, :insert
@@ -67,21 +97,25 @@ module ActionDispatch
delete(target)
end
def delete(target)
middlewares.delete target
end
def use(*args, &block)
middleware = self.class::Middleware.new(*args, &block)
push(middleware)
middlewares.push(middleware)
end
def build(app = nil, &block)
app ||= block
raise "MiddlewareStack#build requires an app" unless app
reverse.inject(app) { |a, e| e.build(a) }
middlewares.reverse.inject(app) { |a, e| e.build(a) }
end
protected
def assert_index(index, where)
i = index.is_a?(Integer) ? index : self.index(index)
i = index.is_a?(Integer) ? index : middlewares.index(index)
raise "No such middleware to insert #{where}: #{index.inspect}" unless i
i
end

View File

@@ -3,10 +3,10 @@ require 'rack/utils'
module ActionDispatch
class FileHandler
def initialize(at, root)
@at, @root = at.chomp('/'), root.chomp('/')
@compiled_at = (Regexp.compile(/^#{Regexp.escape(at)}/) unless @at.blank?)
@compiled_root = Regexp.compile(/^#{Regexp.escape(root)}/)
@file_server = ::Rack::File.new(@root)
@at, @root = at.chomp('/'), root.chomp('/')
@compiled_at = @at.blank? ? nil : /^#{Regexp.escape(at)}/
@compiled_root = /^#{Regexp.escape(root)}/
@file_server = ::Rack::File.new(@root)
end
def match?(path)

View File

@@ -50,12 +50,13 @@ module ActionDispatch
private
def controller_reference(controller_param)
controller_name = "#{controller_param.camelize}Controller"
unless controller = @controllers[controller_param]
controller_name = "#{controller_param.camelize}Controller"
controller = @controllers[controller_param] =
ActiveSupport::Dependencies.ref(controller_name)
ActiveSupport::Dependencies.reference(controller_name)
end
controller.get
controller.get(controller_name)
end
def dispatch(controller, action, env)

View File

@@ -69,7 +69,7 @@ module ActionView
def register_javascript_expansion(expansions)
js_expansions = JavascriptIncludeTag.expansions
expansions.each do |key, values|
js_expansions[key] = (js_expansions[key] || []) | Array(values) if values
js_expansions[key] = (js_expansions[key] || []) | Array(values)
end
end
end

View File

@@ -46,7 +46,7 @@ module ActionView
def register_stylesheet_expansion(expansions)
style_expansions = StylesheetIncludeTag.expansions
expansions.each do |key, values|
style_expansions[key] = (style_expansions[key] || []) | Array(values) if values
style_expansions[key] = (style_expansions[key] || []) | Array(values)
end
end
end

View File

@@ -262,6 +262,24 @@ module ActionView
# ...
# </form>
#
# === Removing hidden model id's
#
# The form_for method automatically includes the model id as a hidden field in the form.
# This is used to maintain the correlation between the form data and it's associated model.
# Some ORM systems do not use id's on nested models so in this case you want to be able
# to disable the hidden id.
#
# In the following example the Post model has many Comments stored within it in a NoSQL database,
# thus there is no primary key for comments.
#
# Example:
#
# <%= form(@post) do |f| %>
# <% f.fields_for(:comments, :include_id => false) do |cf| %>
# ...
# <% end %>
# <% end %>
#
# === Customized form builders
#
# You can also build forms using a customized FormBuilder class. Subclass
@@ -332,7 +350,7 @@ module ActionView
options[:html][:remote] = options.delete(:remote)
options[:html][:authenticity_token] = options.delete(:authenticity_token)
builder = options[:parent_builder] = instantiate_builder(object_name, object, options, &proc)
fields_for = fields_for(object_name, object, options, &proc)
default_options = builder.multipart? ? { :multipart => true } : {}
@@ -1326,7 +1344,9 @@ module ActionView
def fields_for_nested_model(name, object, options, block)
object = convert_to_model(object)
options[:hidden_field_id] = object.persisted?
parent_include_id = self.options.fetch(:include_id, true)
include_id = options.fetch(:include_id, parent_include_id)
options[:hidden_field_id] = object.persisted? && include_id
@template.fields_for(name, object, options, &block)
end

View File

@@ -559,6 +559,11 @@ class ActionCacheTest < ActionController::TestCase
assert_response 404
end
def test_four_oh_four_renders_content
get :four_oh_four
assert_equal "404'd!", @response.body
end
def test_simple_runtime_error_returns_500_for_multiple_requests
get :simple_runtime_error
assert_response 500

View File

@@ -0,0 +1,77 @@
require 'abstract_unit'
require 'action_dispatch/middleware/stack'
module ActionDispatch
class MiddlewareStack
class MiddlewareTest < ActiveSupport::TestCase
class Omg; end
{
'concrete' => Omg,
'anonymous' => Class.new
}.each do |name, klass|
define_method("test_#{name}_klass") do
mw = Middleware.new klass
assert_equal klass, mw.klass
end
define_method("test_#{name}_==") do
mw1 = Middleware.new klass
mw2 = Middleware.new klass
assert_equal mw1, mw2
end
end
def test_string_class
mw = Middleware.new Omg.name
assert_equal Omg, mw.klass
end
def test_double_equal_works_with_classes
k = Class.new
mw = Middleware.new k
assert_operator mw, :==, k
result = mw != Class.new
assert result, 'middleware should not equal other anon class'
end
def test_double_equal_works_with_strings
mw = Middleware.new Omg
assert_operator mw, :==, Omg.name
end
def test_double_equal_normalizes_strings
mw = Middleware.new Omg
assert_operator mw, :==, "::#{Omg.name}"
end
def test_middleware_loads_classnames_from_cache
mw = Class.new(Middleware) {
attr_accessor :classcache
}.new(Omg.name)
fake_cache = { mw.name => Omg }
mw.classcache = fake_cache
assert_equal Omg, mw.klass
fake_cache[mw.name] = Middleware
assert_equal Middleware, mw.klass
end
def test_middleware_always_returns_class
mw = Class.new(Middleware) {
attr_accessor :classcache
}.new(Omg)
fake_cache = { mw.name => Middleware }
mw.classcache = fake_cache
assert_equal Omg, mw.klass
end
end
end
end

View File

@@ -303,8 +303,17 @@ class AssetTagHelperTest < ActionView::TestCase
end
def test_custom_javascript_expansions_with_undefined_symbol
assert_raise(ArgumentError) { javascript_include_tag('first', :unknown, 'last') }
end
def test_custom_javascript_expansions_with_nil_value
ActionView::Helpers::AssetTagHelper::register_javascript_expansion :monkey => nil
assert_raise(ArgumentError) { javascript_include_tag('first', :monkey, 'last') }
assert_dom_equal %(<script src="/javascripts/first.js" type="text/javascript"></script>\n<script src="/javascripts/last.js" type="text/javascript"></script>), javascript_include_tag('first', :monkey, 'last')
end
def test_custom_javascript_expansions_with_empty_array_value
ActionView::Helpers::AssetTagHelper::register_javascript_expansion :monkey => []
assert_dom_equal %(<script src="/javascripts/first.js" type="text/javascript"></script>\n<script src="/javascripts/last.js" type="text/javascript"></script>), javascript_include_tag('first', :monkey, 'last')
end
def test_custom_javascript_and_stylesheet_expansion_with_same_name
@@ -379,9 +388,18 @@ class AssetTagHelperTest < ActionView::TestCase
assert_dom_equal %(<link href="/stylesheets/london.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/wellington.css" media="screen" rel="stylesheet" type="text/css" />\n<link href="/stylesheets/amsterdam.css" media="screen" rel="stylesheet" type="text/css" />), stylesheet_link_tag('london', :cities)
end
def test_custom_stylesheet_expansions_with_undefined_symbol
def test_custom_stylesheet_expansions_with_unknown_symbol
assert_raise(ArgumentError) { stylesheet_link_tag('first', :unknown, 'last') }
end
def test_custom_stylesheet_expansions_with_nil_value
ActionView::Helpers::AssetTagHelper::register_stylesheet_expansion :monkey => nil
assert_raise(ArgumentError) { stylesheet_link_tag('first', :monkey, 'last') }
assert_dom_equal %(<link href="/stylesheets/first.css" rel="stylesheet" type="text/css" media="screen" />\n<link href="/stylesheets/last.css" rel="stylesheet" type="text/css" media="screen" />), stylesheet_link_tag('first', :monkey, 'last')
end
def test_custom_stylesheet_expansions_with_empty_array_value
ActionView::Helpers::AssetTagHelper::register_stylesheet_expansion :monkey => []
assert_dom_equal %(<link href="/stylesheets/first.css" rel="stylesheet" type="text/css" media="screen" />\n<link href="/stylesheets/last.css" rel="stylesheet" type="text/css" media="screen" />), stylesheet_link_tag('first', :monkey, 'last')
end
def test_registering_stylesheet_expansions_merges_with_existing_expansions

View File

@@ -1882,10 +1882,17 @@ class DateHelperTest < ActionView::TestCase
end
def test_datetime_select_defaults_to_time_zone_now_when_config_time_zone_is_set
time = stub(:year => 2004, :month => 6, :day => 15, :hour => 16, :min => 35, :sec => 0)
time_zone = mock()
time_zone.expects(:now).returns time
Time.zone_default = time_zone
# The love zone is UTC+0
mytz = Class.new(ActiveSupport::TimeZone) {
attr_accessor :now
}.create('tenderlove', 0)
now = Time.mktime(2004, 6, 15, 16, 35, 0)
mytz.now = now
Time.zone = mytz
assert_equal mytz, Time.zone
@post = Post.new
expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
@@ -1912,7 +1919,7 @@ class DateHelperTest < ActionView::TestCase
assert_dom_equal expected, datetime_select("post", "updated_at")
ensure
Time.zone_default = nil
Time.zone = nil
end
def test_datetime_select_with_html_options_within_fields_for

View File

@@ -1103,6 +1103,61 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id
@post.author = Author.new(321)
form_for(@post) do |f|
concat f.text_field(:title)
concat f.fields_for(:author, :include_id => false) { |af|
af.text_field(:name)
}
end
expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do
'<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' +
'<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />'
end
assert_dom_equal expected, output_buffer
end
def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_inherited
@post.author = Author.new(321)
form_for(@post, :include_id => false) do |f|
concat f.text_field(:title)
concat f.fields_for(:author) { |af|
af.text_field(:name)
}
end
expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do
'<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' +
'<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />'
end
assert_dom_equal expected, output_buffer
end
def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_override
@post.author = Author.new(321)
form_for(@post, :include_id => false) do |f|
concat f.text_field(:title)
concat f.fields_for(:author, :include_id => true) { |af|
af.text_field(:name)
}
end
expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do
'<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' +
'<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' +
'<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />'
end
assert_dom_equal expected, output_buffer
end
def test_nested_fields_for_with_existing_records_on_a_nested_attributes_one_to_one_association_with_explicit_hidden_field_placement
@post.author = Author.new(321)
@@ -1146,6 +1201,86 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id
@post.comments = Array.new(2) { |id| Comment.new(id + 1) }
@post.author = Author.new(321)
form_for(@post) do |f|
concat f.text_field(:title)
concat f.fields_for(:author) { |af|
concat af.text_field(:name)
}
@post.comments.each do |comment|
concat f.fields_for(:comments, comment, :include_id => false) { |cf|
concat cf.text_field(:name)
}
end
end
expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do
'<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' +
'<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' +
'<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' +
'<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' +
'<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />'
end
assert_dom_equal expected, output_buffer
end
def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_inherited
@post.comments = Array.new(2) { |id| Comment.new(id + 1) }
@post.author = Author.new(321)
form_for(@post, :include_id => false) do |f|
concat f.text_field(:title)
concat f.fields_for(:author) { |af|
concat af.text_field(:name)
}
@post.comments.each do |comment|
concat f.fields_for(:comments, comment) { |cf|
concat cf.text_field(:name)
}
end
end
expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do
'<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' +
'<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' +
'<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' +
'<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />'
end
assert_dom_equal expected, output_buffer
end
def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_override
@post.comments = Array.new(2) { |id| Comment.new(id + 1) }
@post.author = Author.new(321)
form_for(@post, :include_id => false) do |f|
concat f.text_field(:title)
concat f.fields_for(:author, :include_id => true) { |af|
concat af.text_field(:name)
}
@post.comments.each do |comment|
concat f.fields_for(:comments, comment) { |cf|
concat cf.text_field(:name)
}
end
end
expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'put') do
'<input name="post[title]" size="30" type="text" id="post_title" value="Hello World" />' +
'<input id="post_author_attributes_name" name="post[author_attributes][name]" size="30" type="text" value="author #321" />' +
'<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' +
'<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" size="30" type="text" value="comment #1" />' +
'<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" size="30" type="text" value="comment #2" />'
end
assert_dom_equal expected, output_buffer
end
def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_using_erb_and_inline_block
@post.comments = Array.new(2) { |id| Comment.new(id + 1) }

View File

@@ -2,6 +2,8 @@
* Added ActiveModel::SecurePassword to encapsulate dead-simple password usage with BCrypt encryption and salting [DHH]
* ActiveModel::AttributeMethods allows attributes to be defined on demand [Alexander Uvarov]
*Rails 3.0.2 (unreleased)*

View File

@@ -260,30 +260,30 @@ module ActiveModel
# end
# end
def define_attribute_methods(attr_names)
return if attribute_methods_generated?
attr_names.each do |attr_name|
attribute_method_matchers.each do |matcher|
unless instance_method_already_implemented?(matcher.method_name(attr_name))
generate_method = "define_method_#{matcher.prefix}attribute#{matcher.suffix}"
attr_names.each { |attr_name| define_attribute_method(attr_name) }
end
if respond_to?(generate_method)
send(generate_method, attr_name)
else
method_name = matcher.method_name(attr_name)
def define_attribute_method(attr_name)
attribute_method_matchers.each do |matcher|
unless instance_method_already_implemented?(matcher.method_name(attr_name))
generate_method = "define_method_#{matcher.prefix}attribute#{matcher.suffix}"
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
if method_defined?('#{method_name}')
undef :'#{method_name}'
end
define_method('#{method_name}') do |*args|
send('#{matcher.method_missing_target}', '#{attr_name}', *args)
end
STR
end
if respond_to?(generate_method)
send(generate_method, attr_name)
else
method_name = matcher.method_name(attr_name)
generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
if method_defined?('#{method_name}')
undef :'#{method_name}'
end
define_method('#{method_name}') do |*args|
send('#{matcher.method_missing_target}', '#{attr_name}', *args)
end
STR
end
end
end
@attribute_methods_generated = true
end
# Removes all the previously dynamically defined methods from the class
@@ -291,7 +291,6 @@ module ActiveModel
generated_attribute_methods.module_eval do
instance_methods.each { |m| undef_method(m) }
end
@attribute_methods_generated = nil
end
# Returns true if the attribute methods defined have been generated.
@@ -303,11 +302,6 @@ module ActiveModel
end
end
# Returns true if the attribute methods defined have been generated.
def attribute_methods_generated?
@attribute_methods_generated ||= nil
end
protected
def instance_method_already_implemented?(method_name)
method_defined?(method_name)

View File

@@ -295,8 +295,8 @@ module ActiveModel
type = options.delete(:message) if options[:message].is_a?(Symbol)
defaults = @base.class.lookup_ancestors.map do |klass|
[ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
:"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
[ :"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.attributes.#{attribute}.#{type}",
:"#{@base.class.i18n_scope}.errors.models.#{klass.model_name.underscore}.#{type}" ]
end
defaults << options.delete(:message)

View File

@@ -4,7 +4,7 @@ require 'active_support/core_ext/module/introspection'
module ActiveModel
class Name < String
attr_reader :singular, :plural, :element, :collection, :partial_path, :route_key, :param_key, :i18n_key
attr_reader :singular, :plural, :element, :collection, :partial_path, :route_key, :param_key
alias_method :cache_key, :collection
def initialize(klass, namespace = nil)
@@ -20,7 +20,6 @@ module ActiveModel
@partial_path = "#{@collection}/#{@element}".freeze
@param_key = (namespace ? _singularize(@unnamespaced) : @singular).freeze
@route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural).freeze
@i18n_key = _singularize(self, '.').to_sym
end
# Transform the model name into a more humane format, using I18n. By default,
@@ -34,7 +33,7 @@ module ActiveModel
@klass.respond_to?(:i18n_scope)
defaults = @klass.lookup_ancestors.map do |klass|
klass.model_name.i18n_key
klass.model_name.underscore.to_sym
end
defaults << options[:default] if options[:default]
@@ -45,10 +44,9 @@ module ActiveModel
end
private
def _singularize(string, replacement='_')
ActiveSupport::Inflector.underscore(string).tr('/', replacement)
end
def _singularize(str)
ActiveSupport::Inflector.underscore(str).tr('/', '_')
end
end
# == Active Model Naming
@@ -64,9 +62,6 @@ module ActiveModel
# BookCover.model_name # => "BookCover"
# BookCover.model_name.human # => "Book cover"
#
# BookCover.model_name.i18n_key # => "book_cover"
# BookModule::BookCover.model_name.i18n_key # => "book_module.book_cover"
#
# Providing the functionality that ActiveModel::Naming provides in your object
# is required to pass the Active Model Lint test. So either extending the provided
# method below, or rolling your own is required.

View File

@@ -44,7 +44,7 @@ module ActiveModel
# Specify +options+ with additional translating options.
def human_attribute_name(attribute, options = {})
defaults = lookup_ancestors.map do |klass|
:"#{self.i18n_scope}.attributes.#{klass.model_name.i18n_key}.#{attribute}"
:"#{self.i18n_scope}.attributes.#{klass.model_name.underscore}.#{attribute}"
end
defaults << :"attributes.#{attribute}"

View File

@@ -42,10 +42,16 @@ class AttributeMethodsTest < ActiveModel::TestCase
ModelWithAttributes2.send(:attribute_method_matchers)
end
test '#define_attribute_method generates attribute method' do
ModelWithAttributes.define_attribute_method(:foo)
assert_respond_to ModelWithAttributes.new, :foo
assert_equal "value of foo", ModelWithAttributes.new.foo
end
test '#define_attribute_methods generates attribute methods' do
ModelWithAttributes.define_attribute_methods([:foo])
assert ModelWithAttributes.attribute_methods_generated?
assert_respond_to ModelWithAttributes.new, :foo
assert_equal "value of foo", ModelWithAttributes.new.foo
end
@@ -53,7 +59,6 @@ class AttributeMethodsTest < ActiveModel::TestCase
test '#define_attribute_methods generates attribute methods with spaces in their names' do
ModelWithAttributesWithSpaces.define_attribute_methods([:'foo bar'])
assert ModelWithAttributesWithSpaces.attribute_methods_generated?
assert_respond_to ModelWithAttributesWithSpaces.new, :'foo bar'
assert_equal "value of foo bar", ModelWithAttributesWithSpaces.new.send(:'foo bar')
end
@@ -69,7 +74,6 @@ class AttributeMethodsTest < ActiveModel::TestCase
ModelWithAttributes.define_attribute_methods([:foo])
ModelWithAttributes.undefine_attribute_methods
assert !ModelWithAttributes.attribute_methods_generated?
assert !ModelWithAttributes.new.respond_to?(:foo)
assert_raises(NoMethodError) { ModelWithAttributes.new.foo }
end

View File

@@ -49,6 +49,13 @@ class ActiveModelI18nTests < ActiveModel::TestCase
assert_equal 'person name attribute', Child.human_attribute_name('name')
end
def test_translated_model_attributes_with_attribute_matching_namespaced_model_name
I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:gender => 'person gender'}, :"person/gender" => {:attribute => 'person gender attribute'}}}
assert_equal 'person gender', Person.human_attribute_name('gender')
assert_equal 'person gender attribute', Person::Gender.human_attribute_name('attribute')
end
def test_translated_model_names
I18n.backend.store_translations 'en', :activemodel => {:models => {:person => 'person model'} }
assert_equal 'person model', Person.model_name.human

View File

@@ -55,14 +55,6 @@ class I18nValidationTest < ActiveModel::TestCase
assert_equal ["Person's name not found"], @person.errors.full_messages
end
def test_errors_full_messages_translates_human_attribute_name_for_model_in_module_attributes
I18n.backend.store_translations('en', :activemodel => {:attributes => {:person_module => {:person => {:name => "Person in Module's name"}}}})
person = PersonModule::Person.new
person.errors.add(:name, 'not found')
PersonModule::Person.expects(:human_attribute_name).with(:name, :default => 'Name').returns("Person in Module's name")
assert_equal ["Person in Module's name not found"], person.errors.full_messages
end
def test_errors_full_messages_uses_format
I18n.backend.store_translations('en', :errors => {:format => "Field %{attribute} %{message}"})
@person.errors.add('name', 'empty')
@@ -371,15 +363,4 @@ class I18nValidationTest < ActiveModel::TestCase
assert_equal ["I am a custom error"], @person.errors[:title]
end
def test_model_with_module_i18n_scope
I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person_module => {:person => {:blank => 'generic blank'}}}}}
PersonModule::Person.validates_presence_of :title
person = PersonModule::Person.new
person.valid?
assert_equal ['generic blank'], person.errors[:title]
I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person_module => {:person => {:attributes => {:title => {:blank => 'title cannot be blank'}}}}}}}
person.valid?
assert_equal ['title cannot be blank'], person.errors[:title]
end
end

View File

@@ -9,10 +9,9 @@ class Person
end
end
class Child < Person
class Person::Gender
extend ActiveModel::Translation
end
module PersonModule
class Person < ::Person
end
class Child < Person
end

View File

@@ -1,5 +1,17 @@
*Rails 3.1.0 (unreleased)*
* The configuration for the current database connection is now accessible via
ActiveRecord::Base.connection_config. [fxn]
* limits and offsets are removed from COUNT queries unless both are supplied.
For example:
People.limit(1).count # => 'SELECT COUNT(*) FROM people'
People.offset(1).count # => 'SELECT COUNT(*) FROM people'
People.limit(1).offset(1).count # => 'SELECT COUNT(*) FROM people LIMIT 1 OFFSET 1'
[lighthouse #6262]
* ActiveRecord::Associations::AssociationProxy has been split. There is now an Association class
(and subclasses) which are responsible for operating on associations, and then a separate,
thin wrapper called CollectionProxy, which proxies collection associations.

View File

@@ -43,7 +43,6 @@ module ActiveRecord
autoload :ConnectionNotEstablished, 'active_record/errors'
autoload :Aggregations
autoload :AssociationPreload
autoload :Associations
autoload :AttributeMethods
autoload :AutosaveAssociation

View File

@@ -1,433 +0,0 @@
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/enumerable'
module ActiveRecord
# See ActiveRecord::AssociationPreload::ClassMethods for documentation.
module AssociationPreload #:nodoc:
extend ActiveSupport::Concern
# Implements the details of eager loading of Active Record associations.
# Application developers should not use this module directly.
#
# <tt>ActiveRecord::Base</tt> is extended with this module. The source code in
# <tt>ActiveRecord::Base</tt> references methods defined in this module.
#
# Note that 'eager loading' and 'preloading' are actually the same thing.
# However, there are two different eager loading strategies.
#
# The first one is by using table joins. This was only strategy available
# prior to Rails 2.1. Suppose that you have an Author model with columns
# 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using
# this strategy, Active Record would try to retrieve all data for an author
# and all of its books via a single query:
#
# SELECT * FROM authors
# LEFT OUTER JOIN books ON authors.id = books.id
# WHERE authors.name = 'Ken Akamatsu'
#
# However, this could result in many rows that contain redundant data. After
# having received the first row, we already have enough data to instantiate
# the Author object. In all subsequent rows, only the data for the joined
# 'books' table is useful; the joined 'authors' data is just redundant, and
# processing this redundant data takes memory and CPU time. The problem
# quickly becomes worse and worse as the level of eager loading increases
# (i.e. if Active Record is to eager load the associations' associations as
# well).
#
# The second strategy is to use multiple database queries, one for each
# level of association. Since Rails 2.1, this is the default strategy. In
# situations where a table join is necessary (e.g. when the +:conditions+
# option references an association's column), it will fallback to the table
# join strategy.
#
# See also ActiveRecord::Associations::ClassMethods, which explains eager
# loading in a more high-level (application developer-friendly) manner.
module ClassMethods
protected
# Eager loads the named associations for the given Active Record record(s).
#
# In this description, 'association name' shall refer to the name passed
# to an association creation method. For example, a model that specifies
# <tt>belongs_to :author</tt>, <tt>has_many :buyers</tt> has association
# names +:author+ and +:buyers+.
#
# == Parameters
# +records+ is an array of ActiveRecord::Base. This array needs not be flat,
# i.e. +records+ itself may also contain arrays of records. In any case,
# +preload_associations+ will preload the all associations records by
# flattening +records+.
#
# +associations+ specifies one or more associations that you want to
# preload. It may be:
# - a Symbol or a String which specifies a single association name. For
# example, specifying +:books+ allows this method to preload all books
# for an Author.
# - an Array which specifies multiple association names. This array
# is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
# allows this method to preload an author's avatar as well as all of his
# books.
# - a Hash which specifies multiple association names, as well as
# association names for the to-be-preloaded association objects. For
# example, specifying <tt>{ :author => :avatar }</tt> will preload a
# book's author, as well as that author's avatar.
#
# +:associations+ has the same format as the +:include+ option for
# <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this:
#
# :books
# [ :books, :author ]
# { :author => :avatar }
# [ :books, { :author => :avatar } ]
#
# +preload_options+ contains options that will be passed to ActiveRecord::Base#find
# (which is called under the hood for preloading records). But it is passed
# only one level deep in the +associations+ argument, i.e. it's not passed
# to the child associations when +associations+ is a Hash.
def preload_associations(records, associations, preload_options={})
records = Array.wrap(records).compact.uniq
return if records.empty?
case associations
when Array then associations.each {|association| preload_associations(records, association, preload_options)}
when Symbol, String then preload_one_association(records, associations.to_sym, preload_options)
when Hash then
associations.each do |parent, child|
raise "parent must be an association name" unless parent.is_a?(String) || parent.is_a?(Symbol)
preload_associations(records, parent, preload_options)
reflection = reflections[parent]
parents = records.sum { |record| Array.wrap(record.send(reflection.name)) }
unless parents.empty?
parents.first.class.preload_associations(parents, child)
end
end
end
end
private
# Preloads a specific named association for the given records. This is
# called by +preload_associations+ as its base case.
def preload_one_association(records, association, preload_options={})
class_to_reflection = {}
# Not all records have the same class, so group then preload
# group on the reflection itself so that if various subclass share the same association then
# we do not split them unnecessarily
records.group_by { |record| class_to_reflection[record.class] ||= record.class.reflections[association]}.each do |reflection, _records|
raise ConfigurationError, "Association named '#{ association }' was not found; perhaps you misspelled it?" unless reflection
# 'reflection.macro' can return 'belongs_to', 'has_many', etc. Thus,
# the following could call 'preload_belongs_to_association',
# 'preload_has_many_association', etc.
send("preload_#{reflection.macro}_association", _records, reflection, preload_options)
end
end
def add_preloaded_records_to_collection(parent_records, reflection_name, associated_record)
parent_records.each do |parent_record|
association = parent_record.association(reflection_name)
association.loaded!
association.target.concat(Array.wrap(associated_record))
association.set_inverse_instance(associated_record)
end
end
def add_preloaded_record_to_collection(parent_records, reflection_name, associated_record)
parent_records.each do |parent_record|
parent_record.association(reflection_name).target = associated_record
end
end
def set_association_collection_records(id_to_parent_map, reflection_name, associated_records, key)
associated_records.each do |associated_record|
parent_records = id_to_parent_map[associated_record[key].to_s]
add_preloaded_records_to_collection(parent_records, reflection_name, associated_record)
end
end
def set_association_single_records(id_to_record_map, reflection_name, associated_records, key)
seen_keys = {}
associated_records.each do |associated_record|
seen_key = associated_record[key].to_s
#this is a has_one or belongs_to: there should only be one record.
#Unfortunately we can't (in portable way) ask the database for
#'all records where foo_id in (x,y,z), but please
# only one row per distinct foo_id' so this where we enforce that
next if seen_keys.key? seen_key
seen_keys[seen_key] = true
mapped_records = id_to_record_map[seen_key]
mapped_records.each do |mapped_record|
association_proxy = mapped_record.association(reflection_name)
association_proxy.target = associated_record
association_proxy.send(:set_inverse_instance, associated_record)
end
end
id_to_record_map.each do |id, records|
next if seen_keys.include?(id)
add_preloaded_record_to_collection(records, reflection_name, nil)
end
end
# Given a collection of Active Record objects, constructs a Hash which maps
# the objects' IDs to the relevant objects. Returns a 2-tuple
# <tt>(id_to_record_map, ids)</tt> where +id_to_record_map+ is the Hash,
# and +ids+ is an Array of record IDs.
def construct_id_map(records, primary_key=nil)
records.group_by do |record|
primary_key ||= record.class.primary_key
record[primary_key].to_s
end
end
def preload_has_and_belongs_to_many_association(records, reflection, preload_options={})
left = reflection.klass.arel_table
id_to_record_map = construct_id_map(records)
records.each { |record| record.association(reflection.name).loaded! }
options = reflection.options
right = Arel::Table.new(options[:join_table]).alias('t0')
join_condition = left[reflection.klass.primary_key].eq(
right[reflection.association_foreign_key])
join = left.create_join(right, left.create_on(join_condition))
select = [
# FIXME: options[:select] is always nil in the tests. Do we really
# need it?
options[:select] || left[Arel.star],
right[reflection.foreign_key].as(
Arel.sql('the_parent_record_id'))
]
associated_records_proxy = reflection.klass.unscoped.
includes(options[:include]).
order(options[:order])
associated_records_proxy.joins_values = [join]
associated_records_proxy.select_values = select
custom_conditions = append_conditions(reflection, preload_options)
klass = associated_records_proxy.klass
associated_records(id_to_record_map.keys) { |some_ids|
method = in_or_equal(some_ids)
conditions = right.create_and(
[right[reflection.foreign_key].send(*method)] +
custom_conditions)
relation = associated_records_proxy.where(conditions)
klass.connection.select_all(relation.arel.to_sql, 'SQL', relation.bind_values)
}.map! { |row|
parent_records = id_to_record_map[row['the_parent_record_id'].to_s]
associated_record = klass.instantiate row
add_preloaded_records_to_collection(
parent_records, reflection.name, associated_record)
associated_record
}
end
def preload_has_one_association(records, reflection, preload_options={})
return if records.first.association(reflection.name).loaded?
id_to_record_map = construct_id_map(records, reflection.options[:primary_key])
options = reflection.options
add_preloaded_record_to_collection(records, reflection.name, nil)
if options[:through]
through_records = preload_through_records(records, reflection, options[:through])
unless through_records.empty?
through_reflection = reflections[options[:through]]
through_primary_key = through_reflection.foreign_key
source = reflection.source_reflection.name
through_records.first.class.preload_associations(through_records, source)
if through_reflection.macro == :belongs_to
id_to_record_map = construct_id_map(records, through_primary_key)
through_primary_key = through_reflection.klass.primary_key
end
through_records.each do |through_record|
add_preloaded_record_to_collection(id_to_record_map[through_record[through_primary_key].to_s],
reflection.name, through_record.send(source))
end
end
else
set_association_single_records(id_to_record_map, reflection.name, find_associated_records(id_to_record_map.keys, reflection, preload_options), reflection.foreign_key)
end
end
def preload_has_many_association(records, reflection, preload_options={})
return if records.first.send(reflection.name).loaded?
options = reflection.options
foreign_key = reflection.through_reflection_foreign_key
id_to_record_map = construct_id_map(records, foreign_key || reflection.options[:primary_key])
records.each { |record| record.association(reflection.name).loaded! }
if options[:through]
through_records = preload_through_records(records, reflection, options[:through])
unless through_records.empty?
source = reflection.source_reflection.name
through_records.first.class.preload_associations(through_records, source, options)
through_records.each do |through_record|
through_record_id = through_record[reflection.through_reflection_primary_key].to_s
add_preloaded_records_to_collection(id_to_record_map[through_record_id], reflection.name, through_record.send(source))
end
records.each { |record| record.send(reflection.name).target.uniq! } if options[:uniq]
end
else
set_association_collection_records(id_to_record_map, reflection.name, find_associated_records(id_to_record_map.keys, reflection, preload_options),
reflection.foreign_key)
end
end
def preload_through_records(records, reflection, through_association)
if reflection.options[:source_type]
interface = reflection.source_reflection.foreign_type
preload_options = {:conditions => ["#{connection.quote_column_name interface} = ?", reflection.options[:source_type]]}
records.compact!
records.first.class.preload_associations(records, through_association, preload_options)
# Dont cache the association - we would only be caching a subset
records.map { |record|
proxy = record.association(through_association)
if proxy.respond_to?(:target)
Array.wrap(proxy.target).tap { proxy.reset }
else # this is a has_one :through reflection
[proxy].compact
end
}.flatten(1)
else
options = {}
options[:include] = reflection.options[:include] || reflection.options[:source] if reflection.options[:conditions]
options[:order] = reflection.options[:order]
options[:conditions] = reflection.options[:conditions]
records.first.class.preload_associations(records, through_association, options)
records.map { |record|
Array.wrap(record.send(through_association))
}.flatten(1)
end
end
def preload_belongs_to_association(records, reflection, preload_options={})
return if records.first.association(reflection.name).loaded?
options = reflection.options
klasses_and_ids = {}
if options[:polymorphic]
# Construct a mapping from klass to a list of ids to load and a mapping of those ids back
# to their parent_records
records.each do |record|
if klass = record.send(reflection.foreign_type)
klass_id = record.send(reflection.foreign_key)
if klass_id
id_map = klasses_and_ids[klass.constantize] ||= {}
(id_map[klass_id.to_s] ||= []) << record
end
end
end
else
id_map = records.group_by do |record|
key = record.send(reflection.foreign_key)
key && key.to_s
end
klasses_and_ids[reflection.klass] = id_map unless id_map.empty?
end
klasses_and_ids.each do |klass, _id_map|
primary_key = (reflection.options[:primary_key] || klass.primary_key).to_s
keys = _id_map.keys.compact
unless keys.empty?
table = klass.arel_table
method = in_or_equal(keys)
conditions = table[primary_key].send(*method)
custom_conditions = append_conditions(reflection, preload_options)
conditions = custom_conditions.inject(conditions) do |ast, cond|
ast.and cond
end
associated_records = klass.unscoped.where(conditions).apply_finder_options(options.slice(:include, :select, :joins, :order)).to_a
else
associated_records = []
end
set_association_single_records(_id_map, reflection.name, associated_records, primary_key)
end
end
def find_associated_records(ids, reflection, preload_options)
options = reflection.options
table = reflection.klass.arel_table
conditions = []
key = reflection.foreign_key
if interface = reflection.options[:as]
key = "#{interface}_id"
conditions << table["#{interface}_type"].eq(base_class.sti_name)
end
conditions.concat append_conditions(reflection, preload_options)
find_options = {
:select => preload_options[:select] || options[:select] || table[Arel.star],
:include => preload_options[:include] || options[:include],
:joins => options[:joins],
:group => preload_options[:group] || options[:group],
:order => preload_options[:order] || options[:order]
}
associated_records(ids) do |some_ids|
method = in_or_equal(some_ids)
where = table.create_and(conditions + [table[key].send(*method)])
reflection.klass.scoped.apply_finder_options(find_options.merge(:conditions => where)).to_a
end
end
def process_conditions(conditions, klass = self)
if conditions.respond_to?(:to_proc)
conditions = instance_eval(&conditions)
end
klass.send(:sanitize_sql, conditions)
end
def append_conditions(reflection, preload_options)
[
('(' + process_conditions(reflection.options[:conditions], reflection.klass) + ')' if reflection.options[:conditions]),
('(' + process_conditions(preload_options[:conditions]) + ')' if preload_options[:conditions]),
].compact.map { |x| Arel.sql x }
end
def in_or_equal(ids)
ids.length == 1 ? ['eq', ids.first] : ['in', ids]
end
# Some databases impose a limit on the number of ids in a list (in Oracle its 1000)
# Make several smaller queries if necessary or make one query if the adapter supports it
def associated_records(ids)
in_clause_length = connection.in_clause_length || ids.size
records = []
ids.each_slice(in_clause_length) do |some_ids|
records.concat yield(some_ids)
end
records
end
end
end
end

View File

@@ -5,7 +5,6 @@ require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/string/conversions'
require 'active_support/core_ext/module/remove_method'
require 'active_support/core_ext/class/attribute'
require 'active_record/associations/class_methods/join_dependency'
module ActiveRecord
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
@@ -143,6 +142,9 @@ module ActiveRecord
autoload :HasAndBelongsToMany, 'active_record/associations/builder/has_and_belongs_to_many'
end
autoload :Preloader, 'active_record/associations/preloader'
autoload :JoinDependency, 'active_record/associations/join_dependency'
# Clears out the association cache.
def clear_association_cache #:nodoc:
@association_cache.clear if persisted?

View File

@@ -191,8 +191,8 @@ module ActiveRecord
else
attributes[reflection.foreign_key] = owner[reflection.active_record_primary_key]
if options[:as]
attributes["#{options[:as]}_type"] = owner.class.base_class.name
if reflection.options[:as]
attributes[reflection.type] = owner.class.base_class.name
end
end
attributes

View File

@@ -1,233 +0,0 @@
require 'active_record/associations/class_methods/join_dependency/join_part'
require 'active_record/associations/class_methods/join_dependency/join_base'
require 'active_record/associations/class_methods/join_dependency/join_association'
module ActiveRecord
module Associations
module ClassMethods
class JoinDependency # :nodoc:
attr_reader :join_parts, :reflections, :table_aliases, :active_record
def initialize(base, associations, joins)
@active_record = base
@table_joins = joins
@join_parts = [JoinBase.new(base)]
@associations = {}
@reflections = []
@table_aliases = Hash.new do |h,name|
h[name] = count_aliases_from_table_joins(name.downcase)
end
@table_aliases[base.table_name] = 1
build(associations)
end
def graft(*associations)
associations.each do |association|
join_associations.detect {|a| association == a} ||
build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
end
self
end
def join_associations
join_parts.last(join_parts.length - 1)
end
def join_base
join_parts.first
end
def columns
join_parts.collect { |join_part|
table = join_part.aliased_table
join_part.column_names_with_alias.collect{ |column_name, aliased_name|
table[column_name].as Arel.sql(aliased_name)
}
}.flatten
end
def count_aliases_from_table_joins(name)
return 0 if Arel::Table === @table_joins
# quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
quoted_name = active_record.connection.quote_table_name(name).downcase
@table_joins.map { |join|
# Table names + table aliases
join.left.downcase.scan(
/join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
).size
}.sum
end
def instantiate(rows)
primary_key = join_base.aliased_primary_key
parents = {}
records = rows.map { |model|
primary_id = model[primary_key]
parent = parents[primary_id] ||= join_base.instantiate(model)
construct(parent, @associations, join_associations, model)
parent
}.uniq
remove_duplicate_results!(active_record, records, @associations)
records
end
def remove_duplicate_results!(base, records, associations)
case associations
when Symbol, String
reflection = base.reflections[associations]
remove_uniq_by_reflection(reflection, records)
when Array
associations.each do |association|
remove_duplicate_results!(base, records, association)
end
when Hash
associations.keys.each do |name|
reflection = base.reflections[name]
remove_uniq_by_reflection(reflection, records)
parent_records = []
records.each do |record|
if descendant = record.send(reflection.name)
if reflection.collection?
parent_records.concat descendant.target.uniq
else
parent_records << descendant
end
end
end
remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
end
end
end
protected
def cache_joined_association(association)
associations = []
parent = association.parent
while parent != join_base
associations.unshift(parent.reflection.name)
parent = parent.parent
end
ref = @associations
associations.each do |key|
ref = ref[key]
end
ref[association.reflection.name] ||= {}
end
def build(associations, parent = nil, join_type = Arel::InnerJoin)
parent ||= join_parts.last
case associations
when Symbol, String
reflection = parent.reflections[associations.to_s.intern] or
raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
unless join_association = find_join_association(reflection, parent)
@reflections << reflection
join_association = build_join_association(reflection, parent)
join_association.join_type = join_type
@join_parts << join_association
cache_joined_association(join_association)
end
join_association
when Array
associations.each do |association|
build(association, parent, join_type)
end
when Hash
associations.keys.sort_by { |a| a.to_s }.each do |name|
join_association = build(name, parent, join_type)
build(associations[name], join_association, join_type)
end
else
raise ConfigurationError, associations.inspect
end
end
def find_join_association(name_or_reflection, parent)
if String === name_or_reflection
name_or_reflection = name_or_reflection.to_sym
end
join_associations.detect { |j|
j.reflection == name_or_reflection && j.parent == parent
}
end
def remove_uniq_by_reflection(reflection, records)
if reflection && reflection.collection?
records.each { |record| record.send(reflection.name).target.uniq! }
end
end
def build_join_association(reflection, parent)
JoinAssociation.new(reflection, self, parent)
end
def construct(parent, associations, join_parts, row)
case associations
when Symbol, String
name = associations.to_s
join_part = join_parts.detect { |j|
j.reflection.name.to_s == name &&
j.parent_table_name == parent.class.table_name }
raise(ConfigurationError, "No such association") unless join_part
join_parts.delete(join_part)
construct_association(parent, join_part, row)
when Array
associations.each do |association|
construct(parent, association, join_parts, row)
end
when Hash
associations.sort_by { |k,_| k.to_s }.each do |association_name, assoc|
association = construct(parent, association_name, join_parts, row)
construct(association, assoc, join_parts, row) if association
end
else
raise ConfigurationError, associations.inspect
end
end
def construct_association(record, join_part, row)
return if record.id.to_s != join_part.parent.record_id(row).to_s
macro = join_part.reflection.macro
if macro == :has_one
return if record.association_cache.key?(join_part.reflection.name)
association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
set_target_and_inverse(join_part, association, record)
else
return if row[join_part.aliased_primary_key].nil?
association = join_part.instantiate(row)
case macro
when :has_many, :has_and_belongs_to_many
other = record.association(join_part.reflection.name)
other.loaded!
other.target.push(association)
other.set_inverse_instance(association)
when :belongs_to
set_target_and_inverse(join_part, association, record)
else
raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}"
end
end
association
end
def set_target_and_inverse(join_part, association, record)
other = record.association(join_part.reflection.name)
other.target = association
other.set_inverse_instance(association)
end
end
end
end
end

View File

@@ -1,281 +0,0 @@
module ActiveRecord
module Associations
module ClassMethods
class JoinDependency # :nodoc:
class JoinAssociation < JoinPart # :nodoc:
# The reflection of the association represented
attr_reader :reflection
# The JoinDependency object which this JoinAssociation exists within. This is mainly
# relevant for generating aliases which do not conflict with other joins which are
# part of the query.
attr_reader :join_dependency
# A JoinBase instance representing the active record we are joining onto.
# (So in Author.has_many :posts, the Author would be that base record.)
attr_reader :parent
# What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
attr_accessor :join_type
# These implement abstract methods from the superclass
attr_reader :aliased_prefix, :aliased_table_name
delegate :options, :through_reflection, :source_reflection, :to => :reflection
delegate :table, :table_name, :to => :parent, :prefix => :parent
def initialize(reflection, join_dependency, parent = nil)
reflection.check_validity!
if reflection.options[:polymorphic]
raise EagerLoadPolymorphicError.new(reflection)
end
super(reflection.klass)
@reflection = reflection
@join_dependency = join_dependency
@parent = parent
@join_type = Arel::InnerJoin
@aliased_prefix = "t#{ join_dependency.join_parts.size }"
# This must be done eagerly upon initialisation because the alias which is produced
# depends on the state of the join dependency, but we want it to work the same way
# every time.
allocate_aliases
@table = Arel::Table.new(
table_name, :as => aliased_table_name, :engine => arel_engine
)
end
def ==(other)
other.class == self.class &&
other.reflection == reflection &&
other.parent == parent
end
def find_parent_in(other_join_dependency)
other_join_dependency.join_parts.detect do |join_part|
parent == join_part
end
end
def join_to(relation)
send("join_#{reflection.macro}_to", relation)
end
def join_relation(joining_relation)
self.join_type = Arel::OuterJoin
joining_relation.joins(self)
end
attr_reader :table
# More semantic name given we are talking about associations
alias_method :target_table, :table
protected
def aliased_table_name_for(name, suffix = nil)
aliases = @join_dependency.table_aliases
if aliases[name] != 0 # We need an alias
connection = active_record.connection
name = connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
aliases[name] += 1
name = name[0, connection.table_alias_length-3] + "_#{aliases[name]}" if aliases[name] > 1
else
aliases[name] += 1
end
name
end
def pluralize(table_name)
ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
end
private
def allocate_aliases
@aliased_table_name = aliased_table_name_for(table_name)
if reflection.macro == :has_and_belongs_to_many
@aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
elsif [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through]
@aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
end
end
def process_conditions(conditions, table_name)
if conditions.respond_to?(:to_proc)
conditions = instance_eval(&conditions)
end
Arel.sql(sanitize_sql(conditions, table_name))
end
def sanitize_sql(condition, table_name)
active_record.send(:sanitize_sql, condition, table_name)
end
def join_target_table(relation, condition)
conditions = [condition]
# If the target table is an STI model then we must be sure to only include records of
# its type and its sub-types.
unless active_record.descends_from_active_record?
sti_column = target_table[active_record.inheritance_column]
subclasses = active_record.descendants
sti_condition = sti_column.eq(active_record.sti_name)
conditions << subclasses.inject(sti_condition) { |attr,subclass|
attr.or(sti_column.eq(subclass.sti_name))
}
end
# If the reflection has conditions, add them
if options[:conditions]
conditions << process_conditions(options[:conditions], aliased_table_name)
end
ands = relation.create_and(conditions)
join = relation.create_join(
target_table,
relation.create_on(ands),
join_type)
relation.from join
end
def join_has_and_belongs_to_many_to(relation)
join_table = Arel::Table.new(
options[:join_table]
).alias(@aliased_join_table_name)
fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key
klass_fk = options[:association_foreign_key] || reflection.klass.to_s.foreign_key
relation = relation.join(join_table, join_type)
relation = relation.on(
join_table[fk].
eq(parent_table[reflection.active_record.primary_key])
)
join_target_table(
relation,
target_table[reflection.klass.primary_key].
eq(join_table[klass_fk])
)
end
def join_has_many_to(relation)
if reflection.options[:through]
join_has_many_through_to(relation)
elsif reflection.options[:as]
join_has_many_polymorphic_to(relation)
else
foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
primary_key = options[:primary_key] || parent.primary_key
join_target_table(
relation,
target_table[foreign_key].
eq(parent_table[primary_key])
)
end
end
alias :join_has_one_to :join_has_many_to
def join_has_many_through_to(relation)
join_table = Arel::Table.new(
through_reflection.klass.table_name
).alias @aliased_join_table_name
jt_conditions = []
first_key = second_key = nil
if through_reflection.macro == :belongs_to
jt_primary_key = through_reflection.foreign_key
jt_foreign_key = through_reflection.association_primary_key
else
jt_primary_key = through_reflection.active_record_primary_key
jt_foreign_key = through_reflection.foreign_key
if through_reflection.options[:as] # has_many :through against a polymorphic join
jt_conditions <<
join_table["#{through_reflection.options[:as]}_type"].
eq(parent.active_record.base_class.name)
end
end
case source_reflection.macro
when :has_many
second_key = options[:foreign_key] || primary_key
if source_reflection.options[:as]
first_key = "#{source_reflection.options[:as]}_id"
else
first_key = through_reflection.klass.base_class.to_s.foreign_key
end
unless through_reflection.klass.descends_from_active_record?
jt_conditions <<
join_table[through_reflection.active_record.inheritance_column].
eq(through_reflection.klass.sti_name)
end
when :belongs_to
first_key = primary_key
if reflection.options[:source_type]
second_key = source_reflection.association_foreign_key
jt_conditions <<
join_table[reflection.source_reflection.foreign_type].
eq(reflection.options[:source_type])
else
second_key = source_reflection.foreign_key
end
end
jt_conditions <<
parent_table[jt_primary_key].
eq(join_table[jt_foreign_key])
if through_reflection.options[:conditions]
jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_name)
end
relation = relation.join(join_table, join_type).on(*jt_conditions)
join_target_table(
relation,
target_table[first_key].eq(join_table[second_key])
)
end
def join_has_many_polymorphic_to(relation)
join_target_table(
relation,
target_table["#{reflection.options[:as]}_id"].
eq(parent_table[parent.primary_key]).and(
target_table["#{reflection.options[:as]}_type"].
eq(parent.active_record.base_class.name))
)
end
def join_belongs_to_to(relation)
foreign_key = options[:foreign_key] || reflection.foreign_key
primary_key = options[:primary_key] || reflection.klass.primary_key
join_target_table(
relation,
target_table[primary_key].eq(parent_table[foreign_key])
)
end
end
end
end
end
end

View File

@@ -1,26 +0,0 @@
module ActiveRecord
module Associations
module ClassMethods
class JoinDependency # :nodoc:
class JoinBase < JoinPart # :nodoc:
def ==(other)
other.class == self.class &&
other.active_record == active_record
end
def aliased_prefix
"t0"
end
def table
Arel::Table.new(table_name, arel_engine)
end
def aliased_table_name
active_record.table_name
end
end
end
end
end
end

View File

@@ -1,80 +0,0 @@
module ActiveRecord
module Associations
module ClassMethods
class JoinDependency # :nodoc:
# A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited
# by JoinBase and JoinAssociation. A JoinBase represents the Active Record which
# everything else is being joined onto. A JoinAssociation represents an association which
# is joining to the base. A JoinAssociation may result in more than one actual join
# operations (for example a has_and_belongs_to_many JoinAssociation would result in
# two; one for the join table and one for the target table).
class JoinPart # :nodoc:
# The Active Record class which this join part is associated 'about'; for a JoinBase
# this is the actual base model, for a JoinAssociation this is the target model of the
# association.
attr_reader :active_record
delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :active_record
def initialize(active_record)
@active_record = active_record
@cached_record = {}
@column_names_with_alias = nil
end
def aliased_table
Arel::Nodes::TableAlias.new aliased_table_name, table
end
def ==(other)
raise NotImplementedError
end
# An Arel::Table for the active_record
def table
raise NotImplementedError
end
# The prefix to be used when aliasing columns in the active_record's table
def aliased_prefix
raise NotImplementedError
end
# The alias for the active_record's table
def aliased_table_name
raise NotImplementedError
end
# The alias for the primary key of the active_record's table
def aliased_primary_key
"#{aliased_prefix}_r0"
end
# An array of [column_name, alias] pairs for the table
def column_names_with_alias
unless @column_names_with_alias
@column_names_with_alias = []
([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i|
@column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
end
end
@column_names_with_alias
end
def extract_record(row)
Hash[column_names_with_alias.map{|cn, an| [cn, row[an]]}]
end
def record_id(row)
row[aliased_primary_key]
end
def instantiate(row)
@cached_record[record_id(row)] ||= active_record.send(:instantiate, extract_record(row))
end
end
end
end
end
end

View File

@@ -0,0 +1,231 @@
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
autoload :JoinPart, 'active_record/associations/join_dependency/join_part'
autoload :JoinBase, 'active_record/associations/join_dependency/join_base'
autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association'
attr_reader :join_parts, :reflections, :table_aliases, :active_record
def initialize(base, associations, joins)
@active_record = base
@table_joins = joins
@join_parts = [JoinBase.new(base)]
@associations = {}
@reflections = []
@table_aliases = Hash.new do |h,name|
h[name] = count_aliases_from_table_joins(name.downcase)
end
@table_aliases[base.table_name] = 1
build(associations)
end
def graft(*associations)
associations.each do |association|
join_associations.detect {|a| association == a} ||
build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
end
self
end
def join_associations
join_parts.last(join_parts.length - 1)
end
def join_base
join_parts.first
end
def columns
join_parts.collect { |join_part|
table = join_part.aliased_table
join_part.column_names_with_alias.collect{ |column_name, aliased_name|
table[column_name].as Arel.sql(aliased_name)
}
}.flatten
end
def count_aliases_from_table_joins(name)
return 0 if Arel::Table === @table_joins
# quoted_name should be downcased as some database adapters (Oracle) return quoted name in uppercase
quoted_name = active_record.connection.quote_table_name(name).downcase
@table_joins.map { |join|
# Table names + table aliases
join.left.downcase.scan(
/join(?:\s+\w+)?\s+(\S+\s+)?#{quoted_name}\son/
).size
}.sum
end
def instantiate(rows)
primary_key = join_base.aliased_primary_key
parents = {}
records = rows.map { |model|
primary_id = model[primary_key]
parent = parents[primary_id] ||= join_base.instantiate(model)
construct(parent, @associations, join_associations, model)
parent
}.uniq
remove_duplicate_results!(active_record, records, @associations)
records
end
def remove_duplicate_results!(base, records, associations)
case associations
when Symbol, String
reflection = base.reflections[associations]
remove_uniq_by_reflection(reflection, records)
when Array
associations.each do |association|
remove_duplicate_results!(base, records, association)
end
when Hash
associations.keys.each do |name|
reflection = base.reflections[name]
remove_uniq_by_reflection(reflection, records)
parent_records = []
records.each do |record|
if descendant = record.send(reflection.name)
if reflection.collection?
parent_records.concat descendant.target.uniq
else
parent_records << descendant
end
end
end
remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
end
end
end
protected
def cache_joined_association(association)
associations = []
parent = association.parent
while parent != join_base
associations.unshift(parent.reflection.name)
parent = parent.parent
end
ref = @associations
associations.each do |key|
ref = ref[key]
end
ref[association.reflection.name] ||= {}
end
def build(associations, parent = nil, join_type = Arel::InnerJoin)
parent ||= join_parts.last
case associations
when Symbol, String
reflection = parent.reflections[associations.to_s.intern] or
raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
unless join_association = find_join_association(reflection, parent)
@reflections << reflection
join_association = build_join_association(reflection, parent)
join_association.join_type = join_type
@join_parts << join_association
cache_joined_association(join_association)
end
join_association
when Array
associations.each do |association|
build(association, parent, join_type)
end
when Hash
associations.keys.sort_by { |a| a.to_s }.each do |name|
join_association = build(name, parent, join_type)
build(associations[name], join_association, join_type)
end
else
raise ConfigurationError, associations.inspect
end
end
def find_join_association(name_or_reflection, parent)
if String === name_or_reflection
name_or_reflection = name_or_reflection.to_sym
end
join_associations.detect { |j|
j.reflection == name_or_reflection && j.parent == parent
}
end
def remove_uniq_by_reflection(reflection, records)
if reflection && reflection.collection?
records.each { |record| record.send(reflection.name).target.uniq! }
end
end
def build_join_association(reflection, parent)
JoinAssociation.new(reflection, self, parent)
end
def construct(parent, associations, join_parts, row)
case associations
when Symbol, String
name = associations.to_s
join_part = join_parts.detect { |j|
j.reflection.name.to_s == name &&
j.parent_table_name == parent.class.table_name }
raise(ConfigurationError, "No such association") unless join_part
join_parts.delete(join_part)
construct_association(parent, join_part, row)
when Array
associations.each do |association|
construct(parent, association, join_parts, row)
end
when Hash
associations.sort_by { |k,_| k.to_s }.each do |association_name, assoc|
association = construct(parent, association_name, join_parts, row)
construct(association, assoc, join_parts, row) if association
end
else
raise ConfigurationError, associations.inspect
end
end
def construct_association(record, join_part, row)
return if record.id.to_s != join_part.parent.record_id(row).to_s
macro = join_part.reflection.macro
if macro == :has_one
return if record.association_cache.key?(join_part.reflection.name)
association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
set_target_and_inverse(join_part, association, record)
else
return if row[join_part.aliased_primary_key].nil?
association = join_part.instantiate(row)
case macro
when :has_many, :has_and_belongs_to_many
other = record.association(join_part.reflection.name)
other.loaded!
other.target.push(association)
other.set_inverse_instance(association)
when :belongs_to
set_target_and_inverse(join_part, association, record)
else
raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}"
end
end
association
end
def set_target_and_inverse(join_part, association, record)
other = record.association(join_part.reflection.name)
other.target = association
other.set_inverse_instance(association)
end
end
end
end

View File

@@ -0,0 +1,279 @@
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinAssociation < JoinPart # :nodoc:
# The reflection of the association represented
attr_reader :reflection
# The JoinDependency object which this JoinAssociation exists within. This is mainly
# relevant for generating aliases which do not conflict with other joins which are
# part of the query.
attr_reader :join_dependency
# A JoinBase instance representing the active record we are joining onto.
# (So in Author.has_many :posts, the Author would be that base record.)
attr_reader :parent
# What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
attr_accessor :join_type
# These implement abstract methods from the superclass
attr_reader :aliased_prefix, :aliased_table_name
delegate :options, :through_reflection, :source_reflection, :to => :reflection
delegate :table, :table_name, :to => :parent, :prefix => :parent
def initialize(reflection, join_dependency, parent = nil)
reflection.check_validity!
if reflection.options[:polymorphic]
raise EagerLoadPolymorphicError.new(reflection)
end
super(reflection.klass)
@reflection = reflection
@join_dependency = join_dependency
@parent = parent
@join_type = Arel::InnerJoin
@aliased_prefix = "t#{ join_dependency.join_parts.size }"
# This must be done eagerly upon initialisation because the alias which is produced
# depends on the state of the join dependency, but we want it to work the same way
# every time.
allocate_aliases
@table = Arel::Table.new(
table_name, :as => aliased_table_name, :engine => arel_engine
)
end
def ==(other)
other.class == self.class &&
other.reflection == reflection &&
other.parent == parent
end
def find_parent_in(other_join_dependency)
other_join_dependency.join_parts.detect do |join_part|
parent == join_part
end
end
def join_to(relation)
send("join_#{reflection.macro}_to", relation)
end
def join_relation(joining_relation)
self.join_type = Arel::OuterJoin
joining_relation.joins(self)
end
attr_reader :table
# More semantic name given we are talking about associations
alias_method :target_table, :table
protected
def aliased_table_name_for(name, suffix = nil)
aliases = @join_dependency.table_aliases
if aliases[name] != 0 # We need an alias
connection = active_record.connection
name = connection.table_alias_for "#{pluralize(reflection.name)}_#{parent_table_name}#{suffix}"
aliases[name] += 1
name = name[0, connection.table_alias_length-3] + "_#{aliases[name]}" if aliases[name] > 1
else
aliases[name] += 1
end
name
end
def pluralize(table_name)
ActiveRecord::Base.pluralize_table_names ? table_name.to_s.pluralize : table_name
end
private
def allocate_aliases
@aliased_table_name = aliased_table_name_for(table_name)
if reflection.macro == :has_and_belongs_to_many
@aliased_join_table_name = aliased_table_name_for(reflection.options[:join_table], "_join")
elsif [:has_many, :has_one].include?(reflection.macro) && reflection.options[:through]
@aliased_join_table_name = aliased_table_name_for(reflection.through_reflection.klass.table_name, "_join")
end
end
def process_conditions(conditions, table_name)
if conditions.respond_to?(:to_proc)
conditions = instance_eval(&conditions)
end
Arel.sql(sanitize_sql(conditions, table_name))
end
def sanitize_sql(condition, table_name)
active_record.send(:sanitize_sql, condition, table_name)
end
def join_target_table(relation, condition)
conditions = [condition]
# If the target table is an STI model then we must be sure to only include records of
# its type and its sub-types.
unless active_record.descends_from_active_record?
sti_column = target_table[active_record.inheritance_column]
subclasses = active_record.descendants
sti_condition = sti_column.eq(active_record.sti_name)
conditions << subclasses.inject(sti_condition) { |attr,subclass|
attr.or(sti_column.eq(subclass.sti_name))
}
end
# If the reflection has conditions, add them
if options[:conditions]
conditions << process_conditions(options[:conditions], aliased_table_name)
end
ands = relation.create_and(conditions)
join = relation.create_join(
target_table,
relation.create_on(ands),
join_type)
relation.from join
end
def join_has_and_belongs_to_many_to(relation)
join_table = Arel::Table.new(
options[:join_table]
).alias(@aliased_join_table_name)
fk = options[:foreign_key] || reflection.active_record.to_s.foreign_key
klass_fk = options[:association_foreign_key] || reflection.klass.to_s.foreign_key
relation = relation.join(join_table, join_type)
relation = relation.on(
join_table[fk].
eq(parent_table[reflection.active_record.primary_key])
)
join_target_table(
relation,
target_table[reflection.klass.primary_key].
eq(join_table[klass_fk])
)
end
def join_has_many_to(relation)
if reflection.options[:through]
join_has_many_through_to(relation)
elsif reflection.options[:as]
join_has_many_polymorphic_to(relation)
else
foreign_key = options[:foreign_key] || reflection.active_record.name.foreign_key
primary_key = options[:primary_key] || parent.primary_key
join_target_table(
relation,
target_table[foreign_key].
eq(parent_table[primary_key])
)
end
end
alias :join_has_one_to :join_has_many_to
def join_has_many_through_to(relation)
join_table = Arel::Table.new(
through_reflection.klass.table_name
).alias @aliased_join_table_name
jt_conditions = []
first_key = second_key = nil
if through_reflection.macro == :belongs_to
jt_primary_key = through_reflection.foreign_key
jt_foreign_key = through_reflection.association_primary_key
else
jt_primary_key = through_reflection.active_record_primary_key
jt_foreign_key = through_reflection.foreign_key
if through_reflection.options[:as] # has_many :through against a polymorphic join
jt_conditions <<
join_table["#{through_reflection.options[:as]}_type"].
eq(parent.active_record.base_class.name)
end
end
case source_reflection.macro
when :has_many
second_key = options[:foreign_key] || primary_key
if source_reflection.options[:as]
first_key = "#{source_reflection.options[:as]}_id"
else
first_key = through_reflection.klass.base_class.to_s.foreign_key
end
unless through_reflection.klass.descends_from_active_record?
jt_conditions <<
join_table[through_reflection.active_record.inheritance_column].
eq(through_reflection.klass.sti_name)
end
when :belongs_to
first_key = primary_key
if reflection.options[:source_type]
second_key = source_reflection.association_foreign_key
jt_conditions <<
join_table[reflection.source_reflection.foreign_type].
eq(reflection.options[:source_type])
else
second_key = source_reflection.foreign_key
end
end
jt_conditions <<
parent_table[jt_primary_key].
eq(join_table[jt_foreign_key])
if through_reflection.options[:conditions]
jt_conditions << process_conditions(through_reflection.options[:conditions], aliased_table_name)
end
relation = relation.join(join_table, join_type).on(*jt_conditions)
join_target_table(
relation,
target_table[first_key].eq(join_table[second_key])
)
end
def join_has_many_polymorphic_to(relation)
join_target_table(
relation,
target_table["#{reflection.options[:as]}_id"].
eq(parent_table[parent.primary_key]).and(
target_table["#{reflection.options[:as]}_type"].
eq(parent.active_record.base_class.name))
)
end
def join_belongs_to_to(relation)
foreign_key = options[:foreign_key] || reflection.foreign_key
primary_key = options[:primary_key] || reflection.klass.primary_key
join_target_table(
relation,
target_table[primary_key].eq(parent_table[foreign_key])
)
end
end
end
end
end

View File

@@ -0,0 +1,24 @@
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinBase < JoinPart # :nodoc:
def ==(other)
other.class == self.class &&
other.active_record == active_record
end
def aliased_prefix
"t0"
end
def table
Arel::Table.new(table_name, arel_engine)
end
def aliased_table_name
active_record.table_name
end
end
end
end
end

View File

@@ -0,0 +1,78 @@
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
# A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited
# by JoinBase and JoinAssociation. A JoinBase represents the Active Record which
# everything else is being joined onto. A JoinAssociation represents an association which
# is joining to the base. A JoinAssociation may result in more than one actual join
# operations (for example a has_and_belongs_to_many JoinAssociation would result in
# two; one for the join table and one for the target table).
class JoinPart # :nodoc:
# The Active Record class which this join part is associated 'about'; for a JoinBase
# this is the actual base model, for a JoinAssociation this is the target model of the
# association.
attr_reader :active_record
delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :active_record
def initialize(active_record)
@active_record = active_record
@cached_record = {}
@column_names_with_alias = nil
end
def aliased_table
Arel::Nodes::TableAlias.new aliased_table_name, table
end
def ==(other)
raise NotImplementedError
end
# An Arel::Table for the active_record
def table
raise NotImplementedError
end
# The prefix to be used when aliasing columns in the active_record's table
def aliased_prefix
raise NotImplementedError
end
# The alias for the active_record's table
def aliased_table_name
raise NotImplementedError
end
# The alias for the primary key of the active_record's table
def aliased_primary_key
"#{aliased_prefix}_r0"
end
# An array of [column_name, alias] pairs for the table
def column_names_with_alias
unless @column_names_with_alias
@column_names_with_alias = []
([primary_key] + (column_names - [primary_key])).each_with_index do |column_name, i|
@column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
end
end
@column_names_with_alias
end
def extract_record(row)
Hash[column_names_with_alias.map{|cn, an| [cn, row[an]]}]
end
def record_id(row)
row[aliased_primary_key]
end
def instantiate(row)
@cached_record[record_id(row)] ||= active_record.send(:instantiate, extract_record(row))
end
end
end
end
end

View File

@@ -0,0 +1,177 @@
module ActiveRecord
module Associations
# Implements the details of eager loading of Active Record associations.
#
# Note that 'eager loading' and 'preloading' are actually the same thing.
# However, there are two different eager loading strategies.
#
# The first one is by using table joins. This was only strategy available
# prior to Rails 2.1. Suppose that you have an Author model with columns
# 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using
# this strategy, Active Record would try to retrieve all data for an author
# and all of its books via a single query:
#
# SELECT * FROM authors
# LEFT OUTER JOIN books ON authors.id = books.id
# WHERE authors.name = 'Ken Akamatsu'
#
# However, this could result in many rows that contain redundant data. After
# having received the first row, we already have enough data to instantiate
# the Author object. In all subsequent rows, only the data for the joined
# 'books' table is useful; the joined 'authors' data is just redundant, and
# processing this redundant data takes memory and CPU time. The problem
# quickly becomes worse and worse as the level of eager loading increases
# (i.e. if Active Record is to eager load the associations' associations as
# well).
#
# The second strategy is to use multiple database queries, one for each
# level of association. Since Rails 2.1, this is the default strategy. In
# situations where a table join is necessary (e.g. when the +:conditions+
# option references an association's column), it will fallback to the table
# join strategy.
class Preloader #:nodoc:
autoload :Association, 'active_record/associations/preloader/association'
autoload :SingularAssociation, 'active_record/associations/preloader/singular_association'
autoload :CollectionAssociation, 'active_record/associations/preloader/collection_association'
autoload :ThroughAssociation, 'active_record/associations/preloader/through_association'
autoload :HasMany, 'active_record/associations/preloader/has_many'
autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
autoload :HasOne, 'active_record/associations/preloader/has_one'
autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
attr_reader :records, :associations, :options, :model
# Eager loads the named associations for the given Active Record record(s).
#
# In this description, 'association name' shall refer to the name passed
# to an association creation method. For example, a model that specifies
# <tt>belongs_to :author</tt>, <tt>has_many :buyers</tt> has association
# names +:author+ and +:buyers+.
#
# == Parameters
# +records+ is an array of ActiveRecord::Base. This array needs not be flat,
# i.e. +records+ itself may also contain arrays of records. In any case,
# +preload_associations+ will preload the all associations records by
# flattening +records+.
#
# +associations+ specifies one or more associations that you want to
# preload. It may be:
# - a Symbol or a String which specifies a single association name. For
# example, specifying +:books+ allows this method to preload all books
# for an Author.
# - an Array which specifies multiple association names. This array
# is processed recursively. For example, specifying <tt>[:avatar, :books]</tt>
# allows this method to preload an author's avatar as well as all of his
# books.
# - a Hash which specifies multiple association names, as well as
# association names for the to-be-preloaded association objects. For
# example, specifying <tt>{ :author => :avatar }</tt> will preload a
# book's author, as well as that author's avatar.
#
# +:associations+ has the same format as the +:include+ option for
# <tt>ActiveRecord::Base.find</tt>. So +associations+ could look like this:
#
# :books
# [ :books, :author ]
# { :author => :avatar }
# [ :books, { :author => :avatar } ]
#
# +options+ contains options that will be passed to ActiveRecord::Base#find
# (which is called under the hood for preloading records). But it is passed
# only one level deep in the +associations+ argument, i.e. it's not passed
# to the child associations when +associations+ is a Hash.
def initialize(records, associations, options = {})
@records = Array.wrap(records).compact.uniq
@associations = Array.wrap(associations)
@options = options
end
def run
unless records.empty?
associations.each { |association| preload(association) }
end
end
private
def preload(association)
case association
when Hash
preload_hash(association)
when String, Symbol
preload_one(association.to_sym)
else
raise ArgumentError, "#{association.inspect} was not recognised for preload"
end
end
def preload_hash(association)
association.each do |parent, child|
Preloader.new(records, parent, options).run
Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run
end
end
# Not all records have the same class, so group then preload group on the reflection
# itself so that if various subclass share the same association then we do not split
# them unnecessarily
#
# Additionally, polymorphic belongs_to associations can have multiple associated
# classes, depending on the polymorphic_type field. So we group by the classes as
# well.
def preload_one(association)
grouped_records(association).each do |reflection, klasses|
klasses.each do |klass, records|
preloader_for(reflection).new(klass, records, reflection, options).run
end
end
end
def grouped_records(association)
Hash[
records_by_reflection(association).map do |reflection, records|
[reflection, records.group_by { |record| association_klass(reflection, record) }]
end
]
end
def records_by_reflection(association)
records.group_by do |record|
reflection = record.class.reflections[association]
unless reflection
raise ActiveRecord::ConfigurationError, "Association named '#{association}' was not found; " \
"perhaps you misspelled it?"
end
reflection
end
end
def association_klass(reflection, record)
if reflection.macro == :belongs_to && reflection.options[:polymorphic]
klass = record.send(reflection.foreign_type)
klass && klass.constantize
else
reflection.klass
end
end
def preloader_for(reflection)
case reflection.macro
when :has_many
reflection.options[:through] ? HasManyThrough : HasMany
when :has_one
reflection.options[:through] ? HasOneThrough : HasOne
when :has_and_belongs_to_many
HasAndBelongsToMany
when :belongs_to
BelongsTo
end
end
end
end
end

View File

@@ -0,0 +1,126 @@
module ActiveRecord
module Associations
class Preloader
class Association #:nodoc:
attr_reader :owners, :reflection, :preload_options, :model, :klass
def initialize(klass, owners, reflection, preload_options)
@klass = klass
@owners = owners
@reflection = reflection
@preload_options = preload_options || {}
@model = owners.first && owners.first.class
@scoped = nil
@owners_by_key = nil
end
def run
unless owners.first.association(reflection.name).loaded?
preload
end
end
def preload
raise NotImplementedError
end
def scoped
@scoped ||= build_scope
end
def records_for(ids)
scoped.where(association_key.in(ids))
end
def table
klass.arel_table
end
# The name of the key on the associated records
def association_key_name
raise NotImplementedError
end
# This is overridden by HABTM as the condition should be on the foreign_key column in
# the join table
def association_key
table[association_key_name]
end
# The name of the key on the model which declares the association
def owner_key_name
raise NotImplementedError
end
# We're converting to a string here because postgres will return the aliased association
# key in a habtm as a string (for whatever reason)
def owners_by_key
@owners_by_key ||= owners.group_by do |owner|
key = owner[owner_key_name]
key && key.to_s
end
end
def options
reflection.options
end
private
def associated_records_by_owner
owner_keys = owners.map { |owner| owner[owner_key_name] }.compact.uniq
if klass.nil? || owner_keys.empty?
records = []
else
# Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
# Make several smaller queries if necessary or make one query if the adapter supports it
sliced = owner_keys.each_slice(model.connection.in_clause_length || owner_keys.size)
records = sliced.map { |slice| records_for(slice) }.flatten
end
# Each record may have multiple owners, and vice-versa
records_by_owner = Hash[owners.map { |owner| [owner, []] }]
records.each do |record|
owner_key = record[association_key_name].to_s
owners_by_key[owner_key].each do |owner|
records_by_owner[owner] << record
end
end
records_by_owner
end
def build_scope
scope = klass.scoped
scope = scope.where(process_conditions(options[:conditions]))
scope = scope.where(process_conditions(preload_options[:conditions]))
scope = scope.select(preload_options[:select] || options[:select] || table[Arel.star])
scope = scope.includes(preload_options[:include] || options[:include])
if options[:as]
scope = scope.where(
klass.table_name => {
reflection.type => model.base_class.sti_name
}
)
end
scope
end
def process_conditions(conditions)
if conditions.respond_to?(:to_proc)
conditions = klass.send(:instance_eval, &conditions)
end
if conditions
klass.send(:sanitize_sql, conditions)
end
end
end
end
end
end

View File

@@ -0,0 +1,17 @@
module ActiveRecord
module Associations
class Preloader
class BelongsTo < SingularAssociation #:nodoc:
def association_key_name
reflection.options[:primary_key] || klass && klass.primary_key
end
def owner_key_name
reflection.foreign_key
end
end
end
end
end

View File

@@ -0,0 +1,24 @@
module ActiveRecord
module Associations
class Preloader
class CollectionAssociation < Association #:nodoc:
private
def build_scope
super.order(preload_options[:order] || options[:order])
end
def preload
associated_records_by_owner.each do |owner, records|
association = owner.association(reflection.name)
association.loaded!
association.target.concat(records)
records.each { |record| association.set_inverse_instance(record) }
end
end
end
end
end
end

View File

@@ -0,0 +1,60 @@
module ActiveRecord
module Associations
class Preloader
class HasAndBelongsToMany < CollectionAssociation #:nodoc:
attr_reader :join_table
def initialize(klass, records, reflection, preload_options)
super
@join_table = Arel::Table.new(options[:join_table]).alias('t0')
end
# Unlike the other associations, we want to get a raw array of rows so that we can
# access the aliased column on the join table
def records_for(ids)
scope = super
klass.connection.select_all(scope.arel.to_sql, 'SQL', scope.bind_values)
end
def owner_key_name
reflection.active_record_primary_key
end
def association_key_name
'ar_association_key_name'
end
def association_key
join_table[reflection.foreign_key]
end
private
# Once we have used the join table column (in super), we manually instantiate the
# actual records, ensuring that we don't create more than one instances of the same
# record
def associated_records_by_owner
records = {}
super.each do |owner_key, rows|
rows.map! { |row| records[row[klass.primary_key]] ||= klass.instantiate(row) }
end
end
def build_scope
super.joins(join).select(join_select)
end
def join_select
association_key.as(Arel.sql(association_key_name))
end
def join
condition = table[reflection.association_primary_key].eq(
join_table[reflection.association_foreign_key])
table.create_join(join_table, table.create_on(condition))
end
end
end
end
end

View File

@@ -0,0 +1,17 @@
module ActiveRecord
module Associations
class Preloader
class HasMany < CollectionAssociation #:nodoc:
def association_key_name
reflection.foreign_key
end
def owner_key_name
reflection.active_record_primary_key
end
end
end
end
end

View File

@@ -0,0 +1,15 @@
module ActiveRecord
module Associations
class Preloader
class HasManyThrough < CollectionAssociation #:nodoc:
include ThroughAssociation
def associated_records_by_owner
super.each do |owner, records|
records.uniq! if options[:uniq]
end
end
end
end
end
end

View File

@@ -0,0 +1,23 @@
module ActiveRecord
module Associations
class Preloader
class HasOne < SingularAssociation #:nodoc:
def association_key_name
reflection.foreign_key
end
def owner_key_name
reflection.active_record_primary_key
end
private
def build_scope
super.order(preload_options[:order] || options[:order])
end
end
end
end
end

View File

@@ -0,0 +1,9 @@
module ActiveRecord
module Associations
class Preloader
class HasOneThrough < SingularAssociation #:nodoc:
include ThroughAssociation
end
end
end
end

View File

@@ -0,0 +1,21 @@
module ActiveRecord
module Associations
class Preloader
class SingularAssociation < Association #:nodoc:
private
def preload
associated_records_by_owner.each do |owner, associated_records|
record = associated_records.first
association = owner.association(reflection.name)
association.target = record
association.set_inverse_instance(record)
end
end
end
end
end
end

View File

@@ -0,0 +1,66 @@
module ActiveRecord
module Associations
class Preloader
module ThroughAssociation #:nodoc:
def through_reflection
reflection.through_reflection
end
def source_reflection
reflection.source_reflection
end
def associated_records_by_owner
through_records = through_records_by_owner
ActiveRecord::Associations::Preloader.new(
through_records.values.flatten,
source_reflection.name, options
).run
through_records.each do |owner, owner_through_records|
owner_through_records.map! { |r| r.send(source_reflection.name) }.flatten!
end
end
private
def through_records_by_owner
ActiveRecord::Associations::Preloader.new(
owners, through_reflection.name,
through_options
).run
Hash[owners.map do |owner|
through_records = Array.wrap(owner.send(through_reflection.name))
# Dont cache the association - we would only be caching a subset
if reflection.options[:source_type] && through_reflection.collection?
owner.association(through_reflection.name).reset
end
[owner, through_records]
end]
end
def through_options
through_options = {}
if options[:source_type]
through_options[:conditions] = { reflection.foreign_type => options[:source_type] }
else
if options[:conditions]
through_options[:include] = options[:include] || options[:source]
through_options[:conditions] = options[:conditions]
end
through_options[:order] = options[:order]
end
through_options
end
end
end
end
end

View File

@@ -10,7 +10,18 @@ module ActiveRecord
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
def define_attribute_methods
return if attribute_methods_generated?
super(column_names)
@attribute_methods_generated = true
end
def attribute_methods_generated?
@attribute_methods_generated ||= false
end
def undefine_attribute_methods(*args)
super
@attribute_methods_generated = false
end
# Checks whether the method is defined in the model or any of its subclasses

View File

@@ -1950,7 +1950,7 @@ MSG
include AttributeMethods::Dirty
include ActiveModel::MassAssignmentSecurity
include Callbacks, ActiveModel::Observing, Timestamp
include Associations, AssociationPreload, NamedScope
include Associations, NamedScope
include IdentityMap
include ActiveModel::SecurePassword

View File

@@ -89,6 +89,16 @@ module ActiveRecord
retrieve_connection
end
# Returns the configuration of the associated connection as a hash:
#
# ActiveRecord::Base.connection_config
# # => {:pool=>5, :timeout=>5000, :database=>"db/development.sqlite3", :adapter=>"sqlite3"}
#
# Please use only for reading.
def connection_config
connection_pool.spec.config
end
def connection_pool
connection_handler.retrieve_connection_pool(self)
end

View File

@@ -210,6 +210,10 @@ module ActiveRecord
@foreign_type ||= options[:foreign_type] || "#{name}_type"
end
def type
@type ||= "#{options[:as]}_type"
end
def primary_key_column
@primary_key_column ||= klass.columns.find { |c| c.name == klass.primary_key }
end
@@ -359,6 +363,8 @@ module ActiveRecord
# Holds all the meta-data about a :through association as it was specified
# in the Active Record class.
class ThroughReflection < AssociationReflection #:nodoc:
delegate :association_primary_key, :foreign_type, :to => :source_reflection
# Gets the source of the through reflection. It checks both a singularized
# and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
#
@@ -402,10 +408,6 @@ module ActiveRecord
through_reflection.options
end
def association_primary_key
source_reflection.association_primary_key
end
def check_validity!
if through_reflection.nil?
raise HasManyThroughAssociationNotFoundError.new(active_record.name, self)

View File

@@ -86,7 +86,9 @@ module ActiveRecord
preload = @preload_values
preload += @includes_values unless eager_loading?
preload.each {|associations| @klass.send(:preload_associations, @records, associations) }
preload.each do |associations|
ActiveRecord::Associations::Preloader.new(@records, associations).run
end
# @readonly_value is true only if set explicitly. @implicit_readonly is true if there
# are JOINS and no explicit SELECT.
@@ -190,7 +192,7 @@ module ActiveRecord
end
stmt = arel.compile_update(Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)))
stmt.take limit
stmt.take limit if limit
stmt.order(*order)
stmt.key = table[primary_key]
@klass.connection.update stmt.to_sql

View File

@@ -204,7 +204,19 @@ module ActiveRecord
relation.select_values = [select_value]
type_cast_calculated_value(@klass.connection.select_value(relation.to_sql), column_for(column_name), operation)
query_builder = relation.arel
if operation == "count"
limit = relation.limit_value
offset = relation.offset_value
unless limit && offset
query_builder.limit = nil
query_builder.offset = nil
end
end
type_cast_calculated_value(@klass.connection.select_value(query_builder.to_sql), column_for(column_name), operation)
end
def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:

View File

@@ -187,7 +187,7 @@ module ActiveRecord
def find_with_associations
including = (@eager_load_values + @includes_values).uniq
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, [])
join_dependency = ActiveRecord::Associations::JoinDependency.new(@klass, including, [])
relation = construct_relation_for_association_find(join_dependency)
rows = connection.select_all(relation.to_sql, 'SQL', relation.bind_values)
join_dependency.instantiate(rows)
@@ -197,7 +197,7 @@ module ActiveRecord
def construct_relation_for_association_calculations
including = (@eager_load_values + @includes_values).uniq
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(@klass, including, arel.froms.first)
join_dependency = ActiveRecord::Associations::JoinDependency.new(@klass, including, arel.froms.first)
relation = except(:includes, :eager_load, :preload)
apply_join_dependency(relation, join_dependency)
end

View File

@@ -236,7 +236,7 @@ module ActiveRecord
'string_join'
when Hash, Symbol, Array
'association_join'
when ActiveRecord::Associations::ClassMethods::JoinDependency::JoinAssociation
when ActiveRecord::Associations::JoinDependency::JoinAssociation
'stashed_join'
when Arel::Nodes::Join
'join_node'
@@ -254,7 +254,7 @@ module ActiveRecord
join_list = custom_join_ast(manager, string_joins)
join_dependency = ActiveRecord::Associations::ClassMethods::JoinDependency.new(
join_dependency = ActiveRecord::Associations::JoinDependency.new(
@klass,
association_joins,
join_list
@@ -284,7 +284,7 @@ module ActiveRecord
@implicit_readonly = false
arel.project(*selects)
else
arel.project(Arel.sql(@klass.quoted_table_name + '.*'))
arel.project(@klass.arel_table[Arel.star])
end
end

View File

@@ -26,6 +26,7 @@ module ActiveRecord
def assert_sql(*patterns_to_match)
$queries_executed = []
yield
$queries_executed
ensure
failed_patterns = []
patterns_to_match.each do |pattern|

View File

@@ -120,30 +120,29 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_load_associated_records_in_one_query_when_adapter_has_no_limit
Post.connection.expects(:in_clause_length).at_least_once.returns(nil)
Post.expects(:i_was_called).with([1,2,3,4,5,6,7]).returns([1])
associated_records = Post.send(:associated_records, [1,2,3,4,5,6,7]) do |some_ids|
Post.i_was_called(some_ids)
post = posts(:welcome)
assert_queries(2) do
Post.includes(:comments).where(:id => post.id).to_a
end
assert_equal [1], associated_records
end
def test_load_associated_records_in_several_queries_when_many_ids_passed
Post.connection.expects(:in_clause_length).at_least_once.returns(5)
Post.expects(:i_was_called).with([1,2,3,4,5]).returns([1])
Post.expects(:i_was_called).with([6,7]).returns([6])
associated_records = Post.send(:associated_records, [1,2,3,4,5,6,7]) do |some_ids|
Post.i_was_called(some_ids)
Post.connection.expects(:in_clause_length).at_least_once.returns(1)
post1, post2 = posts(:welcome), posts(:thinking)
assert_queries(3) do
Post.includes(:comments).where(:id => [post1.id, post2.id]).to_a
end
assert_equal [1,6], associated_records
end
def test_load_associated_records_in_one_query_when_a_few_ids_passed
Post.connection.expects(:in_clause_length).at_least_once.returns(5)
Post.expects(:i_was_called).with([1,2,3]).returns([1])
associated_records = Post.send(:associated_records, [1,2,3]) do |some_ids|
Post.i_was_called(some_ids)
Post.connection.expects(:in_clause_length).at_least_once.returns(3)
post = posts(:welcome)
assert_queries(2) do
Post.includes(:comments).where(:id => post.id).to_a
end
assert_equal [1], associated_records
end
def test_including_duplicate_objects_from_belongs_to
@@ -526,6 +525,22 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert posts[1].categories.include?(categories(:general))
end
# This is only really relevant when the identity map is off. Since the preloader for habtm
# gets raw row hashes from the database and then instantiates them, this test ensures that
# it only instantiates one actual object per record from the database.
def test_has_and_belongs_to_many_should_not_instantiate_same_records_multiple_times
welcome = posts(:welcome)
categories = Category.includes(:posts)
general = categories.find { |c| c == categories(:general) }
technology = categories.find { |c| c == categories(:technology) }
post1 = general.posts.to_a.find { |p| p == posts(:welcome) }
post2 = technology.posts.to_a.find { |p| p == posts(:welcome) }
assert_equal post1.object_id, post2.object_id
end
def test_eager_with_has_many_and_limit_and_conditions_on_the_eagers
posts = authors(:david).posts.find(:all,
:include => :comments,

View File

@@ -214,7 +214,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_has_many_with_piggyback
assert_equal "2", categories(:sti_test).authors.first.post_id.to_s
assert_equal "2", categories(:sti_test).authors_with_select.first.post_id.to_s
end
def test_include_has_many_through

View File

@@ -134,6 +134,7 @@ class BasicsTest < ActiveRecord::TestCase
fakepool = Class.new(Struct.new(:spec)) {
def with_connection; yield self; end
def connection_pool; self; end
def table_exists?(name); false; end
def quote_table_name(*args); raise "lol quote_table_name"; end
}

View File

@@ -109,6 +109,36 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal [2, 6], c.keys.compact
end
def test_limit_with_offset_is_kept
return if current_adapter?(:OracleAdapter)
queries = assert_sql { Account.limit(1).offset(1).count }
assert_equal 1, queries.length
assert_match(/LIMIT/, queries.first)
assert_match(/OFFSET/, queries.first)
end
def test_offset_without_limit_removes_offset
queries = assert_sql { Account.offset(1).count }
assert_equal 1, queries.length
assert_no_match(/LIMIT/, queries.first)
assert_no_match(/OFFSET/, queries.first)
end
def test_limit_without_offset_removes_limit
queries = assert_sql { Account.limit(1).count }
assert_equal 1, queries.length
assert_no_match(/LIMIT/, queries.first)
assert_no_match(/OFFSET/, queries.first)
end
def test_no_limit_no_offset
queries = assert_sql { Account.count }
assert_equal 1, queries.length
assert_no_match(/LIMIT/, queries.first)
assert_no_match(/OFFSET/, queries.first)
end
def test_should_group_by_summed_field_having_condition
c = Account.sum(:credit_limit, :group => :firm_id,
:having => 'sum(credit_limit) > 50')

View File

@@ -208,7 +208,7 @@ class InheritanceTest < ActiveRecord::TestCase
def test_eager_load_belongs_to_primary_key_quoting
con = Account.connection
assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} = 1/) do
assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} IN \(1\)/) do
Account.find(1, :include => :firm)
end
end

View File

@@ -94,6 +94,11 @@ class PooledConnectionsTest < ActiveRecord::TestCase
ActiveRecord::Base.connection_handler = old_handler
end
def test_connection_config
ActiveRecord::Base.establish_connection(@connection)
assert_equal @connection, ActiveRecord::Base.connection_config
end
def test_with_connection_nesting_safety
ActiveRecord::Base.establish_connection(@connection.merge({:pool => 1, :wait_timeout => 0.1}))

View File

@@ -22,7 +22,8 @@ class Category < ActiveRecord::Base
end
has_many :categorizations
has_many :authors, :through => :categorizations, :select => 'authors.*, categorizations.post_id'
has_many :authors, :through => :categorizations
has_many :authors_with_select, :through => :categorizations, :source => :author, :select => 'authors.*, categorizations.post_id'
scope :general, :conditions => { :name => 'General' }
end

View File

@@ -1,5 +1,14 @@
*Rails 3.1.0 (unreleased)*
* LocalCache strategy is now a real middleware class, not an anonymous class
posing for pictures.
* ActiveSupport::Dependencies::ClassCache class has been introduced for
holding references to reloadable classes.
* ActiveSupport::Dependencies::Reference has been refactored to take direct
advantage of the new ClassCache.
* Backports Range#cover? as an alias for Range#include? in Ruby 1.8 [Diego Carrion, fxn]
* Added weeks_ago and prev_week to Date/DateTime/Time. [Rob Zolkos, fxn]

View File

@@ -50,32 +50,37 @@ module ActiveSupport
end
end
#--
# This class wraps up local storage for middlewares. Only the middleware method should
# construct them.
class Middleware # :nodoc:
attr_reader :name, :thread_local_key
def initialize(name, thread_local_key)
@name = name
@thread_local_key = thread_local_key
@app = nil
end
def new(app)
@app = app
self
end
def call(env)
Thread.current[thread_local_key] = LocalStore.new
@app.call(env)
ensure
Thread.current[thread_local_key] = nil
end
end
# Middleware class can be inserted as a Rack handler to be local cache for the
# duration of request.
def middleware
@middleware ||= begin
klass = Class.new
klass.class_eval(<<-EOS, __FILE__, __LINE__ + 1)
class << self
def name
"ActiveSupport::Cache::Strategy::LocalCache"
end
alias :to_s :name
end
def initialize(app)
@app = app
end
def call(env)
Thread.current[:#{thread_local_key}] = LocalStore.new
@app.call(env)
ensure
Thread.current[:#{thread_local_key}] = nil
end
EOS
klass
end
@middleware ||= Middleware.new(
"ActiveSupport::Cache::Strategy::LocalCache",
thread_local_key)
end
def clear(options = nil) # :nodoc:

View File

@@ -26,7 +26,7 @@ module ActiveSupport
module ClassMethods
def config
@_config ||= if superclass.respond_to?(:config)
@_config ||= if respond_to?(:superclass) && superclass.respond_to?(:config)
superclass.config.inheritable_copy
else
# create a new "anonymous" class that will host the compiled reader methods

View File

@@ -36,9 +36,9 @@ class Date
::Date.current.tomorrow
end
# Returns Time.zone.today when config.time_zone is set, otherwise just returns Date.today.
# Returns Time.zone.today when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise just returns Date.today.
def current
::Time.zone_default ? ::Time.zone.today : ::Date.today
::Time.zone ? ::Time.zone.today : ::Date.today
end
end

View File

@@ -2,10 +2,10 @@ require 'date'
require 'active_support/core_ext/time/zones'
class Date
# Converts Date to a TimeWithZone in the current zone if Time.zone_default is set,
# otherwise converts Date to a Time via Date#to_time
# Converts Date to a TimeWithZone in the current zone if Time.zone or Time.zone_default
# is set, otherwise converts Date to a Time via Date#to_time
def to_time_in_current_zone
if ::Time.zone_default
if ::Time.zone
::Time.zone.local(year, month, day)
else
to_time

View File

@@ -1,6 +1,4 @@
require 'rational' unless RUBY_VERSION >= '1.9.2'
require 'active_support/core_ext/object/acts_like'
require 'active_support/core_ext/time/zones'
class DateTime
class << self
@@ -9,8 +7,9 @@ class DateTime
::Time.local(2007).utc_offset.to_r / 86400
end
# Returns <tt>Time.zone.now.to_datetime</tt> when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise returns <tt>Time.now.to_datetime</tt>.
def current
::Time.zone_default ? ::Time.zone.now.to_datetime : ::Time.now.to_datetime
::Time.zone ? ::Time.zone.now.to_datetime : ::Time.now.to_datetime
end
end
@@ -104,11 +103,7 @@ class DateTime
end
# Layers additional behavior on DateTime#<=> so that Time and ActiveSupport::TimeWithZone instances can be compared with a DateTime
def compare_with_coercion(other)
other = other.comparable_time if other.respond_to?(:comparable_time)
other = other.to_datetime unless other.acts_like?(:date)
compare_without_coercion(other)
def <=>(other)
super other.to_datetime
end
alias_method :compare_without_coercion, :<=>
alias_method :<=>, :compare_with_coercion
end

View File

@@ -1,11 +1,9 @@
class Hash
# Allows for reverse merging two hashes where the keys in the calling hash take precedence over those
# in the <tt>other_hash</tt>. This is particularly useful for initializing an option hash with default values:
# Merges the caller into +other_hash+. For example,
#
# def setup(options = {})
# options.reverse_merge! :size => 25, :velocity => 10
# end
# options = options.reverse_merge(:size => 25, :velocity => 10)
#
<<<<<<< HEAD
# The default <tt>:size</tt> and <tt>:velocity</tt> are only set if the +options+ hash passed in doesn't already
# have the respective key.
#
@@ -14,12 +12,19 @@ class Hash
# def setup(options = {})
# options = { :size => 25, :velocity => 10 }.merge(options)
# end
=======
# is equivalent to
#
# options = {:size => 25, :velocity => 10}.merge(options)
#
# This is particularly useful for initializing an options hash
# with default values.
>>>>>>> 20768176292cbcb883ab152b4aa9ed8c664771cd
def reverse_merge(other_hash)
other_hash.merge(self)
end
# Performs the opposite of <tt>merge</tt>, with the keys and values from the first hash taking precedence over the second.
# Modifies the receiver in place.
# Destructive +reverse_merge+.
def reverse_merge!(other_hash)
# right wins if there is no left
merge!( other_hash ){|key,left,right| left }

View File

@@ -13,7 +13,11 @@ class Object
respond_to?(:empty?) ? empty? : !self
end
<<<<<<< HEAD
# An object is present if it's not #blank?.
=======
# An object is present if it's not <tt>blank?</tt>.
>>>>>>> 20768176292cbcb883ab152b4aa9ed8c664771cd
def present?
!blank?
end

View File

@@ -1,7 +1,4 @@
require 'active_support/duration'
require 'active_support/core_ext/date/acts_like'
require 'active_support/core_ext/date/calculations'
require 'active_support/core_ext/date_time/conversions'
class Time
COMMON_YEAR_DAYS_IN_MONTH = [nil, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
@@ -41,9 +38,9 @@ class Time
time_with_datetime_fallback(:local, *args)
end
# Returns <tt>Time.zone.now</tt> when <tt>config.time_zone</tt> is set, otherwise just returns <tt>Time.now</tt>.
# Returns <tt>Time.zone.now</tt> when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise just returns <tt>Time.now</tt>.
def current
::Time.zone_default ? ::Time.zone.now : ::Time.now
::Time.zone ? ::Time.zone.now : ::Time.now
end
end
@@ -283,14 +280,8 @@ class Time
# Layers additional behavior on Time#<=> so that DateTime and ActiveSupport::TimeWithZone instances
# can be chronologically compared with a Time
def compare_with_coercion(other)
# if other is an ActiveSupport::TimeWithZone, coerce a Time instance from it so we can do <=> comparison
other = other.comparable_time if other.respond_to?(:comparable_time)
if other.acts_like?(:date)
# other is a Date/DateTime, so coerce self #to_datetime and hand off to DateTime#<=>
to_datetime.compare_without_coercion(other)
else
compare_without_coercion(other)
end
# we're avoiding Time#to_datetime cause it's expensive
other.is_a?(Time) ? compare_without_coercion(other.to_time) : to_datetime <=> other
end
alias_method :compare_without_coercion, :<=>
alias_method :<=>, :compare_with_coercion

View File

@@ -5,6 +5,7 @@ require 'active_support/core_ext/module/aliasing'
require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/module/introspection'
require 'active_support/core_ext/module/anonymous'
require 'active_support/core_ext/module/deprecation'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/load_error'
require 'active_support/core_ext/name_error'
@@ -47,9 +48,6 @@ module ActiveSupport #:nodoc:
mattr_accessor :autoloaded_constants
self.autoloaded_constants = []
mattr_accessor :references
self.references = {}
# An array of constant names that need to be unloaded on every request. Used
# to allow arbitrary constants to be marked for unloading.
mattr_accessor :explicitly_unloadable_constants
@@ -524,31 +522,76 @@ module ActiveSupport #:nodoc:
explicitly_unloadable_constants.each { |const| remove_constant const }
end
class Reference
@@constants = Hash.new { |h, k| h[k] = Inflector.constantize(k) }
attr_reader :name
def initialize(name)
@name = name.to_s
@@constants[@name] = name if name.respond_to?(:name)
class ClassCache
def initialize
@store = Hash.new { |h, k| h[k] = Inflector.constantize(k) }
end
def get
@@constants[@name]
def empty?
@store.empty?
end
def self.clear!
@@constants.clear
def key?(key)
@store.key?(key)
end
def []=(key, value)
return unless key.respond_to?(:name)
raise(ArgumentError, 'anonymous classes cannot be cached') if key.name.blank?
@store[key.name] = value
end
def [](key)
key = key.name if key.respond_to?(:name)
@store[key]
end
alias :get :[]
class Getter # :nodoc:
def initialize(name)
@name = name
end
def get
Reference.get @name
end
deprecate :get
end
def new(name)
self[name] = name
Getter.new(name)
end
deprecate :new
def store(name)
self[name] = name
self
end
def clear!
@store.clear
end
end
Reference = ClassCache.new
def ref(name)
references[name] ||= Reference.new(name)
Reference.new(name)
end
deprecate :ref
# Store a reference to a class +klass+.
def reference(klass)
Reference.store klass
end
# Get the reference for class named +name+.
def constantize(name)
ref(name).get
Reference.get(name)
end
# Determine if the given constant has been automatically loaded.

View File

@@ -31,7 +31,7 @@ module ActiveSupport #:nodoc:
def method_missing(name, *args)
if name.to_s =~ /(.*)=$/
self[$1.to_sym] = args.first
self[$1] = args.first
else
self[name]
end

View File

@@ -281,7 +281,7 @@ module ActiveSupport
# A TimeWithZone acts like a Time, so just return +self+.
def to_time
self
utc
end
def to_datetime

View File

@@ -0,0 +1,108 @@
require 'abstract_unit'
require 'active_support/dependencies'
module ActiveSupport
module Dependencies
class ClassCacheTest < ActiveSupport::TestCase
def setup
@cache = ClassCache.new
end
def test_empty?
assert @cache.empty?
@cache[ClassCacheTest] = ClassCacheTest
assert !@cache.empty?
end
def test_clear!
assert @cache.empty?
@cache[ClassCacheTest] = ClassCacheTest
assert !@cache.empty?
@cache.clear!
assert @cache.empty?
end
def test_set_key
@cache[ClassCacheTest] = ClassCacheTest
assert @cache.key?(ClassCacheTest.name)
end
def test_set_rejects_strings
@cache[ClassCacheTest.name] = ClassCacheTest
assert @cache.empty?
end
def test_get_with_class
@cache[ClassCacheTest] = ClassCacheTest
assert_equal ClassCacheTest, @cache[ClassCacheTest]
end
def test_get_with_name
@cache[ClassCacheTest] = ClassCacheTest
assert_equal ClassCacheTest, @cache[ClassCacheTest.name]
end
def test_get_constantizes
assert @cache.empty?
assert_equal ClassCacheTest, @cache[ClassCacheTest.name]
end
def test_get_is_an_alias
assert_equal @cache[ClassCacheTest], @cache.get(ClassCacheTest.name)
end
def test_new
assert_deprecated do
@cache.new ClassCacheTest
end
assert @cache.key?(ClassCacheTest.name)
end
def test_new_rejects_strings
assert_deprecated do
@cache.new ClassCacheTest.name
end
assert !@cache.key?(ClassCacheTest.name)
end
def test_new_rejects_strings
@cache.store ClassCacheTest.name
assert !@cache.key?(ClassCacheTest.name)
end
def test_store_returns_self
x = @cache.store ClassCacheTest
assert_equal @cache, x
end
def test_new_returns_proxy
v = nil
assert_deprecated do
v = @cache.new ClassCacheTest.name
end
assert_deprecated do
assert_equal ClassCacheTest, v.get
end
end
def test_anonymous_class_fail
assert_raises(ArgumentError) do
assert_deprecated do
@cache.new Class.new
end
end
assert_raises(ArgumentError) do
x = Class.new
@cache[x] = x
end
assert_raises(ArgumentError) do
x = Class.new
@cache.store x
end
end
end
end
end

View File

@@ -21,6 +21,12 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase
assert_equal({ :foo => :bar }, Parent.config)
end
test "adds a configuration hash to a module as well" do
mixin = Module.new { include ActiveSupport::Configurable }
mixin.config.foo = :bar
assert_equal({ :foo => :bar }, mixin.config)
end
test "configuration hash is inheritable" do
assert_equal :bar, Child.config.foo
assert_equal :bar, Parent.config.foo
@@ -57,4 +63,4 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase
assert_respond_to child.config, :bar
assert_respond_to child.new.config, :bar
end
end
end

View File

@@ -259,7 +259,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal Date.current - 1, Date.yesterday
end
def test_yesterday_constructor_when_zone_default_is_not_set
def test_yesterday_constructor_when_zone_is_not_set
with_env_tz 'UTC' do
with_tz_default do
Time.stubs(:now).returns Time.local(2000, 1, 1)
@@ -268,7 +268,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
end
def test_yesterday_constructor_when_zone_default_is_set
def test_yesterday_constructor_when_zone_is_set
with_env_tz 'UTC' do
with_tz_default ActiveSupport::TimeZone['Eastern Time (US & Canada)'] do # UTC -5
Time.stubs(:now).returns Time.local(2000, 1, 1)
@@ -281,7 +281,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal Date.current + 1, Date.tomorrow
end
def test_tomorrow_constructor_when_zone_default_is_not_set
def test_tomorrow_constructor_when_zone_is_not_set
with_env_tz 'UTC' do
with_tz_default do
Time.stubs(:now).returns Time.local(1999, 12, 31)
@@ -290,7 +290,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
end
def test_tomorrow_constructor_when_zone_default_is_set
def test_tomorrow_constructor_when_zone_is_set
with_env_tz 'UTC' do
with_tz_default ActiveSupport::TimeZone['Europe/Paris'] do # UTC +1
Time.stubs(:now).returns Time.local(1999, 12, 31, 23)
@@ -303,7 +303,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal Time.local(2005,2,21,0,0,45), Date.new(2005,2,21).since(45)
end
def test_since_when_zone_default_is_set
def test_since_when_zone_is_set
zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
with_env_tz 'UTC' do
with_tz_default zone do
@@ -317,7 +317,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal Time.local(2005,2,20,23,59,15), Date.new(2005,2,21).ago(45)
end
def test_ago_when_zone_default_is_set
def test_ago_when_zone_is_set
zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
with_env_tz 'UTC' do
with_tz_default zone do
@@ -331,7 +331,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal Time.local(2005,2,21,0,0,0), Date.new(2005,2,21).beginning_of_day
end
def test_beginning_of_day_when_zone_default_is_set
def test_beginning_of_day_when_zone_is_set
zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
with_env_tz 'UTC' do
with_tz_default zone do
@@ -345,7 +345,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal Time.local(2005,2,21,23,59,59,999999.999), Date.new(2005,2,21).end_of_day
end
def test_end_of_day_when_zone_default_is_set
def test_end_of_day_when_zone_is_set
zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
with_env_tz 'UTC' do
with_tz_default zone do
@@ -367,7 +367,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
end
def test_xmlschema_when_zone_default_is_set
def test_xmlschema_when_zone_is_set
with_env_tz 'UTC' do
with_tz_default ActiveSupport::TimeZone['Eastern Time (US & Canada)'] do # UTC -5
assert_match(/^1980-02-28T00:00:00-05:?00$/, Date.new(1980, 2, 28).xmlschema)
@@ -407,7 +407,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal true, Date.new(2000,1,2).future?
end
def test_current_returns_date_today_when_zone_default_not_set
def test_current_returns_date_today_when_zone_not_set
with_env_tz 'US/Central' do
Time.stubs(:now).returns Time.local(1999, 12, 31, 23)
assert_equal Date.new(1999, 12, 31), Date.today
@@ -415,15 +415,15 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
end
def test_current_returns_time_zone_today_when_zone_default_set
Time.zone_default = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
def test_current_returns_time_zone_today_when_zone_is_set
Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
with_env_tz 'US/Central' do
Time.stubs(:now).returns Time.local(1999, 12, 31, 23)
assert_equal Date.new(1999, 12, 31), Date.today
assert_equal Date.new(2000, 1, 1), Date.current
end
ensure
Time.zone_default = nil
Time.zone = nil
end
def test_date_advance_should_not_change_passed_options_hash
@@ -441,11 +441,11 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
def with_tz_default(tz = nil)
old_tz = Time.zone_default
Time.zone_default = tz
old_tz = Time.zone
Time.zone = tz
yield
ensure
Time.zone_default = old_tz
Time.zone = old_tz
end
end

View File

@@ -282,21 +282,21 @@ class DateTimeExtCalculationsTest < Test::Unit::TestCase
assert_equal true, DateTime.civil(2005,2,10,20,30,46).future?
end
def test_current_returns_date_today_when_zone_default_not_set
def test_current_returns_date_today_when_zone_is_not_set
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(1999, 12, 31, 23, 59, 59)
assert_equal DateTime.new(1999, 12, 31, 23, 59, 59, Rational(-18000, 86400)), DateTime.current
end
end
def test_current_returns_time_zone_today_when_zone_default_set
Time.zone_default = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
def test_current_returns_time_zone_today_when_zone_is_set
Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(1999, 12, 31, 23, 59, 59)
assert_equal DateTime.new(1999, 12, 31, 23, 59, 59, Rational(-18000, 86400)), DateTime.current
end
ensure
Time.zone_default = nil
Time.zone = nil
end
def test_current_without_time_zone

View File

@@ -89,8 +89,8 @@ class DurationTest < ActiveSupport::TestCase
assert_in_delta((7 * 24 * 1.7).hours.ago(t), 1.7.weeks.ago(t), 1)
end
def test_since_and_ago_anchored_to_time_now_when_time_zone_default_not_set
Time.zone_default = nil
def test_since_and_ago_anchored_to_time_now_when_time_zone_is_not_set
Time.zone = nil
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(2000)
# since
@@ -102,8 +102,8 @@ class DurationTest < ActiveSupport::TestCase
end
end
def test_since_and_ago_anchored_to_time_zone_now_when_time_zone_default_set
Time.zone_default = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
def test_since_and_ago_anchored_to_time_zone_now_when_time_zone_is_set
Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(2000)
# since
@@ -116,7 +116,7 @@ class DurationTest < ActiveSupport::TestCase
assert_equal 'Eastern Time (US & Canada)', 5.seconds.ago.time_zone.name
end
ensure
Time.zone_default = nil
Time.zone = nil
end
def test_adding_hours_across_dst_boundary

View File

@@ -89,8 +89,8 @@ class NumericExtTimeAndDateTimeTest < Test::Unit::TestCase
assert_equal DateTime.civil(2005,2,28,15,15,10), DateTime.civil(2004,2,29,15,15,10) + 1.year
end
def test_since_and_ago_anchored_to_time_now_when_time_zone_default_not_set
Time.zone_default = nil
def test_since_and_ago_anchored_to_time_now_when_time_zone_is_not_set
Time.zone = nil
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(2000)
# since
@@ -102,8 +102,8 @@ class NumericExtTimeAndDateTimeTest < Test::Unit::TestCase
end
end
def test_since_and_ago_anchored_to_time_zone_now_when_time_zone_default_set
Time.zone_default = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
def test_since_and_ago_anchored_to_time_zone_now_when_time_zone_is_set
Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(2000)
# since
@@ -116,7 +116,7 @@ class NumericExtTimeAndDateTimeTest < Test::Unit::TestCase
assert_equal 'Eastern Time (US & Canada)', 5.ago.time_zone.name
end
ensure
Time.zone_default = nil
Time.zone = nil
end
protected

View File

@@ -533,9 +533,19 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
Time::DATE_FORMATS.delete(:custom)
end
def test_conversion_methods_are_publicized
assert Time.public_instance_methods.include?(:to_date) || Time.public_instance_methods.include?('to_date')
assert Time.public_instance_methods.include?(:to_datetime) || Time.public_instance_methods.include?('to_datetime')
def test_to_date
assert_equal Date.new(2005, 2, 21), Time.local(2005, 2, 21, 17, 44, 30).to_date
end
def test_to_datetime
assert_equal Time.utc(2005, 2, 21, 17, 44, 30).to_datetime, DateTime.civil(2005, 2, 21, 17, 44, 30, 0, 0)
with_env_tz 'US/Eastern' do
assert_equal Time.local(2005, 2, 21, 17, 44, 30).to_datetime, DateTime.civil(2005, 2, 21, 17, 44, 30, Rational(Time.local(2005, 2, 21, 17, 44, 30).utc_offset, 86400), 0)
end
with_env_tz 'NZ' do
assert_equal Time.local(2005, 2, 21, 17, 44, 30).to_datetime, DateTime.civil(2005, 2, 21, 17, 44, 30, Rational(Time.local(2005, 2, 21, 17, 44, 30).utc_offset, 86400), 0)
end
assert_equal ::Date::ITALY, Time.utc(2005, 2, 21, 17, 44, 30).to_datetime.start # use Ruby's default start value
end
def test_to_time

View File

@@ -768,10 +768,10 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase
end
def test_localtime
Time.zone_default = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
assert_equal @dt.in_time_zone.localtime, @dt.in_time_zone.utc.to_time.getlocal
ensure
Time.zone_default = nil
Time.zone = nil
end
def test_use_zone
@@ -801,7 +801,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase
assert_equal nil, Time.zone
end
def test_time_zone_getter_and_setter_with_zone_default
def test_time_zone_getter_and_setter_with_zone_default_set
Time.zone_default = ActiveSupport::TimeZone['Alaska']
assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone
Time.zone = ActiveSupport::TimeZone['Hawaii']
@@ -809,6 +809,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase
Time.zone = nil
assert_equal ActiveSupport::TimeZone['Alaska'], Time.zone
ensure
Time.zone = nil
Time.zone_default = nil
end
@@ -849,7 +850,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase
assert_nil Time.zone
end
def test_current_returns_time_now_when_zone_default_not_set
def test_current_returns_time_now_when_zone_not_set
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(2000)
assert_equal false, Time.current.is_a?(ActiveSupport::TimeWithZone)
@@ -857,8 +858,8 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase
end
end
def test_current_returns_time_zone_now_when_zone_default_set
Time.zone_default = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
def test_current_returns_time_zone_now_when_zone_set
Time.zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
with_env_tz 'US/Eastern' do
Time.stubs(:now).returns Time.local(2000)
assert_equal true, Time.current.is_a?(ActiveSupport::TimeWithZone)
@@ -866,7 +867,7 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < Test::Unit::TestCase
assert_equal Time.utc(2000), Time.current.time
end
ensure
Time.zone_default = nil
Time.zone = nil
end
protected

View File

@@ -477,15 +477,15 @@ class DependenciesTest < Test::Unit::TestCase
def test_references_should_work
with_loading 'dependencies' do
c = ActiveSupport::Dependencies.ref("ServiceOne")
c = ActiveSupport::Dependencies.reference("ServiceOne")
service_one_first = ServiceOne
assert_equal service_one_first, c.get
assert_equal service_one_first, c.get("ServiceOne")
ActiveSupport::Dependencies.clear
assert ! defined?(ServiceOne)
service_one_second = ServiceOne
assert_not_equal service_one_first, c.get
assert_equal service_one_second, c.get
assert_not_equal service_one_first, c.get("ServiceOne")
assert_equal service_one_second, c.get("ServiceOne")
end
end

View File

@@ -51,21 +51,21 @@ class InflectorTest < Test::Unit::TestCase
end
SingularToPlural.each do |singular, plural|
define_method "test_pluralize_#{singular}" do
define_method "test_pluralize_singular_#{singular}" do
assert_equal(plural, ActiveSupport::Inflector.pluralize(singular))
assert_equal(plural.capitalize, ActiveSupport::Inflector.pluralize(singular.capitalize))
end
end
SingularToPlural.each do |singular, plural|
define_method "test_singularize_#{plural}" do
define_method "test_singularize_plural_#{plural}" do
assert_equal(singular, ActiveSupport::Inflector.singularize(plural))
assert_equal(singular.capitalize, ActiveSupport::Inflector.singularize(plural.capitalize))
end
end
SingularToPlural.each do |singular, plural|
define_method "test_pluralize_#{plural}" do
define_method "test_pluralize_plural_#{plural}" do
assert_equal(plural, ActiveSupport::Inflector.pluralize(plural))
assert_equal(plural.capitalize, ActiveSupport::Inflector.pluralize(plural.capitalize))
end

View File

@@ -12,6 +12,10 @@ module ActiveSupport
def puke(klass, name, e)
@puked << [klass, name, e]
end
def options
nil
end
end
if defined?(MiniTest::Assertions) && TestCase < MiniTest::Assertions

View File

@@ -82,32 +82,72 @@ end
rm_f "#{root_dir}/activerecord/debug.log"
cd "#{root_dir}/activerecord" do
puts
puts "[CruiseControl] Building Active Record with MySQL"
puts "[CruiseControl] Building Active Record with MySQL IM enabled"
puts
ENV['IM'] = 'true'
build_results[:activerecord_mysql] = rake 'mysql:rebuild_databases', 'mysql:test'
build_results[:activerecord_mysql_isolated] = rake 'mysql:rebuild_databases', 'mysql:isolated_test'
end
cd "#{root_dir}/activerecord" do
puts
puts "[CruiseControl] Building Active Record with MySQL2"
puts "[CruiseControl] Building Active Record with MySQL IM disabled"
puts
ENV['IM'] = 'false'
build_results[:activerecord_mysql] = rake 'mysql:rebuild_databases', 'mysql:test'
build_results[:activerecord_mysql_isolated] = rake 'mysql:rebuild_databases', 'mysql:isolated_test'
end
cd "#{root_dir}/activerecord" do
puts
puts "[CruiseControl] Building Active Record with MySQL2 IM enabled"
puts
ENV['IM'] = 'true'
build_results[:activerecord_mysql2] = rake 'mysql:rebuild_databases', 'mysql2:test'
build_results[:activerecord_mysql2_isolated] = rake 'mysql:rebuild_databases', 'mysql2:isolated_test'
end
cd "#{root_dir}/activerecord" do
puts
puts "[CruiseControl] Building Active Record with PostgreSQL"
puts "[CruiseControl] Building Active Record with MySQL2 IM disabled"
puts
ENV['IM'] = 'false'
build_results[:activerecord_mysql2] = rake 'mysql:rebuild_databases', 'mysql2:test'
build_results[:activerecord_mysql2_isolated] = rake 'mysql:rebuild_databases', 'mysql2:isolated_test'
end
cd "#{root_dir}/activerecord" do
puts
puts "[CruiseControl] Building Active Record with PostgreSQL IM enabled"
puts
ENV['IM'] = 'true'
build_results[:activerecord_postgresql8] = rake 'postgresql:rebuild_databases', 'postgresql:test'
build_results[:activerecord_postgresql8_isolated] = rake 'postgresql:rebuild_databases', 'postgresql:isolated_test'
end
cd "#{root_dir}/activerecord" do
puts
puts "[CruiseControl] Building Active Record with SQLite 3"
puts "[CruiseControl] Building Active Record with PostgreSQL IM disabled"
puts
ENV['IM'] = 'false'
build_results[:activerecord_postgresql8] = rake 'postgresql:rebuild_databases', 'postgresql:test'
build_results[:activerecord_postgresql8_isolated] = rake 'postgresql:rebuild_databases', 'postgresql:isolated_test'
end
cd "#{root_dir}/activerecord" do
puts
puts "[CruiseControl] Building Active Record with SQLite 3 IM enabled"
puts
ENV['IM'] = 'true'
build_results[:activerecord_sqlite3] = rake 'sqlite3:test'
build_results[:activerecord_sqlite3_isolated] = rake 'sqlite3:isolated_test'
end
cd "#{root_dir}/activerecord" do
puts
puts "[CruiseControl] Building Active Record with SQLite 3 IM disabled"
puts
ENV['IM'] = 'false'
build_results[:activerecord_sqlite3] = rake 'sqlite3:test'
build_results[:activerecord_sqlite3_isolated] = rake 'sqlite3:isolated_test'
end

View File

@@ -2793,6 +2793,8 @@ h5. +Date.current+
Active Support defines +Date.current+ to be today in the current time zone. That's like +Date.today+, except that it honors the user time zone, if defined. It also defines +Date.yesterday+ and +Date.tomorrow+, and the instance predicates +past?+, +today?+, and +future?+, all of them relative to +Date.current+.
When making Date comparisons using methods which honor the user time zone, make sure to use +Date.current+ and not +Date.today+. There are cases where the user time zone might be in the future compared to the system time zone, which +Date.today+ uses by default. This means +Date.today+ may equal +Date.yesterday+.
h5. Named dates
h6. +prev_year+, +next_year+
@@ -3107,7 +3109,7 @@ h5. Named Datetimes
h6. +DateTime.current+
Active Support defines +DateTime.current+ to be like +Time.now.to_datetime+, except that it honors the user time zone, if defined. It also defines instance predicates +past?+, and +future?+ relative to +DateTime.current+.
Active Support defines +DateTime.current+ to be like +Time.now.to_datetime+, except that it honors the user time zone, if defined. It also defines +DateTime.yesterday+ and +DateTime.tomorrow+, and the instance predicates +past?+, and +future?+ relative to +DateTime.current+.
h5. Other Extensions
@@ -3284,6 +3286,12 @@ t.advance(:seconds => 1)
* If +since+ or +ago+ jump to a time that can't be expressed with +Time+ a +DateTime+ object is returned instead.
h5. +Time.current+
Active Support defines +Time.current+ to be today in the current time zone. That's like +Time.now+, except that it honors the user time zone, if defined. It also defines +Time.yesterday+ and +Time.tomorrow+, and the instance predicates +past?+, +today?+, and +future?+, all of them relative to +Time.current+.
When making Time comparisons using methods which honor the user time zone, make sure to use +Time.current+ and not +Time.now+. There are cases where the user time zone might be in the future compared to the system time zone, which +Time.today+ uses by default. This means +Time.now+ may equal +Time.yesterday+.
h4. Time Constructors
Active Support defines +Time.current+ to be +Time.zone.now+ if there's a user time zone defined, with fallback to +Time.now+:

View File

@@ -349,7 +349,20 @@ $ git commit -a
$ git format-patch master --stdout > my_new_patch.diff
</shell>
Sanity check the results of this operation: open the diff file in your text editor of choice and make sure that no unintended changes crept in.
Open the diff file in your text editor of choice to sanity check the results, and make sure that no unintended changes crept in.
You can also perform an extra check by applying the patch to a different dedicated branch:
<shell>
$ git checkout -b testing_branch
$ git apply --check my_new_patch.diff
</shell>
Please make sure the patch does not introduce whitespace errors:
<shell>
$ git apply --whitespace=error-all mynew_patch.diff
</shell>
You can check your patches by applying your patch to an different dedicated branch:

View File

@@ -42,6 +42,10 @@ module Rails
set_environment
end
def app
@app ||= super.instance
end
def opt_parser
Options.new
end

View File

@@ -382,7 +382,10 @@ module Rails
# Finds engine with given path
def find(path)
Rails::Engine::Railties.engines.find { |r| File.expand_path(r.root.to_s) == File.expand_path(path.to_s) }
path = path.to_s
Rails::Engine::Railties.engines.find { |r|
File.expand_path(r.root.to_s) == File.expand_path(path)
}
end
end

View File

@@ -264,17 +264,18 @@ module Rails
# readme "README"
#
def readme(path)
say File.read(find_in_source_paths(path))
log File.read(find_in_source_paths(path))
end
protected
# Define log for backwards compatibility. If just one argument is sent,
# invoke say, otherwise invoke say_status.
# invoke say, otherwise invoke say_status. Differently from say and
# similarly to say_status, this method respects the quiet? option given.
#
def log(*args)
if args.size == 1
say args.first.to_s
say args.first.to_s unless options.quiet?
else
args << (self.behavior == :invoke ? :green : :red)
say_status *args

View File

@@ -3,7 +3,7 @@ begin
rescue LoadError
puts "Thor is not available.\nIf you ran this command from a git checkout " \
"of Rails, please make sure thor is installed,\nand run this command " \
"as `ruby #{$0} #{ARGV.join(" ")} --dev`"
"as `ruby #{$0} #{(ARGV | ['--dev']).join(" ")}`"
exit
end

View File

@@ -52,7 +52,6 @@
clear: both;
}
#header, #about, #getting-started {
padding-left: 75px;
padding-right: 30px;
@@ -168,6 +167,9 @@
margin-bottom: 5px;
}
.filename {
font-style: italic;
}
</style>
<script type="text/javascript">
function about() {
@@ -190,10 +192,10 @@
<li>
<h3>Browse the documentation</h3>
<ul class="links">
<li><a href="http://api.rubyonrails.org/">Rails API</a></li>
<li><a href="http://stdlib.rubyonrails.org/">Ruby standard library</a></li>
<li><a href="http://corelib.rubyonrails.org/">Ruby core</a></li>
<li><a href="http://guides.rubyonrails.org/">Rails Guides</a></li>
<li><a href="http://api.rubyonrails.org/">Rails API</a></li>
<li><a href="http://www.ruby-doc.org/core/">Ruby core</a></li>
<li><a href="http://www.ruby-doc.org/stdlib/">Ruby standard library</a></li>
</ul>
</li>
</ul>
@@ -221,13 +223,13 @@
</li>
<li>
<h2>Set up a default route and remove or rename this file</h2>
<p>Routes are set up in config/routes.rb.</p>
<h2>Set up a default route and remove <span class="filename">public/index.html</span></h2>
<p>Routes are set up in <span class="filename">config/routes.rb</span>.</p>
</li>
<li>
<h2>Create your database</h2>
<p>Run <code>rake db:migrate</code> to create your database. If you're not using SQLite (the default), edit <code>config/database.yml</code> with your username and password.</p>
<p>Run <code>rake db:create</code> to create your database. If you're not using SQLite (the default), edit <span class="filename">config/database.yml</span> with your username and password.</p>
</li>
</ol>
</div>

View File

@@ -191,6 +191,31 @@ class ActionsTest < Rails::Generators::TestCase
assert_match(/Welcome to Rails/, action(:readme, "README"))
end
def test_readme_with_quiet
generator(default_arguments, :quiet => true)
run_generator
Rails::Generators::AppGenerator.expects(:source_root).times(2).returns(destination_root)
assert_no_match(/Welcome to Rails/, action(:readme, "README"))
end
def test_log
assert_equal("YES\n", action(:log, "YES"))
end
def test_log_with_status
assert_equal(" yes YES\n", action(:log, :yes, "YES"))
end
def test_log_with_quiet
generator(default_arguments, :quiet => true)
assert_equal("", action(:log, "YES"))
end
def test_log_with_status_with_quiet
generator(default_arguments, :quiet => true)
assert_equal("", action(:log, :yes, "YES"))
end
protected
def action(*args, &block)