mirror of
https://github.com/github/rails.git
synced 2026-04-26 03:00:59 -04:00
merges rails
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
77
actionpack/test/dispatch/middleware_stack/middleware_test.rb
Normal file
77
actionpack/test/dispatch/middleware_stack/middleware_test.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) }
|
||||
|
||||
|
||||
@@ -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)*
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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}"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -43,7 +43,6 @@ module ActiveRecord
|
||||
autoload :ConnectionNotEstablished, 'active_record/errors'
|
||||
|
||||
autoload :Aggregations
|
||||
autoload :AssociationPreload
|
||||
autoload :Associations
|
||||
autoload :AttributeMethods
|
||||
autoload :AutosaveAssociation
|
||||
|
||||
@@ -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
|
||||
@@ -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?
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
231
activerecord/lib/active_record/associations/join_dependency.rb
Normal file
231
activerecord/lib/active_record/associations/join_dependency.rb
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
177
activerecord/lib/active_record/associations/preloader.rb
Normal file
177
activerecord/lib/active_record/associations/preloader.rb
Normal 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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,9 @@
|
||||
module ActiveRecord
|
||||
module Associations
|
||||
class Preloader
|
||||
class HasOneThrough < SingularAssociation #:nodoc:
|
||||
include ThroughAssociation
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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|
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
108
activesupport/test/class_cache_test.rb
Normal file
108
activesupport/test/class_cache_test.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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+:
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
@@ -42,6 +42,10 @@ module Rails
|
||||
set_environment
|
||||
end
|
||||
|
||||
def app
|
||||
@app ||= super.instance
|
||||
end
|
||||
|
||||
def opt_parser
|
||||
Options.new
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user