Merge branch 'master' of github.com:rails/rails

This commit is contained in:
David Heinemeier Hansson 2014-08-29 14:54:08 -07:00
commit 7475b43cdb
181 changed files with 1864 additions and 1129 deletions

@ -1,3 +1,4 @@
language: ruby
script: 'ci/travis.rb'
before_install:
- travis_retry gem install bundler
@ -10,13 +11,16 @@ rvm:
- rbx-2
- jruby
env:
- "GEM=railties"
- "GEM=ap"
- "GEM=am,amo,as,av,aj"
- "GEM=ar:mysql"
- "GEM=ar:mysql2"
- "GEM=ar:sqlite3"
- "GEM=ar:postgresql"
global:
- JRUBY_OPTS='-J-XX:+TieredCompilation -J-XX:TieredStopAtLevel=1 -J-Djruby.compile.mode=OFF -J-Djruby.compile.invokedynamic=false -J-Xmx1024M'
matrix:
- "GEM=railties"
- "GEM=ap"
- "GEM=am,amo,as,av,aj"
- "GEM=ar:mysql"
- "GEM=ar:mysql2"
- "GEM=ar:sqlite3"
- "GEM=ar:postgresql"
matrix:
allow_failures:
- rvm: 1.9.3

@ -1,7 +1,9 @@
* Added #deliver_later in addition to #deliver, which will enqueue a job to render and
deliver the mail instead of delivering it right at that moment. The job is enqueued
using the new Active Job framework in Rails, and will use whatever queue is configured for Rails.
* Added #deliver_later, #deliver_now and deprecate #deliver in favour of
#deliver_now. #deliver_later will enqueue a job to render and deliver
the mail instead of delivering it right at that moment. The job is enqueued
using the new Active Job framework in Rails, and will use whatever queue is
configured for Rails.
*DHH/Abdelkader Boudih/Cristian Bica*
* Make ActionMailer::Previews methods class methods. Previously they were

@ -65,12 +65,12 @@ In order to send mails, you simply call the method and then call +deliver+ on th
Calling the method returns a Mail Message object:
message = Notifier.welcome("david@loudthinking.com") # => Returns a Mail::Message object
message.deliver # => delivers the email
message = Notifier.welcome("david@loudthinking.com") # => Returns a Mail::Message object
message.deliver_now # => delivers the email
Or you can just chain the methods together like:
Notifier.welcome("david@loudthinking.com").deliver # Creates the email and sends it immediately
Notifier.welcome("david@loudthinking.com").deliver_now # Creates the email and sends it immediately
== Setting defaults

@ -21,6 +21,7 @@
s.add_dependency 'actionpack', version
s.add_dependency 'actionview', version
s.add_dependency 'activejob', version
s.add_dependency 'mail', ['~> 2.5', '>= 2.5.4']
s.add_dependency 'rails-dom-testing', '~> 1.0', '>= 1.0.2'

@ -138,9 +138,20 @@ module ActionMailer
# Once a mailer action and template are defined, you can deliver your message or create it and save it
# for delivery later:
#
# Notifier.welcome(david).deliver # sends the email
# mail = Notifier.welcome(david) # => a Mail::Message object
# mail.deliver # sends the email
# Notifier.welcome(User.first).deliver_now # sends the email
# mail = Notifier.welcome(User.first) # => an ActionMailer::MessageDelivery object
# mail.deliver_now # sends the email
#
# The <tt>ActionMailer::MessageDelivery</tt> class is a wrapper around a <tt>Mail::Message</tt> object. If
# you want direct access to the <tt>Mail::Message</tt> object you can call the <tt>message</tt> method on
# the <tt>ActionMailer::MessageDelivery</tt> object.
#
# Notifier.welcome(User.first).message # => a Mail::Message object
#
# Action Mailer is nicely integrated with Active Job so you can send emails in the background (example: outside
# of the request-response cycle, so the user doesn't have to wait on it):
#
# Notifier.welcome(User.first).deliver_later # enqueue the email sending to Active Job
#
# You never instantiate your mailer class. Rather, you just call the method you defined on the class itself.
#
@ -322,8 +333,8 @@ module ActionMailer
# end
#
# Methods must return a <tt>Mail::Message</tt> object which can be generated by calling the mailer
# method without the additional <tt>deliver</tt>. The location of the mailer previews
# directory can be configured using the <tt>preview_path</tt> option which has a default
# method without the additional <tt>deliver_now</tt> / <tt>deliver_later</tt>. The location of the
# mailer previews directory can be configured using the <tt>preview_path</tt> option which has a default
# of <tt>test/mailers/previews</tt>:
#
# config.action_mailer.preview_path = "#{Rails.root}/lib/mailer_previews"

@ -1,10 +1,10 @@
require 'active_job'
module ActionMailer
class DeliveryJob < ActiveJob::Base
class DeliveryJob < ActiveJob::Base #:nodoc:
queue_as :mailers
def perform(mailer, mail_method, delivery_method, *args)
def perform(mailer, mail_method, delivery_method, *args) #:nodoc#
mailer.constantize.public_send(mail_method, *args).send(delivery_method)
end
end

@ -1,45 +1,112 @@
require 'delegate'
module ActionMailer
# The <tt>ActionMailer::MessageDelivery</tt> class is used by
# <tt>ActionMailer::Base</tt> when creating a new mailer.
# <tt>MessageDelivery</tt> is a wrapper (+Delegator+ subclass) around a lazy
# created <tt>Mail::Message</tt>. You can get direct access to the
# <tt>Mail::Message</tt>, deliver the email or schedule the email to be sent
# through Active Job.
#
# Notifier.welcome(User.first) # an ActionMailer::MessageDelivery object
# Notifier.welcome(User.first).deliver_now # sends the email
# Notifier.welcome(User.first).deliver_later # enqueue email delivery as a job through Active Job
# Notifier.welcome(User.first).message # a Mail::Message object
class MessageDelivery < Delegator
def initialize(mailer, mail_method, *args)
def initialize(mailer, mail_method, *args) #:nodoc:
@mailer = mailer
@mail_method = mail_method
@args = args
end
def __getobj__
def __getobj__ #:nodoc:
@obj ||= @mailer.send(:new, @mail_method, *@args).message
end
def __setobj__(obj)
def __setobj__(obj) #:nodoc:
@obj = obj
end
def message #:nodoc:
# Returns the Mail::Message object
def message
__getobj__
end
# Enqueues the email to be delivered through Active Job. When the
# job runs it will send the email using +deliver_now!+. That means
# that the message will be sent bypassing checking +perform_deliveries+
# and +raise_delivery_errors+, so use with caution.
#
# Notifier.welcome(User.first).deliver_later
# Notifier.welcome(User.first).deliver_later(in: 1.hour)
# Notifier.welcome(User.first).deliver_later(at: 10.hours.from_now)
#
# Options:
#
# * <tt>:in</tt> - Enqueue the email to be delivered with a delay
# * <tt>:at</tt> - Enqueue the email to be delivered at (after) a specific date / time
def deliver_later!(options={})
enqueue_delivery :deliver!, options
enqueue_delivery :deliver_now!, options
end
# Enqueues the email to be delivered through Active Job. When the
# job runs it will send the email using +deliver_now+.
#
# Notifier.welcome(User.first).deliver_later
# Notifier.welcome(User.first).deliver_later(in: 1.hour)
# Notifier.welcome(User.first).deliver_later(at: 10.hours.from_now)
#
# Options:
#
# * <tt>:in</tt> - Enqueue the email to be delivered with a delay
# * <tt>:at</tt> - Enqueue the email to be delivered at (after) a specific date / time
def deliver_later(options={})
enqueue_delivery :deliver, options
enqueue_delivery :deliver_now, options
end
# Delivers an email without checking +perform_deliveries+ and +raise_delivery_errors+,
# so use with caution.
#
# Notifier.welcome(User.first).deliver_now!
#
def deliver_now!
message.deliver!
end
# Delivers an email:
#
# Notifier.welcome(User.first).deliver_now
#
def deliver_now
message.deliver
end
def deliver! #:nodoc:
ActiveSupport::Deprecation.warn "#deliver! is deprecated and will be removed in Rails 5. " \
"Use #deliver_now! to deliver immediately or #deliver_later! to deliver through Active Job."
deliver_now!
end
def deliver #:nodoc:
ActiveSupport::Deprecation.warn "#deliver is deprecated and will be removed in Rails 5. " \
"Use #deliver_now to deliver immediately or #deliver_later to deliver through Active Job."
deliver_now
end
private
def enqueue_delivery(delivery_method, options={})
args = @mailer.name, @mail_method.to_s, delivery_method.to_s, *@args
enqueue_method = :enqueue
if options[:at]
enqueue_method = :enqueue_at
args.unshift options[:at]
elsif options[:in]
enqueue_method = :enqueue_in
args.unshift options[:in]
def enqueue_delivery(delivery_method, options={})
args = @mailer.name, @mail_method.to_s, delivery_method.to_s, *@args
enqueue_method = :enqueue
if options[:at]
enqueue_method = :enqueue_at
args.unshift options[:at]
elsif options[:in]
enqueue_method = :enqueue_in
args.unshift options[:in]
end
ActionMailer::DeliveryJob.send enqueue_method, *args
end
ActionMailer::DeliveryJob.send enqueue_method, *args
end
end
end

@ -1,3 +1,4 @@
require 'active_job/railtie'
require "action_mailer"
require "rails"
require "abstract_controller/railties/routes_helpers"

@ -6,9 +6,9 @@ module TestHelper
#
# def test_emails
# assert_emails 0
# ContactMailer.welcome.deliver
# ContactMailer.welcome.deliver_now
# assert_emails 1
# ContactMailer.welcome.deliver
# ContactMailer.welcome.deliver_now
# assert_emails 2
# end
#
@ -17,12 +17,12 @@ module TestHelper
#
# def test_emails_again
# assert_emails 1 do
# ContactMailer.welcome.deliver
# ContactMailer.welcome.deliver_now
# end
#
# assert_emails 2 do
# ContactMailer.welcome.deliver
# ContactMailer.welcome.deliver
# ContactMailer.welcome.deliver_now
# ContactMailer.welcome.deliver_now
# end
# end
def assert_emails(number)
@ -40,7 +40,7 @@ def assert_emails(number)
#
# def test_emails
# assert_no_emails
# ContactMailer.welcome.deliver
# ContactMailer.welcome.deliver_now
# assert_emails 1
# end
#

@ -32,15 +32,6 @@ def self.root
end
end
def set_delivery_method(method)
@old_delivery_method = ActionMailer::Base.delivery_method
ActionMailer::Base.delivery_method = method
end
def restore_delivery_method
ActionMailer::Base.delivery_method = @old_delivery_method
end
# Skips the current run on Rubinius using Minitest::Assertions#skip
def rubinius_skip(message = '')
skip message if RUBY_ENGINE == 'rbx'

@ -26,7 +26,7 @@ def test_assert_select_email
assert_select_email {}
end
AssertSelectMailer.test("<div><p>foo</p><p>bar</p></div>").deliver
AssertSelectMailer.test("<div><p>foo</p><p>bar</p></div>").deliver_now
assert_select_email do
assert_select "div:root" do
assert_select "p:first-child", "foo"
@ -36,7 +36,7 @@ def test_assert_select_email
end
def test_assert_select_email_multipart
AssertMultipartSelectMailer.test(html: "<div><p>foo</p><p>bar</p></div>", text: 'foo bar').deliver
AssertMultipartSelectMailer.test(html: "<div><p>foo</p><p>bar</p></div>", text: 'foo bar').deliver_now
assert_select_email do
assert_select "div:root" do
assert_select "p:first-child", "foo"

@ -9,11 +9,8 @@ def email_with_asset
end
end
class AssetHostTest < ActiveSupport::TestCase
class AssetHostTest < ActionMailer::TestCase
def setup
set_delivery_method :test
ActionMailer::Base.perform_deliveries = true
ActionMailer::Base.deliveries.clear
AssetHostMailer.configure do |c|
c.asset_host = "http://www.example.com"
end

@ -466,12 +466,12 @@ def welcome
test "calling deliver on the action should deliver the mail object" do
BaseMailer.expects(:deliver_mail).once
mail = BaseMailer.welcome.deliver
mail = BaseMailer.welcome.deliver_now
assert_equal 'The first email on new API!', mail.subject
end
test "calling deliver on the action should increment the deliveries collection if using the test mailer" do
BaseMailer.welcome.deliver
BaseMailer.welcome.deliver_now
assert_equal(1, BaseMailer.deliveries.length)
end
@ -484,35 +484,35 @@ def welcome
# Rendering
test "you can specify a different template for implicit render" do
mail = BaseMailer.implicit_different_template('implicit_multipart').deliver
mail = BaseMailer.implicit_different_template('implicit_multipart').deliver_now
assert_equal("HTML Implicit Multipart", mail.html_part.body.decoded)
assert_equal("TEXT Implicit Multipart", mail.text_part.body.decoded)
end
test "should raise if missing template in implicit render" do
assert_raises ActionView::MissingTemplate do
BaseMailer.implicit_different_template('missing_template').deliver
BaseMailer.implicit_different_template('missing_template').deliver_now
end
assert_equal(0, BaseMailer.deliveries.length)
end
test "you can specify a different template for explicit render" do
mail = BaseMailer.explicit_different_template('explicit_multipart_templates').deliver
mail = BaseMailer.explicit_different_template('explicit_multipart_templates').deliver_now
assert_equal("HTML Explicit Multipart Templates", mail.html_part.body.decoded)
assert_equal("TEXT Explicit Multipart Templates", mail.text_part.body.decoded)
end
test "you can specify a different layout" do
mail = BaseMailer.different_layout('different_layout').deliver
mail = BaseMailer.different_layout('different_layout').deliver_now
assert_equal("HTML -- HTML", mail.html_part.body.decoded)
assert_equal("PLAIN -- PLAIN", mail.text_part.body.decoded)
end
test "you can specify the template path for implicit lookup" do
mail = BaseMailer.welcome_from_another_path('another.path/base_mailer').deliver
mail = BaseMailer.welcome_from_another_path('another.path/base_mailer').deliver_now
assert_equal("Welcome from another path", mail.body.encoded)
mail = BaseMailer.welcome_from_another_path(['unknown/invalid', 'another.path/base_mailer']).deliver
mail = BaseMailer.welcome_from_another_path(['unknown/invalid', 'another.path/base_mailer']).deliver_now
assert_equal("Welcome from another path", mail.body.encoded)
end
@ -542,13 +542,13 @@ def welcome
test 'the view is not rendered when mail was never called' do
mail = BaseMailer.without_mail_call
assert_equal('', mail.body.to_s.strip)
mail.deliver
mail.deliver_now
end
test 'the return value of mailer methods is not relevant' do
mail = BaseMailer.with_nil_as_return_value
assert_equal('Welcome', mail.body.to_s.strip)
mail.deliver
mail.deliver_now
end
# Before and After hooks
@ -568,7 +568,7 @@ def self.delivered_email(mail)
ActionMailer::Base.register_observer(MyObserver)
mail = BaseMailer.welcome
MyObserver.expects(:delivered_email).with(mail)
mail.deliver
mail.deliver_now
end
end
@ -577,7 +577,7 @@ def self.delivered_email(mail)
ActionMailer::Base.register_observer("BaseTest::MyObserver")
mail = BaseMailer.welcome
MyObserver.expects(:delivered_email).with(mail)
mail.deliver
mail.deliver_now
end
end
@ -586,7 +586,7 @@ def self.delivered_email(mail)
ActionMailer::Base.register_observer(:"base_test/my_observer")
mail = BaseMailer.welcome
MyObserver.expects(:delivered_email).with(mail)
mail.deliver
mail.deliver_now
end
end
@ -596,7 +596,7 @@ def self.delivered_email(mail)
mail = BaseMailer.welcome
MyObserver.expects(:delivered_email).with(mail)
MySecondObserver.expects(:delivered_email).with(mail)
mail.deliver
mail.deliver_now
end
end
@ -615,7 +615,7 @@ def self.previewing_email(mail); end
ActionMailer::Base.register_interceptor(MyInterceptor)
mail = BaseMailer.welcome
MyInterceptor.expects(:delivering_email).with(mail)
mail.deliver
mail.deliver_now
end
end
@ -624,7 +624,7 @@ def self.previewing_email(mail); end
ActionMailer::Base.register_interceptor("BaseTest::MyInterceptor")
mail = BaseMailer.welcome
MyInterceptor.expects(:delivering_email).with(mail)
mail.deliver
mail.deliver_now
end
end
@ -633,7 +633,7 @@ def self.previewing_email(mail); end
ActionMailer::Base.register_interceptor(:"base_test/my_interceptor")
mail = BaseMailer.welcome
MyInterceptor.expects(:delivering_email).with(mail)
mail.deliver
mail.deliver_now
end
end
@ -643,7 +643,7 @@ def self.previewing_email(mail); end
mail = BaseMailer.welcome
MyInterceptor.expects(:delivering_email).with(mail)
MySecondInterceptor.expects(:delivering_email).with(mail)
mail.deliver
mail.deliver_now
end
end

@ -104,21 +104,21 @@ def welcome(hash={})
test "ActionMailer should be told when Mail gets delivered" do
DeliveryMailer.expects(:deliver_mail).once
DeliveryMailer.welcome.deliver
DeliveryMailer.welcome.deliver_now
end
test "delivery method can be customized per instance" do
Mail::SMTP.any_instance.expects(:deliver!)
email = DeliveryMailer.welcome.deliver
email = DeliveryMailer.welcome.deliver_now
assert_instance_of Mail::SMTP, email.delivery_method
email = DeliveryMailer.welcome(delivery_method: :test).deliver
email = DeliveryMailer.welcome(delivery_method: :test).deliver_now
assert_instance_of Mail::TestMailer, email.delivery_method
end
test "delivery method can be customized in subclasses not changing the parent" do
DeliveryMailer.delivery_method = :test
assert_equal :smtp, ActionMailer::Base.delivery_method
email = DeliveryMailer.welcome.deliver
email = DeliveryMailer.welcome.deliver_now
assert_instance_of Mail::TestMailer, email.delivery_method
end
@ -162,14 +162,14 @@ def welcome(hash={})
test "non registered delivery methods raises errors" do
DeliveryMailer.delivery_method = :unknown
assert_raise RuntimeError do
DeliveryMailer.welcome.deliver
DeliveryMailer.welcome.deliver_now
end
end
test "undefined delivery methods raises errors" do
DeliveryMailer.delivery_method = nil
assert_raise RuntimeError do
DeliveryMailer.welcome.deliver
DeliveryMailer.welcome.deliver_now
end
end
@ -178,7 +178,7 @@ def welcome(hash={})
begin
DeliveryMailer.perform_deliveries = false
Mail::Message.any_instance.expects(:deliver!).never
DeliveryMailer.welcome.deliver
DeliveryMailer.welcome.deliver_now
ensure
DeliveryMailer.perform_deliveries = old_perform_deliveries
end
@ -188,7 +188,7 @@ def welcome(hash={})
old_perform_deliveries = DeliveryMailer.perform_deliveries
begin
DeliveryMailer.perform_deliveries = false
DeliveryMailer.welcome.deliver
DeliveryMailer.welcome.deliver_now
assert_equal [], DeliveryMailer.deliveries
ensure
DeliveryMailer.perform_deliveries = old_perform_deliveries
@ -198,14 +198,14 @@ def welcome(hash={})
test "raise errors on bogus deliveries" do
DeliveryMailer.delivery_method = BogusDelivery
assert_raise RuntimeError do
DeliveryMailer.welcome.deliver
DeliveryMailer.welcome.deliver_now
end
end
test "does not increment the deliveries collection on error" do
DeliveryMailer.delivery_method = BogusDelivery
assert_raise RuntimeError do
DeliveryMailer.welcome.deliver
DeliveryMailer.welcome.deliver_now
end
assert_equal [], DeliveryMailer.deliveries
end
@ -216,7 +216,7 @@ def welcome(hash={})
DeliveryMailer.delivery_method = BogusDelivery
DeliveryMailer.raise_delivery_errors = false
assert_nothing_raised do
DeliveryMailer.welcome.deliver
DeliveryMailer.welcome.deliver_now
end
ensure
DeliveryMailer.raise_delivery_errors = old_raise_delivery_errors
@ -228,7 +228,7 @@ def welcome(hash={})
begin
DeliveryMailer.delivery_method = BogusDelivery
DeliveryMailer.raise_delivery_errors = false
DeliveryMailer.welcome.deliver
DeliveryMailer.welcome.deliver_now
assert_equal [], DeliveryMailer.deliveries
ensure
DeliveryMailer.raise_delivery_errors = old_raise_delivery_errors

@ -17,7 +17,7 @@ def mail_with_i18n_subject(recipient)
class TestController < ActionController::Base
def send_mail
email = I18nTestMailer.mail_with_i18n_subject("test@localhost").deliver
email = I18nTestMailer.mail_with_i18n_subject("test@localhost").deliver_now
render text: "Mail sent - Subject: #{email.subject}"
end
end

@ -22,7 +22,7 @@ def set_logger(logger)
end
def test_deliver_is_notified
BaseMailer.welcome.deliver
BaseMailer.welcome.deliver_now
wait
assert_equal(1, @logger.logged(:info).size)

@ -1,9 +1,8 @@
# encoding: utf-8
gem 'activejob'
require 'active_job'
require 'abstract_unit'
require 'active_job'
require 'minitest/mock'
require_relative 'mailers/delayed_mailer'
require 'mailers/delayed_mailer'
class MessageDeliveryTest < ActiveSupport::TestCase
@ -37,6 +36,17 @@ class MessageDeliveryTest < ActiveSupport::TestCase
assert_respond_to @mail, :deliver!
end
test '.deliver is deprecated' do
assert_deprecated do
@mail.deliver
end
end
test '.deliver! is deprecated' do
assert_deprecated do
@mail.deliver!
end
end
test 'should respond to .deliver_later' do
assert_respond_to @mail, :deliver_later
end
@ -45,30 +55,40 @@ class MessageDeliveryTest < ActiveSupport::TestCase
assert_respond_to @mail, :deliver_later!
end
test 'should enqueue and run correctly in activejob' do
test 'should respond to .deliver_now' do
assert_respond_to @mail, :deliver_now
end
test 'should respond to .deliver_now!' do
assert_respond_to @mail, :deliver_now!
end
def test_should_enqueue_and_run_correctly_in_activejob
@mail.deliver_later!
assert_equal 1 , ActionMailer::Base.deliveries.size
assert_equal 1, ActionMailer::Base.deliveries.size
ensure
ActionMailer::Base.deliveries.clear
end
test 'should enqueue the email with :deliver delivery method' do
ret = ActionMailer::DeliveryJob.stub :enqueue, ->(*args){ args } do
@mail.deliver_later
end
assert_equal ['DelayedMailer', 'test_message', 'deliver', 1, 2, 3], ret
assert_equal ['DelayedMailer', 'test_message', 'deliver_now', 1, 2, 3], ret
end
test 'should enqueue the email with :deliver! delivery method' do
ret = ActionMailer::DeliveryJob.stub :enqueue, ->(*args){ args } do
@mail.deliver_later!
end
assert_equal ['DelayedMailer', 'test_message', 'deliver!', 1, 2, 3], ret
assert_equal ['DelayedMailer', 'test_message', 'deliver_now!', 1, 2, 3], ret
end
test 'should enqueue a delivery with a delay' do
ret = ActionMailer::DeliveryJob.stub :enqueue_in, ->(*args){ args } do
@mail.deliver_later in: 600
end
assert_equal [600, 'DelayedMailer', 'test_message', 'deliver', 1, 2, 3], ret
assert_equal [600, 'DelayedMailer', 'test_message', 'deliver_now', 1, 2, 3], ret
end
test 'should enqueue a delivery at a specific time' do
@ -76,7 +96,7 @@ class MessageDeliveryTest < ActiveSupport::TestCase
ret = ActionMailer::DeliveryJob.stub :enqueue_at, ->(*args){ args } do
@mail.deliver_later at: later_time
end
assert_equal [later_time, 'DelayedMailer', 'test_message', 'deliver', 1, 2, 3], ret
assert_equal [later_time, 'DelayedMailer', 'test_message', 'deliver_now', 1, 2, 3], ret
end
end

@ -48,7 +48,7 @@ def test_read_fixture
def test_assert_emails
assert_nothing_raised do
assert_emails 1 do
TestHelperMailer.test.deliver
TestHelperMailer.test.deliver_now
end
end
end
@ -56,27 +56,27 @@ def test_assert_emails
def test_repeated_assert_emails_calls
assert_nothing_raised do
assert_emails 1 do
TestHelperMailer.test.deliver
TestHelperMailer.test.deliver_now
end
end
assert_nothing_raised do
assert_emails 2 do
TestHelperMailer.test.deliver
TestHelperMailer.test.deliver
TestHelperMailer.test.deliver_now
TestHelperMailer.test.deliver_now
end
end
end
def test_assert_emails_with_no_block
assert_nothing_raised do
TestHelperMailer.test.deliver
TestHelperMailer.test.deliver_now
assert_emails 1
end
assert_nothing_raised do
TestHelperMailer.test.deliver
TestHelperMailer.test.deliver
TestHelperMailer.test.deliver_now
TestHelperMailer.test.deliver_now
assert_emails 3
end
end
@ -92,7 +92,7 @@ def test_assert_no_emails
def test_assert_emails_too_few_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_emails 2 do
TestHelperMailer.test.deliver
TestHelperMailer.test.deliver_now
end
end
@ -102,8 +102,8 @@ def test_assert_emails_too_few_sent
def test_assert_emails_too_many_sent
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_emails 1 do
TestHelperMailer.test.deliver
TestHelperMailer.test.deliver
TestHelperMailer.test.deliver_now
TestHelperMailer.test.deliver_now
end
end
@ -113,7 +113,7 @@ def test_assert_emails_too_many_sent
def test_assert_no_emails_failure
error = assert_raise ActiveSupport::TestCase::Assertion do
assert_no_emails do
TestHelperMailer.test.deliver
TestHelperMailer.test.deliver_now
end
end

@ -68,7 +68,7 @@ def test_signed_up_with_url
created.message_id = '<123@456>'
assert_equal expected.encoded, created.encoded
assert_nothing_raised { UrlTestMailer.signed_up_with_url(@recipient).deliver }
assert_nothing_raised { UrlTestMailer.signed_up_with_url(@recipient).deliver_now }
assert_not_nil ActionMailer::Base.deliveries.first
delivered = ActionMailer::Base.deliveries.first

@ -1,3 +1,40 @@
* Don't rescue `IPAddr::InvalidAddressError`.
`IPAddr::InvalidAddressError` does not exist in Ruby 1.9.3
and fails for JRuby in 1.9 mode.
*Peter Suschlik*
* Fix bug where the router would ignore any constraints added to redirect
routes.
Fixes #16605.
*Agis Anastasopoulos*
* Allow `config.action_dispatch.trusted_proxies` to accept an IPAddr object.
Example:
# config/environments/production.rb
config.action_dispatch.trusted_proxies = IPAddr.new('4.8.15.0/16')
*Sam Aarons*
* Avoid duplicating routes for HEAD requests.
Instead of duplicating the routes, we will first match the HEAD request to
HEAD routes. If no match is found, we will then map the HEAD request to
GET routes.
*Guo Xiang Tan*, *Andrew White*
* Requests that hit `ActionDispatch::Static` can now take advantage
of gzipped assets on disk. By default a gzip asset will be served if
the client supports gzip and a compressed file is on disk.
*Richard Schneeman*
* `ActionController::Parameters` will stop inheriting from `Hash` and
`HashWithIndifferentAccess` in the next major release. If you use any method
that is not available on `ActionController::Parameters` you should consider
@ -33,7 +70,7 @@
*Prem Sichanugrist*
* Deprecated TagAssertions.
* Deprecated `TagAssertions`.
*Kasper Timm Hansen*
@ -65,11 +102,11 @@
If you render a different template, you can now pass the `:template`
option to include its digest instead:
fresh_when @post, template: 'widgets/show'
fresh_when @post, template: 'widgets/show'
Pass `template: false` to skip the lookup. To turn this off entirely, set:
config.action_controller.etag_with_template_digest = false
config.action_controller.etag_with_template_digest = false
*Jeremy Kemper*
@ -123,7 +160,7 @@
*Godfrey Chan*
* Prepend a JS comment to JSONP callbacks. Addresses CVE-2014-4671
("Rosetta Flash")
("Rosetta Flash").
*Greg Campbell*

@ -1,7 +1,7 @@
require 'rake/testtask'
require 'rubygems/package_task'
test_files = Dir.glob('test/{abstract,controller,dispatch,assertions,journey,routing}/**/*_test.rb')
test_files = Dir.glob('test/**/*_test.rb')
desc "Default Task"
task :default => :test
@ -9,10 +9,7 @@ task :default => :test
# Run the unit tests
Rake::TestTask.new do |t|
t.libs << 'test'
# make sure we include the tests in alphabetical order as on some systems
# this will not happen automatically and the tests (as a whole) will error
t.test_files = test_files.sort
t.test_files = test_files
t.warning = true
t.verbose = true

@ -105,6 +105,12 @@ def request_method
@request_method ||= check_method(env["REQUEST_METHOD"])
end
def request_method=(request_method) #:nodoc:
if check_method(request_method)
@request_method = env["REQUEST_METHOD"] = request_method
end
end
# Returns a symbol form of the #request_method
def request_method_symbol
HTTP_METHOD_LOOKUP[request_method]

@ -101,11 +101,13 @@ def find_routes req
r.path.match(req.path_info)
}
if req.env["REQUEST_METHOD"] === "HEAD"
routes.concat get_routes_as_head(routes)
end
routes =
if req.request_method == "HEAD"
match_head_routes(routes, req)
else
match_routes(routes, req)
end
routes.select! { |r| r.matches?(req) }
routes.sort_by!(&:precedence)
routes.map! { |r|
@ -118,19 +120,23 @@ def find_routes req
}
end
def get_routes_as_head(routes)
precedence = (routes.map(&:precedence).max || 0) + 1
routes.select { |r|
r.verb === "GET" && !(r.verb === "HEAD")
}.map! { |r|
Route.new(r.name,
r.app,
r.path,
r.conditions.merge(request_method: "HEAD"),
r.defaults).tap do |route|
route.precedence = r.precedence + precedence
end
}
def match_head_routes(routes, req)
head_routes = match_routes(routes, req)
if head_routes.empty?
begin
req.request_method = "GET"
match_routes(routes, req)
ensure
req.request_method = "HEAD"
end
else
head_routes
end
end
def match_routes(routes, req)
routes.select { |r| r.matches?(req) }
end
end
end

@ -1,3 +1,5 @@
require 'ipaddr'
module ActionDispatch
# This middleware calculates the IP address of the remote client that is
# making the request. It does this by checking various headers that could
@ -28,14 +30,14 @@ class IpSpoofAttackError < StandardError; end
# guaranteed by the IP specification to be private addresses. Those will
# not be the ultimate client IP in production, and so are discarded. See
# http://en.wikipedia.org/wiki/Private_network for details.
TRUSTED_PROXIES = %r{
^127\.0\.0\.1$ | # localhost IPv4
^::1$ | # localhost IPv6
^[fF][cCdD] | # private IPv6 range fc00::/7
^10\. | # private IPv4 range 10.x.x.x
^172\.(1[6-9]|2[0-9]|3[0-1])\.| # private IPv4 range 172.16.0.0 .. 172.31.255.255
^192\.168\. # private IPv4 range 192.168.x.x
}x
TRUSTED_PROXIES = [
"127.0.0.1", # localhost IPv4
"::1", # localhost IPv6
"fc00::/7", # private IPv6 range fc00::/7
"10.0.0.0/8", # private IPv4 range 10.x.x.x
"172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255
"192.168.0.0/16", # private IPv4 range 192.168.x.x
].map { |proxy| IPAddr.new(proxy) }
attr_reader :check_ip, :proxies
@ -47,24 +49,24 @@ class IpSpoofAttackError < StandardError; end
# clients (like WAP devices), or behind proxies that set headers in an
# incorrect or confusing way (like AWS ELB).
#
# The +custom_proxies+ argument can take a regex, which will be used
# instead of +TRUSTED_PROXIES+, or a string, which will be used in addition
# to +TRUSTED_PROXIES+. Any proxy setup will put the value you want in the
# middle (or at the beginning) of the X-Forwarded-For list, with your proxy
# servers after it. If your proxies aren't removed, pass them in via the
# +custom_proxies+ parameter. That way, the middleware will ignore those
# IP addresses, and return the one that you want.
# The +custom_proxies+ argument can take an Array of string, IPAddr, or
# Regexp objects which will be used instead of +TRUSTED_PROXIES+. If a
# single string, IPAddr, or Regexp object is provided, it will be used in
# addition to +TRUSTED_PROXIES+. Any proxy setup will put the value you
# want in the middle (or at the beginning) of the X-Forwarded-For list,
# with your proxy servers after it. If your proxies aren't removed, pass
# them in via the +custom_proxies+ parameter. That way, the middleware will
# ignore those IP addresses, and return the one that you want.
def initialize(app, check_ip_spoofing = true, custom_proxies = nil)
@app = app
@check_ip = check_ip_spoofing
@proxies = case custom_proxies
when Regexp
custom_proxies
when nil
TRUSTED_PROXIES
else
Regexp.union(TRUSTED_PROXIES, custom_proxies)
end
@proxies = if custom_proxies.blank?
TRUSTED_PROXIES
elsif custom_proxies.respond_to?(:any?)
custom_proxies
else
Array(custom_proxies) + TRUSTED_PROXIES
end
end
# Since the IP address may not be needed, we store the object here
@ -80,32 +82,6 @@ def call(env)
# into an actual IP address. If the ActionDispatch::Request#remote_ip method
# is called, this class will calculate the value and then memoize it.
class GetIp
# This constant contains a regular expression that validates every known
# form of IP v4 and v6 address, with or without abbreviations, adapted
# from {this gist}[https://gist.github.com/gazay/1289635].
VALID_IP = %r{
(^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})){3}$) | # ip v4
(^(
(([0-9A-Fa-f]{1,4}:){7}[0-9A-Fa-f]{1,4}) | # ip v6 not abbreviated
(([0-9A-Fa-f]{1,4}:){6}:[0-9A-Fa-f]{1,4}) | # ip v6 with double colon in the end
(([0-9A-Fa-f]{1,4}:){5}:([0-9A-Fa-f]{1,4}:)?[0-9A-Fa-f]{1,4}) | # - ip addresses v6
(([0-9A-Fa-f]{1,4}:){4}:([0-9A-Fa-f]{1,4}:){0,2}[0-9A-Fa-f]{1,4}) | # - with
(([0-9A-Fa-f]{1,4}:){3}:([0-9A-Fa-f]{1,4}:){0,3}[0-9A-Fa-f]{1,4}) | # - double colon
(([0-9A-Fa-f]{1,4}:){2}:([0-9A-Fa-f]{1,4}:){0,4}[0-9A-Fa-f]{1,4}) | # - in the middle
(([0-9A-Fa-f]{1,4}:){6} ((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3} (\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
(([0-9A-Fa-f]{1,4}:){1,5}:((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
(([0-9A-Fa-f]{1,4}:){1}:([0-9A-Fa-f]{1,4}:){0,4}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
(([0-9A-Fa-f]{1,4}:){0,2}:([0-9A-Fa-f]{1,4}:){0,3}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
(([0-9A-Fa-f]{1,4}:){0,3}:([0-9A-Fa-f]{1,4}:){0,2}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
(([0-9A-Fa-f]{1,4}:){0,4}:([0-9A-Fa-f]{1,4}:){1}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d) |(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4}) | # ip v6 with compatible to v4
(::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4}) | # ip v6 with double colon at the beginning
(([0-9A-Fa-f]{1,4}:){1,7}:) # ip v6 without ending
)$)
}x
def initialize(env, middleware)
@env = env
@check_ip = middleware.check_ip
@ -173,12 +149,22 @@ def to_s
def ips_from(header)
# Split the comma-separated list into an array of strings
ips = @env[header] ? @env[header].strip.split(/[,\s]+/) : []
# Only return IPs that are valid according to the regex
ips.select{ |ip| ip =~ VALID_IP }
ips.select do |ip|
begin
# Only return IPs that are valid according to the IPAddr#new method
range = IPAddr.new(ip).to_range
# we want to make sure nobody is sneaking a netmask in
range.begin == range.end
rescue ArgumentError
nil
end
end
end
def filter_proxies(ips)
ips.reject { |ip| ip =~ @proxies }
ips.reject do |ip|
@proxies.any? { |proxy| proxy === ip }
end
end
end

@ -16,43 +16,63 @@ class FileHandler
def initialize(root, cache_control)
@root = root.chomp('/')
@compiled_root = /^#{Regexp.escape(root)}/
headers = cache_control && { 'Cache-Control' => cache_control }
@file_server = ::Rack::File.new(@root, headers)
headers = cache_control && { 'Cache-Control' => cache_control }
@file_server = ::Rack::File.new(@root, headers)
end
def match?(path)
path = unescape_path(path)
path = URI.parser.unescape(path)
return false unless path.valid_encoding?
full_path = path.empty? ? @root : File.join(@root, escape_glob_chars(path))
paths = "#{full_path}#{ext}"
paths = [path, "#{path}#{ext}", "#{path}/index#{ext}"]
matches = Dir[paths]
match = matches.detect { |m| File.file?(m) }
if match
match.sub!(@compiled_root, '')
::Rack::Utils.escape(match)
if match = paths.detect {|p| File.file?(File.join(@root, p)) }
return ::Rack::Utils.escape(match)
end
end
def call(env)
@file_server.call(env)
end
path = env['PATH_INFO']
gzip_path = gzip_file_path(path)
def ext
@ext ||= begin
ext = ::ActionController::Base.default_static_extension
"{,#{ext},/index#{ext}}"
if gzip_path && gzip_encoding_accepted?(env)
env['PATH_INFO'] = gzip_path
status, headers, body = @file_server.call(env)
headers['Content-Encoding'] = 'gzip'
headers['Content-Type'] = content_type(path)
else
status, headers, body = @file_server.call(env)
end
headers['Vary'] = 'Accept-Encoding' if gzip_path
return [status, headers, body]
ensure
env['PATH_INFO'] = path
end
def unescape_path(path)
URI.parser.unescape(path)
end
private
def ext
::ActionController::Base.default_static_extension
end
def escape_glob_chars(path)
path.gsub(/[*?{}\[\]]/, "\\\\\\&")
end
def content_type(path)
::Rack::Mime.mime_type(::File.extname(path), 'text/plain')
end
def gzip_encoding_accepted?(env)
env['HTTP_ACCEPT_ENCODING'] =~ /\bgzip\b/i
end
def gzip_file_path(path)
can_gzip_mime = content_type(path) =~ /\A(?:text\/|application\/javascript)/
gzip_path = "#{path}.gz"
if can_gzip_mime && File.exist?(File.join(@root, ::Rack::Utils.unescape(gzip_path)))
gzip_path
else
false
end
end
end
# This middleware will attempt to return the contents of a file's body from

@ -241,8 +241,6 @@ def add_request_method(via, conditions)
end
def app(blocks)
return to if Redirect === to
if to.respond_to?(:call)
Constraints.new(to, blocks, false)
else

@ -206,7 +206,7 @@ def noop
def setup
super
@request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33")
@request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33", iterations: 2)
@request.env["action_dispatch.signed_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
@request.env["action_dispatch.encrypted_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
@request.env["action_dispatch.encrypted_signed_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"

@ -650,6 +650,18 @@ class RequestMethod < BaseRequestTest
end
end
test "allow request method hacking" do
request = stub_request('REQUEST_METHOD' => 'POST')
assert_equal 'POST', request.request_method
assert_equal 'POST', request.env["REQUEST_METHOD"]
request.request_method = 'GET'
assert_equal 'GET', request.request_method
assert_equal 'GET', request.env["REQUEST_METHOD"]
end
test "invalid http method raises exception" do
assert_raise(ActionController::UnknownHttpMethod) do
stub_request('REQUEST_METHOD' => 'RANDOM_METHOD').request_method

@ -14,6 +14,12 @@ def self.matches?(request)
end
end
class GrumpyRestrictor
def self.matches?(request)
false
end
end
class YoutubeFavoritesRedirector
def self.call(params, request)
"http://www.youtube.com/watch?v=#{params[:youtube_id]}"
@ -89,6 +95,24 @@ def test_namespace_redirect
verify_redirect 'http://www.example.com/private/index'
end
def test_redirect_with_failing_constraint
draw do
get 'hi', to: redirect("/foo"), constraints: ::TestRoutingMapper::GrumpyRestrictor
end
get '/hi'
assert_equal 404, status
end
def test_redirect_with_passing_constraint
draw do
get 'hi', to: redirect("/foo"), constraints: ->(req) { true }
end
get '/hi'
assert_equal 301, status
end
def test_namespace_with_controller_segment
assert_raise(ArgumentError) do
draw do

@ -1,6 +1,7 @@
# encoding: utf-8
require 'abstract_unit'
require 'rbconfig'
require 'zlib'
module StaticTests
def test_serves_dynamic_content
@ -36,6 +37,10 @@ def test_serves_static_index_file_in_directory
assert_html "/foo/index.html", get("/foo")
end
def test_serves_file_with_same_name_before_index_in_directory
assert_html "/bar.html", get("/bar")
end
def test_served_static_file_with_non_english_filename
jruby_skip "Stop skipping if following bug gets fixed: " \
"http://jira.codehaus.org/browse/JRUBY-7192"
@ -106,6 +111,40 @@ def test_serves_static_file_with_at_symbol_in_filename
end
end
def test_serves_gzip_files_when_header_set
file_name = "/gzip/application-a71b3024f80aea3181c09774ca17e712.js"
response = get(file_name, 'HTTP_ACCEPT_ENCODING' => 'gzip')
assert_gzip file_name, response
assert_equal 'application/javascript', response.headers['Content-Type']
assert_equal 'Accept-Encoding', response.headers["Vary"]
assert_equal 'gzip', response.headers["Content-Encoding"]
response = get(file_name, 'HTTP_ACCEPT_ENCODING' => 'Gzip')
assert_gzip file_name, response
response = get(file_name, 'HTTP_ACCEPT_ENCODING' => 'GZIP')
assert_gzip file_name, response
response = get(file_name, 'HTTP_ACCEPT_ENCODING' => '')
assert_not_equal 'gzip', response.headers["Content-Encoding"]
end
def test_does_not_modify_path_info
file_name = "/gzip/application-a71b3024f80aea3181c09774ca17e712.js"
env = {'PATH_INFO' => file_name, 'HTTP_ACCEPT_ENCODING' => 'gzip'}
@app.call(env)
assert_equal file_name, env['PATH_INFO']
end
def test_serves_gzip_with_propper_content_type_fallback
file_name = "/gzip/foo.zoo"
response = get(file_name, 'HTTP_ACCEPT_ENCODING' => 'gzip')
assert_gzip file_name, response
default_response = get(file_name) # no gzip
assert_equal default_response.headers['Content-Type'], response.headers['Content-Type']
end
# Windows doesn't allow \ / : * ? " < > | in filenames
unless RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
def test_serves_static_file_with_colon
@ -125,13 +164,20 @@ def test_serves_static_file_with_asterisk
private
def assert_gzip(file_name, response)
expected = File.read("#{FIXTURE_LOAD_PATH}/#{public_path}" + file_name)
actual = Zlib::GzipReader.new(StringIO.new(response.body)).read
assert_equal expected, actual
end
def assert_html(body, response)
assert_equal body, response.body
assert_equal "text/html", response.headers["Content-Type"]
assert_nil response.headers["Vary"]
end
def get(path)
Rack::MockRequest.new(@app).request("GET", path)
def get(path, headers = {})
Rack::MockRequest.new(@app).request("GET", path, headers)
end
def with_static_file(file)

@ -0,0 +1 @@
/bar.html

@ -0,0 +1 @@
/bar/index.html

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

@ -0,0 +1 @@
/bar.html

@ -0,0 +1 @@
/bar/index.html

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

@ -503,6 +503,25 @@ def test_recognize_literal
assert called
end
def test_recognize_head_route
path = Path::Pattern.from_string "/books(/:action(.:format))"
app = Object.new
conditions = { request_method: 'HEAD' }
@router.routes.add_route(app, path, conditions, {})
env = rails_env(
'PATH_INFO' => '/books/list.rss',
'REQUEST_METHOD' => 'HEAD'
)
called = false
@router.recognize(env) do |r, params|
called = true
end
assert called
end
def test_recognize_head_request_as_get_route
path = Path::Pattern.from_string "/books(/:action(.:format))"
app = Object.new
@ -525,19 +544,24 @@ def test_recognize_head_request_as_get_route
def test_recognize_cares_about_verbs
path = Path::Pattern.from_string "/books(/:action(.:format))"
app = Object.new
conditions = {
:request_method => 'GET'
}
conditions = { request_method: 'GET' }
@router.routes.add_route(app, path, conditions, {})
env = rails_env 'PATH_INFO' => '/books/list.rss',
"REQUEST_METHOD" => "POST"
called = false
@router.recognize(env) do |r, params|
called = true
end
assert_not called
conditions = conditions.dup
conditions[:request_method] = 'POST'
post = @router.routes.add_route(app, path, conditions, {})
env = rails_env 'PATH_INFO' => '/books/list.rss',
"REQUEST_METHOD" => "POST"
called = false
@router.recognize(env) do |r, params|
assert_equal post, r
@ -561,7 +585,7 @@ def add_routes router, paths
end
def rails_env env, klass = ActionDispatch::Request
klass.new env
klass.new(rack_env(env))
end
def rack_env env

@ -1,3 +1,10 @@
* Provide a `builder` object when using the `label` form helper in block form.
The new `builder` object responds to `translation`, allowing I18n fallback support
when you want to customize how a particular label is presented.
*Alex Robbin*
* Add I18n support for input/textarea placeholder text.
Placeholder I18n follows the same convention as `label` I18n.
@ -19,11 +26,11 @@
*Joel Junström*, *Lucas Uyezu*
* Return an absolute instead of relative path from an asset url in the case
of the `asset_host` proc returning nil
of the `asset_host` proc returning nil.
*Jolyon Pawlyn*
* Fix `html_escape_once` to properly handle hex escape sequences (e.g. &#x1a2b;)
* Fix `html_escape_once` to properly handle hex escape sequences (e.g. &#x1a2b;).
*John F. Douthat*
@ -56,7 +63,7 @@
*Zuhao Wan*
* Bring `cache_digest` rake tasks up-to-date with the latest API changes
* Bring `cache_digest` rake tasks up-to-date with the latest API changes.
*Jiri Pospisil*

@ -18,7 +18,7 @@ namespace :test do
Rake::TestTask.new(:template) do |t|
t.libs << 'test'
t.test_files = Dir.glob('test/template/**/*_test.rb').sort
t.test_files = Dir.glob('test/template/**/*_test.rb')
t.warning = true
t.verbose = true
end

@ -35,10 +35,10 @@ module FormTagHelper
# This is helpful when you're fragment-caching the form. Remote forms get the
# authenticity token from the <tt>meta</tt> tag, so embedding is unnecessary unless you
# support browsers without JavaScript.
# * A list of parameters to feed to the URL the form will be posted to.
# * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the
# submit behavior. By default this behavior is an ajax submit.
# * <tt>:enforce_utf8</tt> - If set to false, a hidden input with name utf8 is not output.
# * Any other key creates standard HTML attributes for the tag.
#
# ==== Examples
# form_tag('/posts')

@ -2,6 +2,39 @@ module ActionView
module Helpers
module Tags # :nodoc:
class Label < Base # :nodoc:
class LabelBuilder # :nodoc:
attr_reader :object
def initialize(template_object, object_name, method_name, object, tag_value)
@template_object = template_object
@object_name = object_name
@method_name = method_name
@object = object
@tag_value = tag_value
end
def translation
method_and_value = @tag_value.present? ? "#{@method_name}.#{@tag_value}" : @method_name
@object_name.gsub!(/\[(.*)_attributes\]\[\d+\]/, '.\1')
if object.respond_to?(:to_model)
key = object.model_name.i18n_key
i18n_default = ["#{key}.#{method_and_value}".to_sym, ""]
end
i18n_default ||= ""
content = I18n.t("#{@object_name}.#{method_and_value}", :default => i18n_default, :scope => "helpers.label").presence
content ||= if object && object.class.respond_to?(:human_attribute_name)
object.class.human_attribute_name(method_and_value)
end
content ||= @method_name.humanize
content
end
end
def initialize(object_name, method_name, template_object, content_or_options = nil, options = nil)
options ||= {}
@ -32,33 +65,24 @@ def render(&block)
options.delete("namespace")
options["for"] = name_and_id["id"] unless options.key?("for")
if block_given?
content = @template_object.capture(&block)
builder = LabelBuilder.new(@template_object, @object_name, @method_name, @object, tag_value)
content = if block_given?
@template_object.capture(builder, &block)
elsif @content.present?
@content.to_s
else
method_and_value = tag_value.present? ? "#{@method_name}.#{tag_value}" : @method_name
content = if @content.blank?
@object_name.gsub!(/\[(.*)_attributes\]\[\d+\]/, '.\1')
if object.respond_to?(:to_model)
key = object.model_name.i18n_key
i18n_default = ["#{key}.#{method_and_value}".to_sym, ""]
end
i18n_default ||= ""
I18n.t("#{@object_name}.#{method_and_value}", :default => i18n_default, :scope => "helpers.label").presence
else
@content.to_s
end
content ||= if object && object.class.respond_to?(:human_attribute_name)
object.class.human_attribute_name(method_and_value)
end
content ||= @method_name.humanize
render_component(builder)
end
label_tag(name_and_id["id"], content, options)
end
private
def render_component(builder)
builder.translation
end
end
end
end

@ -6,6 +6,8 @@ def initialize(*)
super
if tag_value = @options[:placeholder]
placeholder = tag_value if tag_value.is_a?(String)
object_name = @object_name.gsub(/\[(.*)_attributes\]\[\d+\]/, '.\1')
method_and_value = tag_value.is_a?(TrueClass) ? @method_name : "#{@method_name}.#{tag_value}"
@ -15,7 +17,7 @@ def initialize(*)
end
i18n_default ||= ""
placeholder = I18n.t("#{object_name}.#{method_and_value}", :default => i18n_default, :scope => "helpers.placeholder").presence
placeholder ||= I18n.t("#{object_name}.#{method_and_value}", :default => i18n_default, :scope => "helpers.placeholder").presence
placeholder ||= if object && object.class.respond_to?(:human_attribute_name)
object.class.human_attribute_name(method_and_value)

@ -242,7 +242,7 @@ def compile!(view) #:nodoc:
end
instrument("!compile_template") do
compile(view, mod)
compile(mod)
end
# Just discard the source if we have a virtual path. This
@ -264,7 +264,7 @@ def compile!(view) #:nodoc:
# encode the source into <tt>Encoding.default_internal</tt>.
# In general, this means that templates will be UTF-8 inside of Rails,
# regardless of the original source encoding.
def compile(view, mod) #:nodoc:
def compile(mod) #:nodoc:
encode!
method_name = self.method_name
code = @handler.call(self)
@ -293,18 +293,8 @@ def #{method_name}(local_assigns, output_buffer)
raise WrongEncodingError.new(@source, Encoding.default_internal)
end
begin
mod.module_eval(source, identifier, 0)
ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
rescue => e # errors from template code
if logger = (view && view.logger)
logger.debug "ERROR: compiling #{method_name} RAISED #{e}"
logger.debug "Function body: #{source}"
logger.debug "Backtrace: #{e.backtrace.join("\n")}"
end
raise ActionView::Template::Error.new(self, e)
end
mod.module_eval(source, identifier, 0)
ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
end
def handle_render_error(view, e) #:nodoc:

@ -319,6 +319,15 @@ def test_label_with_block_and_options
)
end
def test_label_with_block_and_builder
with_locale :label do
assert_dom_equal(
'<label for="post_body"><b>Write entire text here</b></label>',
label(:post, :body) { |b| "<b>#{b.translation}</b>".html_safe }
)
end
end
def test_label_with_block_in_erb
assert_equal(
%{<label for="post_message">\n Message\n <input id="post_message" name="post[message]" type="text" />\n</label>},
@ -344,15 +353,21 @@ def test_text_field_placeholder_with_human_attribute_name
end
end
def test_text_field_placeholder_with_string_value
with_locale :placeholder do
assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="HOW MUCH?" type="text" />', text_field(:post, :cost, placeholder: "HOW MUCH?"))
end
end
def test_text_field_placeholder_with_human_attribute_name_and_value
with_locale :placeholder do
assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="Pounds" type="text" />', text_field(:post, :cost, placeholder: "uk"))
assert_dom_equal('<input id="post_cost" name="post[cost]" placeholder="Pounds" type="text" />', text_field(:post, :cost, placeholder: :uk))
end
end
def test_text_field_placeholder_with_locales_and_value
with_locale :placeholder do
assert_dom_equal('<input id="post_written_on" name="post[written_on]" placeholder="Escrito en" type="text" value="2004-06-15" />', text_field(:post, :written_on, placeholder: "spanish"))
assert_dom_equal('<input id="post_written_on" name="post[written_on]" placeholder="Escrito en" type="text" value="2004-06-15" />', text_field(:post, :written_on, placeholder: :spanish))
end
end
@ -783,11 +798,20 @@ def test_text_area_placeholder_with_human_attribute_name
end
end
def test_text_area_placeholder_with_string_value
with_locale :placeholder do
assert_dom_equal(
%{<textarea id="post_cost" name="post[cost]" placeholder="HOW MUCH?">\n</textarea>},
text_area(:post, :cost, placeholder: "HOW MUCH?")
)
end
end
def test_text_area_placeholder_with_human_attribute_name_and_value
with_locale :placeholder do
assert_dom_equal(
%{<textarea id="post_cost" name="post[cost]" placeholder="Pounds">\n</textarea>},
text_area(:post, :cost, placeholder: "uk")
text_area(:post, :cost, placeholder: :uk)
)
end
end
@ -796,7 +820,7 @@ def test_text_area_placeholder_with_locales_and_value
with_locale :placeholder do
assert_dom_equal(
%{<textarea id="post_written_on" name="post[written_on]" placeholder="Escrito en">\n2004-06-15</textarea>},
text_area(:post, :written_on, placeholder: "spanish")
text_area(:post, :written_on, placeholder: :spanish)
)
end
end

@ -2,7 +2,7 @@
Active Job is a framework for declaring jobs and making them run on a variety
of queueing backends. These jobs can be everything from regularly scheduled
clean-ups, billing charges, or mailings. Anything that can be chopped up into
clean-ups, to billing charges, to mailings. Anything that can be chopped up into
small units of work and run in parallel, really.
It also serves as the backend for ActionMailer's #deliver_later functionality

@ -18,5 +18,6 @@
s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.md', 'lib/**/*']
s.require_path = 'lib'
s.add_dependency 'activesupport', version
s.add_dependency 'globalid', '>= 0.2.3'
end

@ -30,4 +30,5 @@ module ActiveJob
extend ActiveSupport::Autoload
autoload :Base
end
autoload :QueueAdapters
end

@ -1,4 +1,7 @@
module ActiveJob
# Raised when an exception is raised during job arguments deserialization.
#
# Wraps the original exception raised as +original_exception+.
class DeserializationError < StandardError
attr_reader :original_exception
@ -9,6 +12,14 @@ def initialize(e)
end
end
# Raised when an unsupported argument type is being set as job argument. We
# currently support NilClass, Fixnum, Float, String, TrueClass, FalseClass,
# Bignum and object that can be represented as GlobalIDs (ex: Active Record).
# Also raised if you set the key for a Hash something else than a string or
# a symbol.
class SerializationError < ArgumentError
end
module Arguments
extend self
TYPE_WHITELIST = [ NilClass, Fixnum, Float, String, TrueClass, FalseClass, Bignum ]
@ -33,7 +44,7 @@ def serialize_argument(argument)
when Hash
Hash[ argument.map { |key, value| [ serialize_hash_key(key), serialize_argument(value) ] } ]
else
raise "Unsupported argument type: #{argument.class.name}"
raise SerializationError.new("Unsupported argument type: #{argument.class.name}")
end
end
@ -55,7 +66,7 @@ def serialize_hash_key(key)
when String, Symbol
key.to_s
else
raise "Unsupported hash key type: #{key.class.name}"
raise SerializationError.new("Unsupported hash key type: #{key.class.name}")
end
end
end

@ -3,8 +3,8 @@
module ActiveJob
# = Active Job Callbacks
#
# Active Job provides hooks during the lifecycle of a job. Callbacks allows you to trigger
# logic during the lifecycle of a job. Available callbacks:
# Active Job provides hooks during the lifecycle of a job. Callbacks allow you
# to trigger logic during the lifecycle of a job. Available callbacks are:
#
# * <tt>before_enqueue</tt>
# * <tt>around_enqueue</tt>

@ -52,19 +52,19 @@ def logger_tagged_by_active_job?
class LogSubscriber < ActiveSupport::LogSubscriber
def enqueue(event)
info "Enqueued #{event.payload[:job].name} (Job ID: #{event.payload[:job_id]}) to #{queue_name(event)}" + args_info(event)
info { "Enqueued #{event.payload[:job].name} (Job ID: #{event.payload[:job_id]}) to #{queue_name(event)}" + args_info(event) }
end
def enqueue_at(event)
info "Enqueued #{event.payload[:job].name} (Job ID: #{event.payload[:job_id]}) to #{queue_name(event)} at #{enqueued_at(event)}" + args_info(event)
info { "Enqueued #{event.payload[:job].name} (Job ID: #{event.payload[:job_id]}) to #{queue_name(event)} at #{enqueued_at(event)}" + args_info(event) }
end
def perform_start(event)
info "Performing #{event.payload[:job].name} from #{queue_name(event)}" + args_info(event)
info { "Performing #{event.payload[:job].name} from #{queue_name(event)}" + args_info(event) }
end
def perform(event)
info "Performed #{event.payload[:job].name} from #{queue_name(event)} in #{event.duration.round(2).to_s}ms"
info { "Performed #{event.payload[:job].name} from #{queue_name(event)} in #{event.duration.round(2).to_s}ms" }
end
private

@ -17,7 +17,6 @@ def queue_adapter=(name_or_adapter)
private
def load_adapter(name)
require "active_job/queue_adapters/#{name}_adapter"
"ActiveJob::QueueAdapters::#{name.to_s.camelize}Adapter".constantize
end
end

@ -0,0 +1,16 @@
module ActiveJob
module QueueAdapters
extend ActiveSupport::Autoload
autoload :InlineAdapter
autoload :BackburnerAdapter
autoload :DelayedJobAdapter
autoload :QuAdapter
autoload :QueAdapter
autoload :QueueClassicAdapter
autoload :ResqueAdapter
autoload :SidekiqAdapter
autoload :SneakersAdapter
autoload :SuckerPunchAdapter
end
end

@ -9,7 +9,8 @@ def enqueue(job, *args)
end
def enqueue_at(job, timestamp, *args)
raise NotImplementedError
delay = timestamp - Time.current.to_f
Backburner::Worker.enqueue JobWrapper, [ job.name, *args ], queue: job.queue_name, delay: delay
end
end

@ -9,7 +9,7 @@ def enqueue(job, *args)
end
def enqueue_at(job, timestamp, *args)
raise NotImplementedError
JobWrapper.enqueue job.name, *args, queue: job.queue_name, run_at: Time.at(timestamp)
end
end

@ -1,7 +1,7 @@
require_relative 'gem_version'
module ActiveJob
# Returns the version of the currently loaded ActiveJob as a <tt>Gem::Version</tt>
# Returns the version of the currently loaded Active Job as a <tt>Gem::Version</tt>
def self.version
gem_version
end

@ -7,12 +7,14 @@ class JobGenerator < Rails::Generators::NamedBase # :nodoc:
class_option :queue, type: :string, default: 'default', desc: 'The queue name for the generated job'
check_class_collision suffix: 'Job'
hook_for :test_framework
def self.default_generator_root
File.dirname(__FILE__)
end
check_class_collision suffix: 'Job'
def create_job_file
template 'job.rb', File.join('app/jobs', class_path, "#{file_name}_job.rb")
end

@ -1,2 +1,4 @@
require 'support/que/inline'
ActiveJob::Base.queue_adapter = :que
Que.mode = :sync

@ -19,7 +19,7 @@ class ParameterSerializationTest < ActiveSupport::TestCase
assert_equal [ [ 1 ] ], ActiveJob::Arguments.serialize([ [ 1 ] ])
assert_equal [ 1_000_000_000_000_000_000_000 ], ActiveJob::Arguments.serialize([ 1_000_000_000_000_000_000_000 ])
err = assert_raises RuntimeError do
err = assert_raises ActiveJob::SerializationError do
ActiveJob::Arguments.serialize([ 1, self ])
end
assert_equal "Unsupported argument type: #{self.class.name}", err.message
@ -31,14 +31,14 @@ class ParameterSerializationTest < ActiveSupport::TestCase
end
test 'should dive deep into arrays or hashes and raise exception on complex objects' do
err = assert_raises RuntimeError do
err = assert_raises ActiveJob::SerializationError do
ActiveJob::Arguments.serialize([ 1, [self] ])
end
assert_equal "Unsupported argument type: #{self.class.name}", err.message
end
test 'shoud dive deep into hashes and allow raise exception on not string/symbol keys' do
err = assert_raises RuntimeError do
err = assert_raises ActiveJob::SerializationError do
ActiveJob::Arguments.serialize([ [ { 1 => 2 } ] ])
end
assert_equal "Unsupported hash key type: Fixnum", err.message

@ -10,41 +10,15 @@ def sidekiq?
@adapter == 'sidekiq'
end
def rubinius?
RUBY_ENGINE == 'rbx'
end
def ruby_193?
RUBY_VERSION == '1.9.3' && RUBY_ENGINE != 'java'
end
#Sidekiq don't work with MRI 1.9.3
#Travis uses rbx 2.6 which don't support unicode characters in methods.
#Remove the check when Travis change to rbx 2.7+
exit if sidekiq? && (ruby_193? || rubinius?)
# Sidekiq doesn't work with MRI 1.9.3
exit if sidekiq? && ruby_193?
require "adapters/#{@adapter}"
require 'active_support/testing/autorun'
ActiveJob::Base.logger.level = Logger::DEBUG
module JobBuffer
class << self
def clear
@buffer = []
end
def add(value)
@buffer << value
end
def values
@buffer
end
def last_value
@buffer.last
end
end
end

@ -1,3 +1,5 @@
require_relative '../support/job_buffer'
class GidJob < ActiveJob::Base
def perform(person)
JobBuffer.add("Person with ID: #{person.id}")

@ -1,3 +1,5 @@
require_relative '../support/job_buffer'
class HelloJob < ActiveJob::Base
def perform(greeter = "David")
JobBuffer.add("#{greeter} says hello")

@ -1,3 +1,5 @@
require_relative '../support/job_buffer'
class RescueJob < ActiveJob::Base
class OtherError < StandardError; end

@ -0,0 +1,19 @@
module JobBuffer
class << self
def clear
values.clear
end
def add(value)
values << value
end
def values
@values ||= []
end
def last_value
values.last
end
end
end

@ -0,0 +1,9 @@
require 'que'
Que::Job.class_eval do
class << self; alias_method :original_enqueue, :enqueue; end
def self.enqueue(*args)
args.pop if args.last.is_a?(Hash)
self.run(*args)
end
end

@ -6,7 +6,7 @@ task :default => :test
Rake::TestTask.new do |t|
t.libs << "test"
t.test_files = Dir.glob("#{dir}/test/cases/**/*_test.rb").sort
t.test_files = Dir.glob("#{dir}/test/cases/**/*_test.rb")
t.warning = true
t.verbose = true
end

@ -251,11 +251,9 @@ def as_json(options=nil)
# person.errors.to_hash(true) # => {:name=>["name cannot be nil"]}
def to_hash(full_messages = false)
if full_messages
messages = {}
self.messages.each do |attribute, array|
self.messages.each_with_object({}) do |(attribute, array), messages|
messages[attribute] = array.map { |message| full_message(attribute, message) }
end
messages
else
self.messages.dup
end

@ -56,13 +56,13 @@ module ActiveModel
# refer to the specific modules included in <tt>ActiveModel::Model</tt>
# (see below).
module Model
def self.included(base) #:nodoc:
base.class_eval do
extend ActiveModel::Naming
extend ActiveModel::Translation
include ActiveModel::Validations
include ActiveModel::Conversion
end
extend ActiveSupport::Concern
include ActiveModel::Validations
include ActiveModel::Conversion
included do
extend ActiveModel::Naming
extend ActiveModel::Translation
end
# Initializes a new model with the given +params+.

@ -54,7 +54,7 @@ module HelperMethods
#
# Configuration options:
# * <tt>:message</tt> - A custom error message (default is: "doesn't match
# confirmation").
# <tt>%{translated_attribute_name}</tt>").
#
# There is also a list of default options supported by every validator:
# +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.

@ -2,23 +2,6 @@
require 'models/contact'
require 'active_support/core_ext/object/instance_variables'
class Contact
include ActiveModel::Serializers::JSON
include ActiveModel::Validations
def attributes=(hash)
hash.each do |k, v|
instance_variable_set("@#{k}", v)
end
end
remove_method :attributes if method_defined?(:attributes)
def attributes
instance_values
end
end
class JsonSerializationTest < ActiveModel::TestCase
def setup
@contact = Contact.new

@ -3,18 +3,6 @@
require 'active_support/core_ext/object/instance_variables'
require 'ostruct'
class Contact
include ActiveModel::Serializers::Xml
attr_accessor :address, :friends, :contact
remove_method :attributes if method_defined?(:attributes)
def attributes
instance_values.except("address", "friends", "contact")
end
end
module Admin
class Contact < ::Contact
end

@ -18,11 +18,11 @@ def teardown
def test_single_field_validation
r = Reply.new
r.title = "There's no content!"
assert r.invalid?, "A reply without content shouldn't be savable"
assert r.invalid?, "A reply without content should be invalid"
assert r.after_validation_performed, "after_validation callback should be called"
r.content = "Messa content!"
assert r.valid?, "A reply with content should be savable"
assert r.valid?, "A reply with content should be valid"
assert r.after_validation_performed, "after_validation callback should be called"
end

@ -1,8 +1,13 @@
class Contact
extend ActiveModel::Naming
include ActiveModel::Conversion
include ActiveModel::Validations
include ActiveModel::Serializers::JSON
include ActiveModel::Serializers::Xml
attr_accessor :id, :name, :age, :created_at, :awesome, :preferences
attr_accessor :address, :friends, :contact
def social
%w(twitter github)
@ -23,4 +28,14 @@ def pseudonyms
def persisted?
id
end
def attributes=(hash)
hash.each do |k, v|
instance_variable_set("@#{k}", v)
end
end
def attributes
instance_values.except("address", "friends", "contact")
end
end

@ -1,3 +1,21 @@
* `default_sequence_name` from the PostgreSQL adapter returns a `String`.
*Yves Senn*
* Fixed a regression where whitespaces were stripped from DISTINCT queries in
PostgreSQL.
*Agis Anastasopoulos*
Fixes #16623.
* Fix has_many :through relation merging failing when dynamic conditions are
passed as a lambda with an arity of one.
Fixes #16128.
*Agis Anastasopoulos*
* Fixed the `Relation#exists?` to work with polymorphic associations.
Fixes #15821.
@ -9,6 +27,7 @@
will not rescue those errors anymore, and just bubble them up, as the other callbacks.
This adds a opt-in flag to enable that behaviour, of not rescuing the errors.
Example:
# For not swallow errors in after_commit/after_rollback callbacks.
@ -33,7 +52,7 @@
* Fix regression on after_commit that didnt fire when having nested transactions.
Fixes #16425
Fixes #16425.
*arthurnn*

@ -32,8 +32,8 @@ defined in +Rakefile+)
== Config File
If +test/config.yml+ is present, it's parameters are obeyed. Otherwise, the
parameters in +test/config.example.yml+ are obeyed.
If +test/config.yml+ is present, then its parameters are obeyed; otherwise, the
parameters in +test/config.example.yml+ are.
You can override the +connections:+ parameter in either file using the +ARCONN+
(Active Record CONNection) environment variable:

@ -51,7 +51,7 @@ end
t.libs << 'test'
t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject {
|x| x =~ /\/adapters\//
} + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb")).sort
} + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb"))
t.warning = true
t.verbose = true

@ -447,9 +447,11 @@ def association_instance_set(name, association)
#
# Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+.
#
# Should any of the +before_add+ callbacks throw an exception, the object does not get
# added to the collection. Same with the +before_remove+ callbacks; if an exception is
# thrown the object doesn't get removed.
# If any of the +before_add+ callbacks throw an exception, the object will not be
# added to the collection.
#
# Similarly, if any of the +before_remove+ callbacks throw an exception, the object
# will not be removed from the collection.
#
# == Association extensions
#
@ -647,7 +649,7 @@ def association_instance_set(name, association)
# belongs_to :commenter
# end
#
# When using nested association, you will not be able to modify the association because there
# When using a nested association, you will not be able to modify the association because there
# is not enough information to know what modification to make. For example, if you tried to
# add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the
# intermediate <tt>Post</tt> and <tt>Comment</tt> objects.
@ -717,7 +719,7 @@ def association_instance_set(name, association)
# == Eager loading of associations
#
# Eager loading is a way to find objects of a certain class and a number of named associations.
# This is one of the easiest ways of to prevent the dreaded N+1 problem in which fetching 100
# It is one of the easiest ways to prevent the dreaded N+1 problem in which fetching 100
# posts that each need to display their author triggers 101 database queries. Through the
# use of eager loading, the number of queries will be reduced from 101 to 2.
#
@ -749,16 +751,16 @@ def association_instance_set(name, association)
# Post.includes(:author, :comments).each do |post|
#
# This will load all comments with a single query. This reduces the total number of queries
# to 3. More generally the number of queries will be 1 plus the number of associations
# to 3. In general, the number of queries will be 1 plus the number of associations
# named (except if some of the associations are polymorphic +belongs_to+ - see below).
#
# To include a deep hierarchy of associations, use a hash:
#
# Post.includes(:author, {comments: {author: :gravatar}}).each do |post|
# Post.includes(:author, { comments: { author: :gravatar } }).each do |post|
#
# That'll grab not only all the comments but all their authors and gravatar pictures.
# You can mix and match symbols, arrays and hashes in any combination to describe the
# associations you want to load.
# The above code will load all the comments and all of their associated
# authors and gravatars. You can mix and match any combination of symbols,
# arrays, and hashes to retrieve the associations you want to load.
#
# All of this power shouldn't fool you into thinking that you can pull out huge amounts
# of data with no performance penalty just because you've reduced the number of queries.
@ -767,8 +769,8 @@ def association_instance_set(name, association)
# cut down on the number of queries in a situation as the one described above.
#
# Since only one table is loaded at a time, conditions or orders cannot reference tables
# other than the main one. If this is the case Active Record falls back to the previously
# used LEFT OUTER JOIN based strategy. For example
# other than the main one. If this is the case, Active Record falls back to the previously
# used LEFT OUTER JOIN based strategy. For example:
#
# Post.includes([:author, :comments]).where(['comments.approved = ?', true])
#
@ -1133,6 +1135,31 @@ module ClassMethods
# * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</tt>)
# The declaration can also include an +options+ hash to specialize the behavior of the association.
#
# === Scopes
#
# You can pass a second argument +scope+ as a callable (i.e. proc or
# lambda) to retrieve a specific set of records or customize the generated
# query when you access the associated collection.
#
# Scope examples:
# has_many :comments, -> { where(author_id: 1) }
# has_many :employees, -> { joins(:address) }
# has_many :posts, ->(post) { where("max_post_length > ?", post.length) }
#
# === Extensions
#
# The +extension+ argument allows you to pass a block into a has_many
# association. This is useful for adding new finders, creators and other
# factory-type methods to be used as part of the association.
#
# Extension examples:
# has_many :employees do
# def find_or_create_by_name(name)
# first_name, last_name = name.split(" ", 2)
# find_or_create_by(first_name: first_name, last_name: last_name)
# end
# end
#
# === Options
# [:class_name]
# Specify the class name of the association. Use it only if that name can't be inferred
@ -1257,6 +1284,17 @@ def has_many(name, scope = nil, options = {}, &extension)
# * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
# * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save!; b</tt>)
#
# === Scopes
#
# You can pass a second argument +scope+ as a callable (i.e. proc or
# lambda) to retrieve a specific record or customize the generated query
# when you access the associated object.
#
# Scope examples:
# has_one :author, -> { where(comment_id: 1) }
# has_one :employer, -> { joins(:company) }
# has_one :dob, ->(dob) { where("Date.new(2000, 01, 01) > ?", dob) }
#
# === Options
#
# The declaration can also include an +options+ hash to specialize the behavior of the association.
@ -1554,6 +1592,33 @@ def belongs_to(name, scope = nil, options = {})
# * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("developer_id" => id); c.save; c</tt>)
# The declaration may include an +options+ hash to specialize the behavior of the association.
#
# === Scopes
#
# You can pass a second argument +scope+ as a callable (i.e. proc or
# lambda) to retrieve a specific set of records or customize the generated
# query when you access the associated collection.
#
# Scope examples:
# has_and_belongs_to_many :projects, -> { includes :milestones, :manager }
# has_and_belongs_to_many :categories, ->(category) {
# where("default_category = ?", category.name)
# }
#
# === Extensions
#
# The +extension+ argument allows you to pass a block into a
# has_and_belongs_to_many association. This is useful for adding new
# finders, creators and other factory-type methods to be used as part of
# the association.
#
# Extension examples:
# has_and_belongs_to_many :contractors do
# def find_or_create_by_name(name)
# first_name, last_name = name.split(" ", 2)
# find_or_create_by(first_name: first_name, last_name: last_name)
# end
# end
#
# === Options
#
# [:class_name]

@ -783,7 +783,7 @@ def empty?
# person.pets.count # => 0
# person.pets.any? # => true
#
# You can also pass a block to define criteria. The behavior
# You can also pass a +block+ to define criteria. The behavior
# is the same, it returns true if the collection based on the
# criteria is not empty.
#
@ -817,7 +817,7 @@ def any?(&block)
# person.pets.count # => 2
# person.pets.many? # => true
#
# You can also pass a block to define criteria. The
# You can also pass a +block+ to define criteria. The
# behavior is the same, it returns true if the collection
# based on the criteria has more than one record.
#
@ -841,7 +841,7 @@ def many?(&block)
@association.many?(&block)
end
# Returns +true+ if the given object is present in the collection.
# Returns +true+ if the given +record+ is present in the collection.
#
# class Person < ActiveRecord::Base
# has_many :pets
@ -879,7 +879,7 @@ def scope
# Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays
# contain the same number of elements and if each element is equal
# to the corresponding element in the other array, otherwise returns
# to the corresponding element in the +other+ array, otherwise returns
# +false+.
#
# class Person < ActiveRecord::Base

@ -63,12 +63,12 @@ def insert_record(record, validate = true, raise = false)
save_through_record(record)
if has_cached_counter? && !through_reflection_updates_counter_cache?
ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
Automatic updating of counter caches on through associations has been
deprecated, and will be removed in Rails 5.0. Instead, please set the
appropriate counter_cache options on the has_many and belongs_to for
your associations to #{through_reflection.name}.
MESSAGE
ActiveSupport::Deprecation.warn \
"Automatic updating of counter caches on through associations has been " \
"deprecated, and will be removed in Rails 5.0. Instead, please set the " \
"appropriate counter_cache options on the has_many and belongs_to for " \
"your associations to #{through_reflection.name}."
update_counter_in_database(1)
end
record

@ -15,7 +15,11 @@ def target_scope
scope = super
reflection.chain.drop(1).each do |reflection|
relation = reflection.klass.all
relation.merge!(reflection.scope) if reflection.scope
reflection_scope = reflection.scope
if reflection_scope && reflection_scope.arity.zero?
relation.merge!(reflection_scope)
end
scope.merge!(
relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load)

@ -203,11 +203,9 @@ def attribute_names
def column_for_attribute(name)
column = columns_hash[name.to_s]
if column.nil?
ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
`column_for_attribute` will return a null object for non-existent columns
in Rails 5.0. Use `has_attribute?` if you need to check for an
attribute's existence.
MESSAGE
ActiveSupport::Deprecation.warn \
"`column_for_attribute` will return a null object for non-existent columns " \
"in Rails 5.0. Use `has_attribute?` if you need to check for an attribute's existence."
end
column
end

@ -43,14 +43,6 @@ def initialize_dup(other) # :nodoc:
calculate_changes_from_defaults
end
def changed?
super || changed_in_place.any?
end
def changed
super | changed_in_place
end
def changes_applied
super
store_original_raw_attributes
@ -62,7 +54,19 @@ def clear_changes_information
end
def changed_attributes
super.reverse_merge(attributes_changed_in_place).freeze
# This should only be set by methods which will call changed_attributes
# multiple times when it is known that the computed value cannot change.
if defined?(@cached_changed_attributes)
@cached_changed_attributes
else
super.reverse_merge(attributes_changed_in_place).freeze
end
end
def changes
cache_changed_attributes do
super
end
end
private
@ -165,6 +169,13 @@ def store_original_raw_attributes
store_original_raw_attribute(attr)
end
end
def cache_changed_attributes
@cached_changed_attributes = changed_attributes
yield
ensure
remove_instance_variable(:@cached_changed_attributes)
end
end
end
end

@ -46,9 +46,7 @@ module ClassMethods
protected
def cached_attributes_deprecation_warning(method_name)
ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
Calling `#{method_name}` is no longer necessary. All attributes are cached.
MESSAGE
ActiveSupport::Deprecation.warn "Calling `#{method_name}` is no longer necessary. All attributes are cached."
end
if Module.methods_transplantable?

@ -54,10 +54,9 @@ def serialize(attr_name, class_name_or_coder = Object)
end
def serialized_attributes
ActiveSupport::Deprecation.warn(<<-WARNING.strip_heredoc)
`serialized_attributes` is deprecated without replacement, and will
be removed in Rails 5.0.
WARNING
ActiveSupport::Deprecation.warn "`serialized_attributes` is deprecated " \
"without replacement, and will be removed in Rails 5.0."
@serialized_attributes ||= Hash[
columns.select { |t| t.cast_type.is_a?(Type::Serialized) }.map { |c|
[c.name, c.cast_type.coder]

@ -61,12 +61,11 @@ module TimestampDefaultDeprecation # :nodoc:
def emit_warning_if_null_unspecified(options)
return if options.key?(:null)
ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
`timestamp` was called without specifying an option for `null`. In Rails
5.0, this behavior will change to `null: false`. You should manually
specify `null: true` to prevent the behavior of your existing migrations
from changing.
MESSAGE
ActiveSupport::Deprecation.warn \
"`timestamp` was called without specifying an option for `null`. In Rails " \
"5.0, this behavior will change to `null: false`. You should manually " \
"specify `null: true` to prevent the behavior of your existing migrations " \
"from changing."
end
end

@ -25,10 +25,10 @@ def cast_value(value)
if !infinity?(from) && extracted[:exclude_start]
if from.respond_to?(:succ)
from = from.succ
ActiveSupport::Deprecation.warn <<-MESSAGE
Excluding the beginning of a Range is only partialy supported through `#succ`.
This is not reliable and will be removed in the future.
MESSAGE
ActiveSupport::Deprecation.warn \
"Excluding the beginning of a Range is only partialy supported " \
"through `#succ`. This is not reliable and will be removed in " \
"the future."
else
raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')"
end

@ -12,6 +12,8 @@ class Uuid < Type::Value # :nodoc:
[a-fA-F0-9]{4}-?
[a-fA-F0-9]{4}-?\}?\z}x
alias_method :type_cast_for_database, :type_cast_from_database
def type
:uuid
end

@ -281,9 +281,9 @@ def client_min_messages=(level)
def default_sequence_name(table_name, pk = nil) #:nodoc:
result = serial_sequence(table_name, pk || 'id')
return nil unless result
Utils.extract_schema_qualified_name(result)
Utils.extract_schema_qualified_name(result).to_s
rescue ActiveRecord::StatementInvalid
PostgreSQL::Name.new(nil, "#{table_name}_#{pk || 'id'}_seq")
PostgreSQL::Name.new(nil, "#{table_name}_#{pk || 'id'}_seq").to_s
end
def serial_sequence(table, column)
@ -549,7 +549,8 @@ def columns_for_distinct(columns, orders) #:nodoc:
# Convert Arel node to string
s = s.to_sql unless s.is_a?(String)
# Remove any ASC/DESC modifiers
s.gsub(/\s+(?:ASC|DESC)?\s*(?:NULLS\s+(?:FIRST|LAST)\s*)?/i, '')
s.gsub(/\s+(?:ASC|DESC)\b/i, '')
.gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, '')
}.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
[super, *order_columns].join(', ')

@ -151,7 +151,7 @@ def find(*ids)
end
def find_by(*args)
return super if current_scope || args.length > 1 || reflect_on_all_aggregations.any?
return super if current_scope || !(Hash === args.first) || reflect_on_all_aggregations.any?
hash = args.first
@ -177,6 +177,10 @@ def find_by(*args)
end
end
def find_by!(*args)
find_by(*args) or raise RecordNotFound
end
def initialize_generated_modules
super

@ -126,7 +126,7 @@ class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
# that is included in <tt>ActiveRecord::FixtureSet.context_class</tt>.
#
# - define a helper method in `test_helper.rb`
# class FixtureFileHelpers
# module FixtureFileHelpers
# def file_sha(path)
# Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path)))
# end
@ -870,11 +870,11 @@ def fixtures(*fixture_set_names)
def try_to_load_dependency(file_name)
require_dependency file_name
rescue LoadError => e
# Let's hope the developer has included it
# Let's warn in case this is a subdependency, otherwise
# subdependency error messages are totally cryptic
if ActiveRecord::Base.logger
ActiveRecord::Base.logger.warn("Unable to load #{file_name}, underlying cause #{e.message} \n\n #{e.backtrace.join("\n")}")
unless fixture_class_names.key?(file_name.pluralize)
if ActiveRecord::Base.logger
ActiveRecord::Base.logger.warn("Unable to load #{file_name}, make sure you added it to ActiveSupport::TestCase.set_fixture_class")
ActiveRecord::Base.logger.warn("underlying cause #{e.message} \n\n #{e.backtrace.join("\n")}")
end
end
end

@ -161,21 +161,14 @@ def initialize
# in the <tt>db/migrate/</tt> directory where <tt>timestamp</tt> is the
# UTC formatted date and time that the migration was generated.
#
# You may then edit the <tt>up</tt> and <tt>down</tt> methods of
# MyNewMigration.
#
# There is a special syntactic shortcut to generate migrations that add fields to a table.
#
# rails generate migration add_fieldname_to_tablename fieldname:string
#
# This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this:
# class AddFieldnameToTablename < ActiveRecord::Migration
# def up
# add_column :tablenames, :fieldname, :string
# end
#
# def down
# remove_column :tablenames, :fieldname
# def change
# add_column :tablenames, :field, :string
# end
# end
#
@ -188,9 +181,12 @@ def initialize
#
# To roll the database back to a previous migration version, use
# <tt>rake db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which
# you wish to downgrade. If any of the migrations throw an
# <tt>ActiveRecord::IrreversibleMigration</tt> exception, that step will fail and you'll
# have some manual work to do.
# you wish to downgrade. Alternatively, you can also use the STEP option if you
# wish to rollback last few migrations. <tt>rake db:migrate STEP=2</tt> will rollback
# the latest two migrations.
#
# If any of the migrations throw an <tt>ActiveRecord::IrreversibleMigration</tt> exception,
# that step will fail and you'll have some manual work to do.
#
# == Database support
#

@ -38,7 +38,7 @@ def self.add_aggregate_reflection(ar, name, reflection)
ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection)
end
# \Reflection enables interrogating Active Record classes and objects
# \Reflection enables interrogating of Active Record classes and objects
# about their associations and aggregations. This information can,
# for example, be used in a form builder that takes an Active Record object
# and creates input fields for all of the attributes depending on their type
@ -339,12 +339,13 @@ def check_preloadable!
return unless scope
if scope.arity > 0
ActiveSupport::Deprecation.warn <<-WARNING
The association scope '#{name}' is instance dependent (the scope block takes an argument).
Preloading happens before the individual instances are created. This means that there is no instance
being passed to the association scope. This will most likely result in broken or incorrect behavior.
Joining, Preloading and eager loading of these associations is deprecated and will be removed in the future.
WARNING
ActiveSupport::Deprecation.warn \
"The association scope '#{name}' is instance dependent (the scope " \
"block takes an argument). Preloading happens before the individual " \
"instances are created. This means that there is no instance being " \
"passed to the association scope. This will most likely result in " \
"broken or incorrect behavior. Joining, Preloading and eager loading " \
"of these associations is deprecated and will be removed in the future."
end
end
alias :check_eager_loadable! :check_preloadable!
@ -787,15 +788,13 @@ def source_reflection_name # :nodoc:
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
ActiveSupport::Deprecation.warn \
"Ambiguous source reflection for through association. Please " \
"specify a :source directive on your declaration like:\n" \
"\n" \
" class #{active_record.name} < ActiveRecord::Base\n" \
" #{macro} :#{name}, #{example_options}\n" \
" end"
end
@source_reflection_name = names.first

@ -94,10 +94,8 @@ def #{name}_value=(value) # def readonly_value=(value)
def check_cached_relation # :nodoc:
if defined?(@arel) && @arel
@arel = nil
ActiveSupport::Deprecation.warn <<-WARNING
Modifying already cached Relation. The cache will be reset.
Use a cloned Relation to prevent this warning.
WARNING
ActiveSupport::Deprecation.warn "Modifying already cached Relation. The " \
"cache will be reset. Use a cloned Relation to prevent this warning."
end
end

@ -131,10 +131,12 @@ def migrate
verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true
version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil
scope = ENV['SCOPE']
Migration.verbose = verbose
verbose_was = Migration.verbose
Migrator.migrate(Migrator.migrations_paths, version) do |migration|
scope.blank? || scope == migration.scope
end
ensure
Migration.verbose = verbose_was
end
def charset_current(environment = env)
@ -184,10 +186,10 @@ def structure_load(*arguments)
end
def load_schema(format = ActiveRecord::Base.schema_format, file = nil)
ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc)
This method will act on a specific connection in the future.
To act on the current connection, use `load_schema_current` instead.
MESSAGE
ActiveSupport::Deprecation.warn \
"This method will act on a specific connection in the future. " \
"To act on the current connection, use `load_schema_current` instead."
load_schema_current(format, file)
end

@ -3,15 +3,15 @@ module ActiveRecord
module Transactions
extend ActiveSupport::Concern
ACTIONS = [:create, :destroy, :update]
CALLBACK_WARN_MESSAGE = <<-EOF
Currently, Active Record will rescue any errors raised within
after_rollback/after_commit callbacks and print them to the logs. In the next
version, these errors will no longer be rescued. Instead, they will simply
bubble just like other Active Record callbacks.
You can opt into the new behavior and remove this warning by setting
config.active_record.raise_in_transactional_callbacks to true.
EOF
CALLBACK_WARN_MESSAGE = "Currently, Active Record suppresses errors raised " \
"within `after_rollback`/`after_commit` callbacks and only print them to " \
"the logs. In the next version, these errors will no longer be suppressed. " \
"Instead, the errors will propagate normally just like in other Active " \
"Record callbacks.\n" \
"\n" \
"You can opt into the new behavior and remove this warning by setting:\n" \
"\n" \
" config.active_record.raise_in_transactional_callbacks = true"
included do
define_callbacks :commit, :rollback,

Some files were not shown because too many files have changed in this diff Show More