diff options
187 files changed, 2087 insertions, 1233 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 421e7088cb..699b6fd2d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ Ruby on Rails is a volunteer effort. We encourage you to pitch in. [Join the tea *We only accept bug reports and pull requests on GitHub*. -* If you have a question about how to use Ruby on Rails, please [ask the rubyonrails-talk mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-talk). +* If you have a question about how to use Ruby on Rails, please [ask it on the rubyonrails-talk mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-talk). * If you have a change or new feature in mind, please [suggest it on the rubyonrails-core mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core) and start writing code. diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index 86ecb3ee88..acdc4c7dc9 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,3 +1,29 @@ +* `assert_emails` in block form use the given number as expected value. + This makes the error message much easier to understand. + + *Yuji Yaginuma* + +* Add support for inline images in mailer previews by using an interceptor + class to convert cid: urls in image src attributes to data urls. + + *Andrew White* + +* Mailer preview now uses `url_for` to fix links to emails for apps running on + a subdirectory. + + *Remo Mueller* + +* Mailer previews no longer crash when the `mail` method wasn't called + (`NullMail`). + + Fixes #19849. + + *Yves Senn* + +* Make sure labels and values line up in mailer previews. + + *Yves Senn* + * Add `assert_enqueued_emails` and `assert_no_enqueued_emails`. Example: diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc index a4e660d621..e5c2ed8c77 100644 --- a/actionmailer/README.rdoc +++ b/actionmailer/README.rdoc @@ -146,7 +146,7 @@ The Base class has the full list of configuration options. Here's an example: The latest version of Action Mailer can be installed with RubyGems: - % [sudo] gem install actionmailer + % gem install actionmailer Source code can be downloaded as part of the Rails project on GitHub diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index 17d8dcc208..291a8c1e34 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -40,6 +40,7 @@ module ActionMailer autoload :Base autoload :DeliveryMethods + autoload :InlinePreviewInterceptor autoload :MailHelper autoload :Preview autoload :Previews, 'action_mailer/preview' diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb index 754f698e48..218b7a735a 100644 --- a/actionmailer/lib/action_mailer/base.rb +++ b/actionmailer/lib/action_mailer/base.rb @@ -596,6 +596,7 @@ module ActionMailer class NullMail #:nodoc: def body; '' end + def header; {} end def respond_to?(string, include_all=false) true diff --git a/actionmailer/lib/action_mailer/inline_preview_interceptor.rb b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb new file mode 100644 index 0000000000..dea5e52f0c --- /dev/null +++ b/actionmailer/lib/action_mailer/inline_preview_interceptor.rb @@ -0,0 +1,61 @@ +require 'base64' + +module ActionMailer + # Implements a mailer preview interceptor that converts image tag src attributes + # that use inline cid: style urls to data: style urls so that they are visible + # when previewing a HTML email in a web browser. + # + # This interceptor is enabled by default, to remove it delete it from the + # <tt>ActionMailer::Base.preview_interceptors</tt> array: + # + # ActionMailer::Base.preview_interceptors.delete(ActionMailer::InlinePreviewInterceptor) + # + class InlinePreviewInterceptor + PATTERN = /src=(?:"cid:[^"]+"|'cid:[^']+')/i + + include Base64 + + def self.previewing_email(message) #:nodoc: + new(message).transform! + end + + def initialize(message) #:nodoc: + @message = message + end + + def transform! #:nodoc: + return message if html_part.blank? + + html_source.gsub!(PATTERN) do |match| + if part = find_part(match[9..-2]) + %[src="#{data_url(part)}"] + else + match + end + end + + message + end + + private + def message + @message + end + + def html_part + @html_part ||= message.html_part + end + + def html_source + html_part.body.raw_source + end + + def data_url(part) + "data:#{part.mime_type};base64,#{strict_encode64(part.body.raw_source)}" + end + + def find_part(cid) + message.all_parts.find{ |p| p.attachment? && p.cid == cid } + end + end +end diff --git a/actionmailer/lib/action_mailer/preview.rb b/actionmailer/lib/action_mailer/preview.rb index 44cf6665ba..25ad7ee721 100644 --- a/actionmailer/lib/action_mailer/preview.rb +++ b/actionmailer/lib/action_mailer/preview.rb @@ -21,7 +21,7 @@ module ActionMailer # :nodoc: mattr_accessor :preview_interceptors, instance_writer: false - self.preview_interceptors = [] + self.preview_interceptors = [ActionMailer::InlinePreviewInterceptor] end module ClassMethods diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb index 524e6e3af1..4d03a616d2 100644 --- a/actionmailer/lib/action_mailer/test_helper.rb +++ b/actionmailer/lib/action_mailer/test_helper.rb @@ -34,7 +34,7 @@ module ActionMailer original_count = ActionMailer::Base.deliveries.size yield new_count = ActionMailer::Base.deliveries.size - assert_equal original_count + number, new_count, "#{number} emails expected, but #{new_count - original_count} were sent" + assert_equal number, new_count - original_count, "#{number} emails expected, but #{new_count - original_count} were sent" else assert_equal number, ActionMailer::Base.deliveries.size end diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb index 2e4e019bf7..78507ce7dc 100644 --- a/actionmailer/test/delivery_methods_test.rb +++ b/actionmailer/test/delivery_methods_test.rb @@ -1,5 +1,4 @@ require 'abstract_unit' -require 'mail' class MyCustomDelivery end diff --git a/actionmailer/test/mail_helper_test.rb b/actionmailer/test/mail_helper_test.rb index 24ccaab8df..ff6b25b0c7 100644 --- a/actionmailer/test/mail_helper_test.rb +++ b/actionmailer/test/mail_helper_test.rb @@ -59,6 +59,12 @@ The second end end + def use_cache + mail_with_defaults do |format| + format.html { render(inline: "<% cache(:foo) do %>Greetings from a cache helper block<% end %>") } + end + end + protected def mail_with_defaults(&block) @@ -107,5 +113,11 @@ class MailerHelperTest < ActionMailer::TestCase TEXT assert_equal expected.gsub("\n", "\r\n"), mail.body.encoded end -end + def test_use_cache + assert_nothing_raised do + mail = HelperMailer.use_cache + assert_equal "Greetings from a cache helper block", mail.body.encoded + end + end +end diff --git a/actionmailer/test/message_delivery_test.rb b/actionmailer/test/message_delivery_test.rb index e4dd269494..aaa3f218b5 100644 --- a/actionmailer/test/message_delivery_test.rb +++ b/actionmailer/test/message_delivery_test.rb @@ -2,7 +2,6 @@ require 'abstract_unit' require 'active_job' require 'minitest/mock' require 'mailers/delayed_mailer' -require 'active_support/core_ext/numeric/time' class MessageDeliveryTest < ActiveSupport::TestCase include ActiveJob::TestHelper diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb index 089933e245..0a4bc75d3e 100644 --- a/actionmailer/test/test_helper_test.rb +++ b/actionmailer/test/test_helper_test.rb @@ -112,6 +112,17 @@ class TestHelperMailerTest < ActionMailer::TestCase assert_match(/1 .* but 2/, error.message) end + def test_assert_emails_message + TestHelperMailer.test.deliver_now + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_emails 2 do + TestHelperMailer.test.deliver_now + end + end + assert_match "Expected: 2", error.message + assert_match "Actual: 1", error.message + end + def test_assert_no_emails_failure error = assert_raise ActiveSupport::TestCase::Assertion do assert_no_emails do diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index e0076225ba..fbd6c5c747 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -281,7 +281,7 @@ * Ensure `append_info_to_payload` is called even if an exception is raised. - Fixes an issue where when an exception is raised in the request the additonal + Fixes an issue where when an exception is raised in the request the additional payload data is not available. See: diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc index 02a24a7412..44c980b070 100644 --- a/actionpack/README.rdoc +++ b/actionpack/README.rdoc @@ -28,7 +28,7 @@ can be used outside of Rails. The latest version of Action Pack can be installed with RubyGems: - % [sudo] gem install actionpack + % gem install actionpack Source code can be downloaded as part of the Rails project on GitHub diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 909ed19a49..32c3c9652f 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -74,16 +74,16 @@ module ActionController end end - def authenticate_or_request_with_http_basic(realm = "Application", &login_procedure) - authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm) + def authenticate_or_request_with_http_basic(realm = "Application", message = nil, &login_procedure) + authenticate_with_http_basic(&login_procedure) || request_http_basic_authentication(realm, message) end def authenticate_with_http_basic(&login_procedure) HttpAuthentication::Basic.authenticate(request, &login_procedure) end - def request_http_basic_authentication(realm = "Application") - HttpAuthentication::Basic.authentication_request(self, realm) + def request_http_basic_authentication(realm = "Application", message = nil) + HttpAuthentication::Basic.authentication_request(self, realm, message) end end @@ -117,10 +117,11 @@ module ActionController "Basic #{::Base64.strict_encode64("#{user_name}:#{password}")}" end - def authentication_request(controller, realm) + def authentication_request(controller, realm, message) + message ||= "HTTP Basic: Access denied.\n" controller.headers["WWW-Authenticate"] = %(Basic realm="#{realm.tr('"'.freeze, "".freeze)}") controller.status = 401 - controller.response_body = "HTTP Basic: Access denied.\n" + controller.response_body = message end end @@ -170,8 +171,8 @@ module ActionController extend self module ControllerMethods - def authenticate_or_request_with_http_digest(realm = "Application", &password_procedure) - authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm) + def authenticate_or_request_with_http_digest(realm = "Application", message = nil, &password_procedure) + authenticate_with_http_digest(realm, &password_procedure) || request_http_digest_authentication(realm, message) end # Authenticate with HTTP Digest, returns true or false @@ -401,16 +402,16 @@ module ActionController extend self module ControllerMethods - def authenticate_or_request_with_http_token(realm = "Application", &login_procedure) - authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm) + def authenticate_or_request_with_http_token(realm = "Application", message = nil, &login_procedure) + authenticate_with_http_token(&login_procedure) || request_http_token_authentication(realm, message) end def authenticate_with_http_token(&login_procedure) Token.authenticate(self, &login_procedure) end - def request_http_token_authentication(realm = "Application") - Token.authentication_request(self, realm) + def request_http_token_authentication(realm = "Application", message = nil) + Token.authentication_request(self, realm, message) end end @@ -498,9 +499,10 @@ module ActionController # realm - String realm to use in the header. # # Returns nothing. - def authentication_request(controller, realm) + def authentication_request(controller, realm, message = nil) + message ||= "HTTP Token: Access denied.\n" controller.headers["WWW-Authenticate"] = %(Token realm="#{realm.tr('"'.freeze, "".freeze)}") - controller.__send__ :render, :text => "HTTP Token: Access denied.\n", :status => :unauthorized + controller.__send__ :render, :text => message, :status => :unauthorized end end end diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 31c8856437..356493bde6 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -19,7 +19,7 @@ module ActionController #:nodoc: # # Since HTML and JavaScript requests are typically made from the browser, we # need to ensure to verify request authenticity for the web browser. We can - # use session-oriented authentication for these types requests, by using + # use session-oriented authentication for these types of requests, by using # the `protect_form_forgery` method in our controllers. # # GET requests are not protected since they don't have side effects like writing diff --git a/actionpack/lib/action_controller/template_assertions.rb b/actionpack/lib/action_controller/template_assertions.rb new file mode 100644 index 0000000000..304012d24d --- /dev/null +++ b/actionpack/lib/action_controller/template_assertions.rb @@ -0,0 +1,188 @@ +module ActionController + module TemplateAssertions + extend ActiveSupport::Concern + + included do + setup :setup_subscriptions + teardown :teardown_subscriptions + end + + RENDER_TEMPLATE_INSTANCE_VARIABLES = %w{partials templates layouts files}.freeze + + def setup_subscriptions + RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable| + instance_variable_set("@_#{instance_variable}", Hash.new(0)) + end + + @_subscribers = [] + + @_subscribers << ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload| + path = payload[:layout] + if path + @_layouts[path] += 1 + if path =~ /^layouts\/(.*)/ + @_layouts[$1] += 1 + end + end + end + + @_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| + if virtual_path = payload[:virtual_path] + partial = virtual_path =~ /^.*\/_[^\/]*$/ + + if partial + @_partials[virtual_path] += 1 + @_partials[virtual_path.split("/").last] += 1 + end + + @_templates[virtual_path] += 1 + else + path = payload[:identifier] + if path + @_files[path] += 1 + @_files[path.split("/").last] += 1 + end + end + end + end + + def teardown_subscriptions + @_subscribers.each do |subscriber| + ActiveSupport::Notifications.unsubscribe(subscriber) + end + end + + def process(*args) + reset_template_assertion + super + end + + def reset_template_assertion + RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable| + ivar_name = "@_#{instance_variable}" + if instance_variable_defined?(ivar_name) + instance_variable_get(ivar_name).clear + end + end + end + + # Asserts that the request was rendered with the appropriate template file or partials. + # + # # assert that the "new" view template was rendered + # assert_template "new" + # + # # assert that the exact template "admin/posts/new" was rendered + # assert_template %r{\Aadmin/posts/new\Z} + # + # # assert that the layout 'admin' was rendered + # assert_template layout: 'admin' + # assert_template layout: 'layouts/admin' + # assert_template layout: :admin + # + # # assert that no layout was rendered + # assert_template layout: nil + # assert_template layout: false + # + # # assert that the "_customer" partial was rendered twice + # assert_template partial: '_customer', count: 2 + # + # # assert that no partials were rendered + # assert_template partial: false + # + # # assert that a file was rendered + # assert_template file: "README.rdoc" + # + # # assert that no file was rendered + # assert_template file: nil + # assert_template file: false + # + # In a view test case, you can also assert that specific locals are passed + # to partials: + # + # # assert that the "_customer" partial was rendered with a specific object + # assert_template partial: '_customer', locals: { customer: @customer } + def assert_template(options = {}, message = nil) + # Force body to be read in case the template is being streamed. + response.body + + case options + when NilClass, Regexp, String, Symbol + options = options.to_s if Symbol === options + rendered = @_templates + msg = message || sprintf("expecting <%s> but rendering with <%s>", + options.inspect, rendered.keys) + matches_template = + case options + when String + !options.empty? && rendered.any? do |t, num| + options_splited = options.split(File::SEPARATOR) + t_splited = t.split(File::SEPARATOR) + t_splited.last(options_splited.size) == options_splited + end + when Regexp + rendered.any? { |t,num| t.match(options) } + when NilClass + rendered.blank? + end + assert matches_template, msg + when Hash + options.assert_valid_keys(:layout, :partial, :locals, :count, :file) + + if options.key?(:layout) + expected_layout = options[:layout] + msg = message || sprintf("expecting layout <%s> but action rendered <%s>", + expected_layout, @_layouts.keys) + + case expected_layout + when String, Symbol + assert_includes @_layouts.keys, expected_layout.to_s, msg + when Regexp + assert(@_layouts.keys.any? {|l| l =~ expected_layout }, msg) + when nil, false + assert(@_layouts.empty?, msg) + else + raise ArgumentError, "assert_template only accepts a String, Symbol, Regexp, nil or false for :layout" + end + end + + if options[:file] + assert_includes @_files.keys, options[:file] + elsif options.key?(:file) + assert @_files.blank?, "expected no files but #{@_files.keys} was rendered" + end + + if expected_partial = options[:partial] + if expected_locals = options[:locals] + if defined?(@_rendered_views) + view = expected_partial.to_s.sub(/^_/, '').sub(/\/_(?=[^\/]+\z)/, '/') + + partial_was_not_rendered_msg = "expected %s to be rendered but it was not." % view + assert_includes @_rendered_views.rendered_views, view, partial_was_not_rendered_msg + + msg = 'expecting %s to be rendered with %s but was with %s' % [expected_partial, + expected_locals, + @_rendered_views.locals_for(view)] + assert(@_rendered_views.view_rendered?(view, options[:locals]), msg) + else + warn "the :locals option to #assert_template is only supported in a ActionView::TestCase" + end + elsif expected_count = options[:count] + actual_count = @_partials[expected_partial] + msg = message || sprintf("expecting %s to be rendered %s time(s) but rendered %s time(s)", + expected_partial, expected_count, actual_count) + assert(actual_count == expected_count.to_i, msg) + else + msg = message || sprintf("expecting partial <%s> but action rendered <%s>", + options[:partial], @_partials.keys) + assert_includes @_partials, expected_partial, msg + end + elsif options.key?(:partial) + assert @_partials.empty?, + "Expected no partials to be rendered" + end + else + raise ArgumentError, "assert_template only accepts a String, Symbol, Hash, Regexp, or nil" + end + end + end +end diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 33c24999f9..acff22d565 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -2,197 +2,10 @@ require 'rack/session/abstract/id' require 'active_support/core_ext/object/to_query' require 'active_support/core_ext/module/anonymous' require 'active_support/core_ext/hash/keys' - +require 'action_controller/template_assertions' require 'rails-dom-testing' module ActionController - module TemplateAssertions - extend ActiveSupport::Concern - - included do - setup :setup_subscriptions - teardown :teardown_subscriptions - end - - RENDER_TEMPLATE_INSTANCE_VARIABLES = %w{partials templates layouts files}.freeze - - def setup_subscriptions - RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable| - instance_variable_set("@_#{instance_variable}", Hash.new(0)) - end - - @_subscribers = [] - - @_subscribers << ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload| - path = payload[:layout] - if path - @_layouts[path] += 1 - if path =~ /^layouts\/(.*)/ - @_layouts[$1] += 1 - end - end - end - - @_subscribers << ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload| - if virtual_path = payload[:virtual_path] - partial = virtual_path =~ /^.*\/_[^\/]*$/ - - if partial - @_partials[virtual_path] += 1 - @_partials[virtual_path.split("/").last] += 1 - end - - @_templates[virtual_path] += 1 - else - path = payload[:identifier] - if path - @_files[path] += 1 - @_files[path.split("/").last] += 1 - end - end - end - end - - def teardown_subscriptions - @_subscribers.each do |subscriber| - ActiveSupport::Notifications.unsubscribe(subscriber) - end - end - - def process(*args) - reset_template_assertion - super - end - - def reset_template_assertion - RENDER_TEMPLATE_INSTANCE_VARIABLES.each do |instance_variable| - ivar_name = "@_#{instance_variable}" - if instance_variable_defined?(ivar_name) - instance_variable_get(ivar_name).clear - end - end - end - - # Asserts that the request was rendered with the appropriate template file or partials. - # - # # assert that the "new" view template was rendered - # assert_template "new" - # - # # assert that the exact template "admin/posts/new" was rendered - # assert_template %r{\Aadmin/posts/new\Z} - # - # # assert that the layout 'admin' was rendered - # assert_template layout: 'admin' - # assert_template layout: 'layouts/admin' - # assert_template layout: :admin - # - # # assert that no layout was rendered - # assert_template layout: nil - # assert_template layout: false - # - # # assert that the "_customer" partial was rendered twice - # assert_template partial: '_customer', count: 2 - # - # # assert that no partials were rendered - # assert_template partial: false - # - # # assert that a file was rendered - # assert_template file: "README.rdoc" - # - # # assert that no file was rendered - # assert_template file: nil - # assert_template file: false - # - # In a view test case, you can also assert that specific locals are passed - # to partials: - # - # # assert that the "_customer" partial was rendered with a specific object - # assert_template partial: '_customer', locals: { customer: @customer } - def assert_template(options = {}, message = nil) - # Force body to be read in case the template is being streamed. - response.body - - case options - when NilClass, Regexp, String, Symbol - options = options.to_s if Symbol === options - rendered = @_templates - msg = message || sprintf("expecting <%s> but rendering with <%s>", - options.inspect, rendered.keys) - matches_template = - case options - when String - !options.empty? && rendered.any? do |t, num| - options_splited = options.split(File::SEPARATOR) - t_splited = t.split(File::SEPARATOR) - t_splited.last(options_splited.size) == options_splited - end - when Regexp - rendered.any? { |t,num| t.match(options) } - when NilClass - rendered.blank? - end - assert matches_template, msg - when Hash - options.assert_valid_keys(:layout, :partial, :locals, :count, :file) - - if options.key?(:layout) - expected_layout = options[:layout] - msg = message || sprintf("expecting layout <%s> but action rendered <%s>", - expected_layout, @_layouts.keys) - - case expected_layout - when String, Symbol - assert_includes @_layouts.keys, expected_layout.to_s, msg - when Regexp - assert(@_layouts.keys.any? {|l| l =~ expected_layout }, msg) - when nil, false - assert(@_layouts.empty?, msg) - else - raise ArgumentError, "assert_template only accepts a String, Symbol, Regexp, nil or false for :layout" - end - end - - if options[:file] - assert_includes @_files.keys, options[:file] - elsif options.key?(:file) - assert @_files.blank?, "expected no files but #{@_files.keys} was rendered" - end - - if expected_partial = options[:partial] - if expected_locals = options[:locals] - if defined?(@_rendered_views) - view = expected_partial.to_s.sub(/^_/, '').sub(/\/_(?=[^\/]+\z)/, '/') - - partial_was_not_rendered_msg = "expected %s to be rendered but it was not." % view - assert_includes @_rendered_views.rendered_views, view, partial_was_not_rendered_msg - - msg = 'expecting %s to be rendered with %s but was with %s' % [expected_partial, - expected_locals, - @_rendered_views.locals_for(view)] - assert(@_rendered_views.view_rendered?(view, options[:locals]), msg) - else - warn "the :locals option to #assert_template is only supported in a ActionView::TestCase" - end - elsif expected_count = options[:count] - actual_count = @_partials[expected_partial] - msg = message || sprintf("expecting %s to be rendered %s time(s) but rendered %s time(s)", - expected_partial, expected_count, actual_count) - assert(actual_count == expected_count.to_i, msg) - else - msg = message || sprintf("expecting partial <%s> but action rendered <%s>", - options[:partial], @_partials.keys) - assert_includes @_partials, expected_partial, msg - end - elsif options.key?(:partial) - assert @_partials.empty?, - "Expected no partials to be rendered" - end - else - raise ArgumentError, "assert_template only accepts a String, Symbol, Hash, Regexp, or nil" - end - end - end - class TestRequest < ActionDispatch::TestRequest #:nodoc: DEFAULT_ENV = ActionDispatch::TestRequest::DEFAULT_ENV.dup DEFAULT_ENV.delete 'PATH_INFO' @@ -604,7 +417,7 @@ module ActionController def process(action, *args) check_required_ivars - if kwarg_request?(*args) + if kwarg_request?(args) parameters, session, body, flash, http_method, format, xhr = args[0].values_at(:params, :session, :body, :flash, :method, :format, :xhr) else http_method, parameters, session, flash = args @@ -745,7 +558,7 @@ module ActionController private def process_with_kwargs(http_method, action, *args) - if kwarg_request?(*args) + if kwarg_request?(args) args.first.merge!(method: http_method) process(action, *args) else @@ -757,7 +570,7 @@ module ActionController end REQUEST_KWARGS = %i(params session flash method body xhr) - def kwarg_request?(*args) + def kwarg_request?(args) args[0].respond_to?(:keys) && ( (args[0].key?(:format) && args[0].keys.size == 1) || args[0].keys.any? { |k| REQUEST_KWARGS.include?(k) } diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb index 2b851cc28d..3170389b36 100644 --- a/actionpack/lib/action_dispatch/http/filter_parameters.rb +++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb @@ -16,7 +16,7 @@ module ActionDispatch # env["action_dispatch.parameter_filter"] = [:foo, "bar"] # => replaces the value to all keys matching /foo|bar/i with "[FILTERED]" # - # env["action_dispatch.parameter_filter"] = lambda do |k,v| + # env["action_dispatch.parameter_filter"] = -> (k, v) do # v.reverse! if k =~ /secret/i # end # => reverses the value to all keys matching /secret/i diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index bb6a73afb5..dd1f140051 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -79,6 +79,9 @@ module ActionDispatch # domain: %w(.example.com .example.org) # Allow the cookie # # for concrete domain names. # + # * <tt>:tld_length</tt> - When using <tt>:domain => :all</tt>, this option can be used to explicitly + # set the TLD length when using a short (<= 3 character) domain that is being interpreted as part of a TLD. + # For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 1. # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object. # * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers. # Default is +false+. diff --git a/actionpack/lib/action_dispatch/middleware/reloader.rb b/actionpack/lib/action_dispatch/middleware/reloader.rb index 15b5a48535..6c7fba00cb 100644 --- a/actionpack/lib/action_dispatch/middleware/reloader.rb +++ b/actionpack/lib/action_dispatch/middleware/reloader.rb @@ -11,9 +11,9 @@ module ActionDispatch # the response body. This is important for streaming responses such as the # following: # - # self.response_body = lambda { |response, output| + # self.response_body = -> (response, output) do # # code here which refers to application models - # } + # end # # Cleanup callbacks will not be called until after the response_body lambda # is evaluated, ensuring that it can refer to application models and other diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index c47e5d5245..bc5ef1abc9 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -26,7 +26,7 @@ module ActionDispatch # representing the filename. Otherwise, false is returned. # # Used by the `Static` class to check the existence of a valid file - # in the server's `public/` directory. (See Static#call) + # in the server's `public/` directory (see Static#call). def match?(path) path = URI.parser.unescape(path) return false unless path.valid_encoding? diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 49009a45cc..0a444ddffc 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -418,7 +418,7 @@ module ActionDispatch # A pattern can also point to a +Rack+ endpoint i.e. anything that # responds to +call+: # - # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }, via: :get + # match 'photos/:id', to: -> (hash) { [200, {}, ["Coming soon"]] }, via: :get # match 'photos/:id', to: PhotoRackApp, via: :get # # Yes, controller actions are just rack endpoints # match 'photos/:id', to: PhotosController.action(:show), via: :get @@ -470,7 +470,7 @@ module ActionDispatch # +call+ or a string representing a controller's action. # # match 'path', to: 'controller#action', via: :get - # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }, via: :get + # match 'path', to: -> (env) { [200, {}, ["Success!"]] }, via: :get # match 'path', to: RackApp, via: :get # # [:on] @@ -899,7 +899,7 @@ module ActionDispatch # # Requests to routes can be constrained based on specific criteria: # - # constraints(lambda { |req| req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do + # constraints(-> (req) { req.env["HTTP_USER_AGENT"] =~ /iPhone/ }) do # resources :iphones # end # diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 3800c61dab..b1bd6ae6d5 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -81,7 +81,7 @@ module ActionDispatch # # xhr :get, '/feed', params: { since: 201501011400 } def xml_http_request(request_method, path, *args) - if kwarg_request?(*args) + if kwarg_request?(args) params, headers, env = args.first.values_at(:params, :headers, :env) else params = args[0] @@ -291,7 +291,7 @@ module ActionDispatch end def process_with_kwargs(http_method, path, *args) - if kwarg_request?(*args) + if kwarg_request?(args) process(http_method, path, *args) else non_kwarg_request_warning if args.present? @@ -300,7 +300,7 @@ module ActionDispatch end REQUEST_KWARGS = %i(params headers env xhr) - def kwarg_request?(*args) + def kwarg_request?(args) args[0].respond_to?(:keys) && args[0].keys.any? { |k| REQUEST_KWARGS.include?(k) } end diff --git a/actionpack/test/controller/http_basic_authentication_test.rb b/actionpack/test/controller/http_basic_authentication_test.rb index 20962a90cb..10fbee7582 100644 --- a/actionpack/test/controller/http_basic_authentication_test.rb +++ b/actionpack/test/controller/http_basic_authentication_test.rb @@ -36,7 +36,7 @@ class HttpBasicAuthenticationTest < ActionController::TestCase if authenticate_with_http_basic { |username, password| username == 'pretty' && password == 'please' } @logged_in = true else - request_http_basic_authentication("SuperSecret") + request_http_basic_authentication("SuperSecret", "Authentication Failed\n") end end @@ -104,7 +104,7 @@ class HttpBasicAuthenticationTest < ActionController::TestCase get :display assert_response :unauthorized - assert_equal "HTTP Basic: Access denied.\n", @response.body + assert_equal "Authentication Failed\n", @response.body assert_equal 'Basic realm="SuperSecret"', @response.headers['WWW-Authenticate'] end @@ -113,7 +113,7 @@ class HttpBasicAuthenticationTest < ActionController::TestCase get :display assert_response :unauthorized - assert_equal "HTTP Basic: Access denied.\n", @response.body + assert_equal "Authentication Failed\n", @response.body assert_equal 'Basic realm="SuperSecret"', @response.headers['WWW-Authenticate'] end diff --git a/actionpack/test/controller/http_token_authentication_test.rb b/actionpack/test/controller/http_token_authentication_test.rb index a758df2ec6..2521f0322c 100644 --- a/actionpack/test/controller/http_token_authentication_test.rb +++ b/actionpack/test/controller/http_token_authentication_test.rb @@ -30,7 +30,7 @@ class HttpTokenAuthenticationTest < ActionController::TestCase if authenticate_with_http_token { |token, options| token == '"quote" pretty' && options[:algorithm] == 'test' } @logged_in = true else - request_http_token_authentication("SuperSecret") + request_http_token_authentication("SuperSecret", "Authentication Failed\n") end end @@ -91,7 +91,7 @@ class HttpTokenAuthenticationTest < ActionController::TestCase get :display assert_response :unauthorized - assert_equal "HTTP Token: Access denied.\n", @response.body + assert_equal "Authentication Failed\n", @response.body assert_equal 'Token realm="SuperSecret"', @response.headers['WWW-Authenticate'] end @@ -100,7 +100,7 @@ class HttpTokenAuthenticationTest < ActionController::TestCase get :display assert_response :unauthorized - assert_equal "HTTP Token: Access denied.\n", @response.body + assert_equal "Authentication Failed\n", @response.body assert_equal 'Token realm="SuperSecret"', @response.headers['WWW-Authenticate'] end diff --git a/actionpack/test/journey/route_test.rb b/actionpack/test/journey/route_test.rb index 9616f036b3..0980df06e8 100644 --- a/actionpack/test/journey/route_test.rb +++ b/actionpack/test/journey/route_test.rb @@ -30,7 +30,7 @@ module ActionDispatch path = Path::Pattern.new strexp defaults = { name: 'tender' } route = Route.new('name', nil, path, nil, defaults) - assert_equal /love/, route.requirements[:name] + assert_equal(/love/, route.requirements[:name]) end def test_ip_address diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 78ce230b3a..a6d5c08fe9 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,15 @@ +* Remove `:rescue_format` option for `translate` helper since it's no longer + supported by I18n. + + *Bernard Potocki* + +* `translate` should handle `raise` flag correctly in case of both main and default + translation is missing. + + Fixes #19967 + + *Bernard Potocki* + * Load the `default_form_builder` from the controller on initialization, which overrides the global config if it is present. diff --git a/actionview/README.rdoc b/actionview/README.rdoc index 5bb62c7562..8b1f85f748 100644 --- a/actionview/README.rdoc +++ b/actionview/README.rdoc @@ -9,7 +9,7 @@ used to inline short Ruby snippets inside HTML), and XML Builder. The latest version of Action View can be installed with RubyGems: - % [sudo] gem install actionview + % gem install actionview Source code can be downloaded as part of the Rails project on GitHub diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 60fc9ee1a2..e32f8e219e 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -207,6 +207,7 @@ module ActionView # # => <img alt="Icon" class="menu_icon" src="/icons/icon.gif" /> def image_tag(source, options={}) options = options.symbolize_keys + check_for_image_tag_errors(options) src = options[:src] = path_to_image(source) @@ -325,6 +326,12 @@ module ActionView [size, size] end end + + def check_for_image_tag_errors(options) + if options[:size] && (options[:height] || options[:width]) + raise ArgumentError, "Cannot pass a :size option with a :height or :width option" + end + end end end end diff --git a/actionview/lib/action_view/helpers/atom_feed_helper.rb b/actionview/lib/action_view/helpers/atom_feed_helper.rb index d8be4e5678..bb1cdd0f8d 100644 --- a/actionview/lib/action_view/helpers/atom_feed_helper.rb +++ b/actionview/lib/action_view/helpers/atom_feed_helper.rb @@ -16,7 +16,7 @@ module ActionView # end # # app/controllers/posts_controller.rb: - # class PostsController < ApplicationController::Base + # class PostsController < ApplicationController # # GET /posts.html # # GET /posts.atom # def index @@ -51,7 +51,7 @@ module ActionView # * <tt>:language</tt>: Defaults to "en-US". # * <tt>:root_url</tt>: The HTML alternative that this feed is doubling for. Defaults to / on the current host. # * <tt>:url</tt>: The URL for this feed. Defaults to the current URL. - # * <tt>:id</tt>: The id for this feed. Defaults to "tag:#{request.host},#{options[:schema_date]}:#{request.fullpath.split(".")[0]}" + # * <tt>:id</tt>: The id for this feed. Defaults to "tag:localhost,2005:/posts", in this case. # * <tt>:schema_date</tt>: The date at which the tag scheme for the feed was first used. A good default is the year you # created the feed. See http://feedvalidator.org/docs/error/InvalidTAG.html for more information. If not specified, # 2005 is used (as an "I don't care" value). diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb index 0e2a5f90f4..251764a8de 100644 --- a/actionview/lib/action_view/helpers/cache_helper.rb +++ b/actionview/lib/action_view/helpers/cache_helper.rb @@ -75,7 +75,8 @@ module ActionView # render(topics) => render("topics/topic") # render(message.topics) => render("topics/topic") # - # It's not possible to derive all render calls like that, though. Here are a few examples of things that can't be derived: + # It's not possible to derive all render calls like that, though. + # Here are a few examples of things that can't be derived: # # render group_of_attachments # render @project.documents.where(published: true).order('created_at') @@ -97,19 +98,21 @@ module ActionView # <%# Template Dependency: todolists/todolist %> # <%= render_sortable_todolists @project.todolists %> # - # The pattern used to match these is /# Template Dependency: ([^ ]+)/, so it's important that you type it out just so. + # The pattern used to match these is /# Template Dependency: ([^ ]+)/, + # so it's important that you type it out just so. # You can only declare one template dependency per line. # # === External dependencies # - # If you use a helper method, for example, inside of a cached block and you then update that helper, - # you'll have to bump the cache as well. It doesn't really matter how you do it, but the md5 of the template file + # If you use a helper method, for example, inside a cached block and + # you then update that helper, you'll have to bump the cache as well. + # It doesn't really matter how you do it, but the md5 of the template file # must change. One recommendation is to simply be explicit in a comment, like: # # <%# Helper Dependency Updated: May 6, 2012 at 6pm %> # <%= some_helper_method(person) %> # - # Now all you'll have to do is change that timestamp when the helper method changes. + # Now all you have to do is change that timestamp when the helper method changes. # # === Automatic Collection Caching # @@ -118,7 +121,7 @@ module ActionView # <%= render @notifications %> # <%= render partial: 'notifications/notification', collection: @notifications %> # - # If the notifications/_notification partial starts with a cache call like so: + # If the notifications/_notification partial starts with a cache call as: # # <% cache notification do %> # <%= notification.name %> @@ -127,14 +130,15 @@ module ActionView # The collection can then automatically use any cached renders for that # template by reading them at once instead of one by one. # - # See ActionView::Template::Handlers::ERB.resource_cache_call_pattern for more - # information on what cache calls make a template eligible for this collection caching. + # See ActionView::Template::Handlers::ERB.resource_cache_call_pattern for + # more information on what cache calls make a template eligible for this + # collection caching. # # The automatic cache multi read can be turned off like so: # # <%= render @notifications, cache: false %> def cache(name = {}, options = nil, &block) - if controller.perform_caching + if controller.respond_to?(:perform_caching) && controller.perform_caching safe_concat(fragment_for(cache_fragment_name(name, options), options, &block)) else yield diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index 7fdeca5ea8..3a9acafaa2 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -1631,7 +1631,7 @@ module ActionView # target labels for radio_button tags (where the value is used in the ID of the input tag). # # ==== Examples - # label(:post, :title) + # label(:title) # # => <label for="post_title">Title</label> # # You can localize your labels based on model and attribute names. @@ -1644,7 +1644,7 @@ module ActionView # # Which then will result in # - # label(:post, :body) + # label(:body) # # => <label for="post_body">Write your entire text here</label> # # Localization can also be based purely on the translation of the attribute-name @@ -1655,21 +1655,22 @@ module ActionView # post: # cost: "Total cost" # - # label(:post, :cost) + # label(:cost) # # => <label for="post_cost">Total cost</label> # - # label(:post, :title, "A short title") + # label(:title, "A short title") # # => <label for="post_title">A short title</label> # - # label(:post, :title, "A short title", class: "title_label") + # label(:title, "A short title", class: "title_label") # # => <label for="post_title" class="title_label">A short title</label> # - # label(:post, :privacy, "Public Post", value: "public") + # label(:privacy, "Public Post", value: "public") # # => <label for="post_privacy_public">Public Post</label> # - # label(:post, :terms) do + # label(:terms) do # 'Accept <a href="/terms">Terms</a>.'.html_safe # end + # # => <label for="post_terms">Accept <a href="/terms">Terms</a>.</label> def label(method, text = nil, options = {}, &block) @template.label(@object_name, method, text, objectify_options(options), &block) end @@ -1718,16 +1719,17 @@ module ActionView # hashes instead of arrays. # # # Let's say that @post.validated? is 1: - # check_box("post", "validated") + # check_box("validated") # # => <input name="post[validated]" type="hidden" value="0" /> # # <input checked="checked" type="checkbox" id="post_validated" name="post[validated]" value="1" /> # # # Let's say that @puppy.gooddog is "no": - # check_box("puppy", "gooddog", {}, "yes", "no") + # check_box("gooddog", {}, "yes", "no") # # => <input name="puppy[gooddog]" type="hidden" value="no" /> # # <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" /> # - # check_box("eula", "accepted", { class: 'eula_check' }, "yes", "no") + # # Let's say that @eula.accepted is "no": + # check_box("accepted", { class: 'eula_check' }, "yes", "no") # # => <input name="eula[accepted]" type="hidden" value="no" /> # # <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" /> def check_box(method, options = {}, checked_value = "1", unchecked_value = "0") @@ -1742,13 +1744,14 @@ module ActionView # +options+ hash. You may pass HTML options there as well. # # # Let's say that @post.category returns "rails": - # radio_button("post", "category", "rails") - # radio_button("post", "category", "java") + # radio_button("category", "rails") + # radio_button("category", "java") # # => <input type="radio" id="post_category_rails" name="post[category]" value="rails" checked="checked" /> # # <input type="radio" id="post_category_java" name="post[category]" value="java" /> # - # radio_button("user", "receive_newsletter", "yes") - # radio_button("user", "receive_newsletter", "no") + # # Let's say that @user.category returns "no": + # radio_button("receive_newsletter", "yes") + # radio_button("receive_newsletter", "no") # # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" /> # # <input type="radio" id="user_receive_newsletter_no" name="user[receive_newsletter]" value="no" checked="checked" /> def radio_button(method, tag_value, options = {}) @@ -1761,14 +1764,17 @@ module ActionView # shown. # # ==== Examples - # hidden_field(:signup, :pass_confirm) - # # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="#{@signup.pass_confirm}" /> + # # Let's say that @signup.pass_confirm returns true: + # hidden_field(:pass_confirm) + # # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="true" /> # - # hidden_field(:post, :tag_list) - # # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="#{@post.tag_list}" /> + # # Let's say that @post.tag_list returns "blog, ruby": + # hidden_field(:tag_list) + # # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="blog, ruby" /> # - # hidden_field(:user, :token) - # # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" /> + # # Let's say that @user.token returns "abcde": + # hidden_field(:token) + # # => <input type="hidden" id="user_token" name="user[token]" value="abcde" /> # def hidden_field(method, options = {}) @emitted_hidden_id = true if method == :id @@ -1789,19 +1795,24 @@ module ActionView # * <tt>:accept</tt> - If set to one or multiple mime-types, the user will be suggested a filter when choosing a file. You still need to set up model validations. # # ==== Examples - # file_field(:user, :avatar) + # # Let's say that @user has avatar: + # file_field(:avatar) # # => <input type="file" id="user_avatar" name="user[avatar]" /> # - # file_field(:post, :image, :multiple => true) - # # => <input type="file" id="post_image" name="post[image]" multiple="true" /> + # # Let's say that @post has image: + # file_field(:image, :multiple => true) + # # => <input type="file" id="post_image" name="post[image][]" multiple="multiple" /> # - # file_field(:post, :attached, accept: 'text/html') + # # Let's say that @post has attached: + # file_field(:attached, accept: 'text/html') # # => <input accept="text/html" type="file" id="post_attached" name="post[attached]" /> # - # file_field(:post, :image, accept: 'image/png,image/gif,image/jpeg') + # # Let's say that @post has image: + # file_field(:image, accept: 'image/png,image/gif,image/jpeg') # # => <input type="file" id="post_image" name="post[image]" accept="image/png,image/gif,image/jpeg" /> # - # file_field(:attachment, :file, class: 'file_input') + # # Let's say that @attachment has file: + # file_field(:file, class: 'file_input') # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" /> def file_field(method, options = {}) self.multipart = true diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index 8a5928477f..38fee3b314 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -80,7 +80,7 @@ module ActionView # # When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled. # - # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: lambda{|category| category.archived? }}) + # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: -> (category) { category.archived? }}) # # If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return: # <select name="post[category_id]" id="post_category_id"> diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index 089627a221..0615bd2e0d 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -60,12 +60,12 @@ module ActionView # If the user has explicitly decided to NOT raise errors, pass that option to I18n. # Otherwise, tell I18n to raise an exception, which we rescue further in this method. # Note: `raise_error` refers to us re-raising the error in this method. I18n is forced to raise by default. - if options[:raise] == false || (options.key?(:rescue_format) && options[:rescue_format].nil?) + if options[:raise] == false raise_error = false - options[:raise] = false + i18n_raise = false else - raise_error = options[:raise] || options[:rescue_format] || ActionView::Base.raise_on_missing_translations - options[:raise] = true + raise_error = options[:raise] || ActionView::Base.raise_on_missing_translations + i18n_raise = true end if html_safe_translation_key?(key) @@ -75,11 +75,11 @@ module ActionView html_safe_options[name] = ERB::Util.html_escape(value.to_s) end end - translation = I18n.translate(scope_key_by_partial(key), html_safe_options) + translation = I18n.translate(scope_key_by_partial(key), html_safe_options.merge(raise: i18n_raise)) translation.respond_to?(:html_safe) ? translation.html_safe : translation else - I18n.translate(scope_key_by_partial(key), options) + I18n.translate(scope_key_by_partial(key), options.merge(raise: i18n_raise)) end rescue I18n::MissingTranslationData => e if remaining_defaults.present? diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index 02dce71496..6e6ce20924 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -464,6 +464,14 @@ class AssetTagHelperTest < ActionView::TestCase assert_equal({:size => '16x10'}, options) end + def test_image_tag_raises_an_error_for_competing_size_arguments + exception = assert_raise(ArgumentError) do + image_tag("gold.png", :height => "100", :width => "200", :size => "45x70") + end + + assert_equal("Cannot pass a :size option with a :height or :width option", exception.message) + end + def test_favicon_link_tag FaviconLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end diff --git a/actionview/test/template/atom_feed_helper_test.rb b/actionview/test/template/atom_feed_helper_test.rb index 525d99750d..591cd71404 100644 --- a/actionview/test/template/atom_feed_helper_test.rb +++ b/actionview/test/template/atom_feed_helper_test.rb @@ -14,7 +14,7 @@ class ScrollsController < ActionController::Base FEEDS["defaults"] = <<-EOT atom_feed(:schema_date => '2008') do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll) do |entry| @@ -31,7 +31,7 @@ class ScrollsController < ActionController::Base FEEDS["entry_options"] = <<-EOT atom_feed do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll, :url => "/otherstuff/" + scroll.to_param.to_s, :updated => Time.utc(2007, 1, scroll.id)) do |entry| @@ -48,7 +48,7 @@ class ScrollsController < ActionController::Base FEEDS["entry_type_options"] = <<-EOT atom_feed(:schema_date => '2008') do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll, :type => 'text/xml') do |entry| @@ -65,7 +65,7 @@ class ScrollsController < ActionController::Base FEEDS["entry_url_false_option"] = <<-EOT atom_feed do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll, :url => false) do |entry| @@ -82,7 +82,7 @@ class ScrollsController < ActionController::Base FEEDS["xml_block"] = <<-EOT atom_feed do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) feed.author do |author| author.name("DHH") @@ -100,7 +100,7 @@ class ScrollsController < ActionController::Base atom_feed({'xmlns:app' => 'http://www.w3.org/2007/app', 'xmlns:openSearch' => 'http://a9.com/-/spec/opensearch/1.1/'}) do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll) do |entry| @@ -118,7 +118,7 @@ class ScrollsController < ActionController::Base FEEDS["feed_with_overridden_ids"] = <<-EOT atom_feed({:id => 'tag:test.rubyonrails.org,2008:test/'}) do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll, :id => "tag:test.rubyonrails.org,2008:"+scroll.id.to_s) do |entry| @@ -137,7 +137,7 @@ class ScrollsController < ActionController::Base atom_feed(:schema_date => '2008', :instruct => {'xml-stylesheet' => { :href=> 't.css', :type => 'text/css' }}) do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll) do |entry| @@ -155,7 +155,7 @@ class ScrollsController < ActionController::Base atom_feed(:schema_date => '2008', :instruct => {'target1' => [{ :a => '1', :b => '2' }, { :c => '3', :d => '4' }]}) do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll) do |entry| @@ -172,7 +172,7 @@ class ScrollsController < ActionController::Base FEEDS["feed_with_xhtml_content"] = <<-'EOT' atom_feed do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll) do |entry| @@ -197,7 +197,7 @@ class ScrollsController < ActionController::Base new_xml = Builder::XmlMarkup.new(:target=>'') atom_feed(:xml => new_xml) do |feed| feed.title("My great blog!") - feed.updated((@scrolls.first.created_at)) + feed.updated(@scrolls.first.created_at) @scrolls.each do |scroll| feed.entry(scroll) do |entry| diff --git a/actionview/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb index df096b3c3a..5dc281adb2 100644 --- a/actionview/test/template/translation_helper_test.rb +++ b/actionview/test/template/translation_helper_test.rb @@ -41,8 +41,8 @@ class TranslationHelperTest < ActiveSupport::TestCase I18n.backend.reload! end - def test_delegates_to_i18n_setting_the_rescue_format_option_to_html - I18n.expects(:translate).with(:foo, :locale => 'en', :raise=>true).returns("") + def test_delegates_setting_to_i18n + I18n.expects(:translate).with(:foo, :locale => 'en', :raise => true).returns("") translate :foo, :locale => 'en' end @@ -58,12 +58,6 @@ class TranslationHelperTest < ActiveSupport::TestCase assert_equal true, translate(:"translations.missing").html_safe? end - def test_returns_missing_translation_message_using_nil_as_rescue_format - expected = 'translation missing: en.translations.missing' - assert_equal expected, translate(:"translations.missing", :rescue_format => nil) - assert_equal false, translate(:"translations.missing", :rescue_format => nil).html_safe? - end - def test_raises_missing_translation_message_with_raise_config_option ActionView::Base.raise_on_missing_translations = true @@ -96,12 +90,6 @@ class TranslationHelperTest < ActiveSupport::TestCase I18n.exception_handler = old_exception_handler end - def test_i18n_translate_defaults_to_nil_rescue_format - expected = 'translation missing: en.translations.missing' - assert_equal expected, I18n.translate(:"translations.missing") - assert_equal false, I18n.translate(:"translations.missing").html_safe? - end - def test_translation_returning_an_array expected = %w(foo bar) assert_equal expected, translate(:"translations.array") @@ -157,6 +145,19 @@ class TranslationHelperTest < ActiveSupport::TestCase assert_equal true, translation.html_safe? end + def test_translate_with_missing_default + translation = translate(:'translations.missing', :default => :'translations.missing_html') + expected = '<span class="translation_missing" title="translation missing: en.translations.missing_html">Missing Html</span>' + assert_equal expected, translation + assert_equal true, translation.html_safe? + end + + def test_translate_with_missing_default_and_raise_option + assert_raise(I18n::MissingTranslationData) do + translate(:'translations.missing', :default => :'translations.missing_html', :raise => true) + end + end + def test_translate_with_two_defaults_named_html translation = translate(:'translations.missing', :default => [:'translations.missing_html', :'translations.hello_html']) assert_equal '<a>Hello World</a>', translation diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index ef4df0407a..0e35c67516 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -379,6 +379,11 @@ class UrlHelperTest < ActiveSupport::TestCase assert_dom_equal %{<a href="/">Listing</a>}, link_to_if(true, "Listing", url_hash) end + def test_link_to_if_with_block + assert_equal "Fallback", link_to_if(false, "Showing", url_hash) { "Fallback" } + assert_dom_equal %{<a href="/">Listing</a>}, link_to_if(true, "Listing", url_hash) { "Fallback" } + end + def request_for_url(url, opts = {}) env = Rack::MockRequest.env_for("http://www.example.com#{url}", opts) ActionDispatch::Request.new(env) @@ -479,6 +484,11 @@ class UrlHelperTest < ActiveSupport::TestCase link_to_unless_current("Listing", "http://www.example.com/") end + def test_link_to_unless_with_block + assert_dom_equal %{<a href="/">Showing</a>}, link_to_unless(false, "Showing", url_hash) { "Fallback" } + assert_equal "Fallback", link_to_unless(true, "Listing", url_hash) { "Fallback" } + end + def test_mail_to assert_dom_equal %{<a href="mailto:david@loudthinking.com">david@loudthinking.com</a>}, mail_to("david@loudthinking.com") assert_dom_equal %{<a href="mailto:david@loudthinking.com">David Heinemeier Hansson</a>}, mail_to("david@loudthinking.com", "David Heinemeier Hansson") diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index 85a437a1dd..74f4c2588e 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,3 +1,16 @@ +* Allow `DelayedJob`, `Sidekiq`, `qu`, and `que` to report the job id back to + `ActiveJob::Base` as `provider_job_id`. + + Fixes #18821. + + *Kevin Deisz* And *Jeroen van Baarsen* + +* `assert_enqueued_jobs` and `assert_performed_jobs` in block form use the + given number as expected value. This makes the error message much easier to + understand. + + *y-yagi* + * A generated job now inherents from `app/jobs/application_job.rb` by default. *Jeroen van Baarsen* diff --git a/activejob/README.md b/activejob/README.md index 5170ebee6e..f9a3183b1a 100644 --- a/activejob/README.md +++ b/activejob/README.md @@ -102,7 +102,7 @@ see the API Documentation for [ActiveJob::QueueAdapters](http://api.rubyonrails. The latest version of Active Job can be installed with RubyGems: ``` - % [sudo] gem install activejob + % gem install activejob ``` Source code can be downloaded as part of the Rails project on GitHub diff --git a/activejob/Rakefile b/activejob/Rakefile index 6c7e6aae6e..0e36bb81b3 100644 --- a/activejob/Rakefile +++ b/activejob/Rakefile @@ -35,7 +35,7 @@ namespace :test do t.libs << 'test' t.test_files = FileList['test/cases/**/*_test.rb'] t.verbose = true - t.warning = true + t.warning = false t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end @@ -54,7 +54,7 @@ namespace :test do t.libs << 'test' t.test_files = FileList['test/integration/**/*_test.rb'] t.verbose = true - t.warning = true + t.warning = false t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end end diff --git a/activejob/lib/active_job/arguments.rb b/activejob/lib/active_job/arguments.rb index f9392cf278..cdb37879b6 100644 --- a/activejob/lib/active_job/arguments.rb +++ b/activejob/lib/active_job/arguments.rb @@ -5,6 +5,8 @@ module ActiveJob # # Wraps the original exception raised as +original_exception+. class DeserializationError < StandardError + # The original exception that was raised during deserialization of job + # arguments. attr_reader :original_exception def initialize(e) #:nodoc: diff --git a/activejob/lib/active_job/core.rb b/activejob/lib/active_job/core.rb index ddd7d1361c..0528572cd0 100644 --- a/activejob/lib/active_job/core.rb +++ b/activejob/lib/active_job/core.rb @@ -1,4 +1,6 @@ module ActiveJob + # Provides general behavior that will be included into every Active Job + # object that inherits from ActiveJob::Base. module Core extend ActiveSupport::Concern @@ -15,6 +17,9 @@ module ActiveJob # Queue in which the job will reside. attr_writer :queue_name + + # ID optionally provided by adapter + attr_accessor :provider_job_id end # These methods will be included into any Active Job object, adding diff --git a/activejob/lib/active_job/enqueuing.rb b/activejob/lib/active_job/enqueuing.rb index 430c17e1bf..98d92385dd 100644 --- a/activejob/lib/active_job/enqueuing.rb +++ b/activejob/lib/active_job/enqueuing.rb @@ -1,6 +1,7 @@ require 'active_job/arguments' module ActiveJob + # Provides behavior for enqueuing and retrying jobs. module Enqueuing extend ActiveSupport::Concern 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 852a6ee326..ac83da2b9c 100644 --- a/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/delayed_job_adapter.rb @@ -14,11 +14,15 @@ module ActiveJob # Rails.application.config.active_job.queue_adapter = :delayed_job class DelayedJobAdapter def enqueue(job) #:nodoc: - Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name) + delayed_job = Delayed::Job.enqueue(JobWrapper.new(job.serialize), queue: job.queue_name) + job.provider_job_id = delayed_job.id + delayed_job end def enqueue_at(job, timestamp) #:nodoc: - 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, run_at: Time.at(timestamp)) + job.provider_job_id = delayed_job.id + delayed_job end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/inline_adapter.rb b/activejob/lib/active_job/queue_adapters/inline_adapter.rb index 1d06324c18..8ad5f4de07 100644 --- a/activejob/lib/active_job/queue_adapters/inline_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/inline_adapter.rb @@ -14,7 +14,7 @@ module ActiveJob end def enqueue_at(*) #:nodoc: - raise NotImplementedError.new("Use a queueing backend to enqueue jobs in the future. Read more at http://guides.rubyonrails.org/active_job_basics.html") + raise NotImplementedError, "Use a queueing backend to enqueue jobs in the future. Read more at http://guides.rubyonrails.org/active_job_basics.html" end end end diff --git a/activejob/lib/active_job/queue_adapters/qu_adapter.rb b/activejob/lib/active_job/queue_adapters/qu_adapter.rb index 94584ef9d8..0e198922fc 100644 --- a/activejob/lib/active_job/queue_adapters/qu_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/qu_adapter.rb @@ -17,13 +17,17 @@ module ActiveJob # Rails.application.config.active_job.queue_adapter = :qu class QuAdapter def enqueue(job, *args) #:nodoc: - Qu::Payload.new(klass: JobWrapper, args: [job.serialize]).tap do |payload| + qu_job = Qu::Payload.new(klass: JobWrapper, args: [job.serialize]).tap do |payload| payload.instance_variable_set(:@queue, job.queue_name) end.push + + # qu_job can be nil depending on the configured backend + job.provider_job_id = qu_job.id unless qu_job.nil? + qu_job end def enqueue_at(job, timestamp, *args) #:nodoc: - raise NotImplementedError + raise NotImplementedError, "This queueing backend does not support scheduling jobs. To see what features are supported go to http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html" end class JobWrapper < Qu::Job #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/que_adapter.rb b/activejob/lib/active_job/queue_adapters/que_adapter.rb index 84cc2845b0..90947aa98d 100644 --- a/activejob/lib/active_job/queue_adapters/que_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/que_adapter.rb @@ -16,11 +16,15 @@ module ActiveJob # Rails.application.config.active_job.queue_adapter = :que class QueAdapter def enqueue(job) #:nodoc: - JobWrapper.enqueue job.serialize, queue: job.queue_name + que_job = JobWrapper.enqueue job.serialize + job.provider_job_id = que_job.attrs["job_id"] + que_job end def enqueue_at(job, timestamp) #:nodoc: - JobWrapper.enqueue job.serialize, queue: job.queue_name, run_at: Time.at(timestamp) + que_job = JobWrapper.enqueue job.serialize, run_at: Time.at(timestamp) + job.provider_job_id = que_job.attrs["job_id"] + que_job end class JobWrapper < Que::Job #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb index 743d5ea333..c321776bf5 100644 --- a/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/sidekiq_adapter.rb @@ -17,7 +17,7 @@ module ActiveJob class SidekiqAdapter def enqueue(job) #:nodoc: #Sidekiq::Client does not support symbols as keys - Sidekiq::Client.push \ + job.provider_job_id = Sidekiq::Client.push \ 'class' => JobWrapper, 'wrapped' => job.class.to_s, 'queue' => job.queue_name, @@ -25,7 +25,7 @@ module ActiveJob end def enqueue_at(job, timestamp) #:nodoc: - Sidekiq::Client.push \ + job.provider_job_id = Sidekiq::Client.push \ 'class' => JobWrapper, 'wrapped' => job.class.to_s, 'queue' => job.queue_name, diff --git a/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb index f5737487ca..f102c6567e 100644 --- a/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/sneakers_adapter.rb @@ -28,7 +28,7 @@ module ActiveJob end def enqueue_at(job, timestamp) #:nodoc: - raise NotImplementedError + raise NotImplementedError, "This queueing backend does not support scheduling jobs. To see what features are supported go to http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html" end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb b/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb index 64c93e8198..c6c35f8ab4 100644 --- a/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb +++ b/activejob/lib/active_job/queue_adapters/sucker_punch_adapter.rb @@ -23,7 +23,7 @@ module ActiveJob end def enqueue_at(job, timestamp) #:nodoc: - raise NotImplementedError + raise NotImplementedError, "This queueing backend does not support scheduling jobs. To see what features are supported go to http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html" end class JobWrapper #:nodoc: diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb index 4efb4b72d2..9b307e8dc8 100644 --- a/activejob/lib/active_job/test_helper.rb +++ b/activejob/lib/active_job/test_helper.rb @@ -68,7 +68,7 @@ module ActiveJob original_count = enqueued_jobs_size(only: only) yield new_count = enqueued_jobs_size(only: only) - assert_equal original_count + number, new_count, "#{number} jobs expected, but #{new_count - original_count} were enqueued" + assert_equal number, new_count - original_count, "#{number} jobs expected, but #{new_count - original_count} were enqueued" else actual_count = enqueued_jobs_size(only: only) assert_equal number, actual_count, "#{number} jobs expected, but #{actual_count} were enqueued" @@ -164,7 +164,7 @@ module ActiveJob original_count = performed_jobs.size perform_enqueued_jobs(only: only) { yield } new_count = performed_jobs.size - assert_equal original_count + number, new_count, + assert_equal number, new_count - original_count, "#{number} jobs expected, but #{new_count - original_count} were performed" else performed_jobs_size = performed_jobs.size diff --git a/activejob/lib/rails/generators/job/job_generator.rb b/activejob/lib/rails/generators/job/job_generator.rb index 86e4c5266c..2115fb9f71 100644 --- a/activejob/lib/rails/generators/job/job_generator.rb +++ b/activejob/lib/rails/generators/job/job_generator.rb @@ -1,6 +1,6 @@ require 'rails/generators/named_base' -module Rails +module Rails # :nodoc: module Generators # :nodoc: class JobGenerator < Rails::Generators::NamedBase # :nodoc: desc 'This generator creates an active job file at app/jobs' diff --git a/activejob/test/cases/test_helper_test.rb b/activejob/test/cases/test_helper_test.rb index 19a2820a6e..04c4c446e2 100644 --- a/activejob/test/cases/test_helper_test.rb +++ b/activejob/test/cases/test_helper_test.rb @@ -31,6 +31,17 @@ class EnqueuedJobsTest < ActiveJob::TestCase end end + def test_assert_enqueued_jobs_message + HelloJob.perform_later('sean') + e = assert_raises Minitest::Assertion do + assert_enqueued_jobs 2 do + HelloJob.perform_later('sean') + end + end + assert_match "Expected: 2", e.message + assert_match "Actual: 1", e.message + end + def test_assert_enqueued_jobs_with_no_block assert_nothing_raised do HelloJob.perform_later('rafael') @@ -230,6 +241,17 @@ class PerformedJobsTest < ActiveJob::TestCase end end + def test_assert_performed_jobs_message + HelloJob.perform_later('sean') + e = assert_raises Minitest::Assertion do + assert_performed_jobs 2 do + HelloJob.perform_later('sean') + end + end + assert_match "Expected: 2", e.message + assert_match "Actual: 1", e.message + end + def test_assert_performed_jobs_with_no_block assert_nothing_raised do perform_enqueued_jobs do diff --git a/activejob/test/helper.rb b/activejob/test/helper.rb index 72ec2b8904..57907042d9 100644 --- a/activejob/test/helper.rb +++ b/activejob/test/helper.rb @@ -14,5 +14,3 @@ else end require 'active_support/testing/autorun' - -ActiveSupport::TestCase.test_order = :random diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb index 09f5c329cc..d345092dee 100644 --- a/activejob/test/integration/queuing_test.rb +++ b/activejob/test/integration/queuing_test.rb @@ -11,7 +11,7 @@ class QueuingTest < ActiveSupport::TestCase end test 'should not run jobs queued on a non-listening queue' do - skip if adapter_is?(:inline) || adapter_is?(:sucker_punch) + skip if adapter_is?(:inline, :sucker_punch, :que) old_queue = TestJob.queue_name begin @@ -55,4 +55,17 @@ class QueuingTest < ActiveSupport::TestCase skip end end + + test 'should supply a provider_job_id when available for immediate jobs' do + skip unless adapter_is?(:delayed_job, :sidekiq, :qu, :que) + test_job = TestJob.perform_later @id + refute test_job.provider_job_id.nil?, 'Provider job id should be set by provider' + end + + test 'should supply a provider_job_id when available for delayed jobs' do + skip unless adapter_is?(:delayed_job, :sidekiq, :que) + delayed_test_job = TestJob.set(wait: 1.minute).perform_later @id + refute delayed_test_job.provider_job_id.nil?, + 'Provider job id should by set for delayed jobs by provider' + end end diff --git a/activejob/test/support/integration/adapters/que.rb b/activejob/test/support/integration/adapters/que.rb index ff5235bdc8..0cd8952a28 100644 --- a/activejob/test/support/integration/adapters/que.rb +++ b/activejob/test/support/integration/adapters/que.rb @@ -23,7 +23,7 @@ module QueJobsManager @thread = Thread.new do loop do - Que::Job.work("integration_tests") + Que::Job.work sleep 0.5 end end diff --git a/activejob/test/support/integration/test_case_helpers.rb b/activejob/test/support/integration/test_case_helpers.rb index bed28b2900..7e87ede275 100644 --- a/activejob/test/support/integration/test_case_helpers.rb +++ b/activejob/test/support/integration/test_case_helpers.rb @@ -27,8 +27,8 @@ module TestCaseHelpers jobs_manager.clear_jobs end - def adapter_is?(adapter_class_symbol) - ActiveJob::Base.queue_adapter.class.name.split("::").last.gsub(/Adapter$/, '').underscore == adapter_class_symbol.to_s + def adapter_is?(*adapter_class_symbols) + adapter_class_symbols.map(&:to_s).include?(ActiveJob::Base.queue_adapter.class.name.split("::").last.gsub(/Adapter$/, '').underscore) end def wait_for_jobs_to_finish_for(seconds=60) diff --git a/activejob/test/support/que/inline.rb b/activejob/test/support/que/inline.rb index 2e210acb6b..0232da1370 100644 --- a/activejob/test/support/que/inline.rb +++ b/activejob/test/support/que/inline.rb @@ -3,7 +3,11 @@ 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) + if args.last.is_a?(Hash) + options = args.pop + options.delete(:run_at) + args << options unless options.empty? + end self.run(*args) end end diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 4961d69d2a..3c1510b31d 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,3 +1,8 @@ +* Ensure `method_missing` is called for methods passed to + `ActiveModel::Serialization#serializable_hash` that don't exist. + + *Jay Elaraj* + * Add `ActiveModel::Dirty#[attr_name]_previously_changed?` and `ActiveModel::Dirty#[attr_name]_previous_change` to improve access to recorded changes after the model has been saved. diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc index 4920666f27..5c36b1277e 100644 --- a/activemodel/README.rdoc +++ b/activemodel/README.rdoc @@ -242,7 +242,7 @@ behavior out of the box: The latest version of Active Model can be installed with RubyGems: - % [sudo] gem install activemodel + % gem install activemodel Source code can be downloaded as part of the Rails project on GitHub diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index c0fc507286..0169c20e0b 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -102,10 +102,10 @@ module ActiveModel # person.changes # => {"name" => ["Bill", "Bob"]} # # If an attribute is modified in-place then make use of - # +[attribute_name]_will_change!+ to mark that the attribute is changing. + # <tt>[attribute_name]_will_change!</tt> to mark that the attribute is changing. # Otherwise \Active \Model can't track changes to in-place attributes. Note # that Active Record can detect in-place modifications automatically. You do - # not need to call +[attribute_name]_will_change!+ on Active Record models. + # not need to call <tt>[attribute_name]_will_change!</tt> on Active Record models. # # person.name_will_change! # person.name_change # => ["Bill", "Bill"] diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index f843b279ce..287a2559d2 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -452,7 +452,6 @@ module ActiveModel defaults = [] end - defaults << options.delete(:message) defaults << :"#{@base.class.i18n_scope}.errors.messages.#{type}" if @base.class.respond_to?(:i18n_scope) defaults << :"errors.attributes.#{attribute}.#{type}" defaults << :"errors.messages.#{type}" @@ -461,6 +460,7 @@ module ActiveModel defaults.flatten! key = defaults.shift + defaults = options.delete(:message) if options[:message] value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil) options = { diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index 976f50b13e..c06a64c75f 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -107,7 +107,7 @@ module ActiveModel hash = {} attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) } - Array(options[:methods]).each { |m| hash[m.to_s] = send(m) if respond_to?(m) } + Array(options[:methods]).each { |m| hash[m.to_s] = send(m) } serializable_add_includes(options) do |association, records, opts| hash[association.to_s] = if records.respond_to?(:to_ary) diff --git a/activemodel/test/cases/attribute_assignment_test.rb b/activemodel/test/cases/attribute_assignment_test.rb index 3b01644dd1..64a85e01eb 100644 --- a/activemodel/test/cases/attribute_assignment_test.rb +++ b/activemodel/test/cases/attribute_assignment_test.rb @@ -58,8 +58,6 @@ class AttributeAssignmentTest < ActiveModel::TestCase end test "assign private attribute" do - rubinius_skip "https://github.com/rubinius/rubinius/issues/3328" - model = Model.new assert_raises(ActiveModel::UnknownAttributeError) do model.assign_attributes(metadata: { a: 1 }) diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index f781a0017f..d10c20caad 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -149,6 +149,12 @@ class ErrorsTest < ActiveModel::TestCase assert_equal ["cannot be blank"], person.errors[:name] end + test "add an error message on a specific attribute with a defined type" do + person = Person.new + person.errors.add(:name, :blank, message: "cannot be blank") + assert_equal ["cannot be blank"], person.errors[:name] + end + test "add an error with a symbol" do person = Person.new person.errors.add(:name, :blank) diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serialization_test.rb index 4ae41aa19c..8d3165cd78 100644 --- a/activemodel/test/cases/serialization_test.rb +++ b/activemodel/test/cases/serialization_test.rb @@ -16,6 +16,14 @@ class SerializationTest < ActiveModel::TestCase instance_values.except("address", "friends") end + def method_missing(method_name, *args) + if method_name == :bar + 'i_am_bar' + else + super + end + end + def foo 'i_am_foo' end @@ -58,23 +66,22 @@ class SerializationTest < ActiveModel::TestCase end def test_method_serializable_hash_should_work_with_methods_option - expected = {"name"=>"David", "gender"=>"male", "foo"=>"i_am_foo", "email"=>"david@example.com"} - assert_equal expected, @user.serializable_hash(methods: [:foo]) + expected = {"name"=>"David", "gender"=>"male", "foo"=>"i_am_foo", "bar"=>"i_am_bar", "email"=>"david@example.com"} + assert_equal expected, @user.serializable_hash(methods: [:foo, :bar]) end def test_method_serializable_hash_should_work_with_only_and_methods - expected = {"foo"=>"i_am_foo"} - assert_equal expected, @user.serializable_hash(only: [], methods: [:foo]) + expected = {"foo"=>"i_am_foo", "bar"=>"i_am_bar"} + assert_equal expected, @user.serializable_hash(only: [], methods: [:foo, :bar]) end def test_method_serializable_hash_should_work_with_except_and_methods - expected = {"gender"=>"male", "foo"=>"i_am_foo"} - assert_equal expected, @user.serializable_hash(except: [:name, :email], methods: [:foo]) + expected = {"gender"=>"male", "foo"=>"i_am_foo", "bar"=>"i_am_bar"} + assert_equal expected, @user.serializable_hash(except: [:name, :email], methods: [:foo, :bar]) end - def test_should_not_call_methods_that_dont_respond - expected = {"name"=>"David", "gender"=>"male", "email"=>"david@example.com"} - assert_equal expected, @user.serializable_hash(methods: [:bar]) + def test_should_raise_NoMethodError_for_non_existing_method + assert_raise(NoMethodError) { @user.serializable_hash(methods: [:nada]) } end def test_should_use_read_attribute_for_serialization diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index b6a943eca7..3663a6faa4 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,36 @@ +* Allow single table inheritance instantiation to work when storing + demodulized class names. + + *Alex Robbin* + +* Correctly pass MySQL options when using structure_dump or structure_load + + Specifically, it fixes an issue when using SSL authentication. + + *Alex Coomans* + +* Dump indexes in `create_table` instead of `add_index`. + + If the adapter supports indexes in create table, generated SQL is + slightly more efficient. + + *Ryuta Kamizono* + +* Correctly dump `:options` on `create_table` for MySQL. + + *Ryuta Kamizono* + +* PostgreSQL: `:collation` support for string and text columns. + + Example: + + create_table :foos do |t| + t.string :string_en, collation: 'en_US.UTF-8' + t.text :text_ja, collation: 'ja_JP.UTF-8' + end + + *Ryuta Kamizono* + * Make `unscope` aware of "less than" and "greater than" conditions. *TAKAHASHI Kazuaki* @@ -745,7 +778,7 @@ *Yves Senn* -* Fix bug with 'ActiveRecord::Type::Numeric' that caused negative values to +* Fix bug with `ActiveRecord::Type::Numeric` that caused negative values to be marked as having changed when set to the same negative value. Closes #18161. diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index f4777919d3..85d807933a 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -188,7 +188,7 @@ Admit the Database: The latest version of Active Record can be installed with RubyGems: - % [sudo] gem install activerecord + % gem install activerecord Source code can be downloaded as part of the Rails project on GitHub: diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index c5c2178ee2..94f59f9e95 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -185,7 +185,7 @@ module ActiveRecord super end - # Returns the specified association instance if it responds to :loaded?, nil otherwise. + # Returns the specified association instance if it exists, nil otherwise. def association_instance_get(name) @association_cache[name] end @@ -285,7 +285,7 @@ module ActiveRecord # end # # If your model class is <tt>Project</tt>, the module is - # named <tt>Project::GeneratedFeatureMethods</tt>. The GeneratedFeatureMethods module is + # named <tt>Project::GeneratedAssociationMethods</tt>. The GeneratedAssociationMethods module is # included in the model class immediately after the (anonymous) generated attributes methods # module, meaning an association will override the methods for an attribute with the same name. # diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index cc265e2af6..45fdcaa1cd 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -29,7 +29,8 @@ module ActiveRecord assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? end - # Re-raise with the ActiveRecord constant in case of an error + # Tries to assign given value to given attribute. + # In case of an error, re-raises with the ActiveRecord constant. def _assign_attribute(k, v) # :nodoc: super rescue ActiveModel::UnknownAttributeError diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 8c50f3d1a3..77e64a22be 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -220,7 +220,7 @@ module ActiveRecord include MonitorMixin - attr_accessor :automatic_reconnect, :checkout_timeout + attr_accessor :automatic_reconnect, :checkout_timeout, :schema_cache attr_reader :spec, :connections, :size, :reaper # Creates a new ConnectionPool object. +spec+ is a ConnectionSpecification @@ -432,7 +432,9 @@ module ActiveRecord end def new_connection - Base.send(spec.adapter_method, spec.config) + Base.send(spec.adapter_method, spec.config).tap do |conn| + conn.schema_cache = schema_cache.dup if schema_cache + end end def current_connection_id #:nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb index c0a2111571..30b2fca2ca 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -18,9 +18,9 @@ module ActiveRecord end # Returns the maximum allowed length for an index name. This - # limit is enforced by rails and Is less than or equal to + # limit is enforced by \Rails and is less than or equal to # <tt>index_name_length</tt>. The gap between - # <tt>index_name_length</tt> is to allow internal rails + # <tt>index_name_length</tt> is to allow internal \Rails # operations to use prefixes in temporary operations. def allowed_index_name_length index_name_length diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 42c794c828..431fe25501 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -40,8 +40,9 @@ module ActiveRecord # Returns a single value from a record def select_value(arel, name = nil, binds = []) - if result = select_one(arel, name, binds) - result.values.first + arel, binds = binds_from_relation arel, binds + if result = select_rows(to_sql(arel, binds), name, binds).first + result.first end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 91c7298983..2c7409b2dc 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -123,6 +123,8 @@ module ActiveRecord 'f' end + # Quote date/time values for use in SQL input. Includes microseconds + # if the value is a Time responding to usec. def quoted_date(value) if value.acts_like?(:time) zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index f754df93b6..18d943f452 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -14,10 +14,6 @@ module ActiveRecord send m, o end - def visit_AddColumn(o) - "ADD #{accept(o)}" - end - delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, to: :@conn private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql @@ -25,7 +21,7 @@ module ActiveRecord def visit_AlterTable(o) sql = "ALTER TABLE #{quote_table_name(o.name)} " - sql << o.adds.map { |col| visit_AddColumn col }.join(' ') + sql << o.adds.map { |col| accept col }.join(' ') sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ') sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ') end @@ -37,6 +33,10 @@ module ActiveRecord column_sql end + def visit_AddColumnDefinition(o) + "ADD #{accept(o.column)}" + end + def visit_TableDefinition(o) create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE " create_sql << "#{quote_table_name(o.name)} " @@ -70,6 +70,7 @@ module ActiveRecord column_options[:after] = o.after column_options[:auto_increment] = o.auto_increment column_options[:primary_key] = o.primary_key + column_options[:collation] = o.collation column_options end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 4761024ad0..0ccf0c498b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -15,13 +15,16 @@ module ActiveRecord # are typically created by methods in TableDefinition, and added to the # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. - class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :sql_type) #:nodoc: + class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type) #:nodoc: def primary_key? primary_key || type.to_sym == :primary_key end end + class AddColumnDefinition < Struct.new(:column) # :nodoc: + end + class ChangeColumnDefinition < Struct.new(:column, :name) #:nodoc: end @@ -227,7 +230,7 @@ module ActiveRecord # The +type+ parameter is normally one of the migrations native types, # which is one of the following: # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>, - # <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>, + # <tt>:integer</tt>, <tt>:bigint</tt>, <tt>:float</tt>, <tt>:decimal</tt>, # <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>, # <tt>:binary</tt>, <tt>:boolean</tt>. # @@ -434,6 +437,7 @@ module ActiveRecord column.after = options[:after] column.auto_increment = options[:auto_increment] column.primary_key = type == :primary_key || options[:primary_key] + column.collation = options[:collation] column end @@ -476,7 +480,7 @@ module ActiveRecord def add_column(name, type, options) name = name.to_s type = type.to_sym - @adds << @td.new_column_definition(name, type, options) + @adds << AddColumnDefinition.new(@td.new_column_definition(name, type, options)) end 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 999cb0ec5a..deb014ad46 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -35,12 +35,16 @@ module ActiveRecord default = schema_default(column) if column.has_default? spec[:default] = default unless default.nil? + if collation = schema_collation(column) + spec[:collation] = collation + end + spec end # Lists the valid migration options def migration_keys - [:name, :limit, :precision, :scale, :default, :null] + [:name, :limit, :precision, :scale, :default, :null, :collation] end private @@ -56,6 +60,10 @@ module ActiveRecord type.type_cast_for_schema(default) end end + + def schema_collation(column) + column.collation.inspect if column.collation + end end end end 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 879a47f021..9004d86b04 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -14,6 +14,10 @@ module ActiveRecord {} end + def table_options(table_name) + nil + end + # Truncates a table alias according to the limits of the current adapter. def table_alias_for(table_name) table_name[0...table_alias_length].tr('.', '_') @@ -930,13 +934,13 @@ module ActiveRecord def add_index_options(table_name, column_name, options = {}) #:nodoc: column_names = Array(column_name) - index_name = index_name(table_name, column: column_names) options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type) - index_type = options[:unique] ? "UNIQUE" : "" index_type = options[:type].to_s if options.key?(:type) + index_type ||= options[:unique] ? "UNIQUE" : "" index_name = options[:name].to_s if options.key?(:name) + index_name ||= index_name(table_name, column: column_names) max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length if options.key?(:algorithm) @@ -1049,7 +1053,7 @@ module ActiveRecord end end - def validate_index_length!(table_name, new_name) + def validate_index_length!(table_name, new_name) # :nodoc: if new_name.length > allowed_index_name_length raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index ae42e8ef8d..0705c22a8c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -387,8 +387,8 @@ module ActiveRecord end end - def new_column(name, default, sql_type_metadata = nil, null = true) - Column.new(name, default, sql_type_metadata, null) + def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) + Column.new(name, default, sql_type_metadata, null, default_function, collation) end def lookup_cast_type(sql_type) # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 76aee452ca..0862c7678e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -14,7 +14,7 @@ module ActiveRecord end class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition - attr_accessor :charset, :collation + attr_accessor :charset end class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition @@ -28,7 +28,6 @@ module ActiveRecord column.auto_increment = true end column.charset = options[:charset] - column.collation = options[:collation] column end @@ -44,10 +43,6 @@ module ActiveRecord end class SchemaCreation < AbstractAdapter::SchemaCreation - def visit_AddColumn(o) - add_column_position!(super, column_options(o)) - end - private def visit_DropForeignKey(name) @@ -67,6 +62,10 @@ module ActiveRecord create_sql end + def visit_AddColumnDefinition(o) + add_column_position!(super, column_options(o.column)) + end + def visit_ChangeColumnDefinition(o) change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}" add_column_position!(change_column_sql, column_options(o.column)) @@ -75,7 +74,6 @@ module ActiveRecord def column_options(o) column_options = super column_options[:charset] = o.charset - column_options[:collation] = o.collation column_options end @@ -99,8 +97,8 @@ module ActiveRecord end def index_in_create(table_name, column_name, options) - index_name, index_type, index_columns, index_options, index_algorithm, index_using = @conn.add_index_options(table_name, column_name, options) - "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_options} #{index_algorithm}" + index_name, index_type, index_columns, _, _, index_using = @conn.add_index_options(table_name, column_name, options) + "#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns}) " end end @@ -128,20 +126,20 @@ module ActiveRecord spec = super spec.delete(:precision) if /time/ === column.sql_type && column.precision == 0 spec.delete(:limit) if :boolean === column.type + spec + end + + def schema_collation(column) if column.collation && table_name = column.instance_variable_get(:@table_name) @collation_cache ||= {} @collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"] - spec[:collation] = column.collation.inspect if column.collation != @collation_cache[table_name] + column.collation.inspect if column.collation != @collation_cache[table_name] end - spec - end - - def migration_keys - super + [:collation] end + private :schema_collation class Column < ConnectionAdapters::Column # :nodoc: - delegate :strict, :collation, :extra, to: :sql_type_metadata, allow_nil: true + delegate :strict, :extra, to: :sql_type_metadata, allow_nil: true def initialize(*) super @@ -195,12 +193,11 @@ module ActiveRecord end class MysqlTypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: - attr_reader :collation, :extra, :strict + attr_reader :extra, :strict - def initialize(type_metadata, collation: "", extra: "", strict: false) + def initialize(type_metadata, extra: "", strict: false) super(type_metadata) @type_metadata = type_metadata - @collation = collation @extra = extra @strict = strict end @@ -218,7 +215,7 @@ module ActiveRecord protected def attributes_for_hash - [self.class, @type_metadata, collation, extra, strict] + [self.class, @type_metadata, extra, strict] end end @@ -342,8 +339,8 @@ module ActiveRecord raise NotImplementedError end - def new_column(field, default, sql_type_metadata = nil, null = true) # :nodoc: - Column.new(field, default, sql_type_metadata, null) + def new_column(field, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc: + Column.new(field, default, sql_type_metadata, null, default_function, collation) end # Must return the MySQL error number from the exception, if the exception has an @@ -566,8 +563,8 @@ module ActiveRecord each_hash(result).map do |field| field_name = set_field_encoding(field[:Field]) sql_type = field[:Type] - type_metadata = fetch_type_metadata(sql_type, field[:Collation], field[:Extra]) - new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES") + type_metadata = fetch_type_metadata(sql_type, field[:Extra]) + new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES", nil, field[:Collation]) end end end @@ -634,7 +631,7 @@ module ActiveRecord change_column table_name, column_name, column.sql_type, :default => default end - def change_column_null(table_name, column_name, null, default = nil) + def change_column_null(table_name, column_name, null, default = nil) #:nodoc: column = column_for(table_name, column_name) unless null || default.nil? @@ -654,8 +651,8 @@ module ActiveRecord end def add_index(table_name, column_name, options = {}) #:nodoc: - index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options) - execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}" + index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options) + execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}" end def foreign_keys(table_name) @@ -686,33 +683,25 @@ module ActiveRecord end end + def table_options(table_name) + create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + + # strip create_definitions and partition_options + raw_table_options = create_table_info.sub(/\A.*\n\) /m, '').sub(/\n\/\*!.*\*\/\n\z/m, '').strip + + # strip AUTO_INCREMENT + raw_table_options.sub(/(ENGINE=\w+)(?: AUTO_INCREMENT=\d+)/, '\1') + end + # Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) case type.to_s when 'binary' - case limit - when 0..0xfff; "varbinary(#{limit})" - when nil; "blob" - when 0x1000..0xffffffff; "blob(#{limit})" - else raise(ActiveRecordError, "No binary type has character length #{limit}") - end + binary_to_sql(limit) when 'integer' - case limit - when 1; 'tinyint' - when 2; 'smallint' - when 3; 'mediumint' - when nil, 4, 11; 'int(11)' # compatibility with MySQL default - when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}") - end + integer_to_sql(limit) when 'text' - case limit - when 0..0xff; 'tinytext' - when nil, 0x100..0xffff; 'text' - when 0x10000..0xffffff; 'mediumtext' - when 0x1000000..0xffffffff; 'longtext' - else raise(ActiveRecordError, "No text type has character length #{limit}") - end + text_to_sql(limit) else super end @@ -826,21 +815,8 @@ module ActiveRecord end end - def fetch_type_metadata(sql_type, collation = "", extra = "") - MysqlTypeMetadata.new(super(sql_type), collation: collation, extra: extra, strict: strict_mode?) - end - - # MySQL is too stupid to create a temporary table for use subquery, so we have - # to give it some prompting in the form of a subsubquery. Ugh! - def subquery_for(key, select) - subsubselect = select.clone - subsubselect.projections = [key] - - subselect = Arel::SelectManager.new(select.engine) - subselect.project Arel.sql(key.name) - # Materialized subquery by adding distinct - # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' - subselect.from subsubselect.distinct.as('__active_record_temp') + def fetch_type_metadata(sql_type, extra = "") + MysqlTypeMetadata.new(super(sql_type), extra: extra, strict: strict_mode?) end def add_index_length(option_strings, column_names, options = {}) @@ -882,7 +858,7 @@ module ActiveRecord def add_column_sql(table_name, column_name, type, options = {}) td = create_table_definition(table_name) cd = td.new_column_definition(column_name, type, options) - schema_creation.visit_AddColumn cd + schema_creation.accept(AddColumnDefinition.new(cd)) end def change_column_sql(table_name, column_name, type, options = {}) @@ -924,8 +900,9 @@ module ActiveRecord end def add_index_sql(table_name, column_name, options = {}) - index_name, index_type, index_columns = add_index_options(table_name, column_name, options) - "ADD #{index_type} INDEX #{index_name} (#{index_columns})" + index_name, index_type, index_columns, _, index_algorithm, index_using = add_index_options(table_name, column_name, options) + index_algorithm[0, 0] = ", " if index_algorithm.present? + "ADD #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})#{index_algorithm}" end def remove_index_sql(table_name, options = {}) @@ -943,6 +920,19 @@ module ActiveRecord private + # MySQL is too stupid to create a temporary table for use subquery, so we have + # to give it some prompting in the form of a subsubquery. Ugh! + def subquery_for(key, select) + subsubselect = select.clone + subsubselect.projections = [key] + + subselect = Arel::SelectManager.new(select.engine) + subselect.project Arel.sql(key.name) + # Materialized subquery by adding distinct + # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' + subselect.from subsubselect.distinct.as('__active_record_temp') + end + def version @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map(&:to_i) end @@ -1009,6 +999,36 @@ module ActiveRecord TableDefinition.new(native_database_types, name, temporary, options, as) end + def binary_to_sql(limit) # :nodoc: + case limit + when 0..0xfff; "varbinary(#{limit})" + when nil; "blob" + when 0x1000..0xffffffff; "blob(#{limit})" + else raise(ActiveRecordError, "No binary type has character length #{limit}") + end + end + + def integer_to_sql(limit) # :nodoc: + case limit + when 1; 'tinyint' + when 2; 'smallint' + when 3; 'mediumint' + when nil, 4, 11; 'int(11)' # compatibility with MySQL default + when 5..8; 'bigint' + else raise(ActiveRecordError, "No integer type has byte size #{limit}") + end + end + + def text_to_sql(limit) # :nodoc: + case limit + when 0..0xff; 'tinytext' + when nil, 0x100..0xffff; 'text' + when 0x10000..0xffffff; 'mediumtext' + when 0x1000000..0xffffffff; 'longtext' + else raise(ActiveRecordError, "No text type has character length #{limit}") + end + end + class MysqlString < Type::String # :nodoc: def serialize(value) case value diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index f4dda5154e..4b95b0681d 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -12,7 +12,7 @@ module ActiveRecord ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ end - attr_reader :name, :null, :sql_type_metadata, :default, :default_function + attr_reader :name, :null, :sql_type_metadata, :default, :default_function, :collation delegate :precision, :scale, :limit, :type, :sql_type, to: :sql_type_metadata, allow_nil: true @@ -22,12 +22,13 @@ module ActiveRecord # +default+ is the type-casted default value, such as +new+ in <tt>sales_stage varchar(20) default 'new'</tt>. # +sql_type_metadata+ is various information about the type of the column # +null+ determines if this column allows +NULL+ values. - def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil) + def initialize(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) @name = name @sql_type_metadata = sql_type_metadata @null = null @default = default @default_function = default_function + @collation = collation @table_name = nil end @@ -60,7 +61,7 @@ module ActiveRecord protected def attributes_for_hash - [self.class, name, default, sql_type_metadata, null, default_function] + [self.class, name, default, sql_type_metadata, null, default_function, collation] end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 21631be25c..e97e82f056 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,6 +1,6 @@ require 'active_record/connection_adapters/abstract_mysql_adapter' -gem 'mysql2', '~> 0.3.13' +gem 'mysql2', '~> 0.3.18' require 'mysql2' module ActiveRecord diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index b7755c4593..f175730551 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -40,8 +40,7 @@ module ActiveRecord PGconn.quote_ident(name.to_s) end - # Quote date/time values for use in SQL input. Includes microseconds - # if the value is a Time responding to usec. + # Quote date/time values for use in SQL input. def quoted_date(value) #:nodoc: if value.year <= 0 bce_year = format("%04d", -value.year + 1) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 168180cfd3..412999f0fe 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -8,6 +8,13 @@ module ActiveRecord o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.array) super end + + def add_column_options!(sql, options) + if options[:collation] + sql << " COLLATE \"#{options[:collation]}\"" + end + super + end end module SchemaStatements @@ -63,7 +70,7 @@ module ActiveRecord # Returns the list of all tables in the schema search path or a specified schema. def tables(name = nil) - query(<<-SQL, 'SCHEMA').map { |row| row[0] } + select_values(<<-SQL, 'SCHEMA') SELECT tablename FROM pg_tables WHERE schemaname = ANY (current_schemas(false)) @@ -77,7 +84,7 @@ module ActiveRecord name = Utils.extract_schema_qualified_name(name.to_s) return false unless name.identifier - exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 + select_value(<<-SQL, 'SCHEMA').to_i > 0 SELECT COUNT(*) FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace @@ -93,15 +100,16 @@ module ActiveRecord # Returns true if schema exists. def schema_exists?(name) - exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 + select_value(<<-SQL, 'SCHEMA').to_i > 0 SELECT COUNT(*) FROM pg_namespace WHERE nspname = '#{name}' SQL end + # Verifies existence of an index with a given name. def index_name_exists?(table_name, index_name, default) - exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 + select_value(<<-SQL, 'SCHEMA').to_i > 0 SELECT COUNT(*) FROM pg_class t INNER JOIN pg_index d ON t.oid = d.indrelid @@ -158,33 +166,33 @@ module ActiveRecord # Returns the list of all column definitions for a table. def columns(table_name) # Limit, precision, and scale are all handled by the superclass. - column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod| + column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation| oid = oid.to_i fmod = fmod.to_i type_metadata = fetch_type_metadata(column_name, type, oid, fmod) default_value = extract_value_from_default(default) default_function = extract_default_function(default_value, default) - new_column(column_name, default_value, type_metadata, !notnull, default_function) + new_column(column_name, default_value, type_metadata, !notnull, default_function, collation) end end - def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil) # :nodoc: - PostgreSQLColumn.new(name, default, sql_type_metadata, null, default_function) + def new_column(name, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc: + PostgreSQLColumn.new(name, default, sql_type_metadata, null, default_function, collation) end # Returns the current database name. def current_database - query('select current_database()', 'SCHEMA')[0][0] + select_value('select current_database()', 'SCHEMA') end # Returns the current schema name. def current_schema - query('SELECT current_schema', 'SCHEMA')[0][0] + select_value('SELECT current_schema', 'SCHEMA') end # Returns the current database encoding format. def encoding - query(<<-end_sql, 'SCHEMA')[0][0] + select_value(<<-end_sql, 'SCHEMA') SELECT pg_encoding_to_char(pg_database.encoding) FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' end_sql @@ -192,21 +200,21 @@ module ActiveRecord # Returns the current database collation. def collation - query(<<-end_sql, 'SCHEMA')[0][0] + select_value(<<-end_sql, 'SCHEMA') SELECT pg_database.datcollate FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' end_sql end # Returns the current database ctype. def ctype - query(<<-end_sql, 'SCHEMA')[0][0] + select_value(<<-end_sql, 'SCHEMA') SELECT pg_database.datctype FROM pg_database WHERE pg_database.datname LIKE '#{current_database}' end_sql end # Returns an array of schema names. def schema_names - query(<<-SQL, 'SCHEMA').flatten + select_values(<<-SQL, 'SCHEMA') SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' @@ -239,12 +247,12 @@ module ActiveRecord # Returns the active schema search path. def schema_search_path - @schema_search_path ||= query('SHOW search_path', 'SCHEMA')[0][0] + @schema_search_path ||= select_value('SHOW search_path', 'SCHEMA') end # Returns the current client message level. def client_min_messages - query('SHOW client_min_messages', 'SCHEMA')[0][0] + select_value('SHOW client_min_messages', 'SCHEMA') end # Set the client message level. @@ -262,10 +270,7 @@ module ActiveRecord end def serial_sequence(table, column) - result = exec_query(<<-eosql, 'SCHEMA') - SELECT pg_get_serial_sequence('#{table}', '#{column}') - eosql - result.rows.first.first + select_value("SELECT pg_get_serial_sequence('#{table}', '#{column}')", 'SCHEMA') end # Sets the sequence of a table's primary key to the specified value. @@ -276,9 +281,7 @@ module ActiveRecord if sequence quoted_sequence = quote_table_name(sequence) - select_value <<-end_sql, 'SCHEMA' - SELECT setval('#{quoted_sequence}', #{value}) - end_sql + select_value("SELECT setval('#{quoted_sequence}', #{value})", 'SCHEMA') else @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger end @@ -301,7 +304,7 @@ module ActiveRecord if pk && sequence quoted_sequence = quote_table_name(sequence) - select_value <<-end_sql, 'SCHEMA' + select_value(<<-end_sql, 'SCHEMA') SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) end_sql end @@ -363,7 +366,7 @@ module ActiveRecord # Returns just a table's primary key def primary_key(table) - pks = exec_query(<<-end_sql, 'SCHEMA').rows + pks = query(<<-end_sql, 'SCHEMA') SELECT attr.attname FROM pg_attribute attr INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey) @@ -395,20 +398,20 @@ module ActiveRecord rename_table_indexes(table_name, new_name) end - # Adds a new column to the named table. - # See TableDefinition#column for details of the options you can use. - def add_column(table_name, column_name, type, options = {}) + def add_column(table_name, column_name, type, options = {}) #:nodoc: clear_cache! super end - # Changes the column of a table. - def change_column(table_name, column_name, type, options = {}) + def change_column(table_name, column_name, type, options = {}) #:nodoc: clear_cache! quoted_table_name = quote_table_name(table_name) quoted_column_name = quote_column_name(column_name) sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale], options[:array]) sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}" + if options[:collation] + sql << " COLLATE \"#{options[:collation]}\"" + end if options[:using] sql << " USING #{options[:using]}" elsif options[:cast_as] @@ -437,7 +440,7 @@ module ActiveRecord end end - def change_column_null(table_name, column_name, null, default = nil) + def change_column_null(table_name, column_name, null, default = nil) #:nodoc: clear_cache! unless null || default.nil? column = column_for(table_name, column_name) @@ -447,7 +450,7 @@ module ActiveRecord end # Renames a column in a table. - def rename_column(table_name, column_name, new_column_name) + def rename_column(table_name, column_name, new_column_name) #:nodoc: clear_cache! execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}" rename_column_indexes(table_name, column_name, new_column_name) @@ -462,6 +465,8 @@ module ActiveRecord execute "DROP INDEX #{quote_table_name(index_name)}" end + # Renames an index of a table. Raises error if length of new + # index name is greater than allowed limit. def rename_index(table_name, old_name, new_name) validate_index_length!(table_name, new_name) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 332ac9d88c..2b33a5b9cb 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -770,9 +770,11 @@ module ActiveRecord # - format_type includes the column size constraint, e.g. varchar(50) # - ::regclass is a function that gives the id for a table name def column_definitions(table_name) # :nodoc: - exec_query(<<-end_sql, 'SCHEMA').rows + query(<<-end_sql, 'SCHEMA') SELECT a.attname, format_type(a.atttypid, a.atttypmod), - pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod + pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod, + (SELECT c.collname FROM pg_collation c, pg_type t + WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation) FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 37ff4e4613..981d5d7a3c 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -13,6 +13,14 @@ module ActiveRecord @tables = {} end + def initialize_dup(other) + super + @columns = @columns.dup + @columns_hash = @columns_hash.dup + @primary_keys = @primary_keys.dup + @tables = @tables.dup + end + def primary_keys(table_name) @primary_keys[table_name] ||= table_exists?(table_name) ? connection.primary_key(table_name) : nil end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 7e184dd510..3186769510 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -450,7 +450,7 @@ module ActiveRecord end end - def change_column_null(table_name, column_name, null, default = nil) + def change_column_null(table_name, column_name, null, default = nil) #:nodoc: unless null || default.nil? exec_query("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL") end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 1ad910c4bc..8a014e682e 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -88,9 +88,10 @@ module ActiveRecord ## # :singleton-method: # Specifies which database schemas to dump when calling db:structure:dump. - # If :schema_search_path (the default), it will dumps any schemas listed in schema_search_path. - # Use :all to always dumps all schemas regardless of the schema_search_path. - # A string of comma separated schemas can also be used to pass a custom list of schemas. + # If the value is :schema_search_path (the default), any schemas listed in + # schema_search_path are dumped. Use :all to dump all schemas regardless + # of schema_search_path, or a string of comma separated schemas for a + # custom list. mattr_accessor :dump_schemas, instance_writer: false self.dump_schemas = :schema_search_path diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 98aee77557..0f1759abaa 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -71,9 +71,9 @@ module ActiveRecord class RecordNotDestroyed < ActiveRecordError attr_reader :record - def initialize(record) + def initialize(message, record = nil) @record = record - super() + super(message) end end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 24098f72dc..e613d157aa 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -198,14 +198,16 @@ module ActiveRecord def subclass_from_attributes(attrs) subclass_name = attrs.with_indifferent_access[inheritance_column] - if subclass_name.present? && subclass_name != self.name - subclass = subclass_name.safe_constantize + if subclass_name.present? + subclass = find_sti_class(subclass_name) - unless descendants.include?(subclass) - raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}") - end + if subclass.name != self.name + unless descendants.include?(subclass) + raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass.name} is not a subclass of #{name}") + end - subclass + subclass + end end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index a83b90a95f..ef446ce596 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -307,9 +307,8 @@ module ActiveRecord # # == Reversible Migrations # - # Starting with Rails 3.1, you will be able to define reversible migrations. # Reversible migrations are migrations that know how to go +down+ for you. - # You simply supply the +up+ logic, and the Migration system will figure out + # You simply supply the +up+ logic, and the Migration system figures out # how to execute the down commands for you. # # To define a reversible migration, define the +change+ method in your diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index da8f4d027a..466175690e 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -148,7 +148,7 @@ module ActiveRecord # Attributes marked as readonly are silently ignored if the record is # being updated. def save!(*args) - create_or_update(*args) || raise(RecordNotSaved.new(nil, self)) + create_or_update(*args) || raise(RecordNotSaved.new("Failed to save the record", self)) end # Deletes the record in the database and freezes this instance to @@ -193,7 +193,7 @@ module ActiveRecord # and #destroy! raises ActiveRecord::RecordNotDestroyed. # See ActiveRecord::Callbacks for further details. def destroy! - destroy || raise(ActiveRecord::RecordNotDestroyed, self) + destroy || raise(RecordNotDestroyed.new("Failed to destroy the record", self)) end # Returns an instance of the specified +klass+ with the attributes of the diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 7e907beec0..5af64b717a 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -93,6 +93,7 @@ module ActiveRecord cache = Marshal.load File.binread filename if cache.version == ActiveRecord::Migrator.current_version self.connection.schema_cache = cache + self.connection_pool.schema_cache = cache.dup else warn "Ignoring db/schema_cache.dump because it has expired. The current schema version is #{ActiveRecord::Migrator.current_version}, but the one in the cache is #{cache.version}." end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 576a32bf75..2001a89ffb 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -111,23 +111,11 @@ module ActiveRecord # Find the first record (or first N records if a parameter is supplied). # If no order is defined it will order by primary key. # - # Person.first # returns the first object fetched by SELECT * FROM people + # Person.first # returns the first object fetched by SELECT * FROM people ORDER BY people.id LIMIT 1 # Person.where(["user_name = ?", user_name]).first # Person.where(["user_name = :u", { u: user_name }]).first # Person.order("created_on DESC").offset(5).first - # Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3 - # - # ==== Rails 3 - # - # Person.first # SELECT "people".* FROM "people" LIMIT 1 - # - # NOTE: Rails 3 may not order this query by the primary key and the order - # will depend on the database implementation. In order to ensure that behavior, - # use <tt>User.order(:id).first</tt> instead. - # - # ==== Rails 4 - # - # Person.first # SELECT "people".* FROM "people" ORDER BY "people"."id" ASC LIMIT 1 + # Person.first(3) # returns the first three objects fetched by SELECT * FROM people ORDER BY people.id LIMIT 3 # def first(limit = nil) if limit diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index c7f55ebaa1..c2567311bd 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -11,17 +11,15 @@ module ActiveRecord protected - # Accepts an array, hash, or string of SQL conditions and sanitizes + # 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", group_id: 4 } returns "name='foo''bar' and group_id='4'" # "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'" def sanitize_sql_for_conditions(condition, table_name = self.table_name) return nil if condition.blank? case condition when Array; sanitize_sql_array(condition) - when Hash; sanitize_sql_hash_for_conditions(condition, table_name) else condition end end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index eaeaf0321b..a4a986e6ed 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -131,6 +131,10 @@ HEADER tbl.print ", id: false" end tbl.print ", force: :cascade" + + table_options = @connection.table_options(table) + tbl.print ", options: #{table_options.inspect}" unless table_options.blank? + tbl.puts " do |t|" # then dump all non-primary key columns @@ -168,11 +172,11 @@ HEADER tbl.puts end + indexes(table, tbl) + tbl.puts " end" tbl.puts - indexes(table, tbl) - tbl.rewind stream.print tbl.read rescue => e @@ -188,8 +192,7 @@ HEADER if (indexes = @connection.indexes(table)).any? add_index_statements = indexes.map do |index| statement_parts = [ - "add_index #{remove_prefix_and_suffix(index.table).inspect}", - index.columns.inspect, + "t.index #{index.columns.inspect}", "name: #{index.name.inspect}", ] statement_parts << 'unique: true' if index.unique @@ -203,11 +206,10 @@ HEADER statement_parts << "using: #{index.using.inspect}" if index.using statement_parts << "type: #{index.type.inspect}" if index.type - " #{statement_parts.join(', ')}" + " #{statement_parts.join(', ')}" end stream.puts add_index_statements.sort.join("\n") - stream.puts end end diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb index a3023a0cb4..ca11853da7 100644 --- a/activerecord/lib/active_record/secure_token.rb +++ b/activerecord/lib/active_record/secure_token.rb @@ -13,7 +13,7 @@ module ActiveRecord # # user = User.new # user.save - # user.token # => "4kUgL2pdQMSCQtjE" + # user.token # => "pX27zsMN2ViQKta1bGfLmVJE" # user.auth_token # => "77TMHrHJFvFDwodq8w7Ev2m7" # user.regenerate_token # => true # user.regenerate_auth_token # => true diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index eafbb2c249..bc80275a88 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -130,15 +130,21 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; end def prepare_command_options(command) - args = [command] - args.concat(['--user', configuration['username']]) if configuration['username'] - args << "--password=#{configuration['password']}" if configuration['password'] - args.concat(['--default-character-set', configuration['encoding']]) if configuration['encoding'] - configuration.slice('host', 'port', 'socket').each do |k, v| - args.concat([ "--#{k}", v.to_s ]) if v - end - - args + args = { + 'host' => '--host', + 'port' => '--port', + 'socket' => '--socket', + 'username' => '--user', + 'password' => '--password', + 'encoding' => '--default-character-set', + 'sslca' => '--ssl-ca', + 'sslcert' => '--ssl-cert', + 'sslcapath' => '--ssl-capath', + 'sslcipher' => '--ssh-cipher', + 'sslkey' => '--ssl-key' + }.map { |opt, arg| "#{arg}=#{configuration[opt]}" if configuration[opt] }.compact + + [command, *args] end end end diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index 7a3c6f5e95..0d57de4d65 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -14,10 +14,9 @@ module ActiveRecord protected attr_reader :migration_action, :join_tables - # sets the default migration template that is being used for the generation of the migration - # depending on the arguments which would be sent out in the command line, the migration template - # and the table name instance variables are setup. - + # Sets the default migration template that is being used for the generation of the migration. + # Depending on command line arguments, the migration template and the table name instance + # variables are set up. def set_local_assigns! @migration_template = "migration.rb" case file_name @@ -56,6 +55,8 @@ module ActiveRecord attributes.select { |a| !a.reference? && a.has_index? } end + # A migration file name can only contain underscores (_), lowercase characters, + # and numbers 0-9. Any other file name will raise an IllegalMigrationNameError. def validate_file_name! unless file_name =~ /^[_a-z0-9]+$/ raise IllegalMigrationNameError.new(file_name) diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb index 6577d56240..57eb5d0e18 100644 --- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb @@ -59,6 +59,43 @@ class ActiveSchemaTest < ActiveRecord::TestCase assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree) end + def test_index_in_create + def (ActiveRecord::Base.connection).table_exists?(*); false; end + + %w(SPATIAL FULLTEXT UNIQUE).each do |type| + expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`) ) ENGINE=InnoDB" + actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| + t.index :last_name, type: type + end + assert_equal expected, actual + end + + expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10)) ) ENGINE=InnoDB" + actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| + t.index :last_name, length: 10, using: :btree + end + assert_equal expected, actual + end + + def test_index_in_bulk_change + def (ActiveRecord::Base.connection).table_exists?(*); true; end + def (ActiveRecord::Base.connection).index_name_exists?(*); false; end + + %w(SPATIAL FULLTEXT UNIQUE).each do |type| + expected = "ALTER TABLE `people` ADD #{type} INDEX `index_people_on_last_name` (`last_name`)" + actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t| + t.index :last_name, type: type + end + assert_equal expected, actual + end + + expected = "ALTER TABLE `peaple` ADD INDEX `index_peaple_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY" + actual = ActiveRecord::Base.connection.change_table(:peaple, bulk: true) do |t| + t.index :last_name, length: 10, using: :btree, algorithm: :copy + end + assert_equal expected, actual + end + def test_drop_table assert_equal "DROP TABLE `people`", drop_table(:people) end diff --git a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb index 340fc95503..345122b1ad 100644 --- a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb @@ -1,5 +1,4 @@ require "cases/helper" -require 'models/person' class MysqlCaseSensitivityTest < ActiveRecord::TestCase class CollationTest < ActiveRecord::Base diff --git a/activerecord/test/cases/adapters/mysql/table_options_test.rb b/activerecord/test/cases/adapters/mysql/table_options_test.rb new file mode 100644 index 0000000000..0e5b0e8aec --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/table_options_test.rb @@ -0,0 +1,42 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class MysqlTableOptionsTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + def setup + @connection = ActiveRecord::Base.connection + end + + def teardown + @connection.drop_table "mysql_table_options", if_exists: true + end + + test "table options with ENGINE" do + @connection.create_table "mysql_table_options", force: true, options: "ENGINE=MyISAM" + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{ENGINE=MyISAM}, options + end + + test "table options with ROW_FORMAT" do + @connection.create_table "mysql_table_options", force: true, options: "ROW_FORMAT=REDUNDANT" + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{ROW_FORMAT=REDUNDANT}, options + end + + test "table options with CHARSET" do + @connection.create_table "mysql_table_options", force: true, options: "CHARSET=utf8mb4" + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{CHARSET=utf8mb4}, options + end + + test "table options with COLLATE" do + @connection.create_table "mysql_table_options", force: true, options: "COLLATE=utf8mb4_bin" + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{COLLATE=utf8mb4_bin}, options + end +end diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index e87cd3886a..0ea556d4fa 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -59,6 +59,43 @@ class ActiveSchemaTest < ActiveRecord::TestCase assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree) end + def test_index_in_create + def (ActiveRecord::Base.connection).table_exists?(*); false; end + + %w(SPATIAL FULLTEXT UNIQUE).each do |type| + expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`) ) ENGINE=InnoDB" + actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| + t.index :last_name, type: type + end + assert_equal expected, actual + end + + expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10)) ) ENGINE=InnoDB" + actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| + t.index :last_name, length: 10, using: :btree + end + assert_equal expected, actual + end + + def test_index_in_bulk_change + def (ActiveRecord::Base.connection).table_exists?(*); true; end + def (ActiveRecord::Base.connection).index_name_exists?(*); false; end + + %w(SPATIAL FULLTEXT UNIQUE).each do |type| + expected = "ALTER TABLE `people` ADD #{type} INDEX `index_people_on_last_name` (`last_name`)" + actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t| + t.index :last_name, type: type + end + assert_equal expected, actual + end + + expected = "ALTER TABLE `peaple` ADD INDEX `index_peaple_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY" + actual = ActiveRecord::Base.connection.change_table(:peaple, bulk: true) do |t| + t.index :last_name, length: 10, using: :btree, algorithm: :copy + end + assert_equal expected, actual + end + def test_drop_table assert_equal "DROP TABLE `people`", drop_table(:people) end diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb index 09bebf3071..ccf3d84a44 100644 --- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb @@ -1,5 +1,4 @@ require "cases/helper" -require 'models/person' class Mysql2CaseSensitivityTest < ActiveRecord::TestCase class CollationTest < ActiveRecord::Base diff --git a/activerecord/test/cases/adapters/mysql2/table_options_test.rb b/activerecord/test/cases/adapters/mysql2/table_options_test.rb new file mode 100644 index 0000000000..0e5b0e8aec --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/table_options_test.rb @@ -0,0 +1,42 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class MysqlTableOptionsTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + def setup + @connection = ActiveRecord::Base.connection + end + + def teardown + @connection.drop_table "mysql_table_options", if_exists: true + end + + test "table options with ENGINE" do + @connection.create_table "mysql_table_options", force: true, options: "ENGINE=MyISAM" + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{ENGINE=MyISAM}, options + end + + test "table options with ROW_FORMAT" do + @connection.create_table "mysql_table_options", force: true, options: "ROW_FORMAT=REDUNDANT" + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{ROW_FORMAT=REDUNDANT}, options + end + + test "table options with CHARSET" do + @connection.create_table "mysql_table_options", force: true, options: "CHARSET=utf8mb4" + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{CHARSET=utf8mb4}, options + end + + test "table options with COLLATE" do + @connection.create_table "mysql_table_options", force: true, options: "COLLATE=utf8mb4_bin" + output = dump_table_schema("mysql_table_options") + options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] + assert_match %r{COLLATE=utf8mb4_bin}, options + end +end diff --git a/activerecord/test/cases/adapters/postgresql/collation_test.rb b/activerecord/test/cases/adapters/postgresql/collation_test.rb new file mode 100644 index 0000000000..17ef5f304c --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/collation_test.rb @@ -0,0 +1,53 @@ +require "cases/helper" +require 'support/schema_dumping_helper' + +class PostgresqlCollationTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table :postgresql_collations, force: true do |t| + t.string :string_c, collation: 'C' + t.text :text_posix, collation: 'POSIX' + end + end + + def teardown + @connection.drop_table :postgresql_collations, if_exists: true + end + + test "string column with collation" do + column = @connection.columns(:postgresql_collations).find { |c| c.name == 'string_c' } + assert_equal :string, column.type + assert_equal 'C', column.collation + end + + test "text column with collation" do + column = @connection.columns(:postgresql_collations).find { |c| c.name == 'text_posix' } + assert_equal :text, column.type + assert_equal 'POSIX', column.collation + end + + test "add column with collation" do + @connection.add_column :postgresql_collations, :title, :string, collation: 'C' + + column = @connection.columns(:postgresql_collations).find { |c| c.name == 'title' } + assert_equal :string, column.type + assert_equal 'C', column.collation + end + + test "change column with collation" do + @connection.add_column :postgresql_collations, :description, :string + @connection.change_column :postgresql_collations, :description, :text, collation: 'POSIX' + + column = @connection.columns(:postgresql_collations).find { |c| c.name == 'description' } + assert_equal :text, column.type + assert_equal 'POSIX', column.collation + end + + test "schema dump includes collation" do + output = dump_table_schema("postgresql_collations") + assert_match %r{t.string\s+"string_c",\s+collation: "C"$}, output + assert_match %r{t.text\s+"text_posix",\s+collation: "POSIX"$}, output + end +end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 14cdf37f46..2c4e2a875c 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -2131,11 +2131,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase car = Car.create! original_child = FailedBulb.create!(car: car) - assert_raise(ActiveRecord::RecordNotDestroyed) do + error = assert_raise(ActiveRecord::RecordNotDestroyed) do car.failed_bulbs = [FailedBulb.create!] end assert_equal [original_child], car.reload.failed_bulbs + assert_equal "Failed to destroy the record", error.message end test 'updates counter cache when default scope is given' do diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 9734ea2217..190cef55c4 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -1040,14 +1040,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end - def test_save_should_not_raise_exception_when_join_record_has_errors - repair_validations(Categorization) do - Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' } - c = Category.create(:name => 'Fishing', :authors => [Author.first]) - c.save - end - end - def test_assign_array_to_new_record_builds_join_records c = Category.new(:name => 'Fishing', :authors => [Author.first]) assert_equal 1, c.categorizations.size @@ -1072,11 +1064,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end - def test_create_bang_returns_falsy_when_join_record_has_errors + def test_save_returns_falsy_when_join_record_has_errors repair_validations(Categorization) do Categorization.validate { |r| r.errors[:base] << 'Invalid Categorization' } c = Category.new(:name => 'Fishing', :authors => [Author.first]) - assert !c.save + assert_not c.save end end diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 3ae4a6eade..73ac30e547 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -451,6 +451,7 @@ class CallbacksTest < ActiveRecord::TestCase assert !david.save exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! } assert_equal exc.record, david + assert_equal "Failed to save the record", exc.message end david = ImmutableDeveloper.find(1) @@ -494,6 +495,7 @@ class CallbacksTest < ActiveRecord::TestCase assert !david.destroy exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } assert_equal exc.record, david + assert_equal "Failed to destroy the record", exc.message end assert_not_nil ImmutableDeveloper.find_by_id(1) diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index aa50efc979..f5928814a3 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -341,6 +341,21 @@ module ActiveRecord handler.establish_connection anonymous, nil } end + + def test_pool_sets_connection_schema_cache + connection = pool.checkout + schema_cache = SchemaCache.new connection + schema_cache.add(:posts) + pool.schema_cache = schema_cache + + pool.with_connection do |conn| + assert_not_same pool.schema_cache, conn.schema_cache + assert_equal pool.schema_cache.size, conn.schema_cache.size + assert_same pool.schema_cache.columns(:posts), conn.schema_cache.columns(:posts) + end + + pool.checkin connection + end end end end diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 3268555cb8..f67d85603a 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -7,15 +7,32 @@ require 'models/subscriber' require 'models/vegetables' require 'models/shop' +module InheritanceTestHelper + def with_store_full_sti_class(&block) + assign_store_full_sti_class true, &block + end + + def without_store_full_sti_class(&block) + assign_store_full_sti_class false, &block + end + + def assign_store_full_sti_class(flag) + old_store_full_sti_class = ActiveRecord::Base.store_full_sti_class + ActiveRecord::Base.store_full_sti_class = flag + yield + ensure + ActiveRecord::Base.store_full_sti_class = old_store_full_sti_class + end +end + class InheritanceTest < ActiveRecord::TestCase + include InheritanceTestHelper fixtures :companies, :projects, :subscribers, :accounts, :vegetables def test_class_with_store_full_sti_class_returns_full_name - old = ActiveRecord::Base.store_full_sti_class - ActiveRecord::Base.store_full_sti_class = true - assert_equal 'Namespaced::Company', Namespaced::Company.sti_name - ensure - ActiveRecord::Base.store_full_sti_class = old + with_store_full_sti_class do + assert_equal 'Namespaced::Company', Namespaced::Company.sti_name + end end def test_class_with_blank_sti_name @@ -33,39 +50,31 @@ class InheritanceTest < ActiveRecord::TestCase end def test_class_without_store_full_sti_class_returns_demodulized_name - old = ActiveRecord::Base.store_full_sti_class - ActiveRecord::Base.store_full_sti_class = false - assert_equal 'Company', Namespaced::Company.sti_name - ensure - ActiveRecord::Base.store_full_sti_class = old + without_store_full_sti_class do + assert_equal 'Company', Namespaced::Company.sti_name + end end def test_should_store_demodulized_class_name_with_store_full_sti_class_option_disabled - old = ActiveRecord::Base.store_full_sti_class - ActiveRecord::Base.store_full_sti_class = false - item = Namespaced::Company.new - assert_equal 'Company', item[:type] - ensure - ActiveRecord::Base.store_full_sti_class = old + without_store_full_sti_class do + item = Namespaced::Company.new + assert_equal 'Company', item[:type] + end end def test_should_store_full_class_name_with_store_full_sti_class_option_enabled - old = ActiveRecord::Base.store_full_sti_class - ActiveRecord::Base.store_full_sti_class = true - item = Namespaced::Company.new - assert_equal 'Namespaced::Company', item[:type] - ensure - ActiveRecord::Base.store_full_sti_class = old + with_store_full_sti_class do + item = Namespaced::Company.new + assert_equal 'Namespaced::Company', item[:type] + end end def test_different_namespace_subclass_should_load_correctly_with_store_full_sti_class_option - old = ActiveRecord::Base.store_full_sti_class - ActiveRecord::Base.store_full_sti_class = true - item = Namespaced::Company.create :name => "Wolverine 2" - assert_not_nil Company.find(item.id) - assert_not_nil Namespaced::Company.find(item.id) - ensure - ActiveRecord::Base.store_full_sti_class = old + with_store_full_sti_class do + item = Namespaced::Company.create name: "Wolverine 2" + assert_not_nil Company.find(item.id) + assert_not_nil Namespaced::Company.find(item.id) + end end def test_company_descends_from_active_record @@ -204,10 +213,28 @@ class InheritanceTest < ActiveRecord::TestCase assert_raise(ActiveRecord::SubclassNotFound) { Company.new(:type => 'Account') } end + def test_new_with_unrelated_namespaced_type + without_store_full_sti_class do + e = assert_raises ActiveRecord::SubclassNotFound do + Namespaced::Company.new(type: 'Firm') + end + + assert_equal "Invalid single-table inheritance type: Namespaced::Firm is not a subclass of Namespaced::Company", e.message + end + end + + def test_new_with_complex_inheritance assert_nothing_raised { Client.new(type: 'VerySpecialClient') } end + def test_new_without_storing_full_sti_class + without_store_full_sti_class do + item = Company.new(type: 'SpecialCo') + assert_instance_of Company::SpecialCo, item + end + end + def test_new_with_autoload_paths path = File.expand_path('../../models/autoloadable', __FILE__) ActiveSupport::Dependencies.autoload_paths << path @@ -331,6 +358,7 @@ class InheritanceTest < ActiveRecord::TestCase end class InheritanceComputeTypeTest < ActiveRecord::TestCase + include InheritanceTestHelper fixtures :companies def setup @@ -344,27 +372,26 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase end def test_instantiation_doesnt_try_to_require_corresponding_file - ActiveRecord::Base.store_full_sti_class = false - foo = Firm.first.clone - foo.type = 'FirmOnTheFly' - foo.save! + without_store_full_sti_class do + foo = Firm.first.clone + foo.type = 'FirmOnTheFly' + foo.save! - # Should fail without FirmOnTheFly in the type condition. - assert_raise(ActiveRecord::RecordNotFound) { Firm.find(foo.id) } + # Should fail without FirmOnTheFly in the type condition. + assert_raise(ActiveRecord::RecordNotFound) { Firm.find(foo.id) } - # Nest FirmOnTheFly in the test case where Dependencies won't see it. - self.class.const_set :FirmOnTheFly, Class.new(Firm) - assert_raise(ActiveRecord::SubclassNotFound) { Firm.find(foo.id) } + # Nest FirmOnTheFly in the test case where Dependencies won't see it. + self.class.const_set :FirmOnTheFly, Class.new(Firm) + assert_raise(ActiveRecord::SubclassNotFound) { Firm.find(foo.id) } - # Nest FirmOnTheFly in Firm where Dependencies will see it. - # This is analogous to nesting models in a migration. - Firm.const_set :FirmOnTheFly, Class.new(Firm) + # Nest FirmOnTheFly in Firm where Dependencies will see it. + # This is analogous to nesting models in a migration. + Firm.const_set :FirmOnTheFly, Class.new(Firm) - # And instantiate will find the existing constant rather than trying - # to require firm_on_the_fly. - assert_nothing_raised { assert_kind_of Firm::FirmOnTheFly, Firm.find(foo.id) } - ensure - ActiveRecord::Base.store_full_sti_class = true + # And instantiate will find the existing constant rather than trying + # to require firm_on_the_fly. + assert_nothing_raised { assert_kind_of Firm::FirmOnTheFly, Firm.find(foo.id) } + end end def test_sti_type_from_attributes_disabled_in_non_sti_class diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 63612e33af..e6f0fe6f75 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -168,24 +168,24 @@ class SchemaDumperTest < ActiveRecord::TestCase end def test_schema_dumps_index_columns_in_right_order - index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip + index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_index/).first.strip if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) - assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition else - assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index"', index_definition + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index"', index_definition end end def test_schema_dumps_partial_indices - index_definition = standard_dump.split(/\n/).grep(/add_index.*company_partial_index/).first.strip + index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_partial_index/).first.strip if current_adapter?(:PostgreSQLAdapter) - assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition + assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter) - assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition + assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index? - assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition + assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition else - assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index"', index_definition + assert_equal 't.index ["firm_id", "type"], name: "company_partial_index"', index_definition end end @@ -232,8 +232,8 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dumps_index_type output = standard_dump - assert_match %r{add_index "key_tests", \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output - assert_match %r{add_index "key_tests", \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output + assert_match %r{t\.index \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output + assert_match %r{t\.index \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output end end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index f58535f044..8d69741a4a 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -283,12 +283,21 @@ module ActiveRecord def test_structure_dump_with_port_number filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--port", "10000", "--result-file", filename, "--no-data", "test-db").returns(true) + Kernel.expects(:system).with("mysqldump", "--port=10000", "--result-file", filename, "--no-data", "test-db").returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump( @configuration.merge('port' => 10000), filename) end + + def test_structure_dump_with_ssl + filename = "awesome-file.sql" + Kernel.expects(:system).with("mysqldump", "--ssl-ca=ca.crt", "--result-file", filename, "--no-data", "test-db").returns(true) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump( + @configuration.merge("sslca" => "ca.crt"), + filename) + end end class MySQLStructureLoadTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/type/integer_test.rb b/activerecord/test/cases/type/integer_test.rb index 84fb05dd8e..0dcdbd0667 100644 --- a/activerecord/test/cases/type/integer_test.rb +++ b/activerecord/test/cases/type/integer_test.rb @@ -21,7 +21,7 @@ module ActiveRecord type = Type::Integer.new assert_nil type.cast([1,2]) assert_nil type.cast({1 => 2}) - assert_nil type.cast((1..2)) + assert_nil type.cast(1..2) end test "casting ActiveRecord models" do diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 6961f8fd6f..67936e8e5d 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -26,6 +26,9 @@ class Company < AbstractCompany def private_method "I am Jack's innermost fears and aspirations" end + + class SpecialCo < Company + end end module Namespaced diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 008503bc24..872fa595b4 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,5 +1,16 @@ ActiveRecord::Schema.define do + enable_extension!('uuid-ossp', ActiveRecord::Base.connection) + + create_table :uuid_parents, id: :uuid, force: true do |t| + t.string :name + end + + create_table :uuid_children, id: :uuid, force: true do |t| + t.string :name + t.uuid :uuid_parent_id + end + %w(postgresql_times postgresql_oids defaults postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name| drop_table table_name, if_exists: true diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 7b42f8a4a5..66f8f1611d 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -6,20 +6,6 @@ ActiveRecord::Schema.define do end end - #put adapter specific setup here - case adapter_name - when "PostgreSQL" - enable_extension!('uuid-ossp', ActiveRecord::Base.connection) - create_table :uuid_parents, id: :uuid, force: true do |t| - t.string :name - end - create_table :uuid_children, id: :uuid, force: true do |t| - t.string :name - t.uuid :uuid_parent_id - end - end - - # ------------------------------------------------------------------- # # # # Please keep these create table statements in alphabetical order # diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index ac27dc640e..3b905b1d24 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,8 @@ +* Add Integer#positive? and Integer#negative? query methods in the vein of Fixnum#zero? + This makes it nicer to do things like bunch_of_numbers.select(&:positive?). + + *DHH* + * Encoding ActiveSupport::TimeWithZone to YAML now preserves the timezone information. Fixes #9183. diff --git a/activesupport/README.rdoc b/activesupport/README.rdoc index a6424a353a..cd72f53821 100644 --- a/activesupport/README.rdoc +++ b/activesupport/README.rdoc @@ -10,7 +10,7 @@ outside of Rails. The latest version of Active Support can be installed with RubyGems: - % [sudo] gem install activesupport + % gem install activesupport Source code can be downloaded as part of the Rails project on GitHub: diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb index 9525c10112..01153606c9 100644 --- a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb @@ -125,9 +125,21 @@ module DateAndTime alias :at_beginning_of_year :beginning_of_year # Returns a new date/time representing the given day in the next week. + # + # today = Date.today # => Thu, 07 May 2015 + # today.next_week # => Mon, 11 May 2015 + # # The +given_day_in_next_week+ defaults to the beginning of the week # which is determined by +Date.beginning_of_week+ or +config.beginning_of_week+ - # when set. +DateTime+ objects have their time set to 0:00 unless +same_time+ is true. + # when set. + # + # today = Date.today # => Thu, 07 May 2015 + # today.next_week(:friday) # => Fri, 15 May 2015 + # + # +DateTime+ objects have their time set to 0:00 unless +same_time+ is true. + # + # now = Time.current # => Thu, 07 May 2015 13:31:16 UTC +00:00 + # now.next_week # => Mon, 11 May 2015 00:00:00 UTC +00:00 def next_week(given_day_in_next_week = Date.beginning_of_week, same_time: false) result = first_hour(weeks_since(1).beginning_of_week.days_since(days_span(given_day_in_next_week))) same_time ? copy_time_to(result) : result diff --git a/activesupport/lib/active_support/core_ext/integer.rb b/activesupport/lib/active_support/core_ext/integer.rb index a44a1b4c74..f5b185b42b 100644 --- a/activesupport/lib/active_support/core_ext/integer.rb +++ b/activesupport/lib/active_support/core_ext/integer.rb @@ -1,3 +1,4 @@ require 'active_support/core_ext/integer/multiple' require 'active_support/core_ext/integer/inflections' +require 'active_support/core_ext/integer/inquiry' require 'active_support/core_ext/integer/time' diff --git a/activesupport/lib/active_support/core_ext/integer/inquiry.rb b/activesupport/lib/active_support/core_ext/integer/inquiry.rb new file mode 100644 index 0000000000..17a04d4d63 --- /dev/null +++ b/activesupport/lib/active_support/core_ext/integer/inquiry.rb @@ -0,0 +1,19 @@ +class Integer + # Returns true if the number is positive. + # + # 1.positive? # => true + # 0.positive? # => false + # -1.positive? # => false + def positive? + self > 0 + end + + # Returns true if the number is negative. + # + # -1.negative? # => true + # 0.negative? # => false + # 1.negative? # => false + def negative? + self < 0 + end +end diff --git a/activesupport/lib/active_support/core_ext/object/deep_dup.rb b/activesupport/lib/active_support/core_ext/object/deep_dup.rb index 0191d2e973..ad5b2af161 100644 --- a/activesupport/lib/active_support/core_ext/object/deep_dup.rb +++ b/activesupport/lib/active_support/core_ext/object/deep_dup.rb @@ -40,6 +40,7 @@ class Hash # dup[:a][:c] # => "c" def deep_dup each_with_object(dup) do |(key, value), hash| + hash.delete(key) hash[key.deep_dup] = value.deep_dup end end diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb index dedcdfdb60..dfdb8034e5 100644 --- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb +++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb @@ -25,17 +25,17 @@ module ActiveSupport end end - # DeprecatedObjectProxy transforms an object into a deprecated object. It takes an object, - # a deprecation message, and optionally a deprecator. The deprecator defaults to - # <tt>ActiveSupport::Deprecator</tt> if none is specified. + # DeprecatedObjectProxy transforms an object into a deprecated one. It + # takes an object, a deprecation message and optionally a deprecator. The + # deprecator defaults to +ActiveSupport::Deprecator+ if none is specified. # # deprecated_object = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(Object.new, "This object is now deprecated") - # # => <Object:0x007fb9b34c34b0> + # # => #<Object:0x007fb9b34c34b0> # # deprecated_object.to_s # DEPRECATION WARNING: This object is now deprecated. # (Backtrace) - # # => "<Object:0x007fb9b34c34b0>" + # # => "#<Object:0x007fb9b34c34b0>" class DeprecatedObjectProxy < DeprecationProxy def initialize(object, message, deprecator = ActiveSupport::Deprecation.instance) @object = object @@ -53,14 +53,15 @@ module ActiveSupport end end - # DeprecatedInstanceVariableProxy transforms an instance variable into a deprecated - # instance variable. It takes an instance of a class, a method on that class, and an - # instance variable. It optionally takes a deprecator as the last argument. The deprecator - # defaults to <tt>ActiveSupport::Deprecator</tt> if none is specified. + # DeprecatedInstanceVariableProxy transforms an instance variable into a + # deprecated one. It takes an instance of a class, a method on that class + # and an instance variable. It optionally takes a deprecator as the last + # argument. The deprecator defaults to +ActiveSupport::Deprecator+ if none + # is specified. # # class Example # def initialize - # @request = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(self, :request, :@request, deprecator) + # @request = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(self, :request, :@request) # @_request = :special_request # end # @@ -102,10 +103,11 @@ module ActiveSupport end end - # DeprecatedConstantProxy transforms a constant into a deprecated constant. It takes the names of an old - # (deprecated) constant and a new contstant (both in string form), and optionally a deprecator. The - # deprecator defaults to <tt>ActiveSupport::Deprecator</tt> if none is specified. The deprecated constant - # now returns the return value of the new constant. + # DeprecatedConstantProxy transforms a constant into a deprecated one. It + # takes the names of an old (deprecated) constant and of a new contstant + # (both in string form) and optionally a deprecator. The deprecator defaults + # to +ActiveSupport::Deprecator+ if none is specified. The deprecated constant + # now returns the value of the new one. # # PLANETS = %w(mercury venus earth mars jupiter saturn uranus neptune pluto) # diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb index 35548f3f56..2932954f03 100644 --- a/activesupport/lib/active_support/json/decoding.rb +++ b/activesupport/lib/active_support/json/decoding.rb @@ -9,20 +9,14 @@ module ActiveSupport module JSON # matches YAML-formatted dates DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?))$/ - + class << self # Parses a JSON string (JavaScript Object Notation) into a hash. # See http://www.json.org for more info. # # ActiveSupport::JSON.decode("{\"team\":\"rails\",\"players\":\"36\"}") # => {"team" => "rails", "players" => "36"} - def decode(json, options = {}) - if options.present? - raise ArgumentError, "In Rails 4.1, ActiveSupport::JSON.decode no longer " \ - "accepts an options hash for MultiJSON. MultiJSON reached its end of life " \ - "and has been removed." - end - + def decode(json) data = ::JSON.parse(json, quirks_mode: true) if ActiveSupport.parse_json_times diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index 24b8f4b9f9..d9a668c0ea 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -36,14 +36,7 @@ module ActiveSupport # Possible values are +:random+, +:parallel+, +:alpha+, +:sorted+. # Defaults to +:random+. def test_order - test_order = ActiveSupport.test_order - - if test_order.nil? - test_order = :random - self.test_order = test_order - end - - test_order + ActiveSupport.test_order ||= :random end end diff --git a/activesupport/test/core_ext/integer_ext_test.rb b/activesupport/test/core_ext/integer_ext_test.rb index 41736fb672..6eeadb2ace 100644 --- a/activesupport/test/core_ext/integer_ext_test.rb +++ b/activesupport/test/core_ext/integer_ext_test.rb @@ -27,4 +27,16 @@ class IntegerExtTest < ActiveSupport::TestCase assert_equal 'st', 1.ordinal assert_equal 'th', 8.ordinal end + + def test_positive + assert 1.positive? + assert_not 0.positive? + assert_not -1.positive? + end + + def test_negative + assert -1.negative? + assert_not 0.negative? + assert_not 1.negative? + end end diff --git a/activesupport/test/core_ext/object/deep_dup_test.rb b/activesupport/test/core_ext/object/deep_dup_test.rb index 91d558dbb5..791b5e7172 100644 --- a/activesupport/test/core_ext/object/deep_dup_test.rb +++ b/activesupport/test/core_ext/object/deep_dup_test.rb @@ -50,4 +50,10 @@ class DeepDupTest < ActiveSupport::TestCase assert dup.instance_variable_defined?(:@a) end + def test_deep_dup_with_hash_class_key + hash = { Fixnum => 1 } + dup = hash.deep_dup + assert_equal 1, dup.keys.length + end + end diff --git a/activesupport/test/time_travel_test.rb b/activesupport/test/time_travel_test.rb index 676a143692..869bc09991 100644 --- a/activesupport/test/time_travel_test.rb +++ b/activesupport/test/time_travel_test.rb @@ -1,5 +1,4 @@ require 'abstract_unit' -require 'active_support/core_ext/date' require 'active_support/core_ext/date_time' require 'active_support/core_ext/numeric/time' diff --git a/activesupport/test/xml_mini_test.rb b/activesupport/test/xml_mini_test.rb index bcd6997b06..0e4e7427d2 100644 --- a/activesupport/test/xml_mini_test.rb +++ b/activesupport/test/xml_mini_test.rb @@ -1,7 +1,6 @@ require 'abstract_unit' require 'active_support/xml_mini' require 'active_support/builder' -require 'active_support/core_ext/array' require 'active_support/core_ext/hash' require 'active_support/core_ext/big_decimal' diff --git a/guides/source/4_2_release_notes.md b/guides/source/4_2_release_notes.md index d5b3766a5b..684bd286bc 100644 --- a/guides/source/4_2_release_notes.md +++ b/guides/source/4_2_release_notes.md @@ -214,8 +214,9 @@ end Due to a [change in Rack](https://github.com/rack/rack/commit/28b014484a8ac0bbb388e7eaeeef159598ec64fc), `rails server` now listens on `localhost` instead of `0.0.0.0` by default. This -should have minimal impact on the standard development workflow as http://localhost:3000 -will continue to work as before on your own machine. +should have minimal impact on the standard development workflow as both +http://127.0.0.1:3000 and http://localhost:3000 will continue to work as before +on your own machine. However, with this change you will no longer be able to access the Rails server from a different machine, for example if your development environment diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index fab0e20aba..7d95d4792e 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -749,7 +749,7 @@ class ApplicationController < ActionController::Base end ``` -Note that the filter in this case uses `send` because the `logged_in?` method is private and the filter is not run in the scope of the controller. This is not the recommended way to implement this particular filter, but in more simple cases it might be useful. +Note that the filter in this case uses `send` because the `logged_in?` method is private and the filter does not run in the scope of the controller. This is not the recommended way to implement this particular filter, but in more simple cases it might be useful. The second way is to use a class (actually, any object that responds to the right methods will do) to handle the filtering. This is useful in cases that are more complex and cannot be implemented in a readable and reusable way using the two other methods. As an example, you could rewrite the login filter again to use a class: diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index 089ce53f07..92a4081233 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -503,7 +503,7 @@ You will need to use: By using the full URL, your links will now work in your emails. -#### generating URLs with `url_for` +#### Generating URLs with `url_for` `url_for` generate full URL by default in templates. @@ -517,7 +517,7 @@ If you did not configure the `:host` option globally make sure to pass it to action: 'greeting') %> ``` -#### generating URLs with named routes +#### Generating URLs with Named Routes Email clients have no web context and so paths have no base URL to form complete web addresses. Thus, you should always use the "_url" variant of named route diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index 3541bbaa93..88c1345f27 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -552,7 +552,7 @@ end ```ruby atom_feed do |feed| feed.title("Articles Index") - feed.updated((@articles.first.created_at)) + feed.updated(@articles.first.created_at) @articles.each do |article| feed.entry(article) do |entry| diff --git a/guides/source/active_record_postgresql.md b/guides/source/active_record_postgresql.md index 66a11e5785..5e58d5baeb 100644 --- a/guides/source/active_record_postgresql.md +++ b/guides/source/active_record_postgresql.md @@ -242,10 +242,12 @@ article.save! ### UUID -* [type definition](http://www.postgresql.org/docs/9.3/static/datatype-uuid.html) -* [generator functions](http://www.postgresql.org/docs/9.3/static/uuid-ossp.html) +* [type definition](http://www.postgresql.org/docs/9.4/static/datatype-uuid.html) +* [pgcrypto generator function](http://www.postgresql.org/docs/9.4/static/pgcrypto.html#AEN159361) +* [uuid-ossp generator functions](http://www.postgresql.org/docs/9.4/static/uuid-ossp.html) -NOTE: you need to enable the `uuid-ossp` extension to use uuid. +NOTE: you need to enable the `pgcrypto` (only PostgreSQL >= 9.4) or `uuid-ossp` +extension to use uuid. ```ruby # db/migrate/20131220144913_create_revisions.rb @@ -356,12 +358,13 @@ A point is casted to an array containing `x` and `y` coordinates. UUID Primary Keys ----------------- -NOTE: you need to enable the `uuid-ossp` extension to generate UUIDs. +NOTE: you need to enable the `pgcrypto` (only PostgreSQL >= 9.4) or `uuid-ossp` +extension to generate random UUIDs. ```ruby # db/migrate/20131220144913_create_devices.rb -enable_extension 'uuid-ossp' unless extension_enabled?('uuid-ossp') -create_table :devices, id: :uuid, default: 'uuid_generate_v4()' do |t| +enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto') +create_table :devices, id: :uuid, default: 'gen_random_uuid()' do |t| t.string :kind end diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md index 2f10bc4e7c..e3cfabb327 100644 --- a/guides/source/active_record_querying.md +++ b/guides/source/active_record_querying.md @@ -898,7 +898,7 @@ For example: Item.transaction do i = Item.lock.first i.name = 'Jones' - i.save + i.save! end ``` @@ -1787,8 +1787,9 @@ EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `articles` ON `articles`.` under MySQL. -Active Record performs a pretty printing that emulates the one of the database -shells. So, the same query running with the PostgreSQL adapter would yield instead +Active Record performs a pretty printing that emulates that of the +corresponding database shell. So, the same query running with the +PostgreSQL adapter would yield instead ``` EXPLAIN for: SELECT "users".* FROM "users" INNER JOIN "articles" ON "articles"."user_id" = "users"."id" WHERE "users"."id" = 1 diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md index d251c5c0b1..343b761e93 100644 --- a/guides/source/active_record_validations.md +++ b/guides/source/active_record_validations.md @@ -617,7 +617,7 @@ end The validation happens by performing an SQL query into the model's table, searching for an existing record with the same value in that attribute. -There is a `:scope` option that you can use to specify other attributes that +There is a `:scope` option that you can use to specify one or more attributes that are used to limit the uniqueness check: ```ruby diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index 2a643680f7..047999d4cf 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -2201,14 +2201,14 @@ Extensions to `Array` Active Support augments the API of arrays to ease certain ways of accessing them. For example, `to` returns the subarray of elements up to the one at the passed index: ```ruby -%w(a b c d).to(2) # => %w(a b c) +%w(a b c d).to(2) # => ["a", "b", "c"] [].to(7) # => [] ``` Similarly, `from` returns the tail from the element at the passed index to the end. If the index is greater than the length of the array, it returns an empty array. ```ruby -%w(a b c d).from(2) # => %w(c d) +%w(a b c d).from(2) # => ["c", "d"] %w(a b c d).from(10) # => [] [].from(0) # => [] ``` @@ -2216,7 +2216,7 @@ Similarly, `from` returns the tail from the element at the passed index to the e The methods `second`, `third`, `fourth`, and `fifth` return the corresponding element (`first` is built-in). Thanks to social wisdom and positive constructiveness all around, `forty_two` is also available. ```ruby -%w(a b c d).third # => c +%w(a b c d).third # => "c" %w(a b c d).fifth # => nil ``` @@ -2229,7 +2229,7 @@ NOTE: Defined in `active_support/core_ext/array/access.rb`. This method is an alias of `Array#unshift`. ```ruby -%w(a b c d).prepend('e') # => %w(e a b c d) +%w(a b c d).prepend('e') # => ["e", "a", "b", "c", "d"] [].prepend(10) # => [10] ``` @@ -2240,8 +2240,8 @@ NOTE: Defined in `active_support/core_ext/array/prepend_and_append.rb`. This method is an alias of `Array#<<`. ```ruby -%w(a b c d).append('e') # => %w(a b c d e) -[].append([1,2]) # => [[1,2]] +%w(a b c d).append('e') # => ["a", "b", "c", "d", "e"] +[].append([1,2]) # => [[1, 2]] ``` NOTE: Defined in `active_support/core_ext/array/prepend_and_append.rb`. @@ -2465,7 +2465,7 @@ NOTE: Defined in `active_support/core_ext/array/wrap.rb`. ### Duplicating -The method `Array.deep_dup` duplicates itself and all objects inside +The method `Array#deep_dup` duplicates itself and all objects inside recursively with Active Support method `Object#deep_dup`. It works like `Array#map` with sending `deep_dup` method to each object inside. ```ruby @@ -2687,7 +2687,7 @@ NOTE: Defined in `active_support/core_ext/hash/deep_merge.rb`. ### Deep duplicating -The method `Hash.deep_dup` duplicates itself and all keys and values +The method `Hash#deep_dup` duplicates itself and all keys and values inside recursively with Active Support method `Object#deep_dup`. It works like `Enumerator#each_with_object` with sending `deep_dup` method to each pair inside. ```ruby diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md index 716beb9178..782406659d 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -22,7 +22,7 @@ fragment caching. Rails provides by default fragment caching. In order to use page and action caching, you will need to add `actionpack-page_caching` and `actionpack-action_caching` to your Gemfile. -To start playing with caching you'll want to ensure that `config.action_controller.perform_caching` is set to `true`, if you're running in development mode. This flag is normally set in the corresponding `config/environments/*.rb` and caching is disabled by default for development and test, and enabled for production. +To start playing with caching you'll want to ensure that `config.action_controller.perform_caching` is set to `true` if you're running in development mode. This flag is normally set in the corresponding `config/environments/*.rb` and caching is disabled by default for development and test, and enabled for production. ```ruby config.action_controller.perform_caching = true @@ -144,7 +144,7 @@ It's called "Russian Doll Caching" because it nests multiple fragments. The adva ### Low-Level Caching -Sometimes you need to cache a particular value or query result, instead of caching view fragments. Rails caching mechanism works great for storing __any__ kind of information. +Sometimes you need to cache a particular value or query result instead of caching view fragments. Rails' caching mechanism works great for storing __any__ kind of information. The most efficient way to implement low-level caching is using the `Rails.cache.fetch` method. This method does both reading and writing to the cache. When passed only a single argument, the key is fetched and value from the cache is returned. If a block is passed, the result of the block will be cached to the given key and the result is returned. @@ -160,7 +160,7 @@ class Product < ActiveRecord::Base end ``` -NOTE: Notice that in this example we used `cache_key` method, so the resulting cache-key will be something like `products/233-20140225082222765838000/competing_price`. `cache_key` generates a string based on the model’s `id` and `updated_at` attributes. This is a common convention and has the benefit of invalidating the cache whenever the product is updated. In general, when you use low-level caching for instance level information, you need to generate a cache key. +NOTE: Notice that in this example we used the `cache_key` method, so the resulting cache-key will be something like `products/233-20140225082222765838000/competing_price`. `cache_key` generates a string based on the model’s `id` and `updated_at` attributes. This is a common convention and has the benefit of invalidating the cache whenever the product is updated. In general, when you use low-level caching for instance level information, you need to generate a cache key. ### SQL Caching @@ -219,7 +219,7 @@ There are some common options used by all cache implementations. These can be pa * `:compress` - This option can be used to indicate that compression should be used in the cache. This can be useful for transferring large cache entries over a slow network. -* `:compress_threshold` - This options is used in conjunction with the `:compress` option to indicate a threshold under which cache entries should not be compressed. This defaults to 16 kilobytes. +* `:compress_threshold` - This option is used in conjunction with the `:compress` option to indicate a threshold under which cache entries should not be compressed. This defaults to 16 kilobytes. * `:expires_in` - This option sets an expiration time in seconds for the cache entry when it will be automatically removed from the cache. @@ -227,7 +227,7 @@ There are some common options used by all cache implementations. These can be pa ### ActiveSupport::Cache::MemoryStore -This cache store keeps entries in memory in the same Ruby process. The cache store has a bounded size specified by the `:size` options to the initializer (default is 32Mb). When the cache exceeds the allotted size, a cleanup will occur and the least recently used entries will be removed. +This cache store keeps entries in memory in the same Ruby process. The cache store has a bounded size specified by the `:size` option to the initializer (default is 32Mb). When the cache exceeds the allotted size, a cleanup will occur and the least recently used entries will be removed. ```ruby config.cache_store = :memory_store, { size: 64.megabytes } @@ -243,7 +243,7 @@ This cache store uses the file system to store entries. The path to the director config.cache_store = :file_store, "/path/to/cache/directory" ``` -With this cache store, multiple server processes on the same host can share a cache. Servers processes running on different hosts could share a cache by using a shared file system, but that set up would not be ideal and is not recommended. The cache store is appropriate for low to medium traffic sites that are served off one or two hosts. +With this cache store, multiple server processes on the same host can share a cache. Server processes running on different hosts could share a cache by using a shared file system, but that set up would not be ideal and is not recommended. The cache store is appropriate for low to medium traffic sites that are served off one or two hosts. Note that the cache will grow until the disk is full unless you periodically clear out old entries. @@ -255,7 +255,7 @@ This cache store uses Danga's `memcached` server to provide a centralized cache When initializing the cache, you need to specify the addresses for all memcached servers in your cluster. If none is specified, it will assume memcached is running on the local host on the default port, but this is not an ideal set up for larger sites. -The `write` and `fetch` methods on this cache accept two additional options that take advantage of features specific to memcached. You can specify `:raw` to send a value directly to the server with no serialization. The value must be a string or number. You can use memcached direct operation like `increment` and `decrement` only on raw values. You can also specify `:unless_exist` if you don't want memcached to overwrite an existing entry. +The `write` and `fetch` methods on this cache accept two additional options that take advantage of features specific to memcached. You can specify `:raw` to send a value directly to the server with no serialization. The value must be a string or number. You can use memcached direct operations like `increment` and `decrement` only on raw values. You can also specify `:unless_exist` if you don't want memcached to overwrite an existing entry. ```ruby config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.com" @@ -293,7 +293,7 @@ For more information about Ehcache for JRuby and Rails, see [http://ehcache.org/ ### ActiveSupport::Cache::NullStore -This cache store implementation is meant to be used only in development or test environments and it never stores anything. This can be very useful in development when you have code that interacts directly with `Rails.cache`, but caching may interfere with being able to see the results of code changes. With this cache store, all `fetch` and `read` operations will result in a miss. +This cache store implementation is meant to be used only in development or test environments and it never stores anything. This can be very useful in development when you have code that interacts directly with `Rails.cache` but caching may interfere with being able to see the results of code changes. With this cache store, all `fetch` and `read` operations will result in a miss. ```ruby config.cache_store = :null_store @@ -303,7 +303,7 @@ config.cache_store = :null_store You can create your own custom cache store by simply extending `ActiveSupport::Cache::Store` and implementing the appropriate methods. In this way, you can swap in any number of caching technologies into your Rails application. -To use a custom cache store, simple set the cache store to a new instance of the class. +To use a custom cache store, simply set the cache store to a new instance of the class. ```ruby config.cache_store = MyCacheStore.new @@ -311,7 +311,7 @@ config.cache_store = MyCacheStore.new ### Cache Keys -The keys used in a cache can be any object that responds to either `:cache_key` or to `:to_param`. You can implement the `:cache_key` method on your classes if you need to generate custom keys. Active Record will generate keys based on the class name and record id. +The keys used in a cache can be any object that responds to either `:cache_key` or `:to_param`. You can implement the `:cache_key` method on your classes if you need to generate custom keys. Active Record will generate keys based on the class name and record id. You can use Hashes and Arrays of values as cache keys. @@ -353,7 +353,7 @@ class ProductsController < ApplicationController end ``` -Instead of an options hash, you can also simply pass in a model, Rails will use the `updated_at` and `cache_key` methods for setting `last_modified` and `etag`: +Instead of an options hash, you can also simply pass in a model. Rails will use the `updated_at` and `cache_key` methods for setting `last_modified` and `etag`: ```ruby class ProductsController < ApplicationController diff --git a/guides/source/configuring.md b/guides/source/configuring.md index 4da369be5e..342d39a528 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -526,7 +526,7 @@ There are a few configuration options available in Active Support: * `config.active_support.test_order` sets the order that test cases are executed. Possible values are `:random` and `:sorted`. This option is set to `:random` in `config/environments/test.rb` in newly-generated applications. If you have an application that does not specify a `test_order`, it will default to `:sorted`, *until* Rails 5.0, when the default will become `:random`. -* `config.active_support.escape_html_entities_in_json` enables or disables the escaping of HTML entities in JSON serialization. Defaults to `false`. +* `config.active_support.escape_html_entities_in_json` enables or disables the escaping of HTML entities in JSON serialization. Defaults to `true`. * `config.active_support.use_standard_json_time_format` enables or disables serializing dates to ISO 8601 format. Defaults to `true`. diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md index 3d5f8906ca..244add0aee 100644 --- a/guides/source/contributing_to_ruby_on_rails.md +++ b/guides/source/contributing_to_ruby_on_rails.md @@ -532,7 +532,7 @@ pull request". The Rails core team will be notified about your submission. Most pull requests will go through a few iterations before they get merged. Different contributors will sometimes have different opinions, and often -patches will need revised before they can get merged. +patches will need to be revised before they can get merged. Some contributors to Rails have email notifications from GitHub turned on, but others do not. Furthermore, (almost) everyone who works on Rails is a diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md index c6863f68e6..ec3ac62b8c 100644 --- a/guides/source/debugging_rails_applications.md +++ b/guides/source/debugging_rails_applications.md @@ -17,7 +17,7 @@ After reading this guide, you will know: View Helpers for Debugging -------------------------- -One common task is to inspect the contents of a variable. In Rails, you can do this with three methods: +One common task is to inspect the contents of a variable. Rails provides three different ways to do this: * `debug` * `to_yaml` @@ -54,7 +54,7 @@ Title: Rails debugging guide ### `to_yaml` -Displaying an instance variable, or any other object or method, in YAML format can be achieved this way: +Alternatively, calling `to_yaml` on any object converts it to YAML. You can pass this converted object into the `simple_format` helper method to format the output. This is how `debug` does its magic. ```html+erb <%= simple_format @article.to_yaml %> @@ -64,9 +64,7 @@ Displaying an instance variable, or any other object or method, in YAML format c </p> ``` -The `to_yaml` method converts the method to YAML format leaving it more readable, and then the `simple_format` helper is used to render each line as in the console. This is how `debug` method does its magic. - -As a result of this, you will have something like this in your view: +The above code will render something like this: ```yaml --- !ruby/object Article @@ -94,7 +92,7 @@ Another useful method for displaying object values is `inspect`, especially when </p> ``` -Will be rendered as follows: +Will render: ``` [1, 2, 3, 4, 5] @@ -109,9 +107,9 @@ It can also be useful to save information to log files at runtime. Rails maintai ### What is the Logger? -Rails makes use of the `ActiveSupport::Logger` class to write log information. You can also substitute another logger such as `Log4r` if you wish. +Rails makes use of the `ActiveSupport::Logger` class to write log information. Other loggers, such as `Log4r`, may also be substituted. -You can specify an alternative logger in your `environment.rb` or any environment file: +You can specify an alternative logger in `environment.rb` or any other environment file, for example: ```ruby Rails.logger = Logger.new(STDOUT) @@ -130,7 +128,7 @@ TIP: By default, each log is created under `Rails.root/log/` and the log file is ### Log Levels When something is logged, it's printed into the corresponding log if the log -level of the message is equal or higher than the configured log level. If you +level of the message is equal to or higher than the configured log level. If you want to know the current log level, you can call the `Rails.logger.level` method. @@ -143,7 +141,7 @@ config.log_level = :warn # In any environment initializer, or Rails.logger.level = 0 # at any time ``` -This is useful when you want to log under development or staging, but you don't want to flood your production log with unnecessary information. +This is useful when you want to log under development or staging without flooding your production log with unnecessary information. TIP: The default Rails log level is `debug` in all environments. @@ -207,7 +205,7 @@ Adding extra logging like this makes it easy to search for unexpected or unusual When running multi-user, multi-account applications, it's often useful to be able to filter the logs using some custom rules. `TaggedLogging` -in Active Support helps in doing exactly that by stamping log lines with subdomains, request ids, and anything else to aid debugging such applications. +in Active Support helps you do exactly that by stamping log lines with subdomains, request ids, and anything else to aid debugging such applications. ```ruby logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT)) @@ -217,34 +215,33 @@ logger.tagged("BCX") { logger.tagged("Jason") { logger.info "Stuff" } } # Logs " ``` ### Impact of Logs on Performance -Logging will always have a small impact on performance of your rails app, - particularly when logging to disk. However, there are a few subtleties: +Logging will always have a small impact on the performance of your Rails app, + particularly when logging to disk. Additionally, there are a few subtleties: Using the `:debug` level will have a greater performance penalty than `:fatal`, as a far greater number of strings are being evaluated and written to the log output (e.g. disk). -Another potential pitfall is that if you have many calls to `Logger` like this - in your code: +Another potential pitfall is too many calls to `Logger` in your code: ```ruby logger.debug "Person attributes hash: #{@person.attributes.inspect}" ``` -In the above example, There will be a performance impact even if the allowed +In the above example, there will be a performance impact even if the allowed output level doesn't include debug. The reason is that Ruby has to evaluate these strings, which includes instantiating the somewhat heavy `String` object -and interpolating the variables, and which takes time. +and interpolating the variables. Therefore, it's recommended to pass blocks to the logger methods, as these are -only evaluated if the output level is the same or included in the allowed level +only evaluated if the output level is the same as — or included in — the allowed level (i.e. lazy loading). The same code rewritten would be: ```ruby logger.debug {"Person attributes hash: #{@person.attributes.inspect}"} ``` -The contents of the block, and therefore the string interpolation, is only -evaluated if debug is enabled. This performance savings is only really +The contents of the block, and therefore the string interpolation, are only +evaluated if debug is enabled. This performance savings are only really noticeable with large amounts of logging, but it's a good practice to employ. Debugging with the `byebug` gem @@ -258,8 +255,7 @@ is your best companion. The debugger can also help you if you want to learn about the Rails source code but don't know where to start. Just debug any request to your application and -use this guide to learn how to move from the code you have written deeper into -Rails code. +use this guide to learn how to move from the code you have written into the underlying Rails code. ### Setup @@ -290,7 +286,7 @@ As soon as your application calls the `byebug` method, the debugger will be started in a debugger shell inside the terminal window where you launched your application server, and you will be placed at the debugger's prompt `(byebug)`. Before the prompt, the code around the line that is about to be run will be -displayed and the current line will be marked by '=>'. Like this: +displayed and the current line will be marked by '=>', like this: ``` [1, 10] in /PathTo/project/app/controllers/articles_controller.rb @@ -344,7 +340,7 @@ Processing by ArticlesController#index as HTML (byebug) ``` -Now it's time to explore and dig into your application. A good place to start is +Now it's time to explore your application. A good place to start is by asking the debugger for help. Type: `help` ``` @@ -365,9 +361,9 @@ continue edit frame kill pp reload skip undisplay TIP: To view the help menu for any command use `help <command-name>` at the debugger prompt. For example: _`help list`_. You can abbreviate any debugging command by supplying just enough letters to distinguish them from other -commands, so you can also use `l` for the `list` command, for example. +commands. For example, you can use `l` for the `list` command. -To see the previous ten lines you should type `list-` (or `l-`) +To see the previous ten lines you should type `list-` (or `l-`). ``` (byebug) l- @@ -386,7 +382,7 @@ To see the previous ten lines you should type `list-` (or `l-`) ``` -This way you can move inside the file, being able to see the code above and over +This way you can move inside the file and see the code above the line where you added the `byebug` call. Finally, to see where you are in the code again you can type `list=` @@ -416,8 +412,7 @@ contexts as you go through the different parts of the stack. The debugger creates a context when a stopping point or an event is reached. The context has information about the suspended program which enables the debugger to inspect the frame stack, evaluate variables from the perspective of the -debugged program, and contains information about the place where the debugged -program is stopped. +debugged program, and know the place where the debugged program is stopped. At any time you can call the `backtrace` command (or its alias `where`) to print the backtrace of the application. This can be very helpful to know how you got @@ -481,9 +476,8 @@ character and the number indicates the current thread of execution. * `thread resume _n_` resumes thread _n_. * `thread switch _n_` switches the current thread context to _n_. -This command is very helpful, among other occasions, when you are debugging -concurrent threads and need to verify that there are no race conditions in your -code. +This command is very helpful when you are debugging concurrent threads and need +to verify that there are no race conditions in your code. ### Inspecting Variables @@ -545,11 +539,11 @@ Now `@articles` is included in the instance variables, because the line defining was executed. TIP: You can also step into **irb** mode with the command `irb` (of course!). -This way an irb session will be started within the context you invoked it. But +This will start an irb session within the context you invoked it. But be warned: this is an experimental feature. The `var` method is the most convenient way to show variables and their values. -Let's let `byebug` help us with it. +Let's have `byebug` help us with it. ``` (byebug) help var @@ -561,7 +555,7 @@ v[ar] l[ocal] show local variables ``` This is a great way to inspect the values of the current context variables. For -example, to check that we have no local variables currently defined. +example, to check that we have no local variables currently defined: ``` (byebug) var local @@ -592,14 +586,14 @@ tracking the values of a variable while the execution goes on. 1: @articles = nil ``` -The variables inside the displaying list will be printed with their values after +The variables inside the displayed list will be printed with their values after you move in the stack. To stop displaying a variable use `undisplay _n_` where _n_ is the variable number (1 in the last example). ### Step by Step Now you should know where you are in the running trace and be able to print the -available variables. But lets continue and move on with the application +available variables. But let's continue and move on with the application execution. Use `step` (abbreviated `s`) to continue running your program until the next @@ -656,8 +650,8 @@ Next went up a frame because previous frame finished (byebug) ``` -If we use `step` in the same situation, we will literally go the next ruby -instruction to be executed. In this case, the activesupport's `week` method. +If we use `step` in the same situation, we will literally go to the next Ruby +instruction to be executed. In this case, Active Support's `week` method. ``` (byebug) step @@ -677,8 +671,7 @@ instruction to be executed. In this case, the activesupport's `week` method. (byebug) ``` -This is one of the best ways to find bugs in your code, or perhaps in Ruby on -Rails. +This is one of the best ways to find bugs in your code. ### Breakpoints @@ -814,7 +807,7 @@ controller. The console would be rendered next to your HTML content. ### Console -Inside any controller action or view, you can then invoke the console by +Inside any controller action or view, you can invoke the console by calling the `console` method. For example, in a controller: @@ -840,7 +833,7 @@ This will render a console inside your view. You don't need to care about the location of the `console` call; it won't be rendered on the spot of its invocation but next to your HTML content. -The console executes pure Ruby code. You can define and instantiate +The console executes pure Ruby code: You can define and instantiate custom classes, create new models and inspect variables. NOTE: Only one console can be rendered per request. Otherwise `web-console` @@ -865,7 +858,7 @@ to use it in production. Debugging Memory Leaks ---------------------- -A Ruby application (on Rails or not), can leak memory - either in the Ruby code +A Ruby application (on Rails or not), can leak memory — either in the Ruby code or at the C code level. In this section, you will learn how to find and fix such leaks by using tool @@ -896,7 +889,7 @@ footnotes that give request information and link back to your source via TextMate. * [Query Trace](https://github.com/ruckus/active-record-query-trace/tree/master) Adds query origin tracing to your logs. -* [Query Reviewer](https://github.com/nesquena/query_reviewer) This rails plugin +* [Query Reviewer](https://github.com/nesquena/query_reviewer) This Rails plugin not only runs "EXPLAIN" before each of your select queries in development, but provides a small DIV in the rendered output of each page with the summary of warnings for each query that it analyzed. @@ -908,7 +901,7 @@ standard Rails error page with a new one containing more contextual information, like source code and variable inspection. * [RailsPanel](https://github.com/dejan/rails_panel) Chrome extension for Rails development that will end your tailing of development.log. Have all information -about your Rails app requests in the browser - in the Developer Tools panel. +about your Rails app requests in the browser — in the Developer Tools panel. Provides insight to db/rendering/total times, parameter list, rendered views and more. diff --git a/guides/source/engines.md b/guides/source/engines.md index bcb0ee7d5d..a89ed1984f 100644 --- a/guides/source/engines.md +++ b/guides/source/engines.md @@ -582,7 +582,7 @@ the comments, however, is not quite right yet. If you were to create a comment right now, you would see this error: ``` -Missing partial blorgh/comments/comment with {:handlers=>[:erb, :builder], +Missing partial blorgh/comments/_comment with {:handlers=>[:erb, :builder], :formats=>[:html], :locale=>[:en, :en]}. Searched in: * "/Users/ryan/Sites/side_projects/blorgh/test/dummy/app/views" * "/Users/ryan/Sites/side_projects/blorgh/app/views" @@ -591,7 +591,7 @@ Missing partial blorgh/comments/comment with {:handlers=>[:erb, :builder], The engine is unable to find the partial required for rendering the comments. Rails looks first in the application's (`test/dummy`) `app/views` directory and then in the engine's `app/views` directory. When it can't find it, it will throw -this error. The engine knows to look for `blorgh/comments/comment` because the +this error. The engine knows to look for `blorgh/comments/_comment` because the model object it is receiving is from the `Blorgh::Comment` class. This partial will be responsible for rendering just the comment text, for now. diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index 684a53e472..5ef376531d 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -377,7 +377,7 @@ edit_article GET /articles/:id/edit(.:format) articles#edit In the next section, you will add the ability to create new articles in your application and be able to view them. This is the "C" and the "R" from CRUD: -creation and reading. The form for doing this will look like this: +create and read. The form for doing this will look like this: ![The new article form](images/getting_started/new_article.png) @@ -836,7 +836,7 @@ class ArticlesController < ApplicationController def new end - # snipped for brevity + # snippet for brevity ``` A couple of things to note. We use `Article.find` to find the article we're @@ -892,7 +892,7 @@ class ArticlesController < ApplicationController def new end - # snipped for brevity + # snippet for brevity ``` And then finally, add the view for this action, located at @@ -1240,10 +1240,7 @@ article we want to show the form back to the user. We reuse the `article_params` method that we defined earlier for the create action. -TIP: You don't need to pass all attributes to `update`. For -example, if you'd call `@article.update(title: 'A new title')` -Rails would only update the `title` attribute, leaving all other -attributes untouched. +TIP: It is not necessary to pass all the attributes to `update`. For example, if `@article.update(title: 'A new title')` were called, Rails would only update the `title` attribute, leaving all other attributes untouched. Finally, we want to show a link to the `edit` action in the list of all the articles, so let's add that now to `app/views/articles/index.html.erb` to make @@ -1491,7 +1488,7 @@ appear. ![Confirm Dialog](images/getting_started/confirm_dialog.png) TIP: Learn more about jQuery Unobtrusive Adapter (jQuery UJS) on -[Working With Javascript in Rails](working_with_javascript_in_rails.html) guide. +[Working With JavaScript in Rails](working_with_javascript_in_rails.html) guide. Congratulations, you can now create, show, list, update and destroy articles. @@ -2003,7 +2000,7 @@ class ArticlesController < ApplicationController @articles = Article.all end - # snipped for brevity + # snippet for brevity ``` We also want to allow only authenticated users to delete comments, so in the @@ -2019,7 +2016,7 @@ class CommentsController < ApplicationController # ... end - # snipped for brevity + # snippet for brevity ``` Now if you try to create a new article, you will be greeted with a basic HTTP diff --git a/guides/source/i18n.md b/guides/source/i18n.md index 348c60a9d8..51eaf4ba5a 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -40,7 +40,7 @@ Internationalization is a complex problem. Natural languages differ in so many w * providing support for English and similar languages out of the box * making it easy to customize and extend everything for other languages -As part of this solution, **every static string in the Rails framework** - e.g. Active Record validation messages, time and date formats - **has been internationalized**, so _localization_ of a Rails application means "over-riding" these defaults. +As part of this solution, **every static string in the Rails framework** - e.g. Active Record validation messages, time and date formats - **has been internationalized**. _Localization_ of a Rails application means defining translated values for these strings in desired languages. ### The Overall Architecture of the Library @@ -84,13 +84,13 @@ So, let's internationalize a simple Rails application from the ground up in the Setup the Rails Application for Internationalization ---------------------------------------------------- -There are just a few simple steps to get up and running with I18n support for your application. +There are a few steps to get up and running with I18n support for a Rails application. ### Configure the I18n Module -Following the _convention over configuration_ philosophy, Rails will set up your application with reasonable defaults. If you need different settings, you can overwrite them easily. +Following the _convention over configuration_ philosophy, Rails I18n provides reasonable default translation strings. When different translation strings are needed, they can be overridden. -Rails adds all `.rb` and `.yml` files from the `config/locales` directory to your **translations load path**, automatically. +Rails adds all `.rb` and `.yml` files from the `config/locales` directory to the **translations load path**, automatically. The default `en.yml` locale in this directory contains a sample pair of translation strings: @@ -101,15 +101,15 @@ en: This means, that in the `:en` locale, the key _hello_ will map to the _Hello world_ string. Every string inside Rails is internationalized in this way, see for instance Active Model validation messages in the [`activemodel/lib/active_model/locale/en.yml`](https://github.com/rails/rails/blob/master/activemodel/lib/active_model/locale/en.yml) file or time and date formats in the [`activesupport/lib/active_support/locale/en.yml`](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml) file. You can use YAML or standard Ruby Hashes to store translations in the default (Simple) backend. -The I18n library will use **English** as a **default locale**, i.e. if you don't set a different locale, `:en` will be used for looking up translations. +The I18n library will use **English** as a **default locale**, i.e. if a different locale is not set, `:en` will be used for looking up translations. NOTE: The i18n library takes a **pragmatic approach** to locale keys (after [some discussion](http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en)), including only the _locale_ ("language") part, like `:en`, `:pl`, not the _region_ part, like `:en-US` or `:en-GB`, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as `:cs`, `:th` or `:es` (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the `:en-US` locale you would have $ as a currency symbol, while in `:en-GB`, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a `:en-GB` dictionary. Few gems such as [Globalize3](https://github.com/globalize/globalize) may help you implement it. -The **translations load path** (`I18n.load_path`) is just a Ruby Array of paths to your translation files that will be loaded automatically and available in your application. You can pick whatever directory and translation file naming scheme makes sense for you. +The **translations load path** (`I18n.load_path`) is an array of paths to files that will be loaded automatically. Configuring this path allows for customization of translations directory structure and file naming scheme. -NOTE: The backend will lazy-load these translations when a translation is looked up for the first time. This makes it possible to just swap the backend with something else even after translations have already been announced. +NOTE: The backend lazy-loads these translations when a translation is looked up for the first time. This backend can be swapped with something else even after translations have already been announced. -The default `application.rb` file has instructions on how to add locales from another directory and how to set a different default locale. Just uncomment and edit the specific lines. +The default `application.rb` file has instructions on how to add locales from another directory and how to set a different default locale. ```ruby # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. @@ -117,31 +117,25 @@ The default `application.rb` file has instructions on how to add locales from an # config.i18n.default_locale = :de ``` -### Optional: Custom I18n Configuration Setup - -For the sake of completeness, let's mention that if you do not want to use the `application.rb` file for some reason, you can always wire up things manually, too. - -To tell the I18n library where it can find your custom translation files you can specify the load path anywhere in your application - just make sure it gets run before any translations are actually looked up. You might also want to change the default locale. The simplest thing possible is to put the following into an initializer: +The load path must be specified before any translations are looked up. To change the default locale from an initializer instead of `application.rb`: ```ruby -# in config/initializers/locale.rb +# config/initializers/locale.rb -# tell the I18n library where to find your translations +# Where the I18n library should search for translation files I18n.load_path += Dir[Rails.root.join('lib', 'locale', '*.{rb,yml}')] -# set default locale to something other than :en +# Set default locale to something other than :en I18n.default_locale = :pt ``` -### Setting and Passing the Locale +### Managing the Locale across Requests -If you want to translate your Rails application to a **single language other than English** (the default locale), you can set I18n.default_locale to your locale in `application.rb` or an initializer as shown above, and it will persist through the requests. +The default locale is used for all translations unless `I18n.locale` is explicitly set. -However, you would probably like to **provide support for more locales** in your application. In such case, you need to set and pass the locale between requests. +A localized application will likely need to provide support for multiple locales. To accomplish this, the locale should be set at the beginning of each request so that all strings are translated using the desired locale during the lifetime of that request. -WARNING: You may be tempted to store the chosen locale in a _session_ or a *cookie*. However, **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [*RESTful*](http://en.wikipedia.org/wiki/Representational_State_Transfer). Read more about the RESTful approach in [Stefan Tilkov's articles](http://www.infoq.com/articles/rest-introduction). Sometimes there are exceptions to this rule and those are discussed below. - -The _setting part_ is easy. You can set the locale in a `before_action` in the `ApplicationController` like this: +The locale can be set in a `before_action` in the `ApplicationController`: ```ruby before_action :set_locale @@ -151,11 +145,11 @@ def set_locale end ``` -This requires you to pass the locale as a URL query parameter as in `http://example.com/books?locale=pt`. (This is, for example, Google's approach.) So `http://localhost:3000?locale=pt` will load the Portuguese localization, whereas `http://localhost:3000?locale=de` would load the German localization, and so on. You may skip the next section and head over to the **Internationalize your application** section, if you want to try things out by manually placing the locale in the URL and reloading the page. +This example illustrates this using a URL query parameter to set the locale (e.g. `http://example.com/books?locale=pt`). With this approach, `http://localhost:3000?locale=pt` renders the Portuguese localization, while `http://localhost:3000?locale=de` loads a German localization. -Of course, you probably don't want to manually include the locale in every URL all over your application, or want the URLs look differently, e.g. the usual `http://example.com/pt/books` versus `http://example.com/en/books`. Let's discuss the different options you have. +The locale can be set using one of many different approaches. -### Setting the Locale from the Domain Name +#### Setting the Locale from the Domain Name One option you have is to set the locale from the domain name where your application runs. For example, we want `www.example.com` to load the English (or default) locale, and `www.example.es` to load the Spanish locale. Thus the _top-level domain name_ is used for locale setting. This has several advantages: @@ -208,7 +202,7 @@ assuming you would set `APP_CONFIG[:deutsch_website_url]` to some value like `ht This solution has aforementioned advantages, however, you may not be able or may not want to provide different localizations ("language versions") on different domains. The most obvious solution would be to include locale code in the URL params (or request path). -### Setting the Locale from the URL Params +#### Setting the Locale from URL Params The most usual way of setting (and passing) the locale would be to include it in URL params, as we did in the `I18n.locale = params[:locale]` _before_action_ in the first example. We would like to have URLs like `www.example.com/books?locale=ja` or `www.example.com/ja/books` in this case. @@ -266,14 +260,23 @@ Do take special care about the **order of your routes**, so this route declarati NOTE: Have a look at various gems which simplify working with routes: [routing_filter](https://github.com/svenfuchs/routing-filter/tree/master), [rails-translate-routes](https://github.com/francesc/rails-translate-routes), [route_translator](https://github.com/enriclluelles/route_translator). -### Setting the Locale from the Client Supplied Information +#### Setting the Locale from User Preferences + +An application with authenticated users may allow users to set a locale preference through the application's interface. With this approach, a user's selected locale preference is persisted in the database and used to set the locale for authenticated requests by that user. + +```ruby +def set_locale + I18n.locale = current_user.try(:locale) || I18n.default_locale +end +``` -In specific cases, it would make sense to set the locale from client-supplied information, i.e. not from the URL. This information may come for example from the users' preferred language (set in their browser), can be based on the users' geographical location inferred from their IP, or users can provide it simply by choosing the locale in your application interface and saving it to their profile. This approach is more suitable for web-based applications or services, not for websites - see the box about _sessions_, _cookies_ and RESTful architecture above. +#### Choosing an Implied Locale +When an explicit locale has not been set for a request (e.g. via one of the above methods), an application should attempt to infer the desired locale. -#### Using `Accept-Language` +##### Inferring Locale from the Language Header -One source of client supplied information would be an `Accept-Language` HTTP header. People may [set this in their browser](http://www.w3.org/International/questions/qa-lang-priorities) or other clients (such as _curl_). +The `Accept-Language` HTTP header indicates the preferred language for request's response. Browsers [set this header value based on the user's language preference settings](http://www.w3.org/International/questions/qa-lang-priorities), making it a good first choice when inferring a locale. A trivial implementation of using an `Accept-Language` header would be: @@ -290,24 +293,27 @@ private end ``` -Of course, in a production environment you would need much more robust code, and could use a gem such as Iain Hecker's [http_accept_language](https://github.com/iain/http_accept_language/tree/master) or even Rack middleware such as Ryan Tomayko's [locale](https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/locale.rb). -#### Using GeoIP (or Similar) Database +In practice, more robust code is necessary to do this reliably. Iain Hecker's [http_accept_language](https://github.com/iain/http_accept_language/tree/master) library or Ryan Tomayko's [locale](https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/locale.rb) Rack middleware provide solutions to this problem. -Another way of choosing the locale from client information would be to use a database for mapping the client IP to the region, such as [GeoIP Lite Country](http://www.maxmind.com/app/geolitecountry). The mechanics of the code would be very similar to the code above - you would need to query the database for the user's IP, and look up your preferred locale for the country/region/city returned. +##### Inferring the Locale from IP Geolocation -#### User Profile +The IP address of the client making the request can be used to infer the client's region and thus their locale. Services such as [GeoIP Lite Country](http://www.maxmind.com/app/geolitecountry) or gems like [geocoder](https://github.com/alexreisner/geocoder) can be used to implement this approach. -You can also provide users of your application with means to set (and possibly over-ride) the locale in your application interface, as well. Again, mechanics for this approach would be very similar to the code above - you'd probably let users choose a locale from a dropdown list and save it to their profile in the database. Then you'd set the locale to this value. +In general, this approach is far less reliable than using the language header and is not recommended for most web applications. -Internationalizing your Application +#### Storing the Locale from the Session or Cookies + +WARNING: You may be tempted to store the chosen locale in a _session_ or a *cookie*. However, **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [*RESTful*](http://en.wikipedia.org/wiki/Representational_State_Transfer). Read more about the RESTful approach in [Stefan Tilkov's articles](http://www.infoq.com/articles/rest-introduction). Sometimes there are exceptions to this rule and those are discussed below. + +Internationalization and Localization ----------------------------------- -OK! Now you've initialized I18n support for your Ruby on Rails application and told it which locale to use and how to preserve it between requests. With that in place, you're now ready for the really interesting stuff. +OK! Now you've initialized I18n support for your Ruby on Rails application and told it which locale to use and how to preserve it between requests. -Let's _internationalize_ our application, i.e. abstract every locale-specific parts, and then _localize_ it, i.e. provide necessary translations for these abstracts. +Next we need to _internationalize_ our application by abstracting every locale-specific element. Finally, we need to _localize_ it by providing necessary translations for these abstracts. -You most probably have something like this in one of your applications: +Given the following example: ```ruby # config/routes.rb @@ -344,9 +350,9 @@ end ![rails i18n demo untranslated](images/i18n/demo_untranslated.png) -### Adding Translations +### Abstracting Localized Code -Obviously there are **two strings that are localized to English**. In order to internationalize this code, **replace these strings** with calls to Rails' `#t` helper with a key that makes sense for the translation: +There are two strings in our code that are in English and that users will be rendered in our response ("Hello Flash" and "Hello World"). In order to internationalize this code, these strings need to be replaced by calls to Rails' `#t` helper with an appropriate key for each string: ```ruby # app/controllers/home_controller.rb @@ -363,13 +369,15 @@ end <p><%= flash[:notice] %></p> ``` -When you now render this view, it will show an error message which tells you that the translations for the keys `:hello_world` and `:hello_flash` are missing. +Now, when this view is rendered, it will show an error message which tells you that the translations for the keys `:hello_world` and `:hello_flash` are missing. ![rails i18n demo translation missing](images/i18n/demo_translation_missing.png) NOTE: Rails adds a `t` (`translate`) helper method to your views so that you do not need to spell out `I18n.t` all the time. Additionally this helper will catch missing translations and wrap the resulting error message into a `<span class="translation_missing">`. -So let's add the missing translations into the dictionary files (i.e. do the "localization" part): +### Providing Translations for Internationalized Strings + +Add the missing translations into the translation dictionary files: ```yaml # config/locales/en.yml @@ -383,11 +391,11 @@ pirate: hello_flash: Ahoy Flash ``` -There you go. Because you haven't changed the default_locale, I18n will use English. Your application now shows: +Because the `default_locale` hasn't changed, translations use the `:en` locale and the response renders the english strings: ![rails i18n demo translated to English](images/i18n/demo_translated_en.png) -And when you change the URL to pass the pirate locale (`http://localhost:3000?locale=pirate`), you'll get: +If the locale is set via the URL to the pirate locale (`http://localhost:3000?locale=pirate`), the response renders the pirate strings: ![rails i18n demo translated to pirate](images/i18n/demo_translated_pirate.png) @@ -395,21 +403,64 @@ NOTE: You need to restart the server when you add new locale files. You may use YAML (`.yml`) or plain Ruby (`.rb`) files for storing your translations in SimpleStore. YAML is the preferred option among Rails developers. However, it has one big disadvantage. YAML is very sensitive to whitespace and special characters, so the application may not load your dictionary properly. Ruby files will crash your application on first request, so you may easily find what's wrong. (If you encounter any "weird issues" with YAML dictionaries, try putting the relevant portion of your dictionary into a Ruby file.) -### Passing variables to translations +### Passing Variables to Translations + +One key consideration for successfully internationalizing an application is to +avoid making incorrect assumptions about grammar rules when abstracting localized +code. Grammar rules that seem fundamental in one locale may not hold true in +another one. -You can use variables in the translation messages and pass their values from the view. +Improper abstraction is shown in the following example, where assumptions are +made about the ordering of the different parts of the translation. Note that Rails +provides a `number_to_currency` helper to handle the following case. ```erb -# app/views/home/index.html.erb -<%=t 'greet_username', user: "Bill", message: "Goodbye" %> +# app/views/products/show.html.erb +<%= "#{t('currency')}#{@product.price}" %> +``` + +```yaml +# config/locales/en.yml +en: + currency: "$" + +# config/locales/es.yml +es: + currency: "€" +``` + +If the product's price is 10 then the proper translation for Spanish is "10 €" +instead of "€10" but the abstraction cannot give it. + +To create proper abstraction, the I18n gem ships with a feature called variable +interpolation that allows you to use variables in translation definitions and +pass the values for these variables to the translation method. + +Proper abstraction is shown in the following example: + +```erb +# app/views/products/show.html.erb +<%= t('product_price', price: @product.price) %> ``` ```yaml # config/locales/en.yml en: - greet_username: "%{message}, %{user}!" + product_price: "$%{price}" + +# config/locales/es.yml +es: + product_price: "%{price} €" ``` +All grammatical and punctuation decisions are made in the definition itself, so +the abstraction can give a proper translation. + +NOTE: The `default` and `scope` keywords are reserved and can't be used as +variable names. If used, an `I18n::ReservedInterpolationKey` exception is raised. +If a translation expects an interpolation variable, but this has not been passed +to `#translate`, an `I18n::MissingInterpolationArgument` exception is raised. + ### Adding Date/Time Formats OK! Now let's add a timestamp to the view, so we can demo the **date/time localization** feature as well. To localize the time format you pass the Time object to `I18n.l` or (preferably) use Rails' `#l` helper. You can pick a format by passing the `:format` option - by default the `:default` format is used. @@ -610,20 +661,6 @@ class BooksController < ApplicationController end ``` -### Interpolation - -In many cases you want to abstract your translations so that **variables can be interpolated into the translation**. For this reason the I18n API provides an interpolation feature. - -All options besides `:default` and `:scope` that are passed to `#translate` will be interpolated to the translation: - -```ruby -I18n.backend.store_translations :en, thanks: 'Thanks %{name}!' -I18n.translate :thanks, name: 'Jeremy' -# => 'Thanks Jeremy!' -``` - -If a translation uses `:default` or `:scope` as an interpolation variable, an `I18n::ReservedInterpolationKey` exception is raised. If a translation expects an interpolation variable, but this has not been passed to `#translate`, an `I18n::MissingInterpolationArgument` exception is raised. - ### Pluralization In English there are only one singular and one plural form for a given string, e.g. "1 message" and "2 messages". Other languages ([Arabic](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ar), [Japanese](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ja), [Russian](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html#ru) and many more) have different grammars that have additional or fewer [plural forms](http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html). Thus, the I18n API provides a flexible pluralization feature. diff --git a/guides/source/initialization.md b/guides/source/initialization.md index 199545a3b3..fb499e7b6d 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -106,6 +106,7 @@ A standard Rails application depends on several gems, specifically: * activemodel * activerecord * activesupport +* activejob * arel * builder * bundler @@ -532,6 +533,7 @@ require "rails" action_controller action_view action_mailer + active_job rails/test_unit sprockets ).each do |framework| diff --git a/guides/source/nested_model_forms.md b/guides/source/nested_model_forms.md index 1937369776..121cf2b185 100644 --- a/guides/source/nested_model_forms.md +++ b/guides/source/nested_model_forms.md @@ -106,7 +106,7 @@ Consider the following typical RESTful controller which will prepare a new Perso class PeopleController < ApplicationController def new @person = Person.new - @person.built_address + @person.build_address 2.times { @person.projects.build } end diff --git a/guides/source/routing.md b/guides/source/routing.md index 4ccc50a4d9..b1e4c8ad86 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -177,6 +177,8 @@ WARNING: A [long-standing bug](https://github.com/rails/rails/issues/1769) preve ```ruby form_for @geocoder, url: geocoder_path do |f| + +# snippet for brevity ``` ### Controller Namespaces and Routing diff --git a/guides/source/security.md b/guides/source/security.md index 91d520e997..a464b83838 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -572,7 +572,7 @@ NOTE: _When sanitizing, protecting or verifying something, prefer whitelists ove A blacklist can be a list of bad e-mail addresses, non-public actions or bad HTML tags. This is opposed to a whitelist which lists the good e-mail addresses, public actions, good HTML tags and so on. Although sometimes it is not possible to create a whitelist (in a SPAM filter, for example), _prefer to use whitelist approaches_: -* Use before_action only: [...] instead of except: [...]. This way you don't forget to turn it off for newly added actions. +* Use before_action except: [...] instead of only: [...] for security-related actions. This way you don't forget to enable security checks for newly added actions. * Allow <strong> instead of removing <script> against Cross-Site Scripting (XSS). See below for details. * Don't try to correct user input by blacklists: * This will make the attack work: "<sc<script>ript>".gsub("<script>", "") @@ -925,7 +925,7 @@ HTTP/1.1 200 OK [Second New response created by attacker begins] Content-Type: text/html -<html><font color=red>hey</font></html> [Arbitary malicious input is +<html><font color=red>hey</font></html> [Arbitrary malicious input is Keep-Alive: timeout=15, max=100 shown as the redirected page] Connection: Keep-Alive Transfer-Encoding: chunked diff --git a/guides/source/testing.md b/guides/source/testing.md index cc469f4dae..8278bdcd5d 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -435,7 +435,7 @@ specify to make your test failure messages clearer. It's not required. | `assert_nothing_raised( exception1, exception2, ... ) { block }` | Ensures that the given block doesn't raise one of the given exceptions.| | `assert_instance_of( class, obj, [msg] )` | Ensures that `obj` is an instance of `class`.| | `assert_not_instance_of( class, obj, [msg] )` | Ensures that `obj` is not an instance of `class`.| -| `assert_kind_of( class, obj, [msg] )` | Ensures that `obj` is or descends from `class`.| +| `assert_kind_of( class, obj, [msg] )` | Ensures that `obj` is an instance of `class` or is descending from it.| | `assert_not_kind_of( class, obj, [msg] )` | Ensures that `obj` is not an instance of `class` and is not descending from it.| | `assert_respond_to( obj, symbol, [msg] )` | Ensures that `obj` responds to `symbol`.| | `assert_not_respond_to( obj, symbol, [msg] )` | Ensures that `obj` does not respond to `symbol`.| diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 0aa2152d56..5109e15cd2 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -883,6 +883,20 @@ this gem such as `whitelist_attributes` or `mass_assignment_sanitizer` options. * To re-enable the old finders, you can use the [activerecord-deprecated_finders gem](https://github.com/rails/activerecord-deprecated_finders). +* Rails 4.0 has changed to default join table for `has_and_belongs_to_many` relations to strip the common prefix off the second table name. Any existing `has_and_belongs_to_many` relationship between models with a common prefix must be specified with the `join_table` option. For example: + +```ruby +CatalogCategory < ActiveRecord::Base + has_and_belongs_to_many :catalog_products, join_table: 'catalog_categories_catalog_products' +end + +CatalogProduct < ActiveRecord::Base + has_and_belongs_to_many :catalog_categories, join_table: 'catalog_categories_catalog_products' +end +``` + +* Note that the the prefix takes scopes into account as well, so relations between `Catalog::Category` and `Catalog::Product` or `Catalog::Category` and `CatalogProduct` need to be updated similarly. + ### Active Resource Rails 4.0 extracted Active Resource to its own gem. If you still need the feature you can add the [Active Resource gem](https://github.com/rails/activeresource) in your Gemfile. @@ -912,7 +926,7 @@ Rails 4.0 extracted Active Resource to its own gem. If you still need the featur Please note that you should wait to set `secret_key_base` until you have 100% of your userbase on Rails 4.x and are reasonably sure you will not need to rollback to Rails 3.x. This is because cookies signed based on the new `secret_key_base` in Rails 4.x are not backwards compatible with Rails 3.x. You are free to leave your existing `secret_token` in place, not set the new `secret_key_base`, and ignore the deprecation warnings until you are reasonably sure that your upgrade is otherwise complete. -If you are relying on the ability for external applications or Javascript to be able to read your Rails app's signed session cookies (or signed cookies in general) you should not set `secret_key_base` until you have decoupled these concerns. +If you are relying on the ability for external applications or JavaScript to be able to read your Rails app's signed session cookies (or signed cookies in general) you should not set `secret_key_base` until you have decoupled these concerns. * Rails 4.0 encrypts the contents of cookie-based sessions if `secret_key_base` has been set. Rails 3.x signed, but did not encrypt, the contents of cookie-based session. Signed cookies are "secure" in that they are verified to have been generated by your app and are tamper-proof. However, the contents can be viewed by end users, and encrypting the contents eliminates this caveat/concern without a significant performance penalty. diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 43eee9f442..b61c4448b5 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,14 @@ +* `bin/setup` uses built-in rake tasks (`log:clear`, `tmp:clear`). + + *Mohnish Thallavajhula* + +* Fix mailer previews with attachments by using the mail gem's own API to + locate the first part of the correct mime type. + + Fixes #14435. + + *Andrew White* + * Remove sqlite support from `rails dbconsole`. *Andrew White* @@ -77,7 +88,7 @@ *Andrew White* -* Don't remove all line endings from routes.rb when revoking scaffold. +* Don't remove all line endings from `config/routes.rb` when revoking scaffold. Fixes #15913. diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb index b1f7c29b5a..fe789f3c2a 100644 --- a/railties/lib/rails.rb +++ b/railties/lib/rails.rb @@ -52,6 +52,11 @@ module Rails end end + # Returns a Pathname object of the current rails project, + # otherwise it returns nil if there is no project: + # + # Rails.root + # # => #<Pathname:/Users/someuser/some/path/project> def root application && application.config.root end @@ -94,6 +99,11 @@ module Rails groups end + # Returns a Pathname object of the public folder of the current + # rails project, otherwise it returns nil if there is no project: + # + # Rails.public_path + # # => #<Pathname:/Users/someuser/some/path/project/public> def public_path application && Pathname.new(application.paths["public"].first) end diff --git a/railties/lib/rails/commands/console.rb b/railties/lib/rails/commands/console.rb index 5d37a2b699..ea5d20ea24 100644 --- a/railties/lib/rails/commands/console.rb +++ b/railties/lib/rails/commands/console.rb @@ -1,14 +1,13 @@ require 'optparse' require 'irb' require 'irb/completion' +require 'rails/commands/console_helper' module Rails class Console - class << self - def start(*args) - new(*args).start - end + include ConsoleHelper + class << self def parse_arguments(arguments) options = {} @@ -21,23 +20,8 @@ module Rails opt.parse!(arguments) end - if arguments.first && arguments.first[0] != '-' - env = arguments.first - if available_environments.include? env - options[:environment] = env - else - options[:environment] = %w(production development test).detect {|e| e =~ /^#{env}/} || env - end - end - - options + set_options_env(arguments, options) end - - private - - def available_environments - Dir['config/environments/*.rb'].map { |fname| File.basename(fname, '.*') } - end end attr_reader :options, :app, :console @@ -57,12 +41,9 @@ module Rails end def environment - options[:environment] ||= ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development' - end - - def environment? - environment + options[:environment] ||= super end + alias_method :environment?, :environment def set_environment! Rails.env = environment diff --git a/railties/lib/rails/commands/console_helper.rb b/railties/lib/rails/commands/console_helper.rb new file mode 100644 index 0000000000..8ee0b60012 --- /dev/null +++ b/railties/lib/rails/commands/console_helper.rb @@ -0,0 +1,34 @@ +require 'active_support/concern' + +module Rails + module ConsoleHelper # :nodoc: + extend ActiveSupport::Concern + + module ClassMethods + def start(*args) + new(*args).start + end + + private + def set_options_env(arguments, options) + if arguments.first && arguments.first[0] != '-' + env = arguments.first + if available_environments.include? env + options[:environment] = env + else + options[:environment] = %w(production development test).detect { |e| e =~ /^#{env}/ } || env + end + end + options + end + + def available_environments + Dir['config/environments/*.rb'].map { |fname| File.basename(fname, '.*') } + end + end + + def environment + ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" + end + end +end
\ No newline at end of file diff --git a/railties/lib/rails/commands/dbconsole.rb b/railties/lib/rails/commands/dbconsole.rb index 3b22b582cf..dca60f948f 100644 --- a/railties/lib/rails/commands/dbconsole.rb +++ b/railties/lib/rails/commands/dbconsole.rb @@ -1,13 +1,49 @@ require 'erb' require 'yaml' require 'optparse' +require 'rails/commands/console_helper' module Rails class DBConsole + include ConsoleHelper + attr_reader :arguments - def self.start - new.start + class << self + def parse_arguments(arguments) + options = {} + + OptionParser.new do |opt| + opt.banner = "Usage: rails dbconsole [environment] [options]" + opt.on("-p", "--include-password", "Automatically provide the password from database.yml") do |v| + options['include_password'] = true + end + + opt.on("--mode [MODE]", ['html', 'list', 'line', 'column'], + "Automatically put the sqlite3 database in the specified mode (html, list, line, column).") do |mode| + options['mode'] = mode + end + + opt.on("--header") do |h| + options['header'] = h + end + + opt.on("-h", "--help", "Show this help message.") do + puts opt + exit + end + + opt.on("-e", "--environment=name", String, + "Specifies the environment to run this console under (test/development/production).", + "Default: development" + ) { |v| options[:environment] = v.strip } + + opt.parse!(arguments) + abort opt.to_s unless (0..1).include?(arguments.size) + end + + set_options_env(arguments, options) + end end def initialize(arguments = ARGV) @@ -15,7 +51,7 @@ module Rails end def start - options = parse_arguments(arguments) + options = self.class.parse_arguments(arguments) ENV['RAILS_ENV'] = options[:environment] || environment case config["adapter"] @@ -101,90 +137,37 @@ module Rails end def environment - if Rails.respond_to?(:env) - Rails.env - else - ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" - end + Rails.respond_to?(:env) ? Rails.env : super end protected + def configurations + require APP_PATH + ActiveRecord::Base.configurations = Rails.application.config.database_configuration + ActiveRecord::Base.configurations + end - def configurations - require APP_PATH - ActiveRecord::Base.configurations = Rails.application.config.database_configuration - ActiveRecord::Base.configurations - end - - def parse_arguments(arguments) - options = {} - - OptionParser.new do |opt| - opt.banner = "Usage: rails dbconsole [environment] [options]" - opt.on("-p", "--include-password", "Automatically provide the password from database.yml") do |v| - options['include_password'] = true - end + def find_cmd_and_exec(commands, *args) + commands = Array(commands) - opt.on("--mode [MODE]", ['html', 'list', 'line', 'column'], - "Automatically put the sqlite3 database in the specified mode (html, list, line, column).") do |mode| - options['mode'] = mode + dirs_on_path = ENV['PATH'].to_s.split(File::PATH_SEPARATOR) + unless (ext = RbConfig::CONFIG['EXEEXT']).empty? + commands = commands.map{|cmd| "#{cmd}#{ext}"} end - opt.on("--header") do |h| - options['header'] = h + full_path_command = nil + found = commands.detect do |cmd| + dirs_on_path.detect do |path| + full_path_command = File.join(path, cmd) + File.file?(full_path_command) && File.executable?(full_path_command) + end end - opt.on("-h", "--help", "Show this help message.") do - puts opt - exit - end - - opt.on("-e", "--environment=name", String, - "Specifies the environment to run this console under (test/development/production).", - "Default: development" - ) { |v| options[:environment] = v.strip } - - opt.parse!(arguments) - abort opt.to_s unless (0..1).include?(arguments.size) - end - - if arguments.first && arguments.first[0] != '-' - env = arguments.first - if available_environments.include? env - options[:environment] = env + if found + exec full_path_command, *args else - options[:environment] = %w(production development test).detect {|e| e =~ /^#{env}/} || env + abort("Couldn't find database client: #{commands.join(', ')}. Check your $PATH and try again.") end end - - options - end - - def available_environments - Dir['config/environments/*.rb'].map { |fname| File.basename(fname, '.*') } - end - - def find_cmd_and_exec(commands, *args) - commands = Array(commands) - - dirs_on_path = ENV['PATH'].to_s.split(File::PATH_SEPARATOR) - unless (ext = RbConfig::CONFIG['EXEEXT']).empty? - commands = commands.map{|cmd| "#{cmd}#{ext}"} - end - - full_path_command = nil - found = commands.detect do |cmd| - dirs_on_path.detect do |path| - full_path_command = File.join(path, cmd) - File.file?(full_path_command) && File.executable?(full_path_command) - end - end - - if found - exec full_path_command, *args - else - abort("Couldn't find database client: #{commands.join(', ')}. Check your $PATH and try again.") - end - end end end diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb index 83cee28fa3..9c287b3804 100644 --- a/railties/lib/rails/engine.rb +++ b/railties/lib/rails/engine.rb @@ -206,42 +206,51 @@ module Rails # With such an engine, everything that is inside the +MyEngine+ module will be isolated from # the application. # - # Consider such controller: + # Consider this controller: # # module MyEngine # class FooController < ActionController::Base # end # end # - # If an engine is marked as isolated, +FooController+ has access only to helpers from +Engine+ and - # <tt>url_helpers</tt> from <tt>MyEngine::Engine.routes</tt>. + # If the +MyEngine+ engine is marked as isolated, +FooController+ only has + # access to helpers from +MyEngine+, and <tt>url_helpers</tt> from + # <tt>MyEngine::Engine.routes</tt>. # - # The next thing that changes in isolated engines is the behavior of routes. Normally, when you namespace - # your controllers, you also need to namespace all your routes. With an isolated engine, - # the namespace is applied by default, so you can ignore it in routes: + # The next thing that changes in isolated engines is the behavior of routes. + # Normally, when you namespace your controllers, you also need to namespace + # the related routes. With an isolated engine, the engine's namespace is + # automatically applied, so you don't need to specify it explicity in your + # routes: # # MyEngine::Engine.routes.draw do # resources :articles # end # - # The routes above will automatically point to <tt>MyEngine::ArticlesController</tt>. Furthermore, you don't - # need to use longer url helpers like <tt>my_engine_articles_path</tt>. Instead, you should simply use - # <tt>articles_path</tt> as you would do with your application. + # If +MyEngine+ is isolated, The routes above will point to + # <tt>MyEngine::ArticlesController</tt>. You also don't need to use longer + # url helpers like +my_engine_articles_path+. Instead, you should simply use + # +articles_path+, like you would do with your main application. # - # To make that behavior consistent with other parts of the framework, an isolated engine also has influence on - # <tt>ActiveModel::Naming</tt>. When you use a namespaced model, like <tt>MyEngine::Article</tt>, it will normally - # use the prefix "my_engine". In an isolated engine, the prefix will be omitted in url helpers and - # form fields for convenience. + # To make this behavior consistent with other parts of the framework, + # isolated engines also have an effect on <tt>ActiveModel::Naming</tt>. In a + # normal Rails app, when you use a namespaced model such as + # <tt>Namespace::Article</tt>, <tt>ActiveModel::Naming</tt> will generate + # names with the prefix "namespace". In an isolated engine, the prefix will + # be omitted in url helpers and form fields, for convenience. # - # polymorphic_url(MyEngine::Article.new) # => "articles_path" + # polymorphic_url(MyEngine::Article.new) + # # => "articles_path" # not "my_engine_articles_path" # # form_for(MyEngine::Article.new) do # text_field :title # => <input type="text" name="article[title]" id="article_title" /> # end # - # Additionally, an isolated engine will set its name according to namespace, so - # MyEngine::Engine.engine_name will be "my_engine". It will also set MyEngine.table_name_prefix - # to "my_engine_", changing the MyEngine::Article model to use the my_engine_articles table. + # Additionally, an isolated engine will set its own name according to its + # namespace, so <tt>MyEngine::Engine.engine_name</tt> will return + # "my_engine". It will also set +MyEngine.table_name_prefix+ to "my_engine_", + # meaning for example that <tt>MyEngine::Article</tt> will use the + # +my_engine_articles+ database table by default. # # == Using Engine's routes outside Engine # @@ -405,7 +414,7 @@ module Rails end end - # Finds engine with given path + # Finds engine with given path. def find(path) expanded_path = File.expand_path path Rails::Engine.subclasses.each do |klass| @@ -559,7 +568,7 @@ module Rails # and the load_once paths. # # This needs to be an initializer, since it needs to run once - # per engine and get the engine as a block parameter + # per engine and get the engine as a block parameter. initializer :set_autoload_paths, before: :bootstrap_hook do ActiveSupport::Dependencies.autoload_paths.unshift(*_all_autoload_paths) ActiveSupport::Dependencies.autoload_once_paths.unshift(*_all_autoload_once_paths) diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 899b33e529..1047a2c429 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -319,7 +319,7 @@ module Rails if app_const =~ /^\d/ raise Error, "Invalid application name #{app_name}. Please give a name which does not start with numbers." elsif RESERVED_NAMES.include?(app_name) - raise Error, "Invalid application name #{app_name}. Please give a name which does not match one of the reserved rails words." + raise Error, "Invalid application name #{app_name}. Please give a name which does not match one of the reserved rails words: #{RESERVED_NAMES}" elsif Object.const_defined?(app_const_base) raise Error, "Invalid application name #{app_name}, constant #{app_const_base} is already in use. Please choose another application name." end diff --git a/railties/lib/rails/generators/rails/app/templates/bin/setup b/railties/lib/rails/generators/rails/app/templates/bin/setup index eee810be30..3a5a2cc1e3 100644 --- a/railties/lib/rails/generators/rails/app/templates/bin/setup +++ b/railties/lib/rails/generators/rails/app/templates/bin/setup @@ -22,8 +22,7 @@ chdir APP_ROOT do system 'ruby bin/rake db:setup' puts "\n== Removing old logs and tempfiles ==" - rm_f Dir.glob('log/*') - rm_rf 'tmp/cache' + system 'ruby bin/rake log:clear tmp:clear' puts "\n== Restarting application server ==" touch 'tmp/restart.txt' diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb index fe19fa38d7..7f5bf0c8b8 100644 --- a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb +++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb @@ -364,7 +364,7 @@ task default: :test elsif camelized =~ /^\d/ raise Error, "Invalid plugin name #{original_name}. Please give a name which does not start with numbers." elsif RESERVED_NAMES.include?(name) - raise Error, "Invalid plugin name #{original_name}. Please give a name which does not match one of the reserved rails words." + raise Error, "Invalid plugin name #{original_name}. Please give a name which does not match one of the reserved rails words: #{RESERVED_NAMES}" elsif Object.const_defined?(camelized) raise Error, "Invalid plugin name #{original_name}, constant #{camelized} is already in use. Please choose another plugin name." end diff --git a/railties/lib/rails/generators/rails/resource/resource_generator.rb b/railties/lib/rails/generators/rails/resource/resource_generator.rb index 8014feb75f..3acf21df13 100644 --- a/railties/lib/rails/generators/rails/resource/resource_generator.rb +++ b/railties/lib/rails/generators/rails/resource/resource_generator.rb @@ -1,6 +1,5 @@ require 'rails/generators/resource_helpers' require 'rails/generators/rails/model/model_generator' -require 'active_support/core_ext/object/blank' module Rails module Generators diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb index 8d825ae7b0..76313575e9 100644 --- a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb +++ b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb @@ -9,7 +9,6 @@ class <%= controller_class_name %>ControllerTest < ActionController::TestCase test "should get index" do get :index assert_response :success - assert_not_nil assigns(:<%= table_name %>) end test "should get new" do @@ -22,7 +21,7 @@ class <%= controller_class_name %>ControllerTest < ActionController::TestCase post :create, params: { <%= "#{singular_table_name}: { #{attributes_hash} }" %> } end - assert_redirected_to <%= singular_table_name %>_path(assigns(:<%= singular_table_name %>)) + assert_redirected_to <%= singular_table_name %>_path(<%= class_name %>.last) end test "should show <%= singular_table_name %>" do @@ -37,7 +36,7 @@ class <%= controller_class_name %>ControllerTest < ActionController::TestCase test "should update <%= singular_table_name %>" do patch :update, params: { id: <%= "@#{singular_table_name}" %>, <%= "#{singular_table_name}: { #{attributes_hash} }" %> } - assert_redirected_to <%= singular_table_name %>_path(assigns(:<%= singular_table_name %>)) + assert_redirected_to <%= singular_table_name %>_path(<%= "@#{singular_table_name}" %>) end test "should destroy <%= singular_table_name %>" do diff --git a/railties/lib/rails/info_controller.rb b/railties/lib/rails/info_controller.rb index 6e61cc3cb5..778105c5f7 100644 --- a/railties/lib/rails/info_controller.rb +++ b/railties/lib/rails/info_controller.rb @@ -18,7 +18,7 @@ class Rails::InfoController < Rails::ApplicationController # :nodoc: def routes if path = params[:path] - path = URI.escape path + path = URI.parser.escape path normalized_path = with_leading_slash path render json: { exact: match_route {|it| it.match normalized_path }, diff --git a/railties/lib/rails/mailers_controller.rb b/railties/lib/rails/mailers_controller.rb index 32740d66da..41422a656c 100644 --- a/railties/lib/rails/mailers_controller.rb +++ b/railties/lib/rails/mailers_controller.rb @@ -16,10 +16,10 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: @page_title = "Mailer Previews for #{@preview.preview_name}" render action: 'mailer' else - email = File.basename(params[:path]) + @email_action = File.basename(params[:path]) - if @preview.email_exists?(email) - @email = @preview.call(email) + if @preview.email_exists?(@email_action) + @email = @preview.call(@email_action) if params[:part] part_type = Mime::Type.lookup(params[:part]) @@ -28,14 +28,14 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: response.content_type = part_type render text: part.respond_to?(:decoded) ? part.decoded : part else - raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{email}" + raise AbstractController::ActionNotFound, "Email part '#{part_type}' not found in #{@preview.name}##{@email_action}" end else @part = find_preferred_part(request.format, Mime::HTML, Mime::TEXT) render action: 'email', layout: false, formats: %w[html] end else - raise AbstractController::ActionNotFound, "Email '#{email}' not found in #{@preview.name}" + raise AbstractController::ActionNotFound, "Email '#{@email_action}' not found in #{@preview.name}" end end end @@ -54,18 +54,20 @@ class Rails::MailersController < Rails::ApplicationController # :nodoc: end def find_preferred_part(*formats) - if @email.multipart? - formats.each do |format| - return find_part(format) if @email.parts.any?{ |p| p.mime_type == format } + formats.each do |format| + if part = @email.find_first_mime_type(format) + return part end - else + end + + if formats.any?{ |f| @email.mime_type == f } @email end end def find_part(format) - if @email.multipart? - @email.parts.find{ |p| p.mime_type == format } + if part = @email.find_first_mime_type(format) + part elsif @email.mime_type == format @email end diff --git a/railties/lib/rails/rack/logger.rb b/railties/lib/rails/rack/logger.rb index 7aaa353b91..12676b18bc 100644 --- a/railties/lib/rails/rack/logger.rb +++ b/railties/lib/rails/rack/logger.rb @@ -8,8 +8,9 @@ module Rails module Rack # Sets log tags, logs the request, calls the app, and flushes the logs. # - # Log tags (+taggers+) can be an Array containing: methods that the <tt>request</tt> object responds to, a Proc - # that accepts an instance of the <tt>request</tt> object, or something that responds to <tt>to_s</tt>. + # Log tags (+taggers+) can be an Array containing: methods that the +request+ + # object responds to, objects that respond to +to_s+ or Proc objects that accept + # an instance of the +request+ object. class Logger < ActiveSupport::LogSubscriber def initialize(app, taggers = nil) @app = app diff --git a/railties/lib/rails/templates/rails/mailers/email.html.erb b/railties/lib/rails/templates/rails/mailers/email.html.erb index 0b08a01896..bb6e49966d 100644 --- a/railties/lib/rails/templates/rails/mailers/email.html.erb +++ b/railties/lib/rails/templates/rails/mailers/email.html.erb @@ -39,6 +39,10 @@ padding: 1px; } + dd:empty:before { + content: "\00a0"; // + } + iframe { border: 0; width: 100%; @@ -84,7 +88,7 @@ <% unless @email.attachments.nil? || @email.attachments.empty? %> <dt>Attachments:</dt> <dd> - <%= @email.attachments.map { |a| a.respond_to?(:original_filename) ? a.original_filename : a.filename }.inspect %> + <%= @email.attachments.map { |a| a.respond_to?(:original_filename) ? a.original_filename : a.filename }.join(', ') %> </dd> <% end %> @@ -99,7 +103,14 @@ </dl> </header> -<iframe seamless name="messageBody" src="?part=<%= Rack::Utils.escape(@part.mime_type) %>"></iframe> +<% if @part && @part.mime_type %> + <iframe seamless name="messageBody" src="?part=<%= Rack::Utils.escape(@part.mime_type) %>"></iframe> +<% else %> + <p> + You are trying to preview an email that does not have any content. + This is probably because the <em>mail</em> method has not been called in <em><%= @preview.preview_name %>#<%= @email_action %></em>. + </p> +<% end %> </body> </html> diff --git a/railties/lib/rails/templates/rails/mailers/index.html.erb b/railties/lib/rails/templates/rails/mailers/index.html.erb index c4c9757d57..000930c039 100644 --- a/railties/lib/rails/templates/rails/mailers/index.html.erb +++ b/railties/lib/rails/templates/rails/mailers/index.html.erb @@ -1,8 +1,8 @@ <% @previews.each do |preview| %> -<h3><%= link_to preview.preview_name.titleize, "/rails/mailers/#{preview.preview_name}" %></h3> +<h3><%= link_to preview.preview_name.titleize, url_for(controller: "rails/mailers", action: "preview", path: preview.preview_name) %></h3> <ul> <% preview.emails.each do |email| %> -<li><%= link_to email, "/rails/mailers/#{preview.preview_name}/#{email}" %></li> +<li><%= link_to email, url_for(controller: "rails/mailers", action: "preview", path: "#{preview.preview_name}/#{email}") %></li> <% end %> </ul> <% end %> diff --git a/railties/lib/rails/templates/rails/mailers/mailer.html.erb b/railties/lib/rails/templates/rails/mailers/mailer.html.erb index 607c8d1677..c12ead0f90 100644 --- a/railties/lib/rails/templates/rails/mailers/mailer.html.erb +++ b/railties/lib/rails/templates/rails/mailers/mailer.html.erb @@ -1,6 +1,6 @@ <h3><%= @preview.preview_name.titleize %></h3> <ul> <% @preview.emails.each do |email| %> -<li><%= link_to email, "/rails/mailers/#{@preview.preview_name}/#{email}" %></li> +<li><%= link_to email, url_for(controller: "rails/mailers", action: "preview", path: "#{@preview.preview_name}/#{email}") %></li> <% end %> </ul> diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb index 5cf44e6331..a83e39faee 100644 --- a/railties/lib/rails/test_help.rb +++ b/railties/lib/rails/test_help.rb @@ -27,13 +27,15 @@ if defined?(ActiveRecord::Base) end class ActionController::TestCase - setup do + def before_setup @routes = Rails.application.routes + super end end class ActionDispatch::IntegrationTest - setup do + def before_setup @routes = Rails.application.routes + super end end diff --git a/railties/lib/rails/test_unit/sub_test_task.rb b/railties/lib/rails/test_unit/sub_test_task.rb deleted file mode 100644 index 6fa96d2ced..0000000000 --- a/railties/lib/rails/test_unit/sub_test_task.rb +++ /dev/null @@ -1,126 +0,0 @@ -require 'rake/testtask' - -module Rails - class TestTask < Rake::TestTask # :nodoc: all - # A utility class which is used primarily in "rails/test_unit/testing.rake" - # to help define rake tasks corresponding to <tt>rake test</tt>. - # - # This class takes a TestInfo class and defines the appropriate rake task - # based on the information, then invokes it. - class TestCreator # :nodoc: - def initialize(info) - @info = info - end - - def invoke_rake_task - if @info.files.any? - create_and_run_single_test - reset_application_tasks - else - Rake::Task[ENV['TEST'] ? 'test:single' : 'test:run'].invoke - end - end - - private - - def create_and_run_single_test - Rails::TestTask.new('test:single') { |t| - t.test_files = @info.files - } - ENV['TESTOPTS'] ||= @info.opts - Rake::Task['test:single'].invoke - end - - def reset_application_tasks - Rake.application.top_level_tasks.replace @info.tasks - end - end - - # This is a utility class used by the <tt>TestTask::TestCreator</tt> class. - # This class takes a set of test tasks and checks to see if they correspond - # to test files (or can be transformed into test files). Calling <tt>files</tt> - # provides the set of test files and is used when initializing tests after - # a call to <tt>rake test</tt>. - class TestInfo # :nodoc: - def initialize(tasks) - @tasks = tasks - @files = nil - end - - def files - @files ||= @tasks.map { |task| - [task, translate(task)].find { |file| test_file?(file) } - }.compact - end - - def translate(file) - if file =~ /^app\/(.*)$/ - "test/#{$1.sub(/\.rb$/, '')}_test.rb" - else - "test/#{file}_test.rb" - end - end - - def tasks - @tasks - test_file_tasks - opt_names - end - - def opts - opts = opt_names - if opts.any? - "-n #{opts.join ' '}" - end - end - - private - - def test_file_tasks - @tasks.find_all { |task| - [task, translate(task)].any? { |file| test_file?(file) } - } - end - - def test_file?(file) - file =~ /^test/ && File.file?(file) && !File.directory?(file) - end - - def opt_names - (@tasks - test_file_tasks).reject { |t| task_defined? t } - end - - def task_defined?(task) - Rake::Task.task_defined? task - end - end - - def self.test_creator(tasks) - info = TestInfo.new(tasks) - TestCreator.new(info) - end - - def initialize(name = :test) - super - @libs << "test" # lib *and* test seem like a better default - end - - def define - task @name do - if ENV['TESTOPTS'] - ARGV.replace Shellwords.split ENV['TESTOPTS'] - end - libs = @libs - $LOAD_PATH - $LOAD_PATH.unshift(*libs) - file_list.each { |fl| - FileList[fl].to_a.each { |f| require File.expand_path f } - } - end - end - end - - # Silence the default description to cut down on `rake -T` noise. - class SubTestTask < Rake::TestTask # :nodoc: - def desc(string) - # Ignore the description. - end - end -end diff --git a/railties/test/application/bin_setup_test.rb b/railties/test/application/bin_setup_test.rb new file mode 100644 index 0000000000..1bdced02e9 --- /dev/null +++ b/railties/test/application/bin_setup_test.rb @@ -0,0 +1,54 @@ +require 'isolation/abstract_unit' + +module ApplicationTests + class BinSetupTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app + end + + def teardown + teardown_app + end + + def test_bin_setup + Dir.chdir(app_path) do + app_file 'db/schema.rb', <<-RUBY + ActiveRecord::Schema.define(version: 20140423102712) do + create_table(:articles) {} + end + RUBY + + list_tables = lambda { `bin/rails runner 'p ActiveRecord::Base.connection.tables'`.strip } + File.write("log/my.log", "zomg!") + + assert_equal '[]', list_tables.call + assert_equal 5, File.size("log/my.log") + assert_not File.exist?("tmp/restart.txt") + `bin/setup 2>&1` + assert_equal 0, File.size("log/my.log") + assert_equal '["articles", "schema_migrations"]', list_tables.call + assert File.exist?("tmp/restart.txt") + end + end + + def test_bin_setup_output + Dir.chdir(app_path) do + app_file 'db/schema.rb', "" + + output = `bin/setup 2>&1` + assert_equal(<<-OUTPUT, output) +== Installing dependencies == +The Gemfile's dependencies are satisfied + +== Preparing database == + +== Removing old logs and tempfiles == + +== Restarting application server == + OUTPUT + end + end + end +end diff --git a/railties/test/application/configuration/base_test.rb b/railties/test/application/configuration/base_test.rb deleted file mode 100644 index d6a82b139d..0000000000 --- a/railties/test/application/configuration/base_test.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'isolation/abstract_unit' -require 'rack/test' -require 'env_helpers' - -module ApplicationTests - module ConfigurationTests - class BaseTest < ActiveSupport::TestCase - def setup - build_app - boot_rails - FileUtils.rm_rf("#{app_path}/config/environments") - end - - def teardown - teardown_app - FileUtils.rm_rf(new_app) if File.directory?(new_app) - end - - private - def new_app - File.expand_path("#{app_path}/../new_app") - end - - def copy_app - FileUtils.cp_r(app_path, new_app) - end - - def app - @app ||= Rails.application - end - - def require_environment - require "#{app_path}/config/environment" - end - end - end -end
\ No newline at end of file diff --git a/railties/test/application/configuration/custom_test.rb b/railties/test/application/configuration/custom_test.rb index f8d22f6d97..28b3b2f2d6 100644 --- a/railties/test/application/configuration/custom_test.rb +++ b/railties/test/application/configuration/custom_test.rb @@ -1,22 +1,54 @@ -require 'application/configuration/base_test' - -class ApplicationTests::ConfigurationTests::CustomTest < ApplicationTests::ConfigurationTests::BaseTest - test 'access custom configuration point' do - add_to_config <<-RUBY - config.x.payment_processing.schedule = :daily - config.x.payment_processing.retries = 3 - config.x.super_debugger = true - config.x.hyper_debugger = false - config.x.nil_debugger = nil - RUBY - require_environment - - x = Rails.configuration.x - assert_equal :daily, x.payment_processing.schedule - assert_equal 3, x.payment_processing.retries - assert_equal true, x.super_debugger - assert_equal false, x.hyper_debugger - assert_equal nil, x.nil_debugger - assert_nil x.i_do_not_exist.zomg +require 'isolation/abstract_unit' + +module ApplicationTests + module ConfigurationTests + class CustomTest < ActiveSupport::TestCase + def setup + build_app + boot_rails + FileUtils.rm_rf("#{app_path}/config/environments") + end + + def teardown + teardown_app + FileUtils.rm_rf(new_app) if File.directory?(new_app) + end + + test 'access custom configuration point' do + add_to_config <<-RUBY + config.x.payment_processing.schedule = :daily + config.x.payment_processing.retries = 3 + config.x.super_debugger = true + config.x.hyper_debugger = false + config.x.nil_debugger = nil + RUBY + require_environment + + x = Rails.configuration.x + assert_equal :daily, x.payment_processing.schedule + assert_equal 3, x.payment_processing.retries + assert_equal true, x.super_debugger + assert_equal false, x.hyper_debugger + assert_equal nil, x.nil_debugger + assert_nil x.i_do_not_exist.zomg + end + + private + def new_app + File.expand_path("#{app_path}/../new_app") + end + + def copy_app + FileUtils.cp_r(app_path, new_app) + end + + def app + @app ||= Rails.application + end + + def require_environment + require "#{app_path}/config/environment" + end + end end end diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 8f5b2d0d68..38516a1c1a 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -690,7 +690,7 @@ module ApplicationTests _ = ActionMailer::Base - assert_equal [::MyPreviewMailInterceptor], ActionMailer::Base.preview_interceptors + assert_equal [ActionMailer::InlinePreviewInterceptor, ::MyPreviewMailInterceptor], ActionMailer::Base.preview_interceptors end test "registers multiple preview interceptors with ActionMailer" do @@ -703,7 +703,20 @@ module ApplicationTests _ = ActionMailer::Base - assert_equal [MyPreviewMailInterceptor, MyOtherPreviewMailInterceptor], ActionMailer::Base.preview_interceptors + assert_equal [ActionMailer::InlinePreviewInterceptor, MyPreviewMailInterceptor, MyOtherPreviewMailInterceptor], ActionMailer::Base.preview_interceptors + end + + test "default preview interceptor can be removed" do + app_file 'config/initializers/preview_interceptors.rb', <<-RUBY + ActionMailer::Base.preview_interceptors.delete(ActionMailer::InlinePreviewInterceptor) + RUBY + + require "#{app_path}/config/environment" + require "mail" + + _ = ActionMailer::Base + + assert_equal [], ActionMailer::Base.preview_interceptors end test "registers observers with ActionMailer" do diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb index 85066210f3..1027bca2c1 100644 --- a/railties/test/application/loading_test.rb +++ b/railties/test/application/loading_test.rb @@ -210,7 +210,7 @@ class LoadingTest < ActiveSupport::TestCase app_file 'config/routes.rb', <<-RUBY $counter ||= 0 Rails.application.routes.draw do - get '/c', to: lambda { |env| User; [200, {"Content-Type" => "text/plain"}, [$counter.to_s]] } + get '/c', to: lambda { |env| User.name; [200, {"Content-Type" => "text/plain"}, [$counter.to_s]] } end RUBY @@ -243,7 +243,7 @@ class LoadingTest < ActiveSupport::TestCase $counter ||= 1 $counter *= 2 Rails.application.routes.draw do - get '/c', to: lambda { |env| User; [200, {"Content-Type" => "text/plain"}, [$counter.to_s]] } + get '/c', to: lambda { |env| User.name; [200, {"Content-Type" => "text/plain"}, [$counter.to_s]] } end RUBY diff --git a/railties/test/application/mailer_previews_test.rb b/railties/test/application/mailer_previews_test.rb index 1752a9f3c6..e462d2c15e 100644 --- a/railties/test/application/mailer_previews_test.rb +++ b/railties/test/application/mailer_previews_test.rb @@ -1,5 +1,7 @@ require 'isolation/abstract_unit' require 'rack/test' +require 'base64' + module ApplicationTests class MailerPreviewsTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation @@ -327,6 +329,32 @@ module ApplicationTests assert_match "Email 'bar' not found in NotifierPreview", last_response.body end + test "mailer preview NullMail" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + # does not call +mail+ + end + end + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo" + assert_match "You are trying to preview an email that does not have any content.", last_response.body + assert_match "notifier#foo", last_response.body + end + test "mailer preview email part not found" do mailer 'notifier', <<-RUBY class Notifier < ActionMailer::Base @@ -428,6 +456,222 @@ module ApplicationTests assert_match '<option selected value="?part=text%2Fplain">View as plain-text email</option>', last_response.body end + test "mailer previews create correct links when loaded on a subdirectory" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers", {}, 'SCRIPT_NAME' => '/my_app' + assert_match '<h3><a href="/my_app/rails/mailers/notifier">Notifier</a></h3>', last_response.body + assert_match '<li><a href="/my_app/rails/mailers/notifier/foo">foo</a></li>', last_response.body + end + + test "plain text mailer preview with attachment" do + image_file "pixel.png", "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEWzIioca/JlAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJgggo=" + + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + attachments['pixel.png'] = File.read("#{app_path}/public/images/pixel.png", mode: 'rb') + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo" + assert_equal 200, last_response.status + assert_match %r[<iframe seamless name="messageBody"], last_response.body + + get "/rails/mailers/notifier/foo?part=text/plain" + assert_equal 200, last_response.status + assert_match %r[Hello, World!], last_response.body + end + + test "multipart mailer preview with attachment" do + image_file "pixel.png", "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEWzIioca/JlAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJgggo=" + + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + attachments['pixel.png'] = File.read("#{app_path}/public/images/pixel.png", mode: 'rb') + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + html_template 'notifier/foo', <<-RUBY + <p>Hello, World!</p> + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo" + assert_equal 200, last_response.status + assert_match %r[<iframe seamless name="messageBody"], last_response.body + + get "/rails/mailers/notifier/foo?part=text/plain" + assert_equal 200, last_response.status + assert_match %r[Hello, World!], last_response.body + + get "/rails/mailers/notifier/foo?part=text/html" + assert_equal 200, last_response.status + assert_match %r[<p>Hello, World!</p>], last_response.body + end + + test "multipart mailer preview with inline attachment" do + image_file "pixel.png", "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEWzIioca/JlAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJgggo=" + + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + attachments['pixel.png'] = File.read("#{app_path}/public/images/pixel.png", mode: 'rb') + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + html_template 'notifier/foo', <<-RUBY + <p>Hello, World!</p> + <%= image_tag attachments['pixel.png'].url %> + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo" + assert_equal 200, last_response.status + assert_match %r[<iframe seamless name="messageBody"], last_response.body + + get "/rails/mailers/notifier/foo?part=text/plain" + assert_equal 200, last_response.status + assert_match %r[Hello, World!], last_response.body + + get "/rails/mailers/notifier/foo?part=text/html" + assert_equal 200, last_response.status + assert_match %r[<p>Hello, World!</p>], last_response.body + assert_match %r[src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEWzIioca/JlAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJgggo="], last_response.body + end + + test "multipart mailer preview with attached email" do + mailer 'notifier', <<-RUBY + class Notifier < ActionMailer::Base + default from: "from@example.com" + + def foo + message = ::Mail.new do + from 'foo@example.com' + to 'bar@example.com' + subject 'Important Message' + + text_part do + body 'Goodbye, World!' + end + + html_part do + body '<p>Goodbye, World!</p>' + end + end + + attachments['message.eml'] = message.to_s + mail to: "to@example.org" + end + end + RUBY + + text_template 'notifier/foo', <<-RUBY + Hello, World! + RUBY + + html_template 'notifier/foo', <<-RUBY + <p>Hello, World!</p> + RUBY + + mailer_preview 'notifier', <<-RUBY + class NotifierPreview < ActionMailer::Preview + def foo + Notifier.foo + end + end + RUBY + + app('development') + + get "/rails/mailers/notifier/foo" + assert_equal 200, last_response.status + assert_match %r[<iframe seamless name="messageBody"], last_response.body + + get "/rails/mailers/notifier/foo?part=text/plain" + assert_equal 200, last_response.status + assert_match %r[Hello, World!], last_response.body + + get "/rails/mailers/notifier/foo?part=text/html" + assert_equal 200, last_response.status + assert_match %r[<p>Hello, World!</p>], last_response.body + end + private def build_app super @@ -449,5 +693,9 @@ module ApplicationTests def text_template(name, contents) app_file("app/views/#{name}.text.erb", contents) end + + def image_file(name, contents) + app_file("public/images/#{name}", Base64.strict_decode64(contents), 'wb') + end end end diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 0648b11813..de14f269df 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -190,7 +190,7 @@ module ApplicationTests bundle exec rake db:migrate test` end - assert_match(/7 runs, 13 assertions, 0 failures, 0 errors/, output) + assert_match(/7 runs, 12 assertions, 0 failures, 0 errors/, output) assert_no_match(/Errors running/, output) end @@ -203,7 +203,7 @@ module ApplicationTests bundle exec rake db:migrate test` end - assert_match(/7 runs, 13 assertions, 0 failures, 0 errors/, output) + assert_match(/7 runs, 12 assertions, 0 failures, 0 errors/, output) assert_no_match(/Errors running/, output) end diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb index 2e8dd99f36..7950ed6aa7 100644 --- a/railties/test/commands/dbconsole_test.rb +++ b/railties/test/commands/dbconsole_test.rb @@ -99,19 +99,15 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase end def test_rails_env_is_development_when_argument_is_dev - dbconsole = Rails::DBConsole.new - - dbconsole.stub(:available_environments, ['development', 'test']) do - options = dbconsole.send(:parse_arguments, ['dev']) + Rails::DBConsole.stub(:available_environments, ['development', 'test']) do + options = Rails::DBConsole.send(:parse_arguments, ['dev']) assert_match('development', options[:environment]) end end def test_rails_env_is_dev_when_argument_is_dev_and_dev_env_is_present - dbconsole = Rails::DBConsole.new - - dbconsole.stub(:available_environments, ['dev']) do - options = dbconsole.send(:parse_arguments, ['dev']) + Rails::DBConsole.stub(:available_environments, ['dev']) do + options = Rails::DBConsole.send(:parse_arguments, ['dev']) assert_match('dev', options[:environment]) end end diff --git a/railties/test/generators/named_base_test.rb b/railties/test/generators/named_base_test.rb index 18a26fde05..1c32fc1bfd 100644 --- a/railties/test/generators/named_base_test.rb +++ b/railties/test/generators/named_base_test.rb @@ -1,6 +1,6 @@ require 'generators/generators_test_helper' require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator' -require 'mocha/setup' # FIXME: stop using mocha +require 'minitest/mock' class NamedBaseTest < Rails::Generators::TestCase include GeneratorsTestHelper @@ -80,10 +80,13 @@ class NamedBaseTest < Rails::Generators::TestCase def test_application_name g = generator ['Admin::Foo'] - Rails.stubs(:application).returns(Object.new) - assert_name g, "object", :application_name - Rails.stubs(:application).returns(nil) - assert_name g, "application", :application_name + Rails.stub(:application, Object.new) do + assert_name g, "object", :application_name + end + + Rails.stub(:application, nil) do + assert_name g, "application", :application_name + end end def test_index_helper @@ -103,11 +106,11 @@ class NamedBaseTest < Rails::Generators::TestCase def test_hide_namespace g = generator ['Hidden'] - g.class.stubs(:namespace).returns('hidden') - - assert !Rails::Generators.hidden_namespaces.include?('hidden') - g.class.hide! - assert Rails::Generators.hidden_namespaces.include?('hidden') + g.class.stub(:namespace, 'hidden') do + assert !Rails::Generators.hidden_namespaces.include?('hidden') + g.class.hide! + assert Rails::Generators.hidden_namespaces.include?('hidden') + end end def test_scaffold_plural_names_with_model_name_option diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index a0f244da28..d6e5f4bd89 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -38,7 +38,7 @@ class PluginGeneratorTest < Rails::Generators::TestCase assert_equal "Invalid plugin name 43things. Please give a name which does not start with numbers.\n", content content = capture(:stderr){ run_generator [File.join(destination_root, "plugin")] } - assert_equal "Invalid plugin name plugin. Please give a name which does not match one of the reserved rails words.\n", content + assert_equal "Invalid plugin name plugin. Please give a name which does not match one of the reserved rails words: [\"application\", \"destroy\", \"plugin\", \"runner\", \"test\"]\n", content content = capture(:stderr){ run_generator [File.join(destination_root, "Digest")] } assert_equal "Invalid plugin name Digest, constant Digest is already in use. Please choose another plugin name.\n", content diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb index 68f07f29d7..3bcf3894ce 100644 --- a/railties/test/generators/shared_generator_tests.rb +++ b/railties/test/generators/shared_generator_tests.rb @@ -56,7 +56,7 @@ module SharedGeneratorTests reserved_words = %w[application destroy plugin runner test] reserved_words.each do |reserved| content = capture(:stderr){ run_generator [File.join(destination_root, reserved)] } - assert_match(/Invalid \w+ name #{reserved}. Please give a name which does not match one of the reserved rails words.\n/, content) + assert_match(/Invalid \w+ name #{reserved}. Please give a name which does not match one of the reserved rails words: \["application", "destroy", "plugin", "runner", "test"\]\n/, content) end end diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index fdcfb30090..ce75aba4eb 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -37,7 +37,7 @@ class GeneratorsTest < Rails::Generators::TestCase def test_generator_multiple_suggestions name = :tas output = capture(:stdout){ Rails::Generators.invoke name } - assert_match "Maybe you meant 'task', 'job' or 'assets'", output + assert_match "Maybe you meant 'task', 'job' or", output end def test_help_when_a_generator_with_required_arguments_is_invoked_without_arguments diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 4509797da1..65d8a55421 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -270,9 +270,9 @@ module TestHelpers File.open(file, "w+") { |f| f.puts contents } end - def app_file(path, contents) + def app_file(path, contents, mode = 'w') FileUtils.mkdir_p File.dirname("#{app_path}/#{path}") - File.open("#{app_path}/#{path}", 'w') do |f| + File.open("#{app_path}/#{path}", mode) do |f| f.puts contents end end diff --git a/railties/test/path_generation_test.rb b/railties/test/path_generation_test.rb index d22374a1ff..27e64b97b7 100644 --- a/railties/test/path_generation_test.rb +++ b/railties/test/path_generation_test.rb @@ -1,8 +1,6 @@ require 'abstract_unit' require 'active_support/core_ext/object/with_options' require 'active_support/core_ext/object/json' -require 'rails' -require 'rails/application' class PathGenerationTest < ActiveSupport::TestCase attr_reader :app diff --git a/railties/test/test_info_test.rb b/railties/test/test_info_test.rb deleted file mode 100644 index b9c3a9c0c7..0000000000 --- a/railties/test/test_info_test.rb +++ /dev/null @@ -1,60 +0,0 @@ -require 'abstract_unit' -require 'rails/test_unit/sub_test_task' - -module Rails - class TestInfoTest < ActiveSupport::TestCase - def test_test_files - info = new_test_info ['test'] - assert_predicate info.files, :empty? - assert_nil info.opts - assert_equal ['test'], info.tasks - end - - def test_with_file - info = new_test_info ['test', __FILE__] - assert_equal [__FILE__], info.files - assert_nil info.opts - assert_equal ['test'], info.tasks - end - - def test_with_opts - info = new_test_info ['test', __FILE__, '/foo/'] - assert_equal [__FILE__], info.files - assert_equal '-n /foo/', info.opts - assert_equal ['test'], info.tasks - end - - def test_with_model_shorthand - info = new_test_info ['test', 'models/foo', '/foo/'] - - def info.test_file?(file) - file == "test/models/foo_test.rb" || super - end - - assert_equal ['test/models/foo_test.rb'], info.files - assert_equal '-n /foo/', info.opts - assert_equal ['test'], info.tasks - end - - def test_with_model_path - info = new_test_info ['test', 'app/models/foo.rb', '/foo/'] - - def info.test_file?(file) - file == "test/models/foo_test.rb" || super - end - - assert_equal ['test/models/foo_test.rb'], info.files - assert_equal '-n /foo/', info.opts - assert_equal ['test'], info.tasks - end - - private - def new_test_info(tasks) - Class.new(TestTask::TestInfo) { - def task_defined?(task) - task == "test" - end - }.new tasks - end - end -end |