aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md2
-rw-r--r--actionpack/lib/action_controller/metal.rb1
-rw-r--r--actionpack/lib/action_controller/metal/live.rb4
-rw-r--r--actionpack/lib/action_dispatch/http/mime_types.rb2
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb3
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb77
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb6
-rw-r--r--actionpack/test/dispatch/mime_type_test.rb12
-rw-r--r--actionview/CHANGELOG.md5
-rw-r--r--actionview/lib/action_view/helpers/form_helper.rb11
-rw-r--r--actionview/lib/action_view/helpers/form_options_helper.rb22
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_check_boxes.rb25
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_helpers.rb26
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb10
-rw-r--r--actionview/test/template/form_collections_helper_test.rb42
-rw-r--r--actionview/test/template/form_helper_test.rb34
-rw-r--r--actionview/test/template/render_test.rb10
-rw-r--r--activejob/CHANGELOG.md4
-rw-r--r--activejob/lib/active_job/base.rb2
-rw-r--r--activejob/lib/active_job/core.rb8
-rw-r--r--activejob/lib/active_job/enqueuing.rb4
-rw-r--r--activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb4
-rw-r--r--activejob/lib/active_job/queue_adapters/que_adapter.rb4
-rw-r--r--activejob/lib/active_job/queue_priority.rb44
-rw-r--r--activejob/test/cases/queue_priority_test.rb47
-rw-r--r--activejob/test/integration/queuing_test.rb12
-rw-r--r--activejob/test/support/integration/test_case_helpers.rb8
-rw-r--r--activejob/test/support/que/inline.rb1
-rw-r--r--activemodel/lib/active_model/naming.rb2
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb3
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb14
-rw-r--r--activerecord/lib/active_record/attribute.rb6
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb2
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb2
-rw-r--r--activerecord/lib/active_record/attribute_mutation_tracker.rb4
-rw-r--r--activerecord/lib/active_record/attribute_set.rb8
-rw-r--r--activerecord/lib/active_record/attribute_set/builder.rb8
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb2
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb4
-rw-r--r--activerecord/lib/active_record/core.rb4
-rw-r--r--activerecord/lib/active_record/migration.rb2
-rw-r--r--activerecord/lib/active_record/reflection.rb30
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb1
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb2
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb4
-rw-r--r--activerecord/lib/active_record/sanitization.rb75
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb13
-rw-r--r--activerecord/test/cases/attribute_set_test.rb20
-rw-r--r--activerecord/test/models/company.rb3
-rw-r--r--activerecord/test/models/developer.rb3
-rw-r--r--activerecord/test/models/project.rb2
-rw-r--r--activerecord/test/schema/schema.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/hash/keys.rb14
-rw-r--r--activesupport/lib/active_support/core_ext/hash/transform_values.rb12
-rw-r--r--activesupport/lib/active_support/time_with_zone.rb2
-rw-r--r--activesupport/test/core_ext/hash/transform_keys_test.rb12
-rw-r--r--activesupport/test/core_ext/hash/transform_values_test.rb12
-rw-r--r--guides/source/configuring.md2
-rw-r--r--railties/lib/rails/test_unit/minitest_plugin.rb3
-rw-r--r--railties/test/application/configuration_test.rb2
-rw-r--r--railties/test/application/routing_test.rb12
-rw-r--r--railties/test/isolation/abstract_unit.rb1
62 files changed, 541 insertions, 177 deletions
diff --git a/README.md b/README.md
index a17b6245f5..f823a49f7d 100644
--- a/README.md
+++ b/README.md
@@ -34,7 +34,7 @@ Ruby code (ERB files). Views are typically rendered to generate a controller res
or to generate the body of an email. In Rails, View generation is handled by Action View.
You can read more about Action View in its [README](actionview/README.rdoc).
-Active Record, Action Pack, and Action View can each be used independently outside Rails.
+Active Record, Active Model, Action Pack, and Action View can each be used independently outside Rails.
In addition to them, Rails also comes with Action Mailer ([README](actionmailer/README.rdoc)), a library
to generate and send emails; Active Job ([README](activejob/README.md)), a
framework for declaring jobs and making them run on a variety of queueing
diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb
index 3d72755f1d..beeaae9d0c 100644
--- a/actionpack/lib/action_controller/metal.rb
+++ b/actionpack/lib/action_controller/metal.rb
@@ -187,6 +187,7 @@ module ActionController
set_request!(request)
set_response!(response)
process(name)
+ request.commit_flash
to_a
end
diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb
index 5de5b02b32..7db8d13e24 100644
--- a/actionpack/lib/action_controller/metal/live.rb
+++ b/actionpack/lib/action_controller/metal/live.rb
@@ -233,10 +233,6 @@ module ActionController
body.each { |part| buf.write part }
buf
end
-
- def handle_conditional_get!
- super unless committed?
- end
end
def process(name)
diff --git a/actionpack/lib/action_dispatch/http/mime_types.rb b/actionpack/lib/action_dispatch/http/mime_types.rb
index 0af3e5c0df..04828f7c87 100644
--- a/actionpack/lib/action_dispatch/http/mime_types.rb
+++ b/actionpack/lib/action_dispatch/http/mime_types.rb
@@ -32,5 +32,5 @@ Mime::Type.register "application/json", :json, %w( text/x-json application/jsonr
Mime::Type.register "application/pdf", :pdf, [], %w(pdf)
Mime::Type.register "application/zip", :zip, [], %w(zip)
-# Create Mime::ALL but do not add it to the SET.
+# Create Mime::Type[:ALL] but do not add it to the SET.
Mime::Type.add_type :ALL, Mime::Type::All.new("*/*", :all, [])
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index eaa7e88b34..bf20a33d36 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -384,6 +384,9 @@ module ActionDispatch
get_header("action_dispatch.logger".freeze)
end
+ def commit_flash
+ end
+
private
def check_method(name)
HTTP_METHOD_LOOKUP[name] || raise(ActionController::UnknownHttpMethod, "#{name}, accepted HTTP methods are #{HTTP_METHODS[0...-1].join(', ')}, and #{HTTP_METHODS[-1]}")
diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb
index 014b7bee61..c51dcd542a 100644
--- a/actionpack/lib/action_dispatch/middleware/flash.rb
+++ b/actionpack/lib/action_dispatch/middleware/flash.rb
@@ -1,25 +1,6 @@
require 'active_support/core_ext/hash/keys'
module ActionDispatch
- class Request
- # Access the contents of the flash. Use <tt>flash["notice"]</tt> to
- # read a notice you put there or <tt>flash["notice"] = "hello"</tt>
- # to put a new one.
- def flash
- flash = flash_hash
- return flash if flash
- self.flash = Flash::FlashHash.from_session_value(session["flash"])
- end
-
- def flash=(flash)
- set_header Flash::KEY, flash
- end
-
- def flash_hash # :nodoc:
- get_header Flash::KEY
- end
- end
-
# The flash provides a way to pass temporary primitive-types (String, Array, Hash) between actions. Anything you place in the flash will be exposed
# to the very next action and then cleared out. This is a great way of doing notices and alerts, such as a create
# action that sets <tt>flash[:notice] = "Post successfully created"</tt> before redirecting to a display action that can
@@ -57,6 +38,40 @@ module ActionDispatch
class Flash
KEY = 'action_dispatch.request.flash_hash'.freeze
+ module RequestMethods
+ # Access the contents of the flash. Use <tt>flash["notice"]</tt> to
+ # read a notice you put there or <tt>flash["notice"] = "hello"</tt>
+ # to put a new one.
+ def flash
+ flash = flash_hash
+ return flash if flash
+ self.flash = Flash::FlashHash.from_session_value(session["flash"])
+ end
+
+ def flash=(flash)
+ set_header Flash::KEY, flash
+ end
+
+ def flash_hash # :nodoc:
+ get_header Flash::KEY
+ end
+
+ def commit_flash # :nodoc:
+ session = self.session || {}
+ flash_hash = self.flash_hash
+
+ if flash_hash && (flash_hash.present? || session.key?('flash'))
+ session["flash"] = flash_hash.to_session_value
+ self.flash = flash_hash.dup
+ end
+
+ if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?)
+ session.key?('flash') && session['flash'].nil?
+ session.delete('flash')
+ end
+ end
+ end
+
class FlashNow #:nodoc:
attr_accessor :flash
@@ -268,26 +283,10 @@ module ActionDispatch
end
end
- def initialize(app)
- @app = app
- end
-
- def call(env)
- req = ActionDispatch::Request.new env
- @app.call(env)
- ensure
- session = req.session || {}
- flash_hash = req.flash_hash
-
- if flash_hash && (flash_hash.present? || session.key?('flash'))
- session["flash"] = flash_hash.to_session_value
- req.flash = flash_hash.dup
- end
+ def self.new(app) app; end
+ end
- if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?)
- session.key?('flash') && session['flash'].nil?
- session.delete('flash')
- end
- end
+ class Request
+ prepend Flash::RequestMethods
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
index 02b6cfe727..0e636b8257 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
@@ -62,11 +62,7 @@ module ActionDispatch
# would set the session cookie to expire automatically 14 days after creation.
# Other useful options include <tt>:key</tt>, <tt>:secure</tt> and
# <tt>:httponly</tt>.
- class CookieStore < Rack::Session::Abstract::Persisted
- include Compatibility
- include StaleSessionCheck
- include SessionObject
-
+ class CookieStore < AbstractStore
def initialize(app, options={})
super(app, options.merge!(:cookie_only => true))
end
diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb
index d9fb44f06d..68083ed747 100644
--- a/actionpack/test/dispatch/mime_type_test.rb
+++ b/actionpack/test/dispatch/mime_type_test.rb
@@ -18,7 +18,7 @@ class MimeTypeTest < ActiveSupport::TestCase
assert_equal Mime::Type[:MOBILE], Mime::EXTENSION_LOOKUP['mobile']
Mime::Type.unregister(:mobile)
- assert !Mime.const_defined?(:MOBILE), "Mime::MOBILE should not be defined"
+ assert !Mime::Type.registered?(:MOBILE), "Mime::MOBILE should not be defined"
assert !Mime::LOOKUP.has_key?('text/x-mobile'), "Mime::LOOKUP should not have key ['text/x-mobile]"
assert !Mime::EXTENSION_LOOKUP.has_key?('mobile'), "Mime::EXTENSION_LOOKUP should not have key ['mobile]"
ensure
@@ -154,10 +154,10 @@ class MimeTypeTest < ActiveSupport::TestCase
end
test "type convenience methods" do
- # Don't test Mime::ALL, since it Mime::ALL#html? == true
+ # Don't test Mime::Type[:ALL], since it Mime::Type[:ALL].html? == true
types = Mime::SET.symbols.uniq - [:all, :iphone]
- # Remove custom Mime::Type instances set in other tests, like Mime::GIF and Mime::IPHONE
+ # Remove custom Mime::Type instances set in other tests, like Mime::Type[:GIF] and Mime::Type[:IPHONE]
types.delete_if { |type| !Mime::Type.registered?(type.upcase) }
types.each do |type|
@@ -183,10 +183,14 @@ class MimeTypeTest < ActiveSupport::TestCase
end
end
+ test "deprecated const_defined?" do
+ assert_deprecated { Mime.const_defined?(:ALL) }
+ end
+
test "verifiable mime types" do
all_types = Mime::SET.symbols
all_types.uniq!
- # Remove custom Mime::Type instances set in other tests, like Mime::GIF and Mime::IPHONE
+ # Remove custom Mime::Type instances set in other tests, like Mime::Type[:GIF] and Mime::Type[:IPHONE]
all_types.delete_if { |type| !Mime::Type.registered?(type.upcase) }
end
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
index 9ea250643a..82a88952c3 100644
--- a/actionview/CHANGELOG.md
+++ b/actionview/CHANGELOG.md
@@ -1,3 +1,8 @@
+* Add a `hidden_field` on the `collection_radio_buttons` to avoid raising a error
+ when the only input on the form is the `collection_radio_buttons`.
+
+ *Mauro George*
+
* `url_for` does not modify its arguments when generating polymorphic URLs.
*Bernerd Schaefer*
diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb
index 5a9a5d6d18..2a367b85af 100644
--- a/actionview/lib/action_view/helpers/form_helper.rb
+++ b/actionview/lib/action_view/helpers/form_helper.rb
@@ -1038,7 +1038,7 @@ module ActionView
# date_field("user", "born_on")
# # => <input id="user_born_on" name="user[born_on]" type="date" />
#
- # The default value is generated by trying to call "to_date"
+ # The default value is generated by trying to call +strftime+ with "%Y-%m-%d"
# on the object's value, which makes it behave as expected for instances
# of DateTime and ActiveSupport::TimeWithZone. You can still override that
# by passing the "value" option explicitly, e.g.
@@ -1617,7 +1617,14 @@ module ActionView
@auto_index
end
- record_name = index ? "#{object_name}[#{index}][#{record_name}]" : "#{object_name}[#{record_name}]"
+ record_name = if index
+ "#{object_name}[#{index}][#{record_name}]"
+ elsif record_name.to_s.end_with?('[]')
+ record_name = record_name.to_s.sub(/(.*)\[\]$/, "[\\1][#{record_object.id}]")
+ "#{object_name}#{record_name}"
+ else
+ "#{object_name}[#{record_name}]"
+ end
fields_options[:child_index] = index
@template.fields_for(record_name, record_object, fields_options, &block)
diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb
index 728e633bbf..430051379d 100644
--- a/actionview/lib/action_view/helpers/form_options_helper.rb
+++ b/actionview/lib/action_view/helpers/form_options_helper.rb
@@ -548,7 +548,7 @@ module ActionView
end
# Returns a string of option tags for pretty much any time zone in the
- # world. Supply a ActiveSupport::TimeZone name as +selected+ to have it
+ # world. Supply an ActiveSupport::TimeZone name as +selected+ to have it
# marked as the selected option tag. You can also supply an array of
# ActiveSupport::TimeZone objects as +priority_zones+, so that they will
# be listed above the rest of the (long) list. (You can use
@@ -556,7 +556,7 @@ module ActionView
# of the US time zones, or a Regexp to select the zones of your choice)
#
# The +selected+ parameter must be either +nil+, or a string that names
- # a ActiveSupport::TimeZone.
+ # an ActiveSupport::TimeZone.
#
# By default, +model+ is the ActiveSupport::TimeZone constant (which can
# be obtained in Active Record as a value object). The only requirement
@@ -644,6 +644,24 @@ module ActionView
# collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
# b.label(:"data-value" => b.value) { b.radio_button + b.text }
# end
+ #
+ # ==== Gotcha
+ #
+ # The HTML specification says when nothing is select on a collection of radio buttons
+ # web browsers do not send any value to server.
+ # Unfortunately this introduces a gotcha:
+ # if a +User+ model has a +category_id+ field, and in the form none category is selected no +category_id+ parameter is sent. So,
+ # any strong parameters idiom like
+ #
+ # params.require(:user).permit(...)
+ #
+ # will raise an error since no +{user: ...}+ will be present.
+ #
+ # To prevent this the helper generates an auxiliary hidden field before
+ # every collection of radio buttons. The hidden field has the same name as collection radio button and blank value.
+ #
+ # In case if you don't want the helper to generate this hidden field you can specify
+ # <tt>include_hidden: false</tt> option.
def collection_radio_buttons(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block)
Tags::CollectionRadioButtons.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block)
end
diff --git a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
index 1765fa6558..3256d44e18 100644
--- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
@@ -9,29 +9,13 @@ module ActionView
class CheckBoxBuilder < Builder # :nodoc:
def check_box(extra_html_options={})
html_options = extra_html_options.merge(@input_html_options)
+ html_options[:multiple] = true
@template_object.check_box(@object_name, @method_name, html_options, @value, nil)
end
end
def render(&block)
- rendered_collection = render_collection do |item, value, text, default_html_options|
- default_html_options[:multiple] = true
- builder = instantiate_builder(CheckBoxBuilder, item, value, text, default_html_options)
-
- if block_given?
- @template_object.capture(builder, &block)
- else
- render_component(builder)
- end
- end
-
- # Append a hidden field to make sure something will be sent back to the
- # server if all check boxes are unchecked.
- if @options.fetch(:include_hidden, true)
- rendered_collection + hidden_field
- else
- rendered_collection
- end
+ render_collection_for(CheckBoxBuilder, &block)
end
private
@@ -39,11 +23,6 @@ module ActionView
def render_component(builder)
builder.check_box + builder.label
end
-
- def hidden_field
- hidden_name = @html_options[:name] || "#{tag_name(false, @options[:index])}[]"
- @template_object.hidden_field_tag(hidden_name, "", id: nil)
- end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/collection_helpers.rb b/actionview/lib/action_view/helpers/tags/collection_helpers.rb
index 8050638363..fea4c8d4ec 100644
--- a/actionview/lib/action_view/helpers/tags/collection_helpers.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_helpers.rb
@@ -79,6 +79,32 @@ module ActionView
yield item, value, text, default_html_options.merge(additional_html_options)
end.join.html_safe
end
+
+ def render_collection_for(builder_class, &block) #:nodoc:
+ options = @options.stringify_keys
+ rendered_collection = render_collection do |item, value, text, default_html_options|
+ builder = instantiate_builder(builder_class, item, value, text, default_html_options)
+
+ if block_given?
+ @template_object.capture(builder, &block)
+ else
+ render_component(builder)
+ end
+ end
+
+ # Append a hidden field to make sure something will be sent back to the
+ # server if all radio buttons are unchecked.
+ if options.fetch('include_hidden', true)
+ rendered_collection + hidden_field
+ else
+ rendered_collection
+ end
+ end
+
+ def hidden_field #:nodoc:
+ hidden_name = @html_options[:name] || "#{tag_name(false, @options[:index])}[]"
+ @template_object.hidden_field_tag(hidden_name, "", id: nil)
+ end
end
end
end
diff --git a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb
index 20be34c1f2..21aaf122f8 100644
--- a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb
+++ b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb
@@ -14,15 +14,7 @@ module ActionView
end
def render(&block)
- render_collection do |item, value, text, default_html_options|
- builder = instantiate_builder(RadioButtonBuilder, item, value, text, default_html_options)
-
- if block_given?
- @template_object.capture(builder, &block)
- else
- render_component(builder)
- end
- end
+ render_collection_for(RadioButtonBuilder, &block)
end
private
diff --git a/actionview/test/template/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb
index b193d387c3..41932d15ee 100644
--- a/actionview/test/template/form_collections_helper_test.rb
+++ b/actionview/test/template/form_collections_helper_test.rb
@@ -198,6 +198,41 @@ class FormCollectionsHelperTest < ActionView::TestCase
assert_select 'input[type=radio][value=false][checked=checked]'
end
+ test 'collection radio buttons generates only one hidden field for the entire collection, to ensure something will be sent back to the server when posting an empty collection' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_radio_buttons :user, :category_ids, collection, :id, :name
+
+ assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 1
+ end
+
+ test 'collection radio buttons generates a hidden field using the given :name in :html_options' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_radio_buttons :user, :category_ids, collection, :id, :name, {}, { name: "user[other_category_ids][]" }
+
+ assert_select "input[type=hidden][name='user[other_category_ids][]'][value='']", count: 1
+ end
+
+ test 'collection radio buttons generates a hidden field with index if it was provided' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_radio_buttons :user, :category_ids, collection, :id, :name, { index: 322 }
+
+ assert_select "input[type=hidden][name='user[322][category_ids][]'][value='']", count: 1
+ end
+
+ test 'collection radio buttons does not generate a hidden field if include_hidden option is false' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_radio_buttons :user, :category_ids, collection, :id, :name, include_hidden: false
+
+ assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 0
+ end
+
+ test 'collection radio buttons does not generate a hidden field if include_hidden option is false with key as string' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_radio_buttons :user, :category_ids, collection, :id, :name, 'include_hidden' => false
+
+ assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 0
+ end
+
# COLLECTION CHECK BOXES
test 'collection check boxes accepts a collection and generate a series of checkboxes for value method' do
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
@@ -235,6 +270,13 @@ class FormCollectionsHelperTest < ActionView::TestCase
assert_select "input[type=hidden][name='user[category_ids][]'][value='']", :count => 0
end
+ test 'collection check boxes does not generate a hidden field if include_hidden option is false with key as string' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_check_boxes :user, :category_ids, collection, :id, :name, 'include_hidden' => false
+
+ assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 0
+ end
+
test 'collection check boxes accepts a collection and generate a series of checkboxes with labels for label method' do
collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
with_collection_check_boxes :user, :category_ids, collection, :id, :name
diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb
index 77c660d450..41f31f1582 100644
--- a/actionview/test/template/form_helper_test.rb
+++ b/actionview/test/template/form_helper_test.rb
@@ -1596,7 +1596,8 @@ class FormHelperTest < ActionView::TestCase
"<input id='post_active_true' name='post[active]' type='radio' value='true' />" +
"<label for='post_active_true'>true</label>" +
"<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" +
- "<label for='post_active_false'>false</label>"
+ "<label for='post_active_false'>false</label>" +
+ "<input type='hidden' name='post[active][]' value='' />"
end
assert_dom_equal expected, output_buffer
@@ -1619,7 +1620,8 @@ class FormHelperTest < ActionView::TestCase
"true</label>" +
"<label for='post_active_false'>"+
"<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" +
- "false</label>"
+ "false</label>" +
+ "<input type='hidden' name='post[active][]' value='' />"
end
assert_dom_equal expected, output_buffer
@@ -1645,6 +1647,7 @@ class FormHelperTest < ActionView::TestCase
"<label for='post_active_false'>"+
"<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" +
"false</label>"+
+ "<input type='hidden' name='post[active][]' value='' />" +
"<input id='post_id' name='post[id]' type='hidden' value='1' />"
end
@@ -1663,7 +1666,8 @@ class FormHelperTest < ActionView::TestCase
"<input id='foo_post_active_true' name='post[active]' type='radio' value='true' />" +
"<label for='foo_post_active_true'>true</label>" +
"<input checked='checked' id='foo_post_active_false' name='post[active]' type='radio' value='false' />" +
- "<label for='foo_post_active_false'>false</label>"
+ "<label for='foo_post_active_false'>false</label>" +
+ "<input type='hidden' name='post[active][]' value='' />"
end
assert_dom_equal expected, output_buffer
@@ -1681,7 +1685,8 @@ class FormHelperTest < ActionView::TestCase
"<input id='post_1_active_true' name='post[1][active]' type='radio' value='true' />" +
"<label for='post_1_active_true'>true</label>" +
"<input checked='checked' id='post_1_active_false' name='post[1][active]' type='radio' value='false' />" +
- "<label for='post_1_active_false'>false</label>"
+ "<label for='post_1_active_false'>false</label>" +
+ "<input type='hidden' name='post[1][active][]' value='' />"
end
assert_dom_equal expected, output_buffer
@@ -2292,6 +2297,27 @@ class FormHelperTest < ActionView::TestCase
assert_dom_equal expected, output_buffer
end
+ def test_deep_nested_fields_for
+ @comment.save
+ form_for(:posts) do |f|
+ f.fields_for('post[]', @post) do |f2|
+ f2.text_field(:id)
+ @post.comments.each do |comment|
+ concat f2.fields_for('comment[]', comment) { |c|
+ concat c.text_field(:name)
+ }
+ end
+ end
+ end
+
+ expected = whole_form do
+ "<input name='posts[post][0][comment][1][name]' type='text' id='posts_post_0_comment_1_name' value='comment #1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+
def test_nested_fields_for_with_nested_collections
form_for(@post, as: 'post[]') do |f|
concat f.text_field(:title)
diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb
index 9c2c9507b7..00fc28a522 100644
--- a/actionview/test/template/render_test.rb
+++ b/actionview/test/template/render_test.rb
@@ -627,6 +627,16 @@ class CachedCollectionViewRenderTest < CachedViewRenderTest
@view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'key'] })
end
+ test "with caching with custom key and rendering with different key" do
+ customer = Customer.new("david")
+ key = cache_key([customer, 'key'], "test/_customer")
+
+ ActionView::PartialRenderer.collection_cache.write(key, 'Hello')
+
+ assert_equal "Hello: david",
+ @view.render(partial: "test/customer", collection: [customer], cache: ->(item) { [item, 'another_key'] })
+ end
+
test "automatic caching with inferred cache name" do
customer = CachedCustomer.new("david")
key = cache_key(customer, "test/_cached_customer")
diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md
index 965b556fab..b5742191eb 100644
--- a/activejob/CHANGELOG.md
+++ b/activejob/CHANGELOG.md
@@ -1,3 +1,7 @@
+* Add job priorities to Active Job.
+
+ *wvengen*
+
* Implement a simple `AsyncJob` processor and associated `AsyncAdapter` that
queue jobs to a `concurrent-ruby` thread pool.
diff --git a/activejob/lib/active_job/base.rb b/activejob/lib/active_job/base.rb
index 5d7c4cfb91..e5f09f65fb 100644
--- a/activejob/lib/active_job/base.rb
+++ b/activejob/lib/active_job/base.rb
@@ -1,6 +1,7 @@
require 'active_job/core'
require 'active_job/queue_adapter'
require 'active_job/queue_name'
+require 'active_job/queue_priority'
require 'active_job/enqueuing'
require 'active_job/execution'
require 'active_job/callbacks'
@@ -57,6 +58,7 @@ module ActiveJob #:nodoc:
include Core
include QueueAdapter
include QueueName
+ include QueuePriority
include Enqueuing
include Execution
include Callbacks
diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb
index eac7279309..19b900a285 100644
--- a/activejob/lib/active_job/core.rb
+++ b/activejob/lib/active_job/core.rb
@@ -18,6 +18,9 @@ module ActiveJob
# Queue in which the job will reside.
attr_writer :queue_name
+ # Priority that the job will have (lower is more priority).
+ attr_writer :priority
+
# ID optionally provided by adapter
attr_accessor :provider_job_id
@@ -43,6 +46,7 @@ module ActiveJob
# * <tt>:wait</tt> - Enqueues the job with the specified delay
# * <tt>:wait_until</tt> - Enqueues the job at the time specified
# * <tt>:queue</tt> - Enqueues the job on the specified queue
+ # * <tt>:priority</tt> - Enqueues the job with the specified priority
#
# ==== Examples
#
@@ -51,6 +55,7 @@ module ActiveJob
# VideoJob.set(wait_until: Time.now.tomorrow).perform_later(Video.last)
# VideoJob.set(queue: :some_queue, wait: 5.minutes).perform_later(Video.last)
# VideoJob.set(queue: :some_queue, wait_until: Time.now.tomorrow).perform_later(Video.last)
+ # VideoJob.set(queue: :some_queue, wait: 5.minutes, priority: 10).perform_later(Video.last)
def set(options={})
ConfiguredJob.new(self, options)
end
@@ -62,6 +67,7 @@ module ActiveJob
@arguments = arguments
@job_id = SecureRandom.uuid
@queue_name = self.class.queue_name
+ @priority = self.class.priority
end
# Returns a hash with the job data that can safely be passed to the
@@ -71,6 +77,7 @@ module ActiveJob
'job_class' => self.class.name,
'job_id' => job_id,
'queue_name' => queue_name,
+ 'priority' => priority,
'arguments' => serialize_arguments(arguments),
'locale' => I18n.locale
}
@@ -99,6 +106,7 @@ module ActiveJob
def deserialize(job_data)
self.job_id = job_data['job_id']
self.queue_name = job_data['queue_name']
+ self.priority = job_data['priority']
self.serialized_arguments = job_data['arguments']
self.locale = job_data['locale'] || I18n.locale
end
diff --git a/activejob/lib/active_job/enqueuing.rb b/activejob/lib/active_job/enqueuing.rb
index 98d92385dd..22154457fd 100644
--- a/activejob/lib/active_job/enqueuing.rb
+++ b/activejob/lib/active_job/enqueuing.rb
@@ -32,6 +32,7 @@ module ActiveJob
# * <tt>:wait</tt> - Enqueues the job with the specified delay
# * <tt>:wait_until</tt> - Enqueues the job at the time specified
# * <tt>:queue</tt> - Enqueues the job on the specified queue
+ # * <tt>:priority</tt> - Enqueues the job with the specified priority
#
# ==== Examples
#
@@ -54,6 +55,7 @@ module ActiveJob
# * <tt>:wait</tt> - Enqueues the job with the specified delay
# * <tt>:wait_until</tt> - Enqueues the job at the time specified
# * <tt>:queue</tt> - Enqueues the job on the specified queue
+ # * <tt>:priority</tt> - Enqueues the job with the specified priority
#
# ==== Examples
#
@@ -61,10 +63,12 @@ module ActiveJob
# my_job_instance.enqueue wait: 5.minutes
# my_job_instance.enqueue queue: :important
# my_job_instance.enqueue wait_until: Date.tomorrow.midnight
+ # my_job_instance.enqueue priority: 10
def enqueue(options={})
self.scheduled_at = options[:wait].seconds.from_now.to_f if options[:wait]
self.scheduled_at = options[:wait_until].to_f if options[:wait_until]
self.queue_name = self.class.queue_name_from_part(options[:queue]) if options[:queue]
+ self.priority = options[:priority].to_i if options[:priority]
run_callbacks :enqueue do
if self.scheduled_at
self.class.queue_adapter.enqueue_at self, self.scheduled_at
diff --git a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb
index ac83da2b9c..0a785fad3b 100644
--- a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb
@@ -14,13 +14,13 @@ module ActiveJob
# Rails.application.config.active_job.queue_adapter = :delayed_job
class DelayedJobAdapter
def enqueue(job) #:nodoc:
- delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name)
+ delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, priority: job.priority)
job.provider_job_id = delayed_job.id
delayed_job
end
def enqueue_at(job, timestamp) #:nodoc:
- delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, run_at: Time.at(timestamp))
+ delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name, priority: job.priority, run_at: Time.at(timestamp))
job.provider_job_id = delayed_job.id
delayed_job
end
diff --git a/activejob/lib/active_job/queue_adapters/que_adapter.rb b/activejob/lib/active_job/queue_adapters/que_adapter.rb
index 90947aa98d..ab13689747 100644
--- a/activejob/lib/active_job/queue_adapters/que_adapter.rb
+++ b/activejob/lib/active_job/queue_adapters/que_adapter.rb
@@ -16,13 +16,13 @@ module ActiveJob
# Rails.application.config.active_job.queue_adapter = :que
class QueAdapter
def enqueue(job) #:nodoc:
- que_job = JobWrapper.enqueue job.serialize
+ que_job = JobWrapper.enqueue job.serialize, priority: job.priority
job.provider_job_id = que_job.attrs["job_id"]
que_job
end
def enqueue_at(job, timestamp) #:nodoc:
- que_job = JobWrapper.enqueue job.serialize, run_at: Time.at(timestamp)
+ que_job = JobWrapper.enqueue job.serialize, priority: job.priority, run_at: Time.at(timestamp)
job.provider_job_id = que_job.attrs["job_id"]
que_job
end
diff --git a/activejob/lib/active_job/queue_priority.rb b/activejob/lib/active_job/queue_priority.rb
new file mode 100644
index 0000000000..01d84910ff
--- /dev/null
+++ b/activejob/lib/active_job/queue_priority.rb
@@ -0,0 +1,44 @@
+module ActiveJob
+ module QueuePriority
+ extend ActiveSupport::Concern
+
+ # Includes the ability to override the default queue priority.
+ module ClassMethods
+ mattr_accessor(:default_priority)
+
+ # Specifies the priority of the queue to create the job with.
+ #
+ # class PublishToFeedJob < ActiveJob::Base
+ # queue_with_priority 50
+ #
+ # def perform(post)
+ # post.to_feed!
+ # end
+ # end
+ #
+ # Specify either an argument or a block.
+ def queue_with_priority(priority=nil, &block)
+ if block_given?
+ self.priority = block
+ else
+ self.priority = priority
+ end
+ end
+ end
+
+ included do
+ class_attribute :priority, instance_accessor: false
+
+ self.priority = default_priority
+ end
+
+ # Returns the priority that the job will be created with
+ def priority
+ if @priority.is_a?(Proc)
+ @priority = instance_exec(&@priority)
+ end
+ @priority
+ end
+
+ end
+end
diff --git a/activejob/test/cases/queue_priority_test.rb b/activejob/test/cases/queue_priority_test.rb
new file mode 100644
index 0000000000..ca17b51dad
--- /dev/null
+++ b/activejob/test/cases/queue_priority_test.rb
@@ -0,0 +1,47 @@
+require 'helper'
+require 'jobs/hello_job'
+
+class QueuePriorityTest < ActiveSupport::TestCase
+ test 'priority unset by default' do
+ assert_equal nil, HelloJob.priority
+ end
+
+ test 'uses given priority' do
+ original_priority = HelloJob.priority
+
+ begin
+ HelloJob.queue_with_priority 90
+ assert_equal 90, HelloJob.new.priority
+ ensure
+ HelloJob.priority = original_priority
+ end
+ end
+
+ test 'evals block given to priority to determine priority' do
+ original_priority = HelloJob.priority
+
+ begin
+ HelloJob.queue_with_priority { 25 }
+ assert_equal 25, HelloJob.new.priority
+ ensure
+ HelloJob.priority = original_priority
+ end
+ end
+
+ test 'can use arguments to determine priority in priority block' do
+ original_priority = HelloJob.priority
+
+ begin
+ HelloJob.queue_with_priority { self.arguments.first=='1' ? 99 : 11 }
+ assert_equal 99, HelloJob.new('1').priority
+ assert_equal 11, HelloJob.new('3').priority
+ ensure
+ HelloJob.priority = original_priority
+ end
+ end
+
+ test 'uses priority passed to #set' do
+ job = HelloJob.set(priority: 123).perform_later
+ assert_equal 123, job.priority
+ end
+end
diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb
index 125ba54302..e435ed4aa6 100644
--- a/activejob/test/integration/queuing_test.rb
+++ b/activejob/test/integration/queuing_test.rb
@@ -84,4 +84,16 @@ class QueuingTest < ActiveSupport::TestCase
I18n.locale = :en
end
end
+
+ test 'should run job with higher priority first' do
+ skip unless adapter_is?(:delayed_job, :que)
+
+ wait_until = Time.now + 3.seconds
+ TestJob.set(wait_until: wait_until, priority: 20).perform_later "#{@id}.1"
+ TestJob.set(wait_until: wait_until, priority: 10).perform_later "#{@id}.2"
+ wait_for_jobs_to_finish_for(10.seconds)
+ assert job_executed "#{@id}.1"
+ assert job_executed "#{@id}.2"
+ assert job_executed_at("#{@id}.2") < job_executed_at("#{@id}.1")
+ end
end
diff --git a/activejob/test/support/integration/test_case_helpers.rb b/activejob/test/support/integration/test_case_helpers.rb
index 39aee6d407..8319d09520 100644
--- a/activejob/test/support/integration/test_case_helpers.rb
+++ b/activejob/test/support/integration/test_case_helpers.rb
@@ -42,8 +42,12 @@ module TestCaseHelpers
end
end
- def job_executed
- Dummy::Application.root.join("tmp/#{@id}").exist?
+ def job_executed(id=@id)
+ Dummy::Application.root.join("tmp/#{id}").exist?
+ end
+
+ def job_executed_at(id=@id)
+ File.new(Dummy::Application.root.join("tmp/#{id}")).ctime
end
def job_output
diff --git a/activejob/test/support/que/inline.rb b/activejob/test/support/que/inline.rb
index 0232da1370..0950e52d28 100644
--- a/activejob/test/support/que/inline.rb
+++ b/activejob/test/support/que/inline.rb
@@ -6,6 +6,7 @@ Que::Job.class_eval do
if args.last.is_a?(Hash)
options = args.pop
options.delete(:run_at)
+ options.delete(:priority)
args << options unless options.empty?
end
self.run(*args)
diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb
index 213f2d5b6a..de5fb27467 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -164,7 +164,7 @@ module ActiveModel
@route_key << "_index" if @plural == @singular
end
- # Transform the model name into a more humane format, using I18n. By default,
+ # Transform the model name into a more human format, using I18n. By default,
# it will underscore then humanize the class name.
#
# class BlogPost
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index 2b7e4f28c5..021bc32237 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -2,8 +2,7 @@ require 'active_support/core_ext/string/conversions'
module ActiveRecord
module Associations
- # Keeps track of table aliases for ActiveRecord::Associations::ClassMethods::JoinDependency and
- # ActiveRecord::Associations::ThroughAssociationScope
+ # Keeps track of table aliases for ActiveRecord::Associations::JoinDependency
class AliasTracker # :nodoc:
attr_reader :aliases
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index 79b6b80d84..9994b72158 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -127,7 +127,7 @@ module ActiveRecord
# # ]
#
# person.pets.find(1) # => #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>
- # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=4
+ # person.pets.find(4) # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=4
#
# person.pets.find(2) { |pet| pet.name.downcase! }
# # => #<Pet id: 2, name: "fancy-fancy", person_id: 1>
@@ -443,7 +443,7 @@ module ActiveRecord
# person.pets.delete_all
#
# Pet.find(1, 2, 3)
- # # => ActiveRecord::RecordNotFound
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
#
# If it is set to <tt>:delete_all</tt>, all the objects are deleted
# *without* calling their +destroy+ method.
@@ -463,7 +463,7 @@ module ActiveRecord
# person.pets.delete_all
#
# Pet.find(1, 2, 3)
- # # => ActiveRecord::RecordNotFound
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
def delete_all(dependent = nil)
@association.delete_all(dependent)
end
@@ -557,7 +557,7 @@ module ActiveRecord
# # => [#<Pet id: 2, name: "Spook", person_id: 1>]
#
# Pet.find(1, 3)
- # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 3)
+ # # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 3)
#
# If it is set to <tt>:delete_all</tt>, all the +records+ are deleted
# *without* calling their +destroy+ method.
@@ -585,7 +585,7 @@ module ActiveRecord
# # ]
#
# Pet.find(1)
- # # => ActiveRecord::RecordNotFound: Couldn't find Pet with id=1
+ # # => ActiveRecord::RecordNotFound: Couldn't find Pet with 'id'=1
#
# You can pass +Fixnum+ or +String+ values, it finds the records
# responding to the +id+ and executes delete on them.
@@ -649,7 +649,7 @@ module ActiveRecord
# person.pets.size # => 0
# person.pets # => []
#
- # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (1, 2, 3)
+ # Pet.find(1, 2, 3) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (1, 2, 3)
#
# You can pass +Fixnum+ or +String+ values, it finds the records
# responding to the +id+ and then deletes them from the database.
@@ -681,7 +681,7 @@ module ActiveRecord
# person.pets.size # => 0
# person.pets # => []
#
- # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with IDs (4, 5, 6)
+ # Pet.find(4, 5, 6) # => ActiveRecord::RecordNotFound: Couldn't find all Pets with 'id': (4, 5, 6)
def destroy(*records)
@association.destroy(*records)
end
diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb
index 21fe032a9c..74b44810d4 100644
--- a/activerecord/lib/active_record/attribute.rb
+++ b/activerecord/lib/active_record/attribute.rb
@@ -34,14 +34,10 @@ module ActiveRecord
def value
# `defined?` is cheaper than `||=` when we get back falsy values
- @value = original_value unless defined?(@value)
+ @value = type_cast(value_before_type_cast) unless defined?(@value)
@value
end
- def original_value
- type_cast(value_before_type_cast)
- end
-
def value_for_database
type.serialize(value)
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 99b500526d..ca6ba18fe0 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -106,7 +106,7 @@ module ActiveRecord
# end
#
# Person.instance_method_already_implemented?(:save)
- # # => ActiveRecord::DangerousAttributeError: save is defined by ActiveRecord
+ # # => ActiveRecord::DangerousAttributeError: save is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name.
#
# Person.instance_method_already_implemented?(:name)
# # => false
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 84b942d559..43c15841c2 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -47,7 +47,7 @@ module ActiveRecord
def initialize_dup(other) # :nodoc:
super
@mutation_tracker = AttributeMutationTracker.new(@attributes,
- self.class._default_attributes.dup)
+ self.class._default_attributes.deep_dup)
end
def changes_applied
diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb
index 168794fcb4..08eb8ba7ac 100644
--- a/activerecord/lib/active_record/attribute_mutation_tracker.rb
+++ b/activerecord/lib/active_record/attribute_mutation_tracker.rb
@@ -1,5 +1,5 @@
module ActiveRecord
- class AttributeMutationTracker
+ class AttributeMutationTracker # :nodoc:
def initialize(attributes, original_attributes)
@attributes = attributes
@original_attributes = original_attributes
@@ -61,7 +61,7 @@ module ActiveRecord
end
end
- class NullMutationTracker
+ class NullMutationTracker # :nodoc:
def changed_values
{}
end
diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb
index ee278388a4..026b3cf014 100644
--- a/activerecord/lib/active_record/attribute_set.rb
+++ b/activerecord/lib/active_record/attribute_set.rb
@@ -60,8 +60,14 @@ module ActiveRecord
super
end
+ def deep_dup
+ dup.tap do |copy|
+ copy.instance_variable_set(:@attributes, attributes.deep_dup)
+ end
+ end
+
def initialize_dup(_)
- @attributes = attributes.deep_dup
+ @attributes = attributes.dup
super
end
diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb
index 0c730d313f..f974b7a876 100644
--- a/activerecord/lib/active_record/attribute_set/builder.rb
+++ b/activerecord/lib/active_record/attribute_set/builder.rb
@@ -47,8 +47,14 @@ module ActiveRecord
delegate_hash[key] = value
end
+ def deep_dup
+ dup.tap do |copy|
+ copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup))
+ end
+ end
+
def initialize_dup(_)
- @delegate_hash = delegate_hash.transform_values(&:dup)
+ @delegate_hash = Hash[delegate_hash]
super
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
index b944a8631c..a2f58364a4 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -18,7 +18,7 @@ module ActiveRecord
spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type].include?(key) })
end
- # This can be overridden on a Adapter level basis to support other
+ # This can be overridden on an Adapter level basis to support other
# extended datatypes (Example: Adding an array option in the
# PostgreSQLAdapter)
def prepare_column_options(column)
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
index 285b4065b1..7e53f8958a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -507,7 +507,7 @@ module ActiveRecord
raise NotImplementedError, "change_column_default is not implemented"
end
- # Sets or removes a +NOT NULL+ constraint on a column. The +null+ flag
+ # Sets or removes a <tt>NOT NULL</tt> constraint on a column. The +null+ flag
# indicates whether the value can be +NULL+. For example
#
# change_column_null(:users, :nickname, false)
@@ -519,7 +519,7 @@ module ActiveRecord
# allows them to be +NULL+ (drops the constraint).
#
# The method accepts an optional fourth argument to replace existing
- # +NULL+s with some other value. Use that one when enabling the
+ # <tt>NULL</tt>s with some other value. Use that one when enabling the
# constraint if needed, since otherwise those rows would not be valid.
#
# Please note the fourth argument does not set a column's default.
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index ffce2173ec..894d18b79e 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -296,7 +296,7 @@ module ActiveRecord
# # Instantiates a single new object
# User.new(first_name: 'Jamie')
def initialize(attributes = nil)
- @attributes = self.class._default_attributes.dup
+ @attributes = self.class._default_attributes.deep_dup
self.class.define_attribute_methods
init_internals
@@ -366,7 +366,7 @@ module ActiveRecord
##
def initialize_dup(other) # :nodoc:
- @attributes = @attributes.dup
+ @attributes = @attributes.deep_dup
@attributes.reset(self.class.primary_key)
_run_initialize_callbacks
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index 5bdb7213cd..0c50826c63 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -231,7 +231,7 @@ module ActiveRecord
# * <tt>remove_index(table_name, name: index_name)</tt>: Removes the index
# specified by +index_name+.
# * <tt>add_reference(:table_name, :reference_name)</tt>: Adds a new column
- # +reference_name_id+ by default a integer. See
+ # +reference_name_id+ by default an integer. See
# ActiveRecord::ConnectionAdapters::SchemaStatements#add_reference for details.
#
# == Irreversible transformations
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index 4e0fc407bc..65ec356735 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -175,6 +175,20 @@ module ActiveRecord
end
end
+ def inverse_of
+ return unless inverse_name
+
+ @inverse_of ||= klass._reflect_on_association inverse_name
+ end
+
+ def check_validity_of_inverse!
+ unless polymorphic?
+ if has_inverse? && inverse_of.nil?
+ raise InverseOfAssociationNotFoundError.new(self)
+ end
+ end
+ end
+
# This shit is nasty. We need to avoid the following situation:
#
# * An associated record is deleted via record.destroy
@@ -377,14 +391,6 @@ module ActiveRecord
check_validity_of_inverse!
end
- def check_validity_of_inverse!
- unless polymorphic?
- if has_inverse? && inverse_of.nil?
- raise InverseOfAssociationNotFoundError.new(self)
- end
- end
- end
-
def check_preloadable!
return unless scope
@@ -436,12 +442,6 @@ module ActiveRecord
inverse_name
end
- def inverse_of
- return unless inverse_name
-
- @inverse_of ||= klass._reflect_on_association inverse_name
- end
-
def polymorphic_inverse_of(associated_class)
if has_inverse?
if inverse_relationship = associated_class._reflect_on_association(options[:inverse_of])
@@ -924,6 +924,8 @@ module ActiveRecord
klass.primary_key || raise(UnknownPrimaryKey.new(klass))
end
+ def inverse_name; delegate_reflection.send(:inverse_name); end
+
private
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index c562bd9ce4..69f39e5ba9 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -338,7 +338,6 @@ module ActiveRecord
# column_alias_for("sum(id)") # => "sum_id"
# column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
# column_alias_for("count(*)") # => "count_all"
- # column_alias_for("count", "id") # => "count_id"
def column_alias_for(keys)
if keys.respond_to? :name
keys = "#{keys.relation.name}.#{keys.name}"
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index e232516b0c..39e7b42629 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -67,7 +67,7 @@ module ActiveRecord
# Arel::Nodes::And.new([range.start, range.end])
# )
# end
- # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler)
+ # ActiveRecord::PredicateBuilder.new("users").register_handler(MyCustomDateRange, handler)
def register_handler(klass, handler)
@handlers.unshift([klass, handler])
end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index 038280509d..eb53a18f0f 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -264,7 +264,7 @@ module ActiveRecord
# Returns an array with distinct records based on the +group+ attribute:
#
# User.select([:id, :name])
- # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo">
+ # => [#<User id: 1, name: "Oscar">, #<User id: 2, name: "Oscar">, #<User id: 3, name: "Foo">]
#
# User.group(:name)
# => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>]
@@ -713,7 +713,7 @@ module ActiveRecord
#
# users = User.readonly
# users.first.save
- # => ActiveRecord::ReadOnlyRecord: ActiveRecord::ReadOnlyRecord
+ # => ActiveRecord::ReadOnlyRecord: User is marked as readonly
def readonly(value = true)
spawn.readonly!(value)
end
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index 32f4229977..7f6664ea50 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -4,7 +4,7 @@ module ActiveRecord
module ClassMethods
# Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>.
- def sanitize(object) #:nodoc:
+ def sanitize(object) # :nodoc:
connection.quote(object)
end
alias_method :quote_value, :sanitize
@@ -13,8 +13,15 @@ module ActiveRecord
# Accepts an array or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a WHERE clause.
- # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
- # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'"
+ #
+ # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id=4"
+ #
+ # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id='4'"
+ #
+ # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'")
+ # # => "name='foo''bar' and group_id='4'"
def sanitize_sql_for_conditions(condition)
return nil if condition.blank?
@@ -28,7 +35,15 @@ module ActiveRecord
# Accepts an array, hash, or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a SET clause.
- # { name: nil, group_id: 4 } returns "name = NULL , group_id='4'"
+ #
+ # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4])
+ # # => "name=NULL and group_id=4"
+ #
+ # Post.send(:sanitize_sql_for_assignment, { name: nil, group_id: 4 })
+ # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4"
+ #
+ # sanitize_sql_for_assignment("name=NULL and group_id='4'")
+ # # => "name=NULL and group_id='4'"
def sanitize_sql_for_assignment(assignments, default_table_name = self.table_name)
case assignments
when Array; sanitize_sql_array(assignments)
@@ -40,14 +55,18 @@ module ActiveRecord
# Accepts a hash of SQL conditions and replaces those attributes
# that correspond to a +composed_of+ relationship with their expanded
# aggregate attribute values.
+ #
# Given:
- # class Person < ActiveRecord::Base
- # composed_of :address, class_name: "Address",
- # mapping: [%w(address_street street), %w(address_city city)]
- # end
+ #
+ # class Person < ActiveRecord::Base
+ # composed_of :address, class_name: "Address",
+ # mapping: [%w(address_street street), %w(address_city city)]
+ # end
+ #
# Then:
- # { address: Address.new("813 abc st.", "chicago") }
- # # => { address_street: "813 abc st.", address_city: "chicago" }
+ #
+ # { address: Address.new("813 abc st.", "chicago") }
+ # # => { address_street: "813 abc st.", address_city: "chicago" }
def expand_hash_conditions_for_aggregates(attrs)
expanded_attrs = {}
attrs.each do |attr, value|
@@ -68,8 +87,9 @@ module ActiveRecord
end
# Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
- # { status: nil, group_id: 1 }
- # # => "status = NULL , group_id = 1"
+ #
+ # sanitize_sql_hash_for_assignment({ status: nil, group_id: 1 }, "posts")
+ # # => "`posts`.`status` = NULL, `posts`.`group_id` = 1"
def sanitize_sql_hash_for_assignment(attrs, table)
c = connection
attrs.map do |attr, value|
@@ -79,7 +99,19 @@ module ActiveRecord
end
# Sanitizes a +string+ so that it is safe to use within an SQL
- # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%"
+ # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%".
+ #
+ # sanitize_sql_like("100%")
+ # # => "100\\%"
+ #
+ # sanitize_sql_like("snake_cased_string")
+ # # => "snake\\_cased\\_string"
+ #
+ # sanitize_sql_like("100%", "!")
+ # # => "100!%"
+ #
+ # sanitize_sql_like("snake_cased_string", "!")
+ # # => "snake!_cased!_string"
def sanitize_sql_like(string, escape_character = "\\")
pattern = Regexp.union(escape_character, "%", "_")
string.gsub(pattern) { |x| [escape_character, x].join }
@@ -87,7 +119,12 @@ module ActiveRecord
# Accepts an array of conditions. The array has each value
# sanitized and interpolated into the SQL statement.
- # ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
+ #
+ # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id=4"
+ #
+ # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4])
+ # # => "name='foo''bar' and group_id='4'"
def sanitize_sql_array(ary)
statement, *values = ary
if values.first.is_a?(Hash) && statement =~ /:\w+/
@@ -101,7 +138,7 @@ module ActiveRecord
end
end
- def replace_bind_variables(statement, values) #:nodoc:
+ def replace_bind_variables(statement, values) # :nodoc:
raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
bound = values.dup
c = connection
@@ -110,7 +147,7 @@ module ActiveRecord
end
end
- def replace_bind_variable(value, c = connection) #:nodoc:
+ def replace_bind_variable(value, c = connection) # :nodoc:
if ActiveRecord::Relation === value
value.to_sql
else
@@ -118,7 +155,7 @@ module ActiveRecord
end
end
- def replace_named_bind_variables(statement, bind_vars) #:nodoc:
+ def replace_named_bind_variables(statement, bind_vars) # :nodoc:
statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match|
if $1 == ':' # skip postgresql casts
match # return the whole match
@@ -130,7 +167,7 @@ module ActiveRecord
end
end
- def quote_bound_value(value, c = connection) #:nodoc:
+ def quote_bound_value(value, c = connection) # :nodoc:
if value.respond_to?(:map) && !value.acts_like?(:string)
if value.respond_to?(:empty?) && value.empty?
c.quote(nil)
@@ -142,7 +179,7 @@ module ActiveRecord
end
end
- def raise_if_bind_arity_mismatch(statement, expected, provided) #:nodoc:
+ def raise_if_bind_arity_mismatch(statement, expected, provided) # :nodoc:
unless expected == provided
raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}"
end
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index 423b8238b1..ece4dab539 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -13,6 +13,9 @@ require 'models/mixed_case_monkey'
require 'models/admin'
require 'models/admin/account'
require 'models/admin/user'
+require 'models/developer'
+require 'models/company'
+require 'models/project'
class AutomaticInverseFindingTests < ActiveRecord::TestCase
fixtures :ratings, :comments, :cars
@@ -198,6 +201,16 @@ class InverseAssociationTests < ActiveRecord::TestCase
belongs_to_ref = Sponsor.reflect_on_association(:sponsor_club)
assert_nil belongs_to_ref.inverse_of
end
+
+ def test_this_inverse_stuff
+ firm = Firm.create!(name: 'Adequate Holdings')
+ Project.create!(name: 'Project 1', firm: firm)
+ Developer.create!(name: 'Gorbypuff', firm: firm)
+
+ new_project = Project.last
+ assert Project.reflect_on_association(:lead_developer).inverse_of.present?, "Expected inverse of to be present"
+ assert new_project.lead_developer.present?, "Expected lead developer to be present on the project"
+ end
end
class InverseHasOneTests < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb
index 7524243270..5a0e463a48 100644
--- a/activerecord/test/cases/attribute_set_test.rb
+++ b/activerecord/test/cases/attribute_set_test.rb
@@ -29,7 +29,7 @@ module ActiveRecord
assert_equal :bar, attributes[:bar].name
end
- test "duping creates a new hash and dups each attribute" do
+ test "duping creates a new hash, but does not dup the attributes" do
builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
attributes = builder.build_from_database(foo: 1, bar: 'foo')
@@ -43,6 +43,24 @@ module ActiveRecord
assert_equal 1, attributes[:foo].value
assert_equal 2, duped[:foo].value
+ assert_equal 'foobar', attributes[:bar].value
+ assert_equal 'foobar', duped[:bar].value
+ end
+
+ test "deep_duping creates a new hash and dups each attribute" do
+ builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new)
+ attributes = builder.build_from_database(foo: 1, bar: 'foo')
+
+ # Ensure the type cast value is cached
+ attributes[:foo].value
+ attributes[:bar].value
+
+ duped = attributes.deep_dup
+ duped.write_from_database(:foo, 2)
+ duped[:bar].value << 'bar'
+
+ assert_equal 1, attributes[:foo].value
+ assert_equal 2, duped[:foo].value
assert_equal 'foo', attributes[:bar].value
assert_equal 'foobar', duped[:bar].value
end
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index 67936e8e5d..a96b8ef0f2 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -86,6 +86,9 @@ class Firm < Company
has_many :association_with_references, -> { references(:foo) }, :class_name => 'Client'
+ has_one :lead_developer, class_name: "Developer"
+ has_many :projects
+
def log
@log ||= []
end
diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb
index 7565e8f967..7c5941b1af 100644
--- a/activerecord/test/models/developer.rb
+++ b/activerecord/test/models/developer.rb
@@ -54,6 +54,9 @@ class Developer < ActiveRecord::Base
has_many :ratings, through: :comments
has_one :ship, dependent: :nullify
+ belongs_to :firm
+ has_many :contracted_projects, class_name: "Project"
+
scope :jamises, -> { where(:name => 'Jamis') }
validates_inclusion_of :salary, :in => 50000..200000
diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb
index 7f42a4b1f8..5328330653 100644
--- a/activerecord/test/models/project.rb
+++ b/activerecord/test/models/project.rb
@@ -11,6 +11,8 @@ class Project < ActiveRecord::Base
:before_remove => Proc.new {|o, r| o.developers_log << "before_removing#{r.id}"},
:after_remove => Proc.new {|o, r| o.developers_log << "after_removing#{r.id}"}
has_and_belongs_to_many :well_payed_salary_groups, -> { group("developers.salary").having("SUM(salary) > 10000").select("SUM(salary) as salary") }, :class_name => "Developer"
+ belongs_to :firm
+ has_one :lead_developer, through: :firm, inverse_of: :contracted_projects
attr_accessor :developers_log
after_initialize :set_developers_log
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 4b7272f10a..d334a2740e 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -252,6 +252,7 @@ ActiveRecord::Schema.define do
t.string :name
t.string :first_name
t.integer :salary, default: 70000
+ t.integer :firm_id
if subsecond_precision_supported?
t.datetime :created_at, precision: 6
t.datetime :updated_at, precision: 6
@@ -658,6 +659,7 @@ ActiveRecord::Schema.define do
create_table :projects, force: true do |t|
t.string :name
t.string :type
+ t.integer :firm_id
end
create_table :randomly_named_table1, force: true do |t|
diff --git a/activesupport/lib/active_support/core_ext/hash/keys.rb b/activesupport/lib/active_support/core_ext/hash/keys.rb
index c30044b9ff..05679f1dd0 100644
--- a/activesupport/lib/active_support/core_ext/hash/keys.rb
+++ b/activesupport/lib/active_support/core_ext/hash/keys.rb
@@ -1,10 +1,14 @@
class Hash
- # Returns a new hash with all keys converted using the block operation.
+ # Returns a new hash with all keys converted using the +block+ operation.
#
# hash = { name: 'Rob', age: '28' }
#
- # hash.transform_keys{ |key| key.to_s.upcase }
- # # => {"NAME"=>"Rob", "AGE"=>"28"}
+ # hash.transform_keys { |key| key.to_s.upcase } # => {"NAME"=>"Rob", "AGE"=>"28"}
+ #
+ # If you do not provide a +block+, it will return an Enumerator
+ # for chaining with other methods:
+ #
+ # hash.transform_keys.with_index { |k, i| [k, i].join } # => {"name0"=>"Rob", "age1"=>"28"}
def transform_keys
return enum_for(:transform_keys) unless block_given?
result = self.class.new
@@ -14,8 +18,8 @@ class Hash
result
end
- # Destructively converts all keys using the block operations.
- # Same as transform_keys but modifies +self+.
+ # Destructively converts all keys using the +block+ operations.
+ # Same as +transform_keys+ but modifies +self+.
def transform_keys!
return enum_for(:transform_keys!) unless block_given?
keys.each do |key|
diff --git a/activesupport/lib/active_support/core_ext/hash/transform_values.rb b/activesupport/lib/active_support/core_ext/hash/transform_values.rb
index e9bcce761f..9ddb838774 100644
--- a/activesupport/lib/active_support/core_ext/hash/transform_values.rb
+++ b/activesupport/lib/active_support/core_ext/hash/transform_values.rb
@@ -2,10 +2,15 @@ class Hash
# Returns a new hash with the results of running +block+ once for every value.
# The keys are unchanged.
#
- # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 }
- # # => { a: 2, b: 4, c: 6 }
+ # { a: 1, b: 2, c: 3 }.transform_values { |x| x * 2 } # => { a: 2, b: 4, c: 6 }
+ #
+ # If you do not provide a +block+, it will return an Enumerator
+ # for chaining with other methods:
+ #
+ # { a: 1, b: 2 }.transform_values.with_index { |v, i| [v, i].join.to_i } # => { a: 10, b: 21 }
def transform_values
return enum_for(:transform_values) unless block_given?
+ return {} if empty?
result = self.class.new
each do |key, value|
result[key] = yield(value)
@@ -13,7 +18,8 @@ class Hash
result
end
- # Destructive +transform_values+
+ # Destructively converts all values using the +block+ operations.
+ # Same as +transform_values+ but modifies +self+.
def transform_values!
return enum_for(:transform_values!) unless block_given?
each do |key, value|
diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb
index eb26eb5ee5..a82a7dfea0 100644
--- a/activesupport/lib/active_support/time_with_zone.rb
+++ b/activesupport/lib/active_support/time_with_zone.rb
@@ -14,7 +14,7 @@ module ActiveSupport
# Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
# Time.zone.local(2007, 2, 10, 15, 30, 45) # => Sat, 10 Feb 2007 15:30:45 EST -05:00
# Time.zone.parse('2007-02-10 15:30:45') # => Sat, 10 Feb 2007 15:30:45 EST -05:00
- # Time.zone.at(1170361845) # => Sat, 10 Feb 2007 15:30:45 EST -05:00
+ # Time.zone.at(1171139445) # => Sat, 10 Feb 2007 15:30:45 EST -05:00
# Time.zone.now # => Sun, 18 May 2008 13:07:55 EDT -04:00
# Time.utc(2007, 2, 10, 20, 30, 45).in_time_zone # => Sat, 10 Feb 2007 15:30:45 EST -05:00
#
diff --git a/activesupport/test/core_ext/hash/transform_keys_test.rb b/activesupport/test/core_ext/hash/transform_keys_test.rb
index a7e12117f3..5a0b99e22c 100644
--- a/activesupport/test/core_ext/hash/transform_keys_test.rb
+++ b/activesupport/test/core_ext/hash/transform_keys_test.rb
@@ -24,9 +24,21 @@ class TransformKeysTest < ActiveSupport::TestCase
assert_equal Enumerator, enumerator.class
end
+ test "transform_keys! returns an Enumerator if no block is given" do
+ original = { a: 'a', b: 'b' }
+ enumerator = original.transform_keys!
+ assert_equal Enumerator, enumerator.class
+ end
+
test "transform_keys is chainable with Enumerable methods" do
original = { a: 'a', b: 'b' }
mapped = original.transform_keys.with_index { |k, i| [k, i].join.to_sym }
assert_equal({ a0: 'a', b1: 'b' }, mapped)
end
+
+ test "transform_keys! is chainable with Enumerable methods" do
+ original = { a: 'a', b: 'b' }
+ original.transform_keys!.with_index { |k, i| [k, i].join.to_sym }
+ assert_equal({ a0: 'a', b1: 'b' }, original)
+ end
end
diff --git a/activesupport/test/core_ext/hash/transform_values_test.rb b/activesupport/test/core_ext/hash/transform_values_test.rb
index 45ed11fef7..7c33227dc0 100644
--- a/activesupport/test/core_ext/hash/transform_values_test.rb
+++ b/activesupport/test/core_ext/hash/transform_values_test.rb
@@ -53,9 +53,21 @@ class TransformValuesTest < ActiveSupport::TestCase
assert_equal Enumerator, enumerator.class
end
+ test "transform_values! returns an Enumerator if no block is given" do
+ original = { a: 'a', b: 'b' }
+ enumerator = original.transform_values!
+ assert_equal Enumerator, enumerator.class
+ end
+
test "transform_values is chainable with Enumerable methods" do
original = { a: 'a', b: 'b' }
mapped = original.transform_values.with_index { |v, i| [v, i].join }
assert_equal({ a: 'a0', b: 'b1' }, mapped)
end
+
+ test "transform_values! is chainable with Enumerable methods" do
+ original = { a: 'a', b: 'b' }
+ original.transform_values!.with_index { |v, i| [v, i].join }
+ assert_equal({ a: 'a0', b: 'b1' }, original)
+ end
end
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index 0dd99cf8e9..f0d87e4dc8 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -641,7 +641,7 @@ TIP: You don't have to update the database configurations manually. If you look
### Connection Preference
-Since there are two ways to set your connection, via environment variable it is important to understand how the two can interact.
+Since there are two ways to configure your connection (using `config/database.yml` or using an environment variable) it is important to understand how they can interact.
If you have an empty `config/database.yml` file but your `ENV['DATABASE_URL']` is present, then Rails will connect to the database via your environment variable:
diff --git a/railties/lib/rails/test_unit/minitest_plugin.rb b/railties/lib/rails/test_unit/minitest_plugin.rb
index dacab08ec3..1923433c4a 100644
--- a/railties/lib/rails/test_unit/minitest_plugin.rb
+++ b/railties/lib/rails/test_unit/minitest_plugin.rb
@@ -42,7 +42,8 @@ module Minitest
ENV["RAILS_ENV"] = options[:environment] || "test"
unless run_with_autorun
- ::Rails::TestRequirer.require_files @rake_patterns || options[:patterns]
+ patterns = defined?(@rake_patterns) ? @rake_patterns : options[:patterns]
+ ::Rails::TestRequirer.require_files(patterns)
end
unless options[:full_backtrace] || ENV["BACKTRACE"]
diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb
index f677a7c42a..464fc1688c 100644
--- a/railties/test/application/configuration_test.rb
+++ b/railties/test/application/configuration_test.rb
@@ -1304,7 +1304,7 @@ module ApplicationTests
assert_equal 'custom key', Rails.application.config.my_custom_config['key']
end
- test "config_for with syntax error show a more descritive exception" do
+ test "config_for with syntax error show a more descriptive exception" do
app_file 'config/custom.yml', <<-RUBY
development:
key: foo:
diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb
index cbada6be97..0777714d35 100644
--- a/railties/test/application/routing_test.rb
+++ b/railties/test/application/routing_test.rb
@@ -21,6 +21,12 @@ module ApplicationTests
assert_equal 200, last_response.status
end
+ test "rails/info in development" do
+ app("development")
+ get "/rails/info"
+ assert_equal 302, last_response.status
+ end
+
test "rails/info/routes in development" do
app("development")
get "/rails/info/routes"
@@ -63,6 +69,12 @@ module ApplicationTests
assert_equal 404, last_response.status
end
+ test "rails/info in production" do
+ app("production")
+ get "/rails/info"
+ assert_equal 404, last_response.status
+ end
+
test "rails/info/routes in production" do
app("production")
get "/rails/info/routes"
diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb
index 65d8a55421..41d36680e0 100644
--- a/railties/test/isolation/abstract_unit.rb
+++ b/railties/test/isolation/abstract_unit.rb
@@ -159,6 +159,7 @@ module TestHelpers
require "rails"
require "action_controller/railtie"
require "action_view/railtie"
+ require 'action_dispatch/middleware/flash'
app = Class.new(Rails::Application)
app.config.eager_load = false