Merge branch 'master' into mrbrdo-fixserialization
* master: (142 commits) Use Colspan in th Tags Added test for link_to_unless to make sure the result consistency. Escape the string even when the condition of link_to_unless is not satisfied. Add CHANGELOG entry for #10969 Use a case insensitive URI Regexp for #asset_path collection tags accept html attributes as the last element of collection Rewind StringIO instances before be parsed again Use xml instead already parsed xml Updated the doc for const_regexp [ci skip] Make test name descriptive and add reference to original regression commit fixture setup does not rely on `AR::Base.configurations`. regression test + mysql2 adapter raises correct error if conn is closed. cleanup, remove trailing whitespace from AR changelog 'json' gem is no more required under JRuby fix typos Fix AS changelog [ci skip] Update the HTML boolean attributes per the HTML 5.1 spec Changing const_regexp to check for constant name. valid_app_const? -> valid_const? Add CHANGELOG entry for #10740 ...
@ -4,6 +4,8 @@ Ruby on Rails is a volunteer effort. We encourage you to pitch in. [Join the tea
|
||||
|
||||
* If you want to submit a patch, please read the [Contributing to Ruby on Rails](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html) guide.
|
||||
|
||||
* If you want to contribute to Rails documentation, please read the [Contributing to the Rails Documentation](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation) section of the aforementioned guide.
|
||||
|
||||
*We only accept bug reports and pull requests in GitHub*.
|
||||
|
||||
* If you have a question about how to use Ruby on Rails, please [ask the rubyonrails-talk mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-talk).
|
||||
|
1
Gemfile
@ -61,7 +61,6 @@ platforms :ruby do
|
||||
end
|
||||
|
||||
platforms :jruby do
|
||||
gem 'json'
|
||||
gem 'activerecord-jdbcsqlite3-adapter', '>= 1.2.7'
|
||||
|
||||
group :db do
|
||||
|
@ -1,3 +1,36 @@
|
||||
* Use a case insensitive URI Regexp for #asset_path.
|
||||
|
||||
This fix a problem where the same asset path using different case are generating
|
||||
different URIs.
|
||||
|
||||
Before:
|
||||
|
||||
image_tag("HTTP://google.com")
|
||||
# => "<img alt=\"Google\" src=\"/assets/HTTP://google.com\" />"
|
||||
image_tag("http://google.com")
|
||||
# => "<img alt=\"Google\" src=\"http://google.com\" />"
|
||||
|
||||
After:
|
||||
|
||||
image_tag("HTTP://google.com")
|
||||
# => "<img alt=\"Google\" src=\"HTTP://google.com\" />"
|
||||
image_tag("http://google.com")
|
||||
# => "<img alt=\"Google\" src=\"http://google.com\" />"
|
||||
|
||||
*David Celis*
|
||||
|
||||
* Element of the `collection_check_boxes` and `collection_radio_buttons` can
|
||||
optionally contain html attributes as the last element of the array.
|
||||
|
||||
*Vasiliy Ermolovich*
|
||||
|
||||
* Update the HTML `BOOLEAN_ATTRIBUTES` in `ActionView::Helpers::TagHelper`
|
||||
to conform to the latest HTML 5.1 spec. Add attributes `allowfullscreen`,
|
||||
`default`, `inert`, `sortable`, `truespeed`, `typemustmatch`. Fix attribute
|
||||
`seamless` (previously misspelled `seemless`).
|
||||
|
||||
*Alex Peattie*
|
||||
|
||||
* Fix an issue where partials with a number in the filename weren't being digested for cache dependencies.
|
||||
|
||||
*Bryan Ricker*
|
||||
|
@ -77,7 +77,7 @@ def cookie_jar
|
||||
# domain and subdomains.
|
||||
#
|
||||
# * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object.
|
||||
# * <tt>:secure</tt> - Whether this cookie is a only transmitted to HTTPS servers.
|
||||
# * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers.
|
||||
# Default is +false+.
|
||||
# * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
|
||||
# only HTTP. Defaults to +false+.
|
||||
|
@ -20,8 +20,7 @@ class Railtie < Rails::Railtie # :nodoc:
|
||||
config.action_dispatch.default_headers = {
|
||||
'X-Frame-Options' => 'SAMEORIGIN',
|
||||
'X-XSS-Protection' => '1; mode=block',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
'X-UA-Compatible' => 'chrome=1'
|
||||
'X-Content-Type-Options' => 'nosniff'
|
||||
}
|
||||
|
||||
config.eager_load_namespaces << ActionDispatch
|
||||
|
@ -105,7 +105,7 @@ module Helpers
|
||||
# )
|
||||
#
|
||||
module AssetUrlHelper
|
||||
URI_REGEXP = %r{^[-a-z]+://|^(?:cid|data):|^//}
|
||||
URI_REGEXP = %r{^[-a-z]+://|^(?:cid|data):|^//}i
|
||||
|
||||
# Computes the path to asset in public directory. If :type
|
||||
# options is set, a file extension will be appended and scoped
|
||||
|
@ -12,8 +12,11 @@ module TagHelper
|
||||
|
||||
BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple checked autobuffer
|
||||
autoplay controls loop selected hidden scoped async
|
||||
defer reversed ismap seemless muted required
|
||||
autofocus novalidate formnovalidate open pubdate itemscope).to_set
|
||||
defer reversed ismap seamless muted required
|
||||
autofocus novalidate formnovalidate open pubdate
|
||||
itemscope allowfullscreen default inert sortable
|
||||
truespeed typemustmatch).to_set
|
||||
|
||||
BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map {|attribute| attribute.to_sym })
|
||||
|
||||
PRE_CONTENT_STRINGS = {
|
||||
|
@ -73,8 +73,9 @@ def render_collection #:nodoc:
|
||||
value = value_for_collection(item, @value_method)
|
||||
text = value_for_collection(item, @text_method)
|
||||
default_html_options = default_html_options_for_collection(item, value)
|
||||
additional_html_options = option_html_attributes(item)
|
||||
|
||||
yield item, value, text, default_html_options
|
||||
yield item, value, text, default_html_options.merge(additional_html_options)
|
||||
end.join.html_safe
|
||||
end
|
||||
end
|
||||
|
@ -380,7 +380,7 @@ def link_to_unless(condition, name, options = {}, html_options = {}, &block)
|
||||
if block_given?
|
||||
block.arity <= 1 ? capture(name, &block) : capture(name, options, html_options, &block)
|
||||
else
|
||||
name
|
||||
ERB::Util.html_escape(name)
|
||||
end
|
||||
else
|
||||
link_to(name, options, html_options)
|
||||
|
@ -267,7 +267,7 @@ def compile(view, mod) #:nodoc:
|
||||
method_name = self.method_name
|
||||
code = @handler.call(self)
|
||||
|
||||
# Make sure that the resulting String to be evalled is in the
|
||||
# Make sure that the resulting String to be eval'd is in the
|
||||
# encoding of the code
|
||||
source = <<-end_src
|
||||
def #{method_name}(local_assigns, output_buffer)
|
||||
|
@ -182,8 +182,7 @@ def test_response_body_encoding
|
||||
ActionDispatch::Response.default_headers = {
|
||||
'X-Frame-Options' => 'DENY',
|
||||
'X-Content-Type-Options' => 'nosniff',
|
||||
'X-XSS-Protection' => '1;',
|
||||
'X-UA-Compatible' => 'chrome=1'
|
||||
'X-XSS-Protection' => '1;'
|
||||
}
|
||||
resp = ActionDispatch::Response.new.tap { |response|
|
||||
response.body = 'Hello'
|
||||
@ -193,7 +192,6 @@ def test_response_body_encoding
|
||||
assert_equal('DENY', resp.headers['X-Frame-Options'])
|
||||
assert_equal('nosniff', resp.headers['X-Content-Type-Options'])
|
||||
assert_equal('1;', resp.headers['X-XSS-Protection'])
|
||||
assert_equal('chrome=1', resp.headers['X-UA-Compatible'])
|
||||
ensure
|
||||
ActionDispatch::Response.default_headers = nil
|
||||
end
|
||||
|
@ -48,6 +48,9 @@ def url_for(*args)
|
||||
%(asset_path("style.min")) => %(/style.min),
|
||||
%(asset_path("style.min.css")) => %(/style.min.css),
|
||||
|
||||
%(asset_path("http://www.outside.com/image.jpg")) => %(http://www.outside.com/image.jpg),
|
||||
%(asset_path("HTTP://www.outside.com/image.jpg")) => %(HTTP://www.outside.com/image.jpg),
|
||||
|
||||
%(asset_path("style", type: :stylesheet)) => %(/stylesheets/style.css),
|
||||
%(asset_path("xmlhr", type: :javascript)) => %(/javascripts/xmlhr.js),
|
||||
%(asset_path("xml.png", type: :image)) => %(/images/xml.png)
|
||||
@ -445,8 +448,8 @@ def test_image_alt
|
||||
[nil, '/', '/foo/bar/', 'foo/bar/'].each do |prefix|
|
||||
assert_equal 'Rails', image_alt("#{prefix}rails.png")
|
||||
assert_equal 'Rails', image_alt("#{prefix}rails-9c0a079bdd7701d7e729bd956823d153.png")
|
||||
assert_equal 'Long file name with hyphens', image_alt("#{prefix}long-file-name-with-hyphens.png")
|
||||
assert_equal 'Long file name with underscores', image_alt("#{prefix}long_file_name_with_underscores.png")
|
||||
assert_equal 'Long file name with hyphens', image_alt("#{prefix}long-file-name-with-hyphens.png")
|
||||
assert_equal 'Long file name with underscores', image_alt("#{prefix}long_file_name_with_underscores.png")
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -45,7 +45,7 @@ def test_returns_empty_array_if_no_tracker_is_found
|
||||
end
|
||||
end
|
||||
|
||||
class ERBTrackerTest < MiniTest::Unit::TestCase
|
||||
class ERBTrackerTest < Minitest::Test
|
||||
def make_tracker(name, template)
|
||||
ActionView::DependencyTracker::ERBTracker.new(name, template)
|
||||
end
|
||||
|
@ -76,6 +76,14 @@ def with_collection_check_boxes(*args, &block)
|
||||
assert_select 'input[type=radio][value=false].special-radio#user_active_false'
|
||||
end
|
||||
|
||||
test 'collection radio accepts html options as the last element of array' do
|
||||
collection = [[1, true, {class: 'foo'}], [0, false, {class: 'bar'}]]
|
||||
with_collection_radio_buttons :user, :active, collection, :second, :first
|
||||
|
||||
assert_select 'input[type=radio][value=true].foo#user_active_true'
|
||||
assert_select 'input[type=radio][value=false].bar#user_active_false'
|
||||
end
|
||||
|
||||
test 'collection radio does not wrap input inside the label' do
|
||||
with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s
|
||||
|
||||
@ -192,6 +200,14 @@ def with_collection_check_boxes(*args, &block)
|
||||
assert_select 'label[for=user_name_199]', '$1.99'
|
||||
end
|
||||
|
||||
test 'collection check boxes accepts html options as the last element of array' do
|
||||
collection = [[1, 'Category 1', {class: 'foo'}], [2, 'Category 2', {class: 'bar'}]]
|
||||
with_collection_check_boxes :user, :active, collection, :first, :second
|
||||
|
||||
assert_select 'input[type=checkbox][value=1].foo'
|
||||
assert_select 'input[type=checkbox][value=2].bar'
|
||||
end
|
||||
|
||||
test 'collection check boxes accepts selected values as :checked option' do
|
||||
collection = (1..3).map{|i| [i, "Category #{i}"] }
|
||||
with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => [1, 3]
|
||||
|
@ -30,8 +30,8 @@ def test_tag_options_accepts_blank_option
|
||||
end
|
||||
|
||||
def test_tag_options_converts_boolean_option
|
||||
assert_equal '<p disabled="disabled" itemscope="itemscope" multiple="multiple" readonly="readonly" />',
|
||||
tag("p", :disabled => true, :itemscope => true, :multiple => true, :readonly => true)
|
||||
assert_dom_equal '<p disabled="disabled" itemscope="itemscope" multiple="multiple" readonly="readonly" allowfullscreen="allowfullscreen" seamless="seamless" typemustmatch="typemustmatch" sortable="sortable" default="default" inert="inert" truespeed="truespeed" />',
|
||||
tag("p", :disabled => true, :itemscope => true, :multiple => true, :readonly => true, :allowfullscreen => true, :seamless => true, :typemustmatch => true, :sortable => true, :default => true, :inert => true, :truespeed => true)
|
||||
end
|
||||
|
||||
def test_content_tag
|
||||
|
@ -348,6 +348,11 @@ def test_link_to_unless
|
||||
link_to_unless(true, "Showing", url_hash) {
|
||||
"test"
|
||||
}
|
||||
|
||||
assert_equal %{<b>Showing</b>}, link_to_unless(true, "<b>Showing</b>", url_hash)
|
||||
assert_equal %{<a href="/"><b>Showing</b></a>}, link_to_unless(false, "<b>Showing</b>", url_hash)
|
||||
assert_equal %{<b>Showing</b>}, link_to_unless(true, "<b>Showing</b>".html_safe, url_hash)
|
||||
assert_equal %{<a href="/"><b>Showing</b></a>}, link_to_unless(false, "<b>Showing</b>".html_safe, url_hash)
|
||||
end
|
||||
|
||||
def test_link_to_if
|
||||
|
@ -135,7 +135,10 @@ def _define_after_model_callback(klass, callback) #:nodoc:
|
||||
klass.define_singleton_method("after_#{callback}") do |*args, &block|
|
||||
options = args.extract_options!
|
||||
options[:prepend] = true
|
||||
options[:if] = Array(options[:if]) << "value != false"
|
||||
conditional = ActiveSupport::Callbacks::Conditionals::Value.new { |v|
|
||||
v != false
|
||||
}
|
||||
options[:if] = Array(options[:if]) << conditional
|
||||
set_callback(:"#{callback}", :after, *(args << options), &block)
|
||||
end
|
||||
end
|
||||
|
@ -142,23 +142,23 @@ def changed_attributes
|
||||
@changed_attributes ||= {}
|
||||
end
|
||||
|
||||
private
|
||||
# Handle <tt>*_changed?</tt> for +method_missing+.
|
||||
def attribute_changed?(attr)
|
||||
changed_attributes.include?(attr)
|
||||
end
|
||||
|
||||
# Handle <tt>*_changed?</tt> for +method_missing+.
|
||||
def attribute_changed?(attr)
|
||||
changed_attributes.include?(attr)
|
||||
end
|
||||
# Handle <tt>*_was</tt> for +method_missing+.
|
||||
def attribute_was(attr)
|
||||
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Handle <tt>*_change</tt> for +method_missing+.
|
||||
def attribute_change(attr)
|
||||
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
|
||||
end
|
||||
|
||||
# Handle <tt>*_was</tt> for +method_missing+.
|
||||
def attribute_was(attr)
|
||||
attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
|
||||
end
|
||||
|
||||
# Handle <tt>*_will_change!</tt> for +method_missing+.
|
||||
def attribute_will_change!(attr)
|
||||
return if attribute_changed?(attr)
|
||||
|
@ -109,7 +109,7 @@ def as_json(options = nil)
|
||||
#
|
||||
# def attributes=(hash)
|
||||
# hash.each do |key, value|
|
||||
# instance_variable_set("@#{key}", value)
|
||||
# send("#{key}=", value)
|
||||
# end
|
||||
# end
|
||||
#
|
||||
|
@ -142,7 +142,9 @@ def validate(*args, &block)
|
||||
if options.key?(:on)
|
||||
options = options.dup
|
||||
options[:if] = Array(options[:if])
|
||||
options[:if].unshift("validation_context == :#{options[:on]}")
|
||||
options[:if].unshift lambda { |o|
|
||||
o.validation_context == options[:on]
|
||||
}
|
||||
end
|
||||
args << options
|
||||
set_callback(:validate, *args, &block)
|
||||
@ -226,7 +228,6 @@ def clear_validators!
|
||||
# Person.validators_on(:name)
|
||||
# # => [
|
||||
# # #<ActiveModel::Validations::PresenceValidator:0x007fe604914e60 @attributes=[:name], @options={}>,
|
||||
# # #<ActiveModel::Validations::InclusionValidator:0x007fe603bb8780 @attributes=[:age], @options={in:0..99}>
|
||||
# # ]
|
||||
def validators_on(*attributes)
|
||||
attributes.flat_map do |attribute|
|
||||
|
@ -7,8 +7,12 @@ class RailtieTest < ActiveModel::TestCase
|
||||
def setup
|
||||
require 'active_model/railtie'
|
||||
|
||||
# Set a fake logger to avoid creating the log directory automatically
|
||||
fake_logger = mock()
|
||||
|
||||
@app ||= Class.new(::Rails::Application) do
|
||||
config.eager_load = false
|
||||
config.logger = fake_logger
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -1,3 +1,81 @@
|
||||
* Fixture setup does no longer depend on `ActiveRecord::Base.configurations`.
|
||||
This is relevant when `ENV["DATABASE_URL"]` is used in place of a `database.yml`.
|
||||
|
||||
*Yves Senn*
|
||||
|
||||
* Fix mysql2 adapter raises the correct exception when executing a query on a
|
||||
closed connection.
|
||||
|
||||
*Yves Senn*
|
||||
|
||||
* Ambiguous reflections are on :through relationships are no longer supported.
|
||||
For example, you need to change this:
|
||||
|
||||
class Author < ActiveRecord::Base
|
||||
has_many :posts
|
||||
has_many :taggings, :through => :posts
|
||||
end
|
||||
|
||||
class Post < ActiveRecord::Base
|
||||
has_one :tagging
|
||||
has_many :taggings
|
||||
end
|
||||
|
||||
class Tagging < ActiveRecord::Base
|
||||
end
|
||||
|
||||
To this:
|
||||
|
||||
class Author < ActiveRecord::Base
|
||||
has_many :posts
|
||||
has_many :taggings, :through => :posts, :source => :tagging
|
||||
end
|
||||
|
||||
class Post < ActiveRecord::Base
|
||||
has_one :tagging
|
||||
has_many :taggings
|
||||
end
|
||||
|
||||
class Tagging < ActiveRecord::Base
|
||||
end
|
||||
|
||||
* Remove column restrictions for `count`, let the database raise if the SQL is
|
||||
invalid. The previous behavior was untested and surprising for the user.
|
||||
Fixes #5554.
|
||||
|
||||
Example:
|
||||
|
||||
User.select("name, username").count
|
||||
# Before => SELECT count(*) FROM users
|
||||
# After => ActiveRecord::StatementInvalid
|
||||
|
||||
# you can still use `count(:all)` to perform a query unrelated to the
|
||||
# selected columns
|
||||
User.select("name, username").count(:all) # => SELECT count(*) FROM users
|
||||
|
||||
*Yves Senn*
|
||||
|
||||
* Rails now automatically detects inverse associations. If you do not set the
|
||||
`:inverse_of` option on the association, then Active Record will guess the
|
||||
inverse association based on heuristics.
|
||||
|
||||
Note that automatic inverse detection only works on `has_many`, `has_one`,
|
||||
and `belongs_to` associations. Extra options on the associations will
|
||||
also prevent the association's inverse from being found automatically.
|
||||
|
||||
The automatic guessing of the inverse association uses a heuristic based
|
||||
on the name of the class, so it may not work for all associations,
|
||||
especially the ones with non-standard names.
|
||||
|
||||
You can turn off the automatic detection of inverse associations by setting
|
||||
the `:inverse_of` option to `false` like so:
|
||||
|
||||
class Taggable < ActiveRecord::Base
|
||||
belongs_to :tag, inverse_of: false
|
||||
end
|
||||
|
||||
*John Wang*
|
||||
|
||||
* Fix `add_column` with `array` option when using PostgreSQL. Fixes #10432
|
||||
|
||||
*Adam Anderson*
|
||||
@ -19,7 +97,7 @@
|
||||
|
||||
*Yves Senn*
|
||||
|
||||
* Fix bug where tiny types are incorectly coerced as booleand when the length is more than 1.
|
||||
* Fix bug where tiny types are incorrectly coerced as boolean when the length is more than 1.
|
||||
|
||||
Fixes #10620.
|
||||
|
||||
@ -102,8 +180,8 @@
|
||||
|
||||
*Olek Janiszewski*
|
||||
|
||||
* fixes bug introduced by #3329. Now, when autosaving associations,
|
||||
deletions happen before inserts and saves. This prevents a 'duplicate
|
||||
* fixes bug introduced by #3329. Now, when autosaving associations,
|
||||
deletions happen before inserts and saves. This prevents a 'duplicate
|
||||
unique value' database error that would occur if a record being created had
|
||||
the same value on a unique indexed field as that of a record being destroyed.
|
||||
|
||||
|
@ -586,9 +586,10 @@ def association_instance_set(name, association)
|
||||
# belongs_to :tag, inverse_of: :taggings
|
||||
# end
|
||||
#
|
||||
# If you do not set the +:inverse_of+ record, the association will do its
|
||||
# best to match itself up with the correct inverse. Automatic +:inverse_of+
|
||||
# detection only works on +has_many+, +has_one+, and +belongs_to+ associations.
|
||||
# If you do not set the <tt>:inverse_of</tt> record, the association will
|
||||
# do its best to match itself up with the correct inverse. Automatic
|
||||
# inverse detection only works on <tt>has_many</tt>, <tt>has_one</tt>, and
|
||||
# <tt>belongs_to</tt> associations.
|
||||
#
|
||||
# Extra options on the associations, as defined in the
|
||||
# <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt> constant, will
|
||||
@ -599,10 +600,10 @@ def association_instance_set(name, association)
|
||||
# especially the ones with non-standard names.
|
||||
#
|
||||
# You can turn off the automatic detection of inverse associations by setting
|
||||
# the +:automatic_inverse_of+ option to +false+ like so:
|
||||
# the <tt>:inverse_of</tt> option to <tt>false</tt> like so:
|
||||
#
|
||||
# class Taggable < ActiveRecord::Base
|
||||
# belongs_to :tag, automatic_inverse_of: false
|
||||
# belongs_to :tag, inverse_of: false
|
||||
# end
|
||||
#
|
||||
# == Nested \Associations
|
||||
|
@ -200,13 +200,14 @@ def set_owner_attributes(record)
|
||||
creation_attributes.each { |key, value| record[key] = value }
|
||||
end
|
||||
|
||||
# Should be true if there is a foreign key present on the owner which
|
||||
# Returns true if there is a foreign key present on the owner which
|
||||
# references the target. This is used to determine whether we can load
|
||||
# the target if the owner is currently a new record (and therefore
|
||||
# without a key).
|
||||
# without a key). If the owner is a new record then foreign_key must
|
||||
# be present in order to load target.
|
||||
#
|
||||
# Currently implemented by belongs_to (vanilla and polymorphic) and
|
||||
# has_one/has_many :through associations which go through a belongs_to
|
||||
# has_one/has_many :through associations which go through a belongs_to.
|
||||
def foreign_key_present?
|
||||
false
|
||||
end
|
||||
|
@ -96,7 +96,7 @@ def add_constraints(scope)
|
||||
item = eval_scope(klass, scope_chain_item)
|
||||
|
||||
if scope_chain_item == self.reflection.scope
|
||||
scope.merge! item.except(:where, :includes)
|
||||
scope.merge! item.except(:where, :includes, :bind)
|
||||
end
|
||||
|
||||
scope.includes! item.includes_values
|
||||
|
@ -111,13 +111,8 @@ def configure_dependency
|
||||
)
|
||||
end
|
||||
|
||||
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
||||
def #{macro}_dependent_for_#{name}
|
||||
association(:#{name}).handle_dependency
|
||||
end
|
||||
CODE
|
||||
|
||||
model.before_destroy "#{macro}_dependent_for_#{name}"
|
||||
n = name
|
||||
model.before_destroy lambda { |o| o.association(n).handle_dependency }
|
||||
end
|
||||
|
||||
def valid_dependent_options
|
||||
|
@ -19,82 +19,116 @@ def build
|
||||
reflection
|
||||
end
|
||||
|
||||
def add_counter_cache_callbacks(reflection)
|
||||
cache_column = reflection.counter_cache_column
|
||||
foreign_key = reflection.foreign_key
|
||||
def valid_dependent_options
|
||||
[:destroy, :delete]
|
||||
end
|
||||
|
||||
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
||||
def belongs_to_counter_cache_after_create_for_#{name}
|
||||
if record = #{name}
|
||||
record.class.increment_counter(:#{cache_column}, record.id)
|
||||
private
|
||||
|
||||
def add_counter_cache_methods(mixin)
|
||||
return if mixin.method_defined? :belongs_to_counter_cache_after_create
|
||||
|
||||
mixin.class_eval do
|
||||
def belongs_to_counter_cache_after_create(association, reflection)
|
||||
if record = send(association.name)
|
||||
cache_column = reflection.counter_cache_column
|
||||
record.class.increment_counter(cache_column, record.id)
|
||||
@_after_create_counter_called = true
|
||||
end
|
||||
end
|
||||
|
||||
def belongs_to_counter_cache_before_destroy_for_#{name}
|
||||
unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == #{foreign_key.to_sym.inspect}
|
||||
record = #{name}
|
||||
def belongs_to_counter_cache_before_destroy(association, reflection)
|
||||
foreign_key = reflection.foreign_key.to_sym
|
||||
unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key
|
||||
record = send association.name
|
||||
if record && !self.destroyed?
|
||||
record.class.decrement_counter(:#{cache_column}, record.id)
|
||||
cache_column = reflection.counter_cache_column
|
||||
record.class.decrement_counter(cache_column, record.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def belongs_to_counter_cache_after_update_for_#{name}
|
||||
def belongs_to_counter_cache_after_update(association, reflection)
|
||||
foreign_key = reflection.foreign_key
|
||||
cache_column = reflection.counter_cache_column
|
||||
|
||||
if (@_after_create_counter_called ||= false)
|
||||
@_after_create_counter_called = false
|
||||
elsif self.#{foreign_key}_changed? && !new_record? && defined?(#{name.to_s.camelize})
|
||||
model = #{name.to_s.camelize}
|
||||
foreign_key_was = self.#{foreign_key}_was
|
||||
foreign_key = self.#{foreign_key}
|
||||
elsif attribute_changed?(foreign_key) && !new_record? && association.constructable?
|
||||
model = reflection.klass
|
||||
foreign_key_was = attribute_was foreign_key
|
||||
foreign_key = attribute foreign_key
|
||||
|
||||
if foreign_key && model.respond_to?(:increment_counter)
|
||||
model.increment_counter(:#{cache_column}, foreign_key)
|
||||
model.increment_counter(cache_column, foreign_key)
|
||||
end
|
||||
if foreign_key_was && model.respond_to?(:decrement_counter)
|
||||
model.decrement_counter(:#{cache_column}, foreign_key_was)
|
||||
model.decrement_counter(cache_column, foreign_key_was)
|
||||
end
|
||||
end
|
||||
end
|
||||
CODE
|
||||
end
|
||||
end
|
||||
|
||||
model.after_create "belongs_to_counter_cache_after_create_for_#{name}"
|
||||
model.before_destroy "belongs_to_counter_cache_before_destroy_for_#{name}"
|
||||
model.after_update "belongs_to_counter_cache_after_update_for_#{name}"
|
||||
def add_counter_cache_callbacks(reflection)
|
||||
cache_column = reflection.counter_cache_column
|
||||
add_counter_cache_methods mixin
|
||||
association = self
|
||||
|
||||
model.after_create lambda { |record|
|
||||
record.belongs_to_counter_cache_after_create(association, reflection)
|
||||
}
|
||||
|
||||
model.before_destroy lambda { |record|
|
||||
record.belongs_to_counter_cache_before_destroy(association, reflection)
|
||||
}
|
||||
|
||||
model.after_update lambda { |record|
|
||||
record.belongs_to_counter_cache_after_update(association, reflection)
|
||||
}
|
||||
|
||||
klass = reflection.class_name.safe_constantize
|
||||
klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly)
|
||||
end
|
||||
|
||||
def add_touch_callbacks(reflection)
|
||||
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
|
||||
def belongs_to_touch_after_save_or_destroy_for_#{name}
|
||||
foreign_key_field = #{reflection.foreign_key.inspect}
|
||||
old_foreign_id = attribute_was(foreign_key_field)
|
||||
def self.touch_record(o, foreign_key, name, touch) # :nodoc:
|
||||
old_foreign_id = o.attribute_was(foreign_key)
|
||||
|
||||
if old_foreign_id
|
||||
klass = association(#{name.inspect}).klass
|
||||
old_record = klass.find_by(klass.primary_key => old_foreign_id)
|
||||
if old_foreign_id
|
||||
klass = o.association(name).klass
|
||||
old_record = klass.find_by(klass.primary_key => old_foreign_id)
|
||||
|
||||
if old_record
|
||||
old_record.touch #{options[:touch].inspect if options[:touch] != true}
|
||||
end
|
||||
end
|
||||
|
||||
record = #{name}
|
||||
unless record.nil? || record.new_record?
|
||||
record.touch #{options[:touch].inspect if options[:touch] != true}
|
||||
if old_record
|
||||
if touch != true
|
||||
old_record.touch touch
|
||||
else
|
||||
old_record.touch
|
||||
end
|
||||
end
|
||||
CODE
|
||||
end
|
||||
|
||||
model.after_save "belongs_to_touch_after_save_or_destroy_for_#{name}"
|
||||
model.after_touch "belongs_to_touch_after_save_or_destroy_for_#{name}"
|
||||
model.after_destroy "belongs_to_touch_after_save_or_destroy_for_#{name}"
|
||||
record = o.send name
|
||||
unless record.nil? || record.new_record?
|
||||
if touch != true
|
||||
record.touch touch
|
||||
else
|
||||
record.touch
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def valid_dependent_options
|
||||
[:destroy, :delete]
|
||||
def add_touch_callbacks(reflection)
|
||||
foreign_key = reflection.foreign_key
|
||||
n = name
|
||||
touch = options[:touch]
|
||||
|
||||
callback = lambda { |record|
|
||||
BelongsTo.touch_record(record, foreign_key, n, touch)
|
||||
}
|
||||
|
||||
model.after_save callback
|
||||
model.after_touch callback
|
||||
model.after_destroy callback
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -5,7 +5,7 @@ def macro
|
||||
end
|
||||
|
||||
def valid_options
|
||||
super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :automatic_inverse_of, :counter_cache]
|
||||
super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache]
|
||||
end
|
||||
|
||||
def valid_dependent_options
|
||||
|
@ -3,7 +3,7 @@
|
||||
module ActiveRecord::Associations::Builder
|
||||
class SingularAssociation < Association #:nodoc:
|
||||
def valid_options
|
||||
super + [:remote, :dependent, :counter_cache, :primary_key, :inverse_of, :automatic_inverse_of]
|
||||
super + [:remote, :dependent, :counter_cache, :primary_key, :inverse_of]
|
||||
end
|
||||
|
||||
def constructable?
|
||||
|
@ -25,6 +25,9 @@ def primary_key?
|
||||
end
|
||||
end
|
||||
|
||||
class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc:
|
||||
end
|
||||
|
||||
# Represents the schema of an SQL table in an abstract way. This class
|
||||
# provides methods for manipulating the schema representation.
|
||||
#
|
||||
|
@ -694,17 +694,6 @@ def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc:
|
||||
end
|
||||
end
|
||||
|
||||
def add_column_options!(sql, options) #:nodoc:
|
||||
sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options)
|
||||
# must explicitly check for :null to allow change_column to work on migrations
|
||||
if options[:null] == false
|
||||
sql << " NOT NULL"
|
||||
end
|
||||
if options[:auto_increment] == true
|
||||
sql << " AUTO_INCREMENT"
|
||||
end
|
||||
end
|
||||
|
||||
# SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
|
||||
#
|
||||
# distinct("posts.id", ["posts.created_at desc"])
|
||||
|
@ -116,6 +116,12 @@ def accept(o)
|
||||
send m, o
|
||||
end
|
||||
|
||||
def visit_AddColumn(o)
|
||||
sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
|
||||
sql = "ADD #{quote_column_name(o.name)} #{sql_type}"
|
||||
add_column_options!(sql, column_options(o))
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def visit_AlterTable(o)
|
||||
@ -123,12 +129,6 @@ def visit_AlterTable(o)
|
||||
sql << o.adds.map { |col| visit_AddColumn col }.join(' ')
|
||||
end
|
||||
|
||||
def visit_AddColumn(o)
|
||||
sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
|
||||
sql = "ADD #{quote_column_name(o.name)} #{sql_type}"
|
||||
add_column_options!(sql, column_options(o))
|
||||
end
|
||||
|
||||
def visit_ColumnDefinition(o)
|
||||
sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
|
||||
column_sql = "#{quote_column_name(o.name)} #{sql_type}"
|
||||
@ -149,6 +149,8 @@ def column_options(o)
|
||||
column_options[:null] = o.null unless o.null.nil?
|
||||
column_options[:default] = o.default unless o.default.nil?
|
||||
column_options[:column] = o
|
||||
column_options[:first] = o.first
|
||||
column_options[:after] = o.after
|
||||
column_options
|
||||
end
|
||||
|
||||
@ -164,9 +166,20 @@ def type_to_sql(type, limit, precision, scale)
|
||||
@conn.type_to_sql type.to_sym, limit, precision, scale
|
||||
end
|
||||
|
||||
def add_column_options!(column_sql, column_options)
|
||||
@conn.add_column_options! column_sql, column_options
|
||||
column_sql
|
||||
def add_column_options!(sql, options)
|
||||
sql << " DEFAULT #{@conn.quote(options[:default], options[:column])}" if options_include_default?(options)
|
||||
# must explicitly check for :null to allow change_column to work on migrations
|
||||
if options[:null] == false
|
||||
sql << " NOT NULL"
|
||||
end
|
||||
if options[:auto_increment] == true
|
||||
sql << " AUTO_INCREMENT"
|
||||
end
|
||||
sql
|
||||
end
|
||||
|
||||
def options_include_default?(options)
|
||||
options.include?(:default) && !(options[:null] == false && options[:default].nil?)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -4,17 +4,26 @@ module ActiveRecord
|
||||
module ConnectionAdapters
|
||||
class AbstractMysqlAdapter < AbstractAdapter
|
||||
class SchemaCreation < AbstractAdapter::SchemaCreation
|
||||
private
|
||||
|
||||
def visit_AddColumn(o)
|
||||
add_column_position!(super, o)
|
||||
add_column_position!(super, column_options(o))
|
||||
end
|
||||
|
||||
def add_column_position!(sql, column)
|
||||
if column.first
|
||||
private
|
||||
def visit_ChangeColumnDefinition(o)
|
||||
column = o.column
|
||||
options = o.options
|
||||
sql_type = type_to_sql(o.type, options[:limit], options[:precision], options[:scale])
|
||||
change_column_sql = "CHANGE #{quote_column_name(column.name)} #{quote_column_name(options[:name])} #{sql_type}"
|
||||
add_column_options!(change_column_sql, options)
|
||||
add_column_position!(change_column_sql, options)
|
||||
end
|
||||
|
||||
def add_column_position!(sql, options)
|
||||
if options[:first]
|
||||
sql << " FIRST"
|
||||
elsif column.after
|
||||
sql << " AFTER #{quote_column_name(column.after)}"
|
||||
elsif options[:after]
|
||||
sql << " AFTER #{quote_column_name(options[:after])}"
|
||||
end
|
||||
sql
|
||||
end
|
||||
@ -661,10 +670,9 @@ def translate_exception(exception, message)
|
||||
end
|
||||
|
||||
def add_column_sql(table_name, column_name, type, options = {})
|
||||
add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
||||
add_column_options!(add_column_sql, options)
|
||||
add_column_position!(add_column_sql, options)
|
||||
add_column_sql
|
||||
td = create_table_definition table_name, options[:temporary], options[:options]
|
||||
cd = td.new_column_definition(column_name, type, options)
|
||||
schema_creation.visit_AddColumn cd
|
||||
end
|
||||
|
||||
def change_column_sql(table_name, column_name, type, options = {})
|
||||
@ -678,14 +686,12 @@ def change_column_sql(table_name, column_name, type, options = {})
|
||||
options[:null] = column.null
|
||||
end
|
||||
|
||||
change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
||||
add_column_options!(change_column_sql, options)
|
||||
add_column_position!(change_column_sql, options)
|
||||
change_column_sql
|
||||
options[:name] = column.name
|
||||
schema_creation.accept ChangeColumnDefinition.new column, type, options
|
||||
end
|
||||
|
||||
def rename_column_sql(table_name, column_name, new_column_name)
|
||||
options = {}
|
||||
options = { name: new_column_name }
|
||||
|
||||
if column = columns(table_name).find { |c| c.name == column_name.to_s }
|
||||
options[:default] = column.default
|
||||
@ -696,9 +702,7 @@ def rename_column_sql(table_name, column_name, new_column_name)
|
||||
end
|
||||
|
||||
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"]
|
||||
rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
|
||||
add_column_options!(rename_column_sql, options)
|
||||
rename_column_sql
|
||||
schema_creation.accept ChangeColumnDefinition.new column, current_type, options
|
||||
end
|
||||
|
||||
def remove_column_sql(table_name, column_name, type = nil, options = {})
|
||||
|
@ -213,9 +213,11 @@ def select_rows(sql, name = nil)
|
||||
|
||||
# Executes the SQL statement in the context of this connection.
|
||||
def execute(sql, name = nil)
|
||||
# make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
|
||||
# made since we established the connection
|
||||
@connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
|
||||
if @connection
|
||||
# make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
|
||||
# made since we established the connection
|
||||
@connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
@ -50,7 +50,7 @@ def reset_counters(id, *counters)
|
||||
# ==== Parameters
|
||||
#
|
||||
# * +id+ - The id of the object you wish to update a counter on or an Array of ids.
|
||||
# * +counters+ - An Array of Hashes containing the names of the fields
|
||||
# * +counters+ - A Hash containing the names of the fields
|
||||
# to update as keys and the amount to update the field by as values.
|
||||
#
|
||||
# ==== Examples
|
||||
|
@ -69,10 +69,6 @@ def initialize(message, original_exception = nil)
|
||||
end
|
||||
end
|
||||
|
||||
# Raised when SQL statement is invalid and the application gets a blank result.
|
||||
class ThrowResult < ActiveRecordError
|
||||
end
|
||||
|
||||
# Defunct wrapper class kept for compatibility.
|
||||
# +StatementInvalid+ wraps the original exception now.
|
||||
class WrappedDatabaseException < StatementInvalid
|
||||
|
@ -841,8 +841,6 @@ def run_in_transaction?
|
||||
end
|
||||
|
||||
def setup_fixtures
|
||||
return if ActiveRecord::Base.configurations.blank?
|
||||
|
||||
if pre_loaded_fixtures && !use_transactional_fixtures
|
||||
raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
|
||||
end
|
||||
@ -875,8 +873,6 @@ def setup_fixtures
|
||||
end
|
||||
|
||||
def teardown_fixtures
|
||||
return if ActiveRecord::Base.configurations.blank?
|
||||
|
||||
# Rollback changes if a transaction is active.
|
||||
if run_in_transaction?
|
||||
@fixture_connections.each do |connection|
|
||||
|
@ -5,7 +5,7 @@ module Inheritance
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
# Determine whether to store the full constant name including namespace when using STI
|
||||
# Determines whether to store the full constant name including namespace when using STI.
|
||||
class_attribute :store_full_sti_class, instance_writer: false
|
||||
self.store_full_sti_class = true
|
||||
end
|
||||
@ -13,7 +13,7 @@ module Inheritance
|
||||
module ClassMethods
|
||||
# Determines if one of the attributes passed in is the inheritance column,
|
||||
# and if the inheritance column is attr accessible, it initializes an
|
||||
# instance of the given subclass instead of the base class
|
||||
# instance of the given subclass instead of the base class.
|
||||
def new(*args, &block)
|
||||
if abstract_class? || self == Base
|
||||
raise NotImplementedError, "#{self} is an abstract class and can not be instantiated."
|
||||
@ -27,7 +27,8 @@ def new(*args, &block)
|
||||
super
|
||||
end
|
||||
|
||||
# True if this isn't a concrete subclass needing a STI type condition.
|
||||
# Returns +true+ if this does not need STI type condition. Returns
|
||||
# +false+ if STI type condition needs to be applied.
|
||||
def descends_from_active_record?
|
||||
if self == Base
|
||||
false
|
||||
|
@ -230,6 +230,10 @@ class TooManyRecords < ActiveRecordError
|
||||
# validates_presence_of :member
|
||||
# end
|
||||
#
|
||||
# Note that if you do not specify the <tt>inverse_of</tt> option, then
|
||||
# Active Record will try to automatically guess the inverse association
|
||||
# based on heuristics.
|
||||
#
|
||||
# For one-to-one nested associations, if you build the new (in-memory)
|
||||
# child object yourself before assignment, then this module will not
|
||||
# overwrite it, e.g.:
|
||||
@ -302,14 +306,9 @@ def accepts_nested_attributes_for(*attr_names)
|
||||
|
||||
attr_names.each do |association_name|
|
||||
if reflection = reflect_on_association(association_name)
|
||||
reflection.options[:autosave] = true
|
||||
reflection.autosave = true
|
||||
add_autosave_association_callbacks(reflection)
|
||||
|
||||
# Clear cached values of any inverse associations found in the
|
||||
# reflection and prevent the reflection from finding inverses
|
||||
# automatically in the future.
|
||||
reflection.remove_automatic_inverse_of!
|
||||
|
||||
nested_attributes_options = self.nested_attributes_options.dup
|
||||
nested_attributes_options[association_name.to_sym] = options
|
||||
self.nested_attributes_options = nested_attributes_options
|
||||
|
@ -39,7 +39,7 @@ def many?
|
||||
end
|
||||
|
||||
def to_sql
|
||||
@to_sql ||= ""
|
||||
""
|
||||
end
|
||||
|
||||
def where_values_hash
|
||||
@ -55,7 +55,11 @@ def sum(*)
|
||||
end
|
||||
|
||||
def calculate(_operation, _column_name, _options = {})
|
||||
nil
|
||||
if _operation == :count
|
||||
0
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def exists?(_id = false)
|
||||
|
@ -324,7 +324,7 @@ db_namespace = namespace :db do
|
||||
ActiveRecord::Schema.verbose = false
|
||||
db_namespace["schema:load"].invoke
|
||||
ensure
|
||||
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env])
|
||||
ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env])
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -5,7 +5,9 @@ module Reflection # :nodoc:
|
||||
|
||||
included do
|
||||
class_attribute :reflections
|
||||
class_attribute :aggregate_reflections
|
||||
self.reflections = {}
|
||||
self.aggregate_reflections = {}
|
||||
end
|
||||
|
||||
# \Reflection enables to interrogate Active Record classes and objects
|
||||
@ -27,13 +29,18 @@ def create_reflection(macro, name, scope, options, active_record)
|
||||
|
||||
reflection = klass.new(macro, name, scope, options, active_record)
|
||||
|
||||
self.reflections = self.reflections.merge(name => reflection)
|
||||
if klass == AggregateReflection
|
||||
self.aggregate_reflections = self.aggregate_reflections.merge(name => reflection)
|
||||
else
|
||||
self.reflections = self.reflections.merge(name => reflection)
|
||||
end
|
||||
|
||||
reflection
|
||||
end
|
||||
|
||||
# Returns an array of AggregateReflection objects for all the aggregations in the class.
|
||||
def reflect_on_all_aggregations
|
||||
reflections.values.grep(AggregateReflection)
|
||||
aggregate_reflections.values
|
||||
end
|
||||
|
||||
# Returns the AggregateReflection object for the named +aggregation+ (use the symbol).
|
||||
@ -41,8 +48,7 @@ def reflect_on_all_aggregations
|
||||
# Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection
|
||||
#
|
||||
def reflect_on_aggregation(aggregation)
|
||||
reflection = reflections[aggregation]
|
||||
reflection if reflection.is_a?(AggregateReflection)
|
||||
aggregate_reflections[aggregation]
|
||||
end
|
||||
|
||||
# Returns an array of AssociationReflection objects for all the
|
||||
@ -56,7 +62,7 @@ def reflect_on_aggregation(aggregation)
|
||||
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
|
||||
#
|
||||
def reflect_on_all_associations(macro = nil)
|
||||
association_reflections = reflections.values.grep(AssociationReflection)
|
||||
association_reflections = reflections.values
|
||||
macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
|
||||
end
|
||||
|
||||
@ -66,8 +72,7 @@ def reflect_on_all_associations(macro = nil)
|
||||
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
|
||||
#
|
||||
def reflect_on_association(association)
|
||||
reflection = reflections[association]
|
||||
reflection if reflection.is_a?(AssociationReflection)
|
||||
reflections[association]
|
||||
end
|
||||
|
||||
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
|
||||
@ -118,6 +123,11 @@ def initialize(macro, name, scope, options, active_record)
|
||||
name.to_s.pluralize : name.to_s
|
||||
end
|
||||
|
||||
def autosave=(autosave)
|
||||
@automatic_inverse_of = false
|
||||
@options[:autosave] = autosave
|
||||
end
|
||||
|
||||
# Returns the class for the macro.
|
||||
#
|
||||
# <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class
|
||||
@ -179,10 +189,14 @@ def klass
|
||||
@klass ||= active_record.send(:compute_type, class_name)
|
||||
end
|
||||
|
||||
attr_reader :type, :foreign_type
|
||||
|
||||
def initialize(*args)
|
||||
super
|
||||
@collection = [:has_many, :has_and_belongs_to_many].include?(macro)
|
||||
@automatic_inverse_of = nil
|
||||
@type = options[:as] && "#{options[:as]}_type"
|
||||
@foreign_type = options[:foreign_type] || "#{name}_type"
|
||||
end
|
||||
|
||||
# Returns a new, unsaved instance of the associated class. +attributes+ will
|
||||
@ -192,11 +206,11 @@ def build_association(attributes, &block)
|
||||
end
|
||||
|
||||
def table_name
|
||||
@table_name ||= klass.table_name
|
||||
klass.table_name
|
||||
end
|
||||
|
||||
def quoted_table_name
|
||||
@quoted_table_name ||= klass.quoted_table_name
|
||||
klass.quoted_table_name
|
||||
end
|
||||
|
||||
def join_table
|
||||
@ -207,16 +221,8 @@ def foreign_key
|
||||
@foreign_key ||= options[:foreign_key] || derive_foreign_key
|
||||
end
|
||||
|
||||
def foreign_type
|
||||
@foreign_type ||= options[:foreign_type] || "#{name}_type"
|
||||
end
|
||||
|
||||
def type
|
||||
@type ||= options[:as] && "#{options[:as]}_type"
|
||||
end
|
||||
|
||||
def primary_key_column
|
||||
@primary_key_column ||= klass.columns.find { |c| c.name == klass.primary_key }
|
||||
klass.columns_hash[klass.primary_key]
|
||||
end
|
||||
|
||||
def association_foreign_key
|
||||
@ -240,14 +246,6 @@ def counter_cache_column
|
||||
end
|
||||
end
|
||||
|
||||
def columns(tbl_name)
|
||||
@columns ||= klass.connection.columns(tbl_name)
|
||||
end
|
||||
|
||||
def reset_column_information
|
||||
@columns = nil
|
||||
end
|
||||
|
||||
def check_validity!
|
||||
check_validity_of_inverse!
|
||||
|
||||
@ -291,30 +289,13 @@ def scope_chain
|
||||
alias :source_macro :macro
|
||||
|
||||
def has_inverse?
|
||||
@options[:inverse_of] || find_inverse_of_automatically
|
||||
inverse_name
|
||||
end
|
||||
|
||||
def inverse_of
|
||||
@inverse_of ||= if options[:inverse_of]
|
||||
klass.reflect_on_association(options[:inverse_of])
|
||||
else
|
||||
find_inverse_of_automatically
|
||||
end
|
||||
end
|
||||
return unless inverse_name
|
||||
|
||||
# Clears the cached value of +@inverse_of+ on this object. This will
|
||||
# not remove the :inverse_of option however, so future calls on the
|
||||
# +inverse_of+ will have to recompute the inverse.
|
||||
def clear_inverse_of_cache!
|
||||
@inverse_of = nil
|
||||
end
|
||||
|
||||
# Removes the cached inverse association that was found automatically
|
||||
# and prevents this object from finding the inverse association
|
||||
# automatically in the future.
|
||||
def remove_automatic_inverse_of!
|
||||
@automatic_inverse_of = nil
|
||||
options[:automatic_inverse_of] = false
|
||||
@inverse_of ||= klass.reflect_on_association inverse_name
|
||||
end
|
||||
|
||||
def polymorphic_inverse_of(associated_class)
|
||||
@ -389,26 +370,21 @@ def polymorphic?
|
||||
INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key]
|
||||
|
||||
private
|
||||
# Attempts to find the inverse association automatically.
|
||||
# If it cannot find a suitable inverse association, it returns
|
||||
# Attempts to find the inverse association name automatically.
|
||||
# If it cannot find a suitable inverse association name, it returns
|
||||
# nil.
|
||||
def find_inverse_of_automatically
|
||||
if @automatic_inverse_of == false
|
||||
nil
|
||||
elsif @automatic_inverse_of.nil?
|
||||
set_automatic_inverse_of
|
||||
else
|
||||
klass.reflect_on_association(@automatic_inverse_of)
|
||||
def inverse_name
|
||||
options.fetch(:inverse_of) do
|
||||
if @automatic_inverse_of == false
|
||||
nil
|
||||
else
|
||||
@automatic_inverse_of ||= automatic_inverse_of
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Sets the +@automatic_inverse_of+ instance variable, and returns
|
||||
# either nil or the inverse association that it finds.
|
||||
#
|
||||
# This method caches the inverse association that is found so that
|
||||
# future calls to +find_inverse_of_automatically+ have much less
|
||||
# overhead.
|
||||
def set_automatic_inverse_of
|
||||
# returns either nil or the inverse association name that it finds.
|
||||
def automatic_inverse_of
|
||||
if can_find_inverse_of_automatically?(self)
|
||||
inverse_name = active_record.name.downcase.to_sym
|
||||
|
||||
@ -421,16 +397,11 @@ def set_automatic_inverse_of
|
||||
end
|
||||
|
||||
if valid_inverse_reflection?(reflection)
|
||||
@automatic_inverse_of = inverse_name
|
||||
reflection
|
||||
else
|
||||
@automatic_inverse_of = false
|
||||
nil
|
||||
return inverse_name
|
||||
end
|
||||
else
|
||||
@automatic_inverse_of = false
|
||||
nil
|
||||
end
|
||||
|
||||
false
|
||||
end
|
||||
|
||||
# Checks if the inverse reflection that is returned from the
|
||||
@ -442,22 +413,23 @@ def set_automatic_inverse_of
|
||||
# from calling +klass+, +reflection+ will already be set to false.
|
||||
def valid_inverse_reflection?(reflection)
|
||||
reflection &&
|
||||
klass.name == reflection.active_record.try(:name) &&
|
||||
klass.name == reflection.active_record.name &&
|
||||
klass.primary_key == reflection.active_record_primary_key &&
|
||||
can_find_inverse_of_automatically?(reflection)
|
||||
end
|
||||
|
||||
# Checks to see if the reflection doesn't have any options that prevent
|
||||
# us from being able to guess the inverse automatically. First, the
|
||||
# +automatic_inverse_of+ option cannot be set to false. Second, we must
|
||||
# have +has_many+, +has_one+, +belongs_to+ associations. Third, we must
|
||||
# not have options such as +:polymorphic+ or +:foreign_key+ which prevent us
|
||||
# from correctly guessing the inverse association.
|
||||
# <tt>inverse_of</tt> option cannot be set to false. Second, we must
|
||||
# have <tt>has_many</tt>, <tt>has_one</tt>, <tt>belongs_to</tt> associations.
|
||||
# Third, we must not have options such as <tt>:polymorphic</tt> or
|
||||
# <tt>:foreign_key</tt> which prevent us from correctly guessing the
|
||||
# inverse association.
|
||||
#
|
||||
# Anything with a scope can additionally ruin our attempt at finding an
|
||||
# inverse, so we exclude reflections with scopes.
|
||||
def can_find_inverse_of_automatically?(reflection)
|
||||
reflection.options[:automatic_inverse_of] != false &&
|
||||
reflection.options[:inverse_of] != false &&
|
||||
VALID_AUTOMATIC_INVERSE_MACROS.include?(reflection.macro) &&
|
||||
!INVALID_AUTOMATIC_INVERSE_OPTIONS.any? { |opt| reflection.options[opt] } &&
|
||||
!reflection.scope
|
||||
@ -494,6 +466,11 @@ class ThroughReflection < AssociationReflection #:nodoc:
|
||||
delegate :foreign_key, :foreign_type, :association_foreign_key,
|
||||
:active_record_primary_key, :type, :to => :source_reflection
|
||||
|
||||
def initialize(macro, name, scope, options, active_record)
|
||||
super
|
||||
@source_reflection_name = options[:source]
|
||||
end
|
||||
|
||||
# Returns the source of the through reflection. It checks both a singularized
|
||||
# and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
|
||||
#
|
||||
@ -512,7 +489,7 @@ class ThroughReflection < AssociationReflection #:nodoc:
|
||||
# # => <ActiveRecord::Reflection::AssociationReflection: @macro=:belongs_to, @name=:tag, @active_record=Tagging, @plural_name="tags">
|
||||
#
|
||||
def source_reflection
|
||||
@source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
|
||||
through_reflection.klass.reflect_on_association(source_reflection_name)
|
||||
end
|
||||
|
||||
# Returns the AssociationReflection object specified in the <tt>:through</tt> option
|
||||
@ -528,7 +505,7 @@ def source_reflection
|
||||
# # => <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @active_record=Post, @plural_name="taggings">
|
||||
#
|
||||
def through_reflection
|
||||
@through_reflection ||= active_record.reflect_on_association(options[:through])
|
||||
active_record.reflect_on_association(options[:through])
|
||||
end
|
||||
|
||||
# Returns an array of reflections which are involved in this association. Each item in the
|
||||
@ -630,7 +607,32 @@ def association_primary_key(klass = nil)
|
||||
# # => [:tag, :tags]
|
||||
#
|
||||
def source_reflection_names
|
||||
@source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
|
||||
(options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }.uniq
|
||||
end
|
||||
|
||||
def source_reflection_name # :nodoc:
|
||||
return @source_reflection_name if @source_reflection_name
|
||||
|
||||
names = [name.to_s.singularize, name].collect { |n| n.to_sym }.uniq
|
||||
names = names.find_all { |n|
|
||||
through_reflection.klass.reflect_on_association(n)
|
||||
}
|
||||
|
||||
if names.length > 1
|
||||
example_options = options.dup
|
||||
example_options[:source] = source_reflection_names.first
|
||||
ActiveSupport::Deprecation.warn <<-eowarn
|
||||
Ambiguous source reflection for through association. Please specify a :source
|
||||
directive on your declaration like:
|
||||
|
||||
class #{active_record.name} < ActiveRecord::Base
|
||||
#{macro} :#{name}, #{example_options}
|
||||
end
|
||||
|
||||
eowarn
|
||||
end
|
||||
|
||||
@source_reflection_name = names.first
|
||||
end
|
||||
|
||||
def source_options
|
||||
|
@ -247,7 +247,7 @@ def size
|
||||
def empty?
|
||||
return @records.empty? if loaded?
|
||||
|
||||
c = count
|
||||
c = count(:all)
|
||||
c.respond_to?(:zero?) ? c.zero? : c.empty?
|
||||
end
|
||||
|
||||
|
@ -11,7 +11,7 @@ module Batches
|
||||
# The #find_each method uses #find_in_batches with a batch size of 1000 (or as
|
||||
# specified by the +:batch_size+ option).
|
||||
#
|
||||
# Person.all.find_each do |person|
|
||||
# Person.find_each do |person|
|
||||
# person.do_awesome_stuff
|
||||
# end
|
||||
#
|
||||
@ -19,8 +19,26 @@ module Batches
|
||||
# person.party_all_night!
|
||||
# end
|
||||
#
|
||||
# You can also pass the +:start+ option to specify
|
||||
# an offset to control the starting point.
|
||||
# ==== Options
|
||||
# * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
|
||||
# * <tt>:start</tt> - Specifies the starting point for the batch processing.
|
||||
# This is especially useful if you want multiple workers dealing with
|
||||
# the same processing queue. You can make worker 1 handle all the records
|
||||
# between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
|
||||
# (by setting the +:start+ option on that worker).
|
||||
#
|
||||
# # Let's process for a batch of 2000 records, skiping the first 2000 rows
|
||||
# Person.find_each(start: 2000, batch_size: 2000) do |person|
|
||||
# person.party_all_night!
|
||||
# end
|
||||
#
|
||||
# NOTE: It's not possible to set the order. That is automatically set to
|
||||
# ascending on the primary key ("id ASC") to make the batch ordering
|
||||
# work. This also means that this method only works with integer-based
|
||||
# primary keys.
|
||||
#
|
||||
# NOTE: You can't set the limit either, that's used to control
|
||||
# the batch sizes.
|
||||
def find_each(options = {})
|
||||
find_in_batches(options) do |records|
|
||||
records.each { |record| yield record }
|
||||
@ -28,31 +46,33 @@ def find_each(options = {})
|
||||
end
|
||||
|
||||
# Yields each batch of records that was found by the find +options+ as
|
||||
# an array. The size of each batch is set by the +:batch_size+
|
||||
# option; the default is 1000.
|
||||
#
|
||||
# You can control the starting point for the batch processing by
|
||||
# supplying the +:start+ option. This is especially useful if you
|
||||
# want multiple workers dealing with the same processing queue. You can
|
||||
# make worker 1 handle all the records between id 0 and 10,000 and
|
||||
# worker 2 handle from 10,000 and beyond (by setting the +:start+
|
||||
# option on that worker).
|
||||
#
|
||||
# It's not possible to set the order. That is automatically set to
|
||||
# ascending on the primary key ("id ASC") to make the batch ordering
|
||||
# work. This also means that this method only works with integer-based
|
||||
# primary keys. You can't set the limit either, that's used to control
|
||||
# the batch sizes.
|
||||
# an array.
|
||||
#
|
||||
# Person.where("age > 21").find_in_batches do |group|
|
||||
# sleep(50) # Make sure it doesn't get too crowded in there!
|
||||
# group.each { |person| person.party_all_night! }
|
||||
# end
|
||||
#
|
||||
# ==== Options
|
||||
# * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
|
||||
# * <tt>:start</tt> - Specifies the starting point for the batch processing.
|
||||
# This is especially useful if you want multiple workers dealing with
|
||||
# the same processing queue. You can make worker 1 handle all the records
|
||||
# between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
|
||||
# (by setting the +:start+ option on that worker).
|
||||
#
|
||||
# # Let's process the next 2000 records
|
||||
# Person.all.find_in_batches(start: 2000, batch_size: 2000) do |group|
|
||||
# Person.find_in_batches(start: 2000, batch_size: 2000) do |group|
|
||||
# group.each { |person| person.party_all_night! }
|
||||
# end
|
||||
#
|
||||
# NOTE: It's not possible to set the order. That is automatically set to
|
||||
# ascending on the primary key ("id ASC") to make the batch ordering
|
||||
# work. This also means that this method only works with integer-based
|
||||
# primary keys.
|
||||
#
|
||||
# NOTE: You can't set the limit either, that's used to control
|
||||
# the batch sizes.
|
||||
def find_in_batches(options = {})
|
||||
options.assert_valid_keys(:start, :batch_size)
|
||||
|
||||
|
@ -106,8 +106,6 @@ def calculate(operation, column_name, options = {})
|
||||
else
|
||||
relation.calculate(operation, column_name, options)
|
||||
end
|
||||
rescue ThrowResult
|
||||
0
|
||||
end
|
||||
|
||||
# Use <tt>pluck</tt> as a shortcut to select one or more attributes without
|
||||
@ -209,15 +207,18 @@ def perform_calculation(operation, column_name, options = {})
|
||||
end
|
||||
|
||||
if operation == "count"
|
||||
column_name ||= (select_for_count || :all)
|
||||
if select_values.present?
|
||||
column_name ||= select_values.join(", ")
|
||||
else
|
||||
column_name ||= :all
|
||||
end
|
||||
|
||||
unless arel.ast.grep(Arel::Nodes::OuterJoin).empty?
|
||||
distinct = true
|
||||
end
|
||||
|
||||
column_name = primary_key if column_name == :all && distinct
|
||||
|
||||
distinct = nil if column_name =~ /\s*DISTINCT\s+/i
|
||||
distinct = nil if column_name =~ /\s*DISTINCT[\s(]+/i
|
||||
end
|
||||
|
||||
if group_values.any?
|
||||
@ -378,13 +379,6 @@ def type_cast_using_column(value, column)
|
||||
column ? column.type_cast(value) : value
|
||||
end
|
||||
|
||||
def select_for_count
|
||||
if select_values.present?
|
||||
select = select_values.join(", ")
|
||||
select if select !~ /[,*]/
|
||||
end
|
||||
end
|
||||
|
||||
def build_count_subquery(relation, column_name, distinct)
|
||||
column_alias = Arel.sql('count_column')
|
||||
subquery_alias = Arel.sql('subquery_for_count')
|
||||
|
@ -11,9 +11,11 @@ module FinderMethods
|
||||
# Person.find([1]) # returns an array for the object with ID = 1
|
||||
# Person.where("administrator = 1").order("created_on DESC").find(1)
|
||||
#
|
||||
# Note that returned records may not be in the same order as the ids you
|
||||
# provide since database rows are unordered. Give an explicit <tt>order</tt>
|
||||
# to ensure the results are sorted.
|
||||
# <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found.
|
||||
#
|
||||
# NOTE: The returned records may not be in the same order as the ids you
|
||||
# provide since database rows are unordered. You'd need to provide an explicit <tt>order</tt>
|
||||
# option if you want the results are sorted.
|
||||
#
|
||||
# ==== Find with lock
|
||||
#
|
||||
@ -28,6 +30,34 @@ module FinderMethods
|
||||
# person.visits += 1
|
||||
# person.save!
|
||||
# end
|
||||
#
|
||||
# ==== Variations of +find+
|
||||
#
|
||||
# Person.where(name: 'Spartacus', rating: 4)
|
||||
# # returns a chainable list (which can be empty).
|
||||
#
|
||||
# Person.find_by(name: 'Spartacus', rating: 4)
|
||||
# # returns the first item or nil.
|
||||
#
|
||||
# Person.where(name: 'Spartacus', rating: 4).first_or_initialize
|
||||
# # returns the first item or returns a new instance (requires you call .save to persist against the database).
|
||||
#
|
||||
# Person.where(name: 'Spartacus', rating: 4).first_or_create
|
||||
# # returns the first item or creates it and returns it, available since Rails 3.2.1.
|
||||
#
|
||||
# ==== Alternatives for +find+
|
||||
#
|
||||
# Person.where(name: 'Spartacus', rating: 4).exists?(conditions = :none)
|
||||
# # returns a boolean indicating if any record with the given conditions exist.
|
||||
#
|
||||
# Person.where(name: 'Spartacus', rating: 4).select("field1, field2, field3")
|
||||
# # returns a chainable list of instances with only the mentioned fields.
|
||||
#
|
||||
# Person.where(name: 'Spartacus', rating: 4).ids
|
||||
# # returns an Array of ids, available since Rails 3.2.1.
|
||||
#
|
||||
# Person.where(name: 'Spartacus', rating: 4).pluck(:field1, :field2)
|
||||
# # returns an Array of the required fields, available since Rails 3.1.
|
||||
def find(*args)
|
||||
if block_given?
|
||||
to_a.find { |*block_args| yield(*block_args) }
|
||||
@ -79,13 +109,22 @@ def take!
|
||||
# Person.where(["user_name = :u", { u: user_name }]).first
|
||||
# Person.order("created_on DESC").offset(5).first
|
||||
# Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3
|
||||
#
|
||||
# ==== Rails 3
|
||||
#
|
||||
# Person.first # SELECT "people".* FROM "people" LIMIT 1
|
||||
#
|
||||
# NOTE: Rails 3 may not order this query by the primary key and the order
|
||||
# will depend on the database implementation. In order to ensure that behavior,
|
||||
# use <tt>User.order(:id).first</tt> instead.
|
||||
#
|
||||
# ==== Rails 4
|
||||
#
|
||||
# Person.first # SELECT "people".* FROM "people" ORDER BY "people"."id" ASC LIMIT 1
|
||||
#
|
||||
def first(limit = nil)
|
||||
if limit
|
||||
if order_values.empty? && primary_key
|
||||
order(arel_table[primary_key].asc).limit(limit).to_a
|
||||
else
|
||||
limit(limit).to_a
|
||||
end
|
||||
find_first_with_limit(order_values, limit)
|
||||
else
|
||||
find_first
|
||||
end
|
||||
@ -160,8 +199,9 @@ def exists?(conditions = :none)
|
||||
conditions = conditions.id if Base === conditions
|
||||
return false if !conditions
|
||||
|
||||
join_dependency = construct_join_dependency
|
||||
relation = construct_relation_for_association_find(join_dependency)
|
||||
relation = construct_relation_for_association_find(construct_join_dependency)
|
||||
return false if ActiveRecord::NullRelation === relation
|
||||
|
||||
relation = relation.except(:select, :order).select("1 AS one").limit(1)
|
||||
|
||||
case conditions
|
||||
@ -171,9 +211,8 @@ def exists?(conditions = :none)
|
||||
relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none
|
||||
end
|
||||
|
||||
connection.select_value(relation, "#{name} Exists", relation.bind_values)
|
||||
rescue ThrowResult
|
||||
false
|
||||
relation = relation.with_default_scope
|
||||
connection.select_value(relation.arel, "#{name} Exists", relation.bind_values)
|
||||
end
|
||||
|
||||
# This method is called whenever no records are found with either a single
|
||||
@ -203,10 +242,12 @@ def raise_record_not_found_exception!(ids, result_size, expected_size) #:nodoc:
|
||||
def find_with_associations
|
||||
join_dependency = construct_join_dependency
|
||||
relation = construct_relation_for_association_find(join_dependency)
|
||||
rows = connection.select_all(relation, 'SQL', relation.bind_values.dup)
|
||||
join_dependency.instantiate(rows)
|
||||
rescue ThrowResult
|
||||
[]
|
||||
if ActiveRecord::NullRelation === relation
|
||||
[]
|
||||
else
|
||||
rows = connection.select_all(relation, 'SQL', relation.bind_values.dup)
|
||||
join_dependency.instantiate(rows)
|
||||
end
|
||||
end
|
||||
|
||||
def construct_join_dependency(joins = [])
|
||||
@ -230,21 +271,22 @@ def apply_join_dependency(relation, join_dependency)
|
||||
if using_limitable_reflections?(join_dependency.reflections)
|
||||
relation
|
||||
else
|
||||
relation.where!(construct_limited_ids_condition(relation)) if relation.limit_value
|
||||
if relation.limit_value
|
||||
limited_ids = limited_ids_for(relation)
|
||||
limited_ids.empty? ? relation.none! : relation.where!(table[primary_key].in(limited_ids))
|
||||
end
|
||||
relation.except(:limit, :offset)
|
||||
end
|
||||
end
|
||||
|
||||
def construct_limited_ids_condition(relation)
|
||||
def limited_ids_for(relation)
|
||||
values = @klass.connection.columns_for_distinct(
|
||||
"#{quoted_table_name}.#{quoted_primary_key}", relation.order_values)
|
||||
|
||||
relation = relation.except(:select).select(values).distinct!
|
||||
|
||||
id_rows = @klass.connection.select_all(relation.arel, 'SQL', relation.bind_values)
|
||||
ids_array = id_rows.map {|row| row[primary_key]}
|
||||
|
||||
ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array)
|
||||
id_rows.map {|row| row[primary_key]}
|
||||
end
|
||||
|
||||
def find_with_ids(*ids)
|
||||
@ -312,12 +354,15 @@ def find_first
|
||||
if loaded?
|
||||
@records.first
|
||||
else
|
||||
@first ||=
|
||||
if with_default_scope.order_values.empty? && primary_key
|
||||
order(arel_table[primary_key].asc).limit(1).to_a.first
|
||||
else
|
||||
limit(1).to_a.first
|
||||
end
|
||||
@first ||= find_first_with_limit(with_default_scope.order_values, 1).first
|
||||
end
|
||||
end
|
||||
|
||||
def find_first_with_limit(order_values, limit)
|
||||
if order_values.empty? && primary_key
|
||||
order(arel_table[primary_key].asc).limit(limit).to_a
|
||||
else
|
||||
limit(limit).to_a
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -10,9 +10,9 @@ module ActiveRecord
|
||||
#
|
||||
# config.active_record.record_timestamps = false
|
||||
#
|
||||
# Timestamps are in the local timezone by default but you can use UTC by setting:
|
||||
# Timestamps are in UTC by default but you can use the local timezone by setting:
|
||||
#
|
||||
# config.active_record.default_timezone = :utc
|
||||
# config.active_record.default_timezone = :local
|
||||
#
|
||||
# == Time Zone aware attributes
|
||||
#
|
||||
|
@ -250,7 +250,8 @@ def test_loading_with_no_associations
|
||||
assert_nil Post.all.merge!(:includes => :author).find(posts(:authorless).id).author
|
||||
end
|
||||
|
||||
def test_nested_loading_with_no_associations
|
||||
# Regression test for 21c75e5
|
||||
def test_nested_loading_does_not_raise_exception_when_association_does_not_exist
|
||||
assert_nothing_raised do
|
||||
Post.all.merge!(:includes => {:author => :author_addresss}).find(posts(:authorless).id)
|
||||
end
|
||||
|
@ -6,6 +6,7 @@
|
||||
require 'models/organization'
|
||||
require 'models/possession'
|
||||
require 'models/topic'
|
||||
require 'models/reply'
|
||||
require 'models/minivan'
|
||||
require 'models/speedometer'
|
||||
require 'models/ship_part'
|
||||
@ -166,6 +167,15 @@ def test_no_limit_no_offset
|
||||
assert_no_match(/OFFSET/, queries.first)
|
||||
end
|
||||
|
||||
def test_count_on_invalid_columns_raises
|
||||
e = assert_raises(ActiveRecord::StatementInvalid) {
|
||||
Account.select("credit_limit, firm_name").count
|
||||
}
|
||||
|
||||
assert_match "accounts", e.message
|
||||
assert_match "credit_limit, firm_name", e.message
|
||||
end
|
||||
|
||||
def test_should_group_by_summed_field_having_condition
|
||||
c = Account.group(:firm_id).having('sum(credit_limit) > 50').sum(:credit_limit)
|
||||
assert_nil c[1]
|
||||
@ -472,6 +482,11 @@ def test_pluck
|
||||
assert_equal [1,2,3,4], Topic.order(:id).pluck(:id)
|
||||
end
|
||||
|
||||
def test_pluck_without_column_names
|
||||
assert_equal [[1, "Firm", 1, nil, "37signals", nil, 1, nil, ""]],
|
||||
Company.order(:id).limit(1).pluck
|
||||
end
|
||||
|
||||
def test_pluck_type_cast
|
||||
topic = topics(:first)
|
||||
relation = Topic.where(:id => topic.id)
|
||||
@ -533,6 +548,11 @@ def test_plucks_with_ids
|
||||
assert_equal Company.all.map(&:id).sort, Company.ids.sort
|
||||
end
|
||||
|
||||
def test_pluck_with_includes_limit_and_empty_result
|
||||
assert_equal [], Topic.includes(:replies).limit(0).pluck(:id)
|
||||
assert_equal [], Topic.includes(:replies).limit(1).where('0 = 1').pluck(:id)
|
||||
end
|
||||
|
||||
def test_pluck_not_auto_table_name_prefix_if_column_included
|
||||
Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)])
|
||||
ids = Company.includes(:contracts).pluck(:developer_id)
|
||||
|
@ -329,7 +329,7 @@ def test_pool_sets_connection_visitor
|
||||
end
|
||||
|
||||
# make sure exceptions are thrown when establish_connection
|
||||
# is called with a anonymous class
|
||||
# is called with an anonymous class
|
||||
def test_anonymous_class_exception
|
||||
anonymous = Class.new(ActiveRecord::Base)
|
||||
handler = ActiveRecord::Base.connection_handler
|
||||
|
26
activerecord/test/cases/disconnected_test.rb
Normal file
@ -0,0 +1,26 @@
|
||||
require "cases/helper"
|
||||
|
||||
class TestRecord < ActiveRecord::Base
|
||||
end
|
||||
|
||||
class TestDisconnectedAdapter < ActiveRecord::TestCase
|
||||
self.use_transactional_fixtures = false
|
||||
|
||||
def setup
|
||||
@connection = ActiveRecord::Base.connection
|
||||
end
|
||||
|
||||
def teardown
|
||||
spec = ActiveRecord::Base.connection_config
|
||||
ActiveRecord::Base.establish_connection(spec)
|
||||
@connection = nil
|
||||
end
|
||||
|
||||
test "can't execute statements while disconnected" do
|
||||
@connection.execute "SELECT count(*) from products"
|
||||
@connection.disconnect!
|
||||
assert_raises(ActiveRecord::StatementInvalid) do
|
||||
@connection.execute "SELECT count(*) from products"
|
||||
end
|
||||
end
|
||||
end
|
@ -245,6 +245,22 @@ def test_binary_in_fixtures
|
||||
def test_serialized_fixtures
|
||||
assert_equal ["Green", "Red", "Orange"], traffic_lights(:uk).state
|
||||
end
|
||||
|
||||
def test_fixtures_are_set_up_with_database_env_variable
|
||||
ENV.stubs(:[]).with("DATABASE_URL").returns("sqlite3:///:memory:")
|
||||
ActiveRecord::Base.stubs(:configurations).returns({})
|
||||
test_case = Class.new(ActiveRecord::TestCase) do
|
||||
fixtures :accounts
|
||||
|
||||
def test_fixtures
|
||||
assert accounts(:signals37)
|
||||
end
|
||||
end
|
||||
|
||||
result = test_case.new(:test_fixtures).run
|
||||
|
||||
assert result.passed?, "Expected #{result.name} to pass:\n#{result}"
|
||||
end
|
||||
end
|
||||
|
||||
if Account.connection.respond_to?(:reset_pk_sequence!)
|
||||
|
@ -242,7 +242,7 @@ def test_polymorphic_destroy_with_dependencies_and_lock_version
|
||||
car = Car.create!
|
||||
|
||||
assert_difference 'car.wheels.count' do
|
||||
car.wheels << Wheel.create!
|
||||
car.wheels << Wheel.create!
|
||||
end
|
||||
assert_difference 'car.wheels.count', -1 do
|
||||
car.destroy
|
||||
|
@ -797,26 +797,6 @@ def test_validate_presence_of_parent_works_with_inverse_of
|
||||
end
|
||||
end
|
||||
|
||||
def test_validate_presence_of_parent_fails_without_inverse_of
|
||||
Man.accepts_nested_attributes_for(:interests)
|
||||
Man.reflect_on_association(:interests).options.delete(:inverse_of)
|
||||
Man.reflect_on_association(:interests).clear_inverse_of_cache!
|
||||
Interest.reflect_on_association(:man).options.delete(:inverse_of)
|
||||
Interest.reflect_on_association(:man).clear_inverse_of_cache!
|
||||
|
||||
repair_validations(Interest) do
|
||||
Interest.validates_presence_of(:man)
|
||||
assert_no_difference ['Man.count', 'Interest.count'] do
|
||||
man = Man.create(:name => 'John',
|
||||
:interests_attributes => [{:topic=>'Cars'}, {:topic=>'Sports'}])
|
||||
assert !man.errors[:"interests.man"].empty?
|
||||
end
|
||||
end
|
||||
ensure
|
||||
Man.reflect_on_association(:interests).options[:inverse_of] = :man
|
||||
Interest.reflect_on_association(:man).options[:inverse_of] = :interests
|
||||
end
|
||||
|
||||
def test_can_use_symbols_as_object_identifier
|
||||
@pirate.attributes = { :parrots_attributes => { :foo => { :name => 'Lovely Day' }, :bar => { :name => 'Blown Away' } } }
|
||||
assert_nothing_raised(NoMethodError) { @pirate.save! }
|
||||
|
@ -278,8 +278,9 @@ def test_null_relation_content_size_methods
|
||||
|
||||
def test_null_relation_calculations_methods
|
||||
assert_no_queries do
|
||||
assert_equal 0, Developer.none.count
|
||||
assert_equal nil, Developer.none.calculate(:average, 'salary')
|
||||
assert_equal 0, Developer.none.count
|
||||
assert_equal 0, Developer.none.calculate(:count, nil, {})
|
||||
assert_equal nil, Developer.none.calculate(:average, 'salary')
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -85,7 +85,7 @@ class Author < ActiveRecord::Base
|
||||
has_many :author_favorites
|
||||
has_many :favorite_authors, -> { order('name') }, :through => :author_favorites
|
||||
|
||||
has_many :taggings, :through => :posts
|
||||
has_many :taggings, :through => :posts, :source => :taggings
|
||||
has_many :taggings_2, :through => :posts, :source => :tagging
|
||||
has_many :tags, :through => :posts
|
||||
has_many :post_categories, :through => :posts, :source => :categories
|
||||
|
@ -1,6 +1,6 @@
|
||||
class Club < ActiveRecord::Base
|
||||
has_one :membership
|
||||
has_many :memberships, :automatic_inverse_of => false
|
||||
has_many :memberships, :inverse_of => false
|
||||
has_many :members, :through => :memberships
|
||||
has_many :current_memberships
|
||||
has_one :sponsor
|
||||
|
@ -141,7 +141,7 @@ class Client < Company
|
||||
belongs_to :firm_with_primary_key_symbols, :class_name => "Firm", :primary_key => :name, :foreign_key => :firm_name
|
||||
belongs_to :readonly_firm, -> { readonly }, :class_name => "Firm", :foreign_key => "firm_id"
|
||||
belongs_to :bob_firm, -> { where :name => "Bob" }, :class_name => "Firm", :foreign_key => "client_of"
|
||||
has_many :accounts, :through => :firm
|
||||
has_many :accounts, :through => :firm, :source => :accounts
|
||||
belongs_to :account
|
||||
|
||||
class RaisedOnSave < RuntimeError; end
|
||||
|
@ -1,5 +1,5 @@
|
||||
class Interest < ActiveRecord::Base
|
||||
belongs_to :man, :inverse_of => :interests, :automatic_inverse_of => false
|
||||
belongs_to :man, :inverse_of => :interests
|
||||
belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_interests
|
||||
belongs_to :zine, :inverse_of => :interests
|
||||
end
|
||||
|
@ -1,7 +1,7 @@
|
||||
class Man < ActiveRecord::Base
|
||||
has_one :face, :inverse_of => :man
|
||||
has_one :polymorphic_face, :class_name => 'Face', :as => :polymorphic_man, :inverse_of => :polymorphic_man
|
||||
has_many :interests, :inverse_of => :man, :automatic_inverse_of => false
|
||||
has_many :interests, :inverse_of => :man
|
||||
has_many :polymorphic_interests, :class_name => 'Interest', :as => :polymorphic_man, :inverse_of => :polymorphic_man
|
||||
# These are "broken" inverse_of associations for the purposes of testing
|
||||
has_one :dirty_face, :class_name => 'Face', :inverse_of => :dirty_man
|
||||
|
@ -9,7 +9,7 @@ class Member < ActiveRecord::Base
|
||||
has_one :hairy_club, -> { where :clubs => {:name => "Moustache and Eyebrow Fancier Club"} }, :through => :membership, :source => :club
|
||||
has_one :sponsor, :as => :sponsorable
|
||||
has_one :sponsor_club, :through => :sponsor
|
||||
has_one :member_detail, :automatic_inverse_of => false
|
||||
has_one :member_detail, :inverse_of => false
|
||||
has_one :organization, :through => :member_detail
|
||||
belongs_to :member_type
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
class MemberDetail < ActiveRecord::Base
|
||||
belongs_to :member, :automatic_inverse_of => false
|
||||
belongs_to :member, :inverse_of => false
|
||||
belongs_to :organization
|
||||
has_one :member_type, :through => :member
|
||||
|
||||
|
@ -1,5 +1,29 @@
|
||||
* `HashWithIndifferentAccess#select` now returns a `HashWithIndifferentAccess`
|
||||
instance instead of a `Hash` instance.
|
||||
|
||||
Fixes #10723
|
||||
|
||||
*Albert Llop*
|
||||
|
||||
* Add `DateTime#usec` and `DateTime#nsec` so that `ActiveSupport::TimeWithZone` keeps
|
||||
sub-second resolution when wrapping a `DateTime` value.
|
||||
|
||||
Fixes #10855
|
||||
|
||||
*Andrew White*
|
||||
|
||||
* Fix `ActiveSupport::Dependencies::Loadable#load_dependency` calling
|
||||
`#blame_file!` on Exceptions that do not have the Blamable mixin
|
||||
|
||||
*Andrew Kreiling*
|
||||
|
||||
* Override `Time.at` to support the passing of Time-like values when called with a single argument.
|
||||
|
||||
*Andrew White*
|
||||
|
||||
* Prevent side effects to hashes inside arrays when
|
||||
`Hash#with_indifferent_access` is called.
|
||||
|
||||
Fixes #10526
|
||||
|
||||
*Yves Senn*
|
||||
|
@ -13,17 +13,17 @@ module ActiveSupport
|
||||
# can focus on the rest.
|
||||
#
|
||||
# bc = BacktraceCleaner.new
|
||||
# bc.add_filter { |line| line.gsub(Rails.root, '') }
|
||||
# bc.add_silencer { |line| line =~ /mongrel|rubygems/ }
|
||||
# bc.clean(exception.backtrace) # will strip the Rails.root prefix and skip any lines from mongrel or rubygems
|
||||
# bc.add_filter { |line| line.gsub(Rails.root, '') } # strip the Rails.root prefix
|
||||
# bc.add_silencer { |line| line =~ /mongrel|rubygems/ } # skip any lines from mongrel or rubygems
|
||||
# bc.clean(exception.backtrace) # perform the cleanup
|
||||
#
|
||||
# To reconfigure an existing BacktraceCleaner (like the default one in Rails)
|
||||
# and show as much data as possible, you can always call
|
||||
# <tt>BacktraceCleaner#remove_silencers!</tt>, which will restore the
|
||||
# backtrace to a pristine state. If you need to reconfigure an existing
|
||||
# BacktraceCleaner so that it does not filter or modify the paths of any lines
|
||||
# of the backtrace, you can call BacktraceCleaner#remove_filters! These two
|
||||
# methods will give you a completely untouched backtrace.
|
||||
# of the backtrace, you can call <tt>BacktraceCleaner#remove_filters!<tt>
|
||||
# These two methods will give you a completely untouched backtrace.
|
||||
#
|
||||
# Inspired by the Quiet Backtrace gem by Thoughtbot.
|
||||
class BacktraceCleaner
|
||||
|
@ -228,7 +228,7 @@ def self.instrument
|
||||
#
|
||||
# Setting <tt>:race_condition_ttl</tt> is very useful in situations where
|
||||
# a cache entry is used very frequently and is under heavy load. If a
|
||||
# cache expires and due to heavy load seven different processes will try
|
||||
# cache expires and due to heavy load several different processes will try
|
||||
# to read data natively and then they all will try to write to cache. To
|
||||
# avoid that case the first process to find an expired cache entry will
|
||||
# bump the cache expiration time by the value set in <tt>:race_condition_ttl</tt>.
|
||||
|
@ -94,6 +94,15 @@ def run_callbacks(kind, &block)
|
||||
def halted_callback_hook(filter)
|
||||
end
|
||||
|
||||
module Conditionals # :nodoc:
|
||||
class Value
|
||||
def initialize(&block)
|
||||
@block = block
|
||||
end
|
||||
def call(target, value); @block.call(value); end
|
||||
end
|
||||
end
|
||||
|
||||
module Filters
|
||||
Environment = Struct.new(:target, :halted, :value, :run_block)
|
||||
|
||||
@ -403,8 +412,8 @@ def invert_lambda(l)
|
||||
# the same after this point:
|
||||
#
|
||||
# Symbols:: Already methods.
|
||||
# Strings:: class_eval'ed into methods.
|
||||
# Procs:: define_method'ed into methods.
|
||||
# Strings:: class_eval'd into methods.
|
||||
# Procs:: using define_method compiled into methods.
|
||||
# Objects::
|
||||
# a method is created that calls the before_foo method
|
||||
# on the object.
|
||||
@ -415,6 +424,7 @@ def make_lambda(filter)
|
||||
when String
|
||||
l = eval "lambda { |value| #{filter} }"
|
||||
lambda { |target, value| target.instance_exec(value, &l) }
|
||||
when Conditionals::Value then filter
|
||||
when ::Proc
|
||||
if filter.arity > 1
|
||||
return lambda { |target, _, &block|
|
||||
@ -648,7 +658,7 @@ def reset_callbacks(symbol)
|
||||
#
|
||||
# * <tt>:terminator</tt> - Determines when a before filter will halt the
|
||||
# callback chain, preventing following callbacks from being called and
|
||||
# the event from being triggered. This is a string to be eval'ed. The
|
||||
# the event from being triggered. This is a string to be eval'd. The
|
||||
# result of the callback is available in the +result+ variable.
|
||||
#
|
||||
# define_callbacks :validate, terminator: 'result == false'
|
||||
|
@ -80,6 +80,16 @@ def to_i
|
||||
seconds_since_unix_epoch.to_i
|
||||
end
|
||||
|
||||
# Returns the fraction of a second as microseconds
|
||||
def usec
|
||||
(sec_fraction * 1_000_000).to_i
|
||||
end
|
||||
|
||||
# Returns the fraction of a second as nanoseconds
|
||||
def nsec
|
||||
(sec_fraction * 1_000_000_000).to_i
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def offset_in_seconds
|
||||
|
@ -164,7 +164,7 @@ def delegate(*methods)
|
||||
#
|
||||
# Reason is twofold: On one hand doing less calls is in general better.
|
||||
# On the other hand it could be that the target has side-effects,
|
||||
# whereas conceptualy, from the user point of view, the delegator should
|
||||
# whereas conceptually, from the user point of view, the delegator should
|
||||
# be doing one call.
|
||||
if allow_nil
|
||||
module_eval(<<-EOS, file, line - 3)
|
||||
|
@ -65,6 +65,18 @@ def local_time(*args)
|
||||
def current
|
||||
::Time.zone ? ::Time.zone.now : ::Time.now
|
||||
end
|
||||
|
||||
# Layers additional behavior on Time.at so that ActiveSupport::TimeWithZone and DateTime
|
||||
# instances can be used when called with a single argument
|
||||
def at_with_coercion(*args)
|
||||
if args.size == 1 && args.first.acts_like?(:time)
|
||||
at_without_coercion(args.first.to_i)
|
||||
else
|
||||
at_without_coercion(*args)
|
||||
end
|
||||
end
|
||||
alias_method :at_without_coercion, :at
|
||||
alias_method :at, :at_with_coercion
|
||||
end
|
||||
|
||||
# Seconds since midnight: Time.now.seconds_since_midnight
|
||||
|
@ -213,7 +213,7 @@ def load_dependency(file)
|
||||
yield
|
||||
end
|
||||
rescue Exception => exception # errors from loading file
|
||||
exception.blame_file! file
|
||||
exception.blame_file! file if exception.respond_to? :blame_file!
|
||||
raise
|
||||
end
|
||||
|
||||
|
@ -115,7 +115,7 @@ def max_mtime(paths)
|
||||
end
|
||||
|
||||
def compile_glob(hash)
|
||||
hash.freeze # Freeze so changes aren't accidently pushed
|
||||
hash.freeze # Freeze so changes aren't accidentally pushed
|
||||
return if hash.empty?
|
||||
|
||||
globs = hash.map do |key, value|
|
||||
|
@ -227,6 +227,10 @@ def symbolize_keys; to_hash.symbolize_keys! end
|
||||
def deep_symbolize_keys; to_hash.deep_symbolize_keys end
|
||||
def to_options!; self end
|
||||
|
||||
def select(*args, &block)
|
||||
dup.select!(*args, &block)
|
||||
end
|
||||
|
||||
# Convert to a regular hash with string keys.
|
||||
def to_hash
|
||||
_new_hash= {}
|
||||
|
@ -219,7 +219,12 @@ def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
|
||||
# unknown.
|
||||
def constantize(camel_cased_word)
|
||||
names = camel_cased_word.split('::')
|
||||
names.shift if names.empty? || names.first.empty?
|
||||
|
||||
# Trigger a builtin NameError exception including the ill-formed constant in the message.
|
||||
Object.const_get(camel_cased_word) if names.empty?
|
||||
|
||||
# Remove the first blank element in case of '::ClassName' notation.
|
||||
names.shift if names.size > 1 && names.first.empty?
|
||||
|
||||
names.inject(Object) do |constant, name|
|
||||
if constant == Object
|
||||
@ -314,9 +319,14 @@ def ordinalize(number)
|
||||
private
|
||||
|
||||
# Mount a regular expression that will match part by part of the constant.
|
||||
# For instance, Foo::Bar::Baz will generate Foo(::Bar(::Baz)?)?
|
||||
#
|
||||
# const_regexp("Foo::Bar::Baz") # => /Foo(::Bar(::Baz)?)?/
|
||||
# const_regexp("::") # => /::/
|
||||
def const_regexp(camel_cased_word) #:nodoc:
|
||||
parts = camel_cased_word.split("::")
|
||||
|
||||
return Regexp.escape(camel_cased_word) if parts.blank?
|
||||
|
||||
last = parts.pop
|
||||
|
||||
parts.reverse.inject(last) do |acc, part|
|
||||
|
@ -143,8 +143,8 @@ module ActiveSupport
|
||||
#
|
||||
# == Default Queue
|
||||
#
|
||||
# Notifications ships with a queue implementation that consumes and publish events
|
||||
# to log subscribers in a thread. You can use any queue implementation you want.
|
||||
# Notifications ships with a queue implementation that consumes and publishes events
|
||||
# to all log subscribers. You can use any queue implementation you want.
|
||||
#
|
||||
module Notifications
|
||||
class << self
|
||||
|
@ -292,7 +292,7 @@ def advance(options)
|
||||
end
|
||||
end
|
||||
|
||||
%w(year mon month day mday wday yday hour min sec to_date).each do |method_name|
|
||||
%w(year mon month day mday wday yday hour min sec usec nsec to_date).each do |method_name|
|
||||
class_eval <<-EOV, __FILE__, __LINE__ + 1
|
||||
def #{method_name} # def month
|
||||
time.#{method_name} # time.month
|
||||
@ -300,10 +300,6 @@ def #{method_name} # def month
|
||||
EOV
|
||||
end
|
||||
|
||||
def usec
|
||||
time.respond_to?(:usec) ? time.usec : 0
|
||||
end
|
||||
|
||||
def to_a
|
||||
[time.sec, time.min, time.hour, time.day, time.mon, time.year, time.wday, time.yday, dst?, zone]
|
||||
end
|
||||
|
@ -34,8 +34,6 @@ def run_constantize_tests_on
|
||||
assert_equal Case::Dice, yield("Object::Case::Dice")
|
||||
assert_equal ConstantizeTestCases, yield("ConstantizeTestCases")
|
||||
assert_equal ConstantizeTestCases, yield("::ConstantizeTestCases")
|
||||
assert_equal Object, yield("")
|
||||
assert_equal Object, yield("::")
|
||||
assert_raises(NameError) { yield("UnknownClass") }
|
||||
assert_raises(NameError) { yield("UnknownClass::Ace") }
|
||||
assert_raises(NameError) { yield("UnknownClass::Ace::Base") }
|
||||
@ -45,6 +43,8 @@ def run_constantize_tests_on
|
||||
assert_raises(NameError) { yield("Ace::Base::ConstantizeTestCases") }
|
||||
assert_raises(NameError) { yield("Ace::Gas::Base") }
|
||||
assert_raises(NameError) { yield("Ace::Gas::ConstantizeTestCases") }
|
||||
assert_raises(NameError) { yield("") }
|
||||
assert_raises(NameError) { yield("::") }
|
||||
end
|
||||
|
||||
def run_safe_constantize_tests_on
|
||||
@ -58,8 +58,8 @@ def run_safe_constantize_tests_on
|
||||
assert_equal Case::Dice, yield("Object::Case::Dice")
|
||||
assert_equal ConstantizeTestCases, yield("ConstantizeTestCases")
|
||||
assert_equal ConstantizeTestCases, yield("::ConstantizeTestCases")
|
||||
assert_equal Object, yield("")
|
||||
assert_equal Object, yield("::")
|
||||
assert_nil yield("")
|
||||
assert_nil yield("::")
|
||||
assert_nil yield("UnknownClass")
|
||||
assert_nil yield("UnknownClass::Ace")
|
||||
assert_nil yield("UnknownClass::Ace::Base")
|
||||
|
@ -327,6 +327,16 @@ def test_to_i
|
||||
assert_equal 946684800, DateTime.civil(1999,12,31,19,0,0,Rational(-5,24)).to_i
|
||||
end
|
||||
|
||||
def test_usec
|
||||
assert_equal 0, DateTime.civil(2000).usec
|
||||
assert_equal 500000, DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)).usec
|
||||
end
|
||||
|
||||
def test_nsec
|
||||
assert_equal 0, DateTime.civil(2000).nsec
|
||||
assert_equal 500000000, DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)).nsec
|
||||
end
|
||||
|
||||
protected
|
||||
def with_env_tz(new_tz = 'US/Eastern')
|
||||
old_tz, ENV['TZ'] = ENV['TZ'], new_tz
|
||||
|
@ -480,6 +480,36 @@ def test_indifferent_deleting
|
||||
assert_equal hash.delete('a'), nil
|
||||
end
|
||||
|
||||
def test_indifferent_select
|
||||
hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).select {|k,v| v == 1}
|
||||
|
||||
assert_equal({ 'a' => 1 }, hash)
|
||||
assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
|
||||
end
|
||||
|
||||
def test_indifferent_select_bang
|
||||
indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings)
|
||||
indifferent_strings.select! {|k,v| v == 1}
|
||||
|
||||
assert_equal({ 'a' => 1 }, indifferent_strings)
|
||||
assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings
|
||||
end
|
||||
|
||||
def test_indifferent_reject
|
||||
hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).reject {|k,v| v != 1}
|
||||
|
||||
assert_equal({ 'a' => 1 }, hash)
|
||||
assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
|
||||
end
|
||||
|
||||
def test_indifferent_reject_bang
|
||||
indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings)
|
||||
indifferent_strings.reject! {|k,v| v != 1}
|
||||
|
||||
assert_equal({ 'a' => 1 }, indifferent_strings)
|
||||
assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings
|
||||
end
|
||||
|
||||
def test_indifferent_to_hash
|
||||
# Should convert to a Hash with String keys.
|
||||
assert_equal @strings, @mixed.with_indifferent_access.to_hash
|
||||
|
@ -741,6 +741,28 @@ def test_compare_with_time_with_zone
|
||||
assert_equal(-1, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 0, 0, 1), ActiveSupport::TimeZone['UTC'] ))
|
||||
end
|
||||
|
||||
def test_at_with_datetime
|
||||
assert_equal Time.utc(2000, 1, 1, 0, 0, 0), Time.at(DateTime.civil(2000, 1, 1, 0, 0, 0))
|
||||
|
||||
# Only test this if the underlying Time.at raises a TypeError
|
||||
begin
|
||||
Time.at_without_coercion(Time.now, 0)
|
||||
rescue TypeError
|
||||
assert_raise(TypeError) { assert_equal(Time.utc(2000, 1, 1, 0, 0, 0), Time.at(DateTime.civil(2000, 1, 1, 0, 0, 0), 0)) }
|
||||
end
|
||||
end
|
||||
|
||||
def test_at_with_time_with_zone
|
||||
assert_equal Time.utc(2000, 1, 1, 0, 0, 0), Time.at(ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone['UTC']))
|
||||
|
||||
# Only test this if the underlying Time.at raises a TypeError
|
||||
begin
|
||||
Time.at_without_coercion(Time.now, 0)
|
||||
rescue TypeError
|
||||
assert_raise(TypeError) { assert_equal(Time.utc(2000, 1, 1, 0, 0, 0), Time.at(ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone['UTC']), 0)) }
|
||||
end
|
||||
end
|
||||
|
||||
def test_eql?
|
||||
assert_equal true, Time.utc(2000).eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['UTC']) )
|
||||
assert_equal true, Time.utc(2000).eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Hawaii"]) )
|
||||
|
@ -445,6 +445,16 @@ def test_usec_returns_0_when_datetime_is_wrapped
|
||||
assert_equal 0, twz.usec
|
||||
end
|
||||
|
||||
def test_usec_returns_sec_fraction_when_datetime_is_wrapped
|
||||
twz = ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)), @time_zone)
|
||||
assert_equal 500000, twz.usec
|
||||
end
|
||||
|
||||
def test_nsec_returns_sec_fraction_when_datetime_is_wrapped
|
||||
twz = ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)), @time_zone)
|
||||
assert_equal 500000000, twz.nsec
|
||||
end
|
||||
|
||||
def test_utc_to_local_conversion_saves_period_in_instance_variable
|
||||
assert_nil @twz.instance_variable_get('@period')
|
||||
@twz.time
|
||||
|
@ -0,0 +1,5 @@
|
||||
exception = Exception.new('I am not blamable!')
|
||||
class << exception
|
||||
undef_method(:blame_file!)
|
||||
end
|
||||
raise exception
|
@ -76,6 +76,14 @@ def test_dependency_which_raises_exception_isnt_added_to_loaded_set
|
||||
end
|
||||
end
|
||||
|
||||
def test_dependency_which_raises_doesnt_blindly_call_blame_file!
|
||||
with_loading do
|
||||
filename = 'dependencies/raises_exception_without_blame_file'
|
||||
|
||||
assert_raises(Exception) { require_dependency filename }
|
||||
end
|
||||
end
|
||||
|
||||
def test_warnings_should_be_enabled_on_first_load
|
||||
with_loading 'dependencies' do
|
||||
old_warnings, ActiveSupport::Dependencies.warnings_on_first_load = ActiveSupport::Dependencies.warnings_on_first_load, true
|
||||
|
@ -10,7 +10,7 @@ def test_uniq_load_paths
|
||||
}
|
||||
load_paths_count[File.expand_path('../../lib', __FILE__)] -= 1
|
||||
|
||||
filtered = load_paths_count.select { |k, v| v > 1 }
|
||||
assert filtered.empty?, filtered.inspect
|
||||
load_paths_count.select! { |k, v| v > 1 }
|
||||
assert load_paths_count.empty?, load_paths_count.inspect
|
||||
end
|
||||
end
|
||||
|
@ -178,7 +178,7 @@ def test_children_with_non_adjacent_text
|
||||
private
|
||||
def assert_equal_rexml(xml)
|
||||
parsed_xml = XmlMini.parse(xml)
|
||||
hash = XmlMini.with_backend('REXML') { parsed_xml }
|
||||
hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
|
||||
assert_equal(hash, parsed_xml)
|
||||
end
|
||||
end
|
||||
|
@ -195,7 +195,8 @@ def test_children_with_blank_text_and_attribute
|
||||
private
|
||||
def assert_equal_rexml(xml)
|
||||
parsed_xml = XmlMini.parse(xml)
|
||||
hash = XmlMini.with_backend('REXML') { parsed_xml }
|
||||
xml.rewind if xml.respond_to?(:rewind)
|
||||
hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
|
||||
assert_equal(hash, parsed_xml)
|
||||
end
|
||||
end
|
||||
|
@ -186,7 +186,8 @@ def test_children_with_blank_text
|
||||
private
|
||||
def assert_equal_rexml(xml)
|
||||
parsed_xml = XmlMini.parse(xml)
|
||||
hash = XmlMini.with_backend('REXML') { parsed_xml }
|
||||
xml.rewind if xml.respond_to?(:rewind)
|
||||
hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
|
||||
assert_equal(hash, parsed_xml)
|
||||
end
|
||||
end
|
||||
|
@ -208,7 +208,8 @@ def test_children_with_blank_text_and_attribute
|
||||
private
|
||||
def assert_equal_rexml(xml)
|
||||
parsed_xml = XmlMini.parse(xml)
|
||||
hash = XmlMini.with_backend('REXML') { parsed_xml }
|
||||
xml.rewind if xml.respond_to?(:rewind)
|
||||
hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
|
||||
assert_equal(hash, parsed_xml)
|
||||
end
|
||||
end
|
||||
|
@ -209,7 +209,8 @@ def test_children_with_blank_text_and_attribute
|
||||
private
|
||||
def assert_equal_rexml(xml)
|
||||
parsed_xml = XmlMini.parse(xml)
|
||||
hash = XmlMini.with_backend('REXML') { parsed_xml }
|
||||
xml.rewind if xml.respond_to?(:rewind)
|
||||
hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
|
||||
assert_equal(hash, parsed_xml)
|
||||
end
|
||||
end
|
||||
|
@ -30,7 +30,8 @@ def test_parse_from_io
|
||||
private
|
||||
def assert_equal_rexml(xml)
|
||||
parsed_xml = XmlMini.parse(xml)
|
||||
hash = XmlMini.with_backend('REXML') { parsed_xml }
|
||||
xml.rewind if xml.respond_to?(:rewind)
|
||||
hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
|
||||
assert_equal(hash, parsed_xml)
|
||||
end
|
||||
end
|
||||
|
@ -1,3 +1,5 @@
|
||||
* No changes.
|
||||
* Removed repetitive th tags. Instead of them added one th tag with a colspan attribute.
|
||||
|
||||
*Sıtkı Bağdat*
|
||||
|
||||
Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/guides/CHANGELOG.md) for previous changes.
|
||||
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.6 KiB |
Before Width: | Height: | Size: 43 B After Width: | Height: | Size: 35 B |
Before Width: | Height: | Size: 44 B After Width: | Height: | Size: 36 B |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 5.4 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 11 KiB |