diff options
60 files changed, 597 insertions, 159 deletions
diff --git a/actioncable/MIT-LICENSE b/actioncable/MIT-LICENSE index a4910677eb..27a17cf41b 100644 --- a/actioncable/MIT-LICENSE +++ b/actioncable/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2015 Basecamp, LLC +Copyright (c) 2015-2016 Basecamp, LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actioncable/lib/action_cable.rb b/actioncable/lib/action_cable.rb index f27698765c..97f485b32e 100644 --- a/actioncable/lib/action_cable.rb +++ b/actioncable/lib/action_cable.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2015 Basecamp, LLC +# Copyright (c) 2015-2016 Basecamp, LLC # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actioncable/lib/assets/javascripts/action_cable/connection.coffee b/actioncable/lib/assets/javascripts/action_cable/connection.coffee index 2f69a9b26c..fbd7dbd35b 100644 --- a/actioncable/lib/assets/javascripts/action_cable/connection.coffee +++ b/actioncable/lib/assets/javascripts/action_cable/connection.coffee @@ -79,6 +79,3 @@ class ActionCable.Connection return if @disconnected @disconnected = true @consumer.subscriptions.notifyAll("disconnected") - - toJSON: -> - state: @getState() diff --git a/actioncable/lib/assets/javascripts/action_cable/connection_monitor.coffee b/actioncable/lib/assets/javascripts/action_cable/connection_monitor.coffee index b594802be1..99b9a1c6d5 100644 --- a/actioncable/lib/assets/javascripts/action_cable/connection_monitor.coffee +++ b/actioncable/lib/assets/javascripts/action_cable/connection_monitor.coffee @@ -69,11 +69,6 @@ class ActionCable.ConnectionMonitor @consumer.connection.reopen() , 200 - toJSON: -> - interval = @getInterval() - connectionIsStale = @connectionIsStale() - {@startedAt, @stoppedAt, @pingedAt, @reconnectAttempts, connectionIsStale, interval} - now = -> new Date().getTime() diff --git a/actioncable/lib/assets/javascripts/action_cable/consumer.coffee b/actioncable/lib/assets/javascripts/action_cable/consumer.coffee index 5cf8978d77..fcd8d0fb6c 100644 --- a/actioncable/lib/assets/javascripts/action_cable/consumer.coffee +++ b/actioncable/lib/assets/javascripts/action_cable/consumer.coffee @@ -23,9 +23,3 @@ class ActionCable.Consumer send: (data) -> @connection.send(data) - - inspect: -> - JSON.stringify(this, null, 2) - - toJSON: -> - {@url, @subscriptions, @connection, @connectionMonitor} diff --git a/actioncable/lib/assets/javascripts/action_cable/subscriptions.coffee b/actioncable/lib/assets/javascripts/action_cable/subscriptions.coffee index 0316f76a24..ae041ffa2b 100644 --- a/actioncable/lib/assets/javascripts/action_cable/subscriptions.coffee +++ b/actioncable/lib/assets/javascripts/action_cable/subscriptions.coffee @@ -9,7 +9,6 @@ class ActionCable.Subscriptions constructor: (@consumer) -> @subscriptions = [] - @history = [] create: (channelName, mixin) -> channel = channelName @@ -57,22 +56,9 @@ class ActionCable.Subscriptions for subscription in subscriptions subscription[callbackName]?(args...) - if callbackName in ["initialized", "connected", "disconnected", "rejected"] - {identifier} = subscription - @record(notification: {identifier, callbackName, args}) - sendCommand: (subscription, command) -> {identifier} = subscription if identifier is ActionCable.INTERNAL.identifiers.ping @consumer.connection.isOpen() else @consumer.send({command, identifier}) - - record: (data) -> - data.time = new Date() - @history = @history.slice(-19) - @history.push(data) - - toJSON: -> - history: @history - identifiers: (subscription.identifier for subscription in @subscriptions) diff --git a/actionmailer/MIT-LICENSE b/actionmailer/MIT-LICENSE index 3ec7a617cf..8573eb1225 100644 --- a/actionmailer/MIT-LICENSE +++ b/actionmailer/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2015 David Heinemeier Hansson +Copyright (c) 2004-2016 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb index 312dd1997c..55c017e338 100644 --- a/actionmailer/lib/action_mailer.rb +++ b/actionmailer/lib/action_mailer.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 93f75ba635..ac767652b8 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,15 @@ +* Add tests and documentation for `ActionController::Renderers::use_renderers`. + + *Benjamin Fleischer* + +* Fix `ActionController::Parameters#convert_parameters_to_hashes` to return filtered + or unfiltered values based on from where it is called, `to_h` or `to_unsafe_h` + respectively. + + Fixes #22841 + + *Prathamesh Sonpatki* + * Add `ActionController::Parameters#include?` *Justin Coyne* diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE index 3ec7a617cf..8573eb1225 100644 --- a/actionpack/MIT-LICENSE +++ b/actionpack/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2015 David Heinemeier Hansson +Copyright (c) 2004-2016 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index d86a793e4c..f8e0d9cf6c 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -228,7 +228,7 @@ module ActionController expires_in 100.years, public: public yield if stale?(etag: "#{version}-#{request.fullpath}", - last_modified: Time.parse('2011-01-01').utc, + last_modified: Time.new(2011, 1, 1).utc, public: public) end diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb index 22e0bb5955..1f77f9ecaa 100644 --- a/actionpack/lib/action_controller/metal/renderers.rb +++ b/actionpack/lib/action_controller/metal/renderers.rb @@ -26,6 +26,40 @@ module ActionController end module ClassMethods + + # Adds, by name, a renderer or renderers to the +_renderers+ available + # to call within controller actions. + # + # It is useful when rendering from an <tt>ActionController::Metal</tt> controller or + # otherwise to add an available renderer proc to a specific controller. + # + # Both <tt>ActionController::Base</tt> and <tt>ActionController::API</tt> + # include <tt>ActionController::Renderers::All</tt>, making all renderers + # avaialable in the controller. See <tt>Renderers::RENDERERS</tt> and <tt>Renderers.add</tt>. + # + # Since <tt>ActionController::Metal</tt> controllers cannot render, the controller + # must include <tt>AbstractController::Rendering</tt>, <tt>ActionController::Rendering</tt>, + # and <tt>ActionController::Renderers</tt>, and have at lest one renderer. + # + # Rather than including <tt>ActionController::Renderers::All</tt> and including all renderers, + # you may specify which renderers to include by passing the renderer name or names to + # +use_renderers+. For example, a controller that includes only the <tt>:json</tt> renderer + # (+_render_with_renderer_json+) might look like: + # + # class MetalRenderingController < ActionController::Metal + # include AbstractController::Rendering + # include ActionController::Rendering + # include ActionController::Renderers + # + # use_renderers :json + # + # def show + # render json: record + # end + # end + # + # You must specify a +use_renderer+, else the +controller.renderer+ and + # +controller._renderers+ will be <tt>nil</tt>, and the action will fail. def use_renderers(*args) renderers = _renderers + args self._renderers = renderers.freeze diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 7e2745c75a..4cd67a85cc 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -176,7 +176,7 @@ module ActionController # safe_params.to_h # => {"name"=>"Senjougahara Hitagi"} def to_h if permitted? - convert_parameters_to_hashes(@parameters) + convert_parameters_to_hashes(@parameters, :to_h) else slice(*self.class.always_permitted_parameters).permit!.to_h end @@ -186,7 +186,7 @@ module ActionController # <tt>ActiveSupport::HashWithIndifferentAccess</tt> representation of this # parameter. def to_unsafe_h - convert_parameters_to_hashes(@parameters) + convert_parameters_to_hashes(@parameters, :to_unsafe_h) end alias_method :to_unsafe_hash, :to_unsafe_h @@ -595,16 +595,16 @@ module ActionController end end - def convert_parameters_to_hashes(value) + def convert_parameters_to_hashes(value, using) case value when Array - value.map { |v| convert_parameters_to_hashes(v) } + value.map { |v| convert_parameters_to_hashes(v, using) } when Hash value.transform_values do |v| - convert_parameters_to_hashes(v) + convert_parameters_to_hashes(v, using) end.with_indifferent_access when Parameters - value.to_h + value.send(using) else value end diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index f6336c8c7a..7bc6575ccd 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb index 8f8f1bab8b..735b5939dd 100644 --- a/actionpack/lib/action_dispatch/middleware/ssl.rb +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -4,16 +4,18 @@ module ActionDispatch # requests: # # 1. TLS redirect: Permanently redirects http:// requests to https:// - # with the same URL host, path, etc. This is always enabled. Set - # `config.ssl_options` to modify the destination URL - # (e.g. `redirect: { host: "secure.widgets.com", port: 8080 }`) + # with the same URL host, path, etc. Enabled by default. Set `config.ssl_options` + # to modify the destination URL + # (e.g. `redirect: { host: "secure.widgets.com", port: 8080 }`), or set + # `redirect: false` to disable this feature. # # 2. Secure cookies: Sets the `secure` flag on cookies to tell browsers they - # mustn't be sent along with http:// requests. This is always enabled. + # mustn't be sent along with http:// requests. Enabled by default. Set + # `config.ssl_options` with `secure_cookies: false` to disable this feature. # # 3. HTTP Strict Transport Security (HSTS): Tells the browser to remember # this site as TLS-only and automatically redirect non-TLS requests. - # Enabled by default. Pass `hsts: false` to disable. + # Enabled by default. Configure `config.ssl_options` with `hsts: false` to disable. # # Set `config.ssl_options` with `hsts: { … }` to configure HSTS: # * `expires`: How long, in seconds, these settings will stick. Defaults to @@ -41,7 +43,7 @@ module ActionDispatch { expires: HSTS_EXPIRES_IN, subdomains: false, preload: false } end - def initialize(app, redirect: {}, hsts: {}, **options) + def initialize(app, redirect: {}, hsts: {}, secure_cookies: true, **options) @app = app if options[:host] || options[:port] @@ -54,6 +56,7 @@ module ActionDispatch @redirect = redirect end + @secure_cookies = secure_cookies @hsts_header = build_hsts_header(normalize_hsts_options(hsts)) end @@ -63,10 +66,11 @@ module ActionDispatch if request.ssl? @app.call(env).tap do |status, headers, body| set_hsts_header! headers - flag_cookies_as_secure! headers + flag_cookies_as_secure! headers if @secure_cookies end else - redirect_to_https request + return redirect_to_https request if @redirect + @app.call(env) end end diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb index f664dab620..941877d10d 100644 --- a/actionpack/lib/action_pack.rb +++ b/actionpack/lib/action_pack.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb index ef7aab72c6..604ba267d6 100644 --- a/actionpack/test/abstract_unit.rb +++ b/actionpack/test/abstract_unit.rb @@ -371,6 +371,12 @@ module RoutingTestHelpers end end +class MetalRenderingController < ActionController::Metal + include AbstractController::Rendering + include ActionController::Rendering + include ActionController::Renderers +end + class ResourcesController < ActionController::Base def index() head :ok end alias_method :show, :index diff --git a/actionpack/test/controller/metal/renderers_test.rb b/actionpack/test/controller/metal/renderers_test.rb new file mode 100644 index 0000000000..007866a559 --- /dev/null +++ b/actionpack/test/controller/metal/renderers_test.rb @@ -0,0 +1,42 @@ +require 'abstract_unit' +require 'active_support/core_ext/hash/conversions' + +class MetalRenderingJsonController < MetalRenderingController + class Model + def to_json(options = {}) + { a: 'b' }.to_json(options) + end + + def to_xml(options = {}) + { a: 'b' }.to_xml(options) + end + end + + use_renderers :json + + def one + render json: Model.new + end + + def two + render xml: Model.new + end +end + +class RenderersMetalTest < ActionController::TestCase + tests MetalRenderingJsonController + + def test_render_json + get :one + assert_response :success + assert_equal({ a: 'b' }.to_json, @response.body) + assert_equal 'application/json', @response.content_type + end + + def test_render_xml + get :two + assert_response :success + assert_equal(" ", @response.body) + assert_equal 'text/plain', @response.content_type + end +end diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index 598cac5156..896bda2597 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -298,6 +298,14 @@ class ParametersPermitTest < ActiveSupport::TestCase assert_not @params.to_unsafe_h.is_a? ActionController::Parameters end + test "to_unsafe_h returns unfiltered params even after accessing few keys" do + params = ActionController::Parameters.new("f"=>{"language_facet"=>["Tibetan"]}) + expected = {"f"=>{"language_facet"=>["Tibetan"]}} + + assert params['f'].is_a? ActionController::Parameters + assert_equal expected, params.to_unsafe_h + end + test "to_h only deep dups Ruby collections" do company = Class.new do attr_reader :dupped diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb index 7a5b8393dc..11251c86e7 100644 --- a/actionpack/test/dispatch/ssl_test.rb +++ b/actionpack/test/dispatch/ssl_test.rb @@ -12,25 +12,31 @@ class SSLTest < ActionDispatch::IntegrationTest end class RedirectSSLTest < SSLTest - def assert_not_redirected(url, headers: {}) - self.app = build_app + + def assert_not_redirected(url, headers: {}, redirect: {}, deprecated_host: nil, + deprecated_port: nil) + + self.app = build_app ssl_options: { redirect: redirect, + host: deprecated_host, port: deprecated_port + } + get url, headers: headers assert_response :ok end - def assert_redirected(host: nil, port: nil, status: 301, body: [], - deprecated_host: nil, deprecated_port: nil, + def assert_redirected(redirect: {}, deprecated_host: nil, deprecated_port: nil, from: 'http://a/b?c=d', to: from.sub('http', 'https')) - self.app = build_app ssl_options: { - redirect: { host: host, port: port, status: status, body: body }, + redirect = { status: 301, body: [] }.merge(redirect) + + self.app = build_app ssl_options: { redirect: redirect, host: deprecated_host, port: deprecated_port } get from - assert_response status + assert_response redirect[:status] || 301 assert_redirected_to to - assert_equal body.join, @response.body + assert_equal redirect[:body].join, @response.body end test 'https is not redirected' do @@ -46,31 +52,31 @@ class RedirectSSLTest < SSLTest end test 'redirect with non-301 status' do - assert_redirected status: 307 + assert_redirected redirect: { status: 307 } end test 'redirect with custom body' do - assert_redirected body: ['foo'] + assert_redirected redirect: { body: ['foo'] } end test 'redirect to specific host' do - assert_redirected host: 'ssl', to: 'https://ssl/b?c=d' + assert_redirected redirect: { host: 'ssl' }, to: 'https://ssl/b?c=d' end test 'redirect to default port' do - assert_redirected port: 443 + assert_redirected redirect: { port: 443 } end test 'redirect to non-default port' do - assert_redirected port: 8443, to: 'https://a:8443/b?c=d' + assert_redirected redirect: { port: 8443 }, to: 'https://a:8443/b?c=d' end test 'redirect to different host and non-default port' do - assert_redirected host: 'ssl', port: 8443, to: 'https://ssl:8443/b?c=d' + assert_redirected redirect: { host: 'ssl', port: 8443 }, to: 'https://ssl:8443/b?c=d' end test 'redirect to different host including port' do - assert_redirected host: 'ssl:443', to: 'https://ssl:443/b?c=d' + assert_redirected redirect: { host: 'ssl:443' }, to: 'https://ssl:443/b?c=d' end test ':host is deprecated, moved within redirect: { host: … }' do @@ -84,6 +90,10 @@ class RedirectSSLTest < SSLTest assert_redirected deprecated_port: 1, to: 'https://a:1/b?c=d' end end + + test 'no redirect with redirect set to false' do + assert_not_redirected 'http://example.org', redirect: false + end end class StrictTransportSecurityTest < SSLTest @@ -187,6 +197,11 @@ class SecureCookiesTest < SSLTest assert_cookies 'problem=def; path=/; Secure; HttpOnly' end + def test_cookies_as_not_secure_with_secure_cookies_disabled + get headers: { 'Set-Cookie' => DEFAULT }, ssl_options: { secure_cookies: false } + assert_cookies *DEFAULT.split("\n") + end + def test_no_cookies get assert_nil response.headers['Set-Cookie'] diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 91d47cda11..dbdd4378d0 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,11 @@ +* Fix `collection_radio_buttons` hidden_field name and make it appear + before the actual input radio tags to make the real value override + the hidden when passed. + + Fixes #22773 + + *Santiago Pastorino* + * `ActionView::TestCase::Controller#params` returns an instance of `ActionController::Parameters`. diff --git a/actionview/MIT-LICENSE b/actionview/MIT-LICENSE index 3ec7a617cf..8573eb1225 100644 --- a/actionview/MIT-LICENSE +++ b/actionview/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2015 David Heinemeier Hansson +Copyright (c) 2004-2016 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb index c3bbac27fd..0a87500a52 100644 --- a/actionview/lib/action_view.rb +++ b/actionview/lib/action_view.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb index 3256d44e18..3dda47a458 100644 --- a/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb +++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb @@ -23,6 +23,10 @@ module ActionView def render_component(builder) builder.check_box + builder.label end + + def hidden_field_name #:nodoc: + "#{super}[]" + end end end end diff --git a/actionview/lib/action_view/helpers/tags/collection_helpers.rb b/actionview/lib/action_view/helpers/tags/collection_helpers.rb index b87b4281d6..1d3b1ecf0b 100644 --- a/actionview/lib/action_view/helpers/tags/collection_helpers.rb +++ b/actionview/lib/action_view/helpers/tags/collection_helpers.rb @@ -97,16 +97,20 @@ module ActionView # Append a hidden field to make sure something will be sent back to the # server if all radio buttons are unchecked. if options.fetch('include_hidden', true) - rendered_collection + hidden_field + hidden_field + rendered_collection else rendered_collection end end def hidden_field #:nodoc: - hidden_name = @html_options[:name] || "#{tag_name(false, @options[:index])}[]" + hidden_name = @html_options[:name] || hidden_field_name @template_object.hidden_field_tag(hidden_name, "", id: nil) end + + def hidden_field_name #:nodoc: + "#{tag_name(false, @options[:index])}" + end end end end diff --git a/actionview/test/template/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb index b59be8e36c..4f7ea88169 100644 --- a/actionview/test/template/form_collections_helper_test.rb +++ b/actionview/test/template/form_collections_helper_test.rb @@ -202,35 +202,35 @@ class FormCollectionsHelperTest < ActionView::TestCase collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] with_collection_radio_buttons :user, :category_ids, collection, :id, :name - assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 1 + assert_select "input[type=hidden][name='user[category_ids]'][value='']", count: 1 end test 'collection radio buttons generates a hidden field using the given :name in :html_options' do collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] - with_collection_radio_buttons :user, :category_ids, collection, :id, :name, {}, { name: "user[other_category_ids][]" } + with_collection_radio_buttons :user, :category_ids, collection, :id, :name, {}, { name: "user[other_category_ids]" } - assert_select "input[type=hidden][name='user[other_category_ids][]'][value='']", count: 1 + assert_select "input[type=hidden][name='user[other_category_ids]'][value='']", count: 1 end test 'collection radio buttons generates a hidden field with index if it was provided' do collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] with_collection_radio_buttons :user, :category_ids, collection, :id, :name, { index: 322 } - assert_select "input[type=hidden][name='user[322][category_ids][]'][value='']", count: 1 + assert_select "input[type=hidden][name='user[322][category_ids]'][value='']", count: 1 end test 'collection radio buttons does not generate a hidden field if include_hidden option is false' do collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] with_collection_radio_buttons :user, :category_ids, collection, :id, :name, include_hidden: false - assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 0 + assert_select "input[type=hidden][name='user[category_ids]'][value='']", count: 0 end test 'collection radio buttons does not generate a hidden field if include_hidden option is false with key as string' do collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')] with_collection_radio_buttons :user, :category_ids, collection, :id, :name, 'include_hidden' => false - assert_select "input[type=hidden][name='user[category_ids][]'][value='']", count: 0 + assert_select "input[type=hidden][name='user[category_ids]'][value='']", count: 0 end # COLLECTION CHECK BOXES diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index 1f7ff3ca7c..bd40fa06ac 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -1600,11 +1600,11 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do + "<input type='hidden' name='post[active]' value='' />" + "<input id='post_active_true' name='post[active]' type='radio' value='true' />" + "<label for='post_active_true'>true</label>" + "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" + - "<label for='post_active_false'>false</label>" + - "<input type='hidden' name='post[active][]' value='' />" + "<label for='post_active_false'>false</label>" end assert_dom_equal expected, output_buffer @@ -1622,13 +1622,13 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do + "<input type='hidden' name='post[active]' value='' />" + "<label for='post_active_true'>"+ "<input id='post_active_true' name='post[active]' type='radio' value='true' />" + "true</label>" + "<label for='post_active_false'>"+ "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" + - "false</label>" + - "<input type='hidden' name='post[active][]' value='' />" + "false</label>" end assert_dom_equal expected, output_buffer @@ -1648,13 +1648,13 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post_1", "new_post") do + "<input type='hidden' name='post[active]' value='' />" + "<label for='post_active_true'>"+ "<input id='post_active_true' name='post[active]' type='radio' value='true' />" + "true</label>" + "<label for='post_active_false'>"+ "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" + "false</label>"+ - "<input type='hidden' name='post[active][]' value='' />" + "<input id='post_id' name='post[id]' type='hidden' value='1' />" end @@ -1670,11 +1670,11 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "foo_new_post", "new_post") do + "<input type='hidden' name='post[active]' value='' />" + "<input id='foo_post_active_true' name='post[active]' type='radio' value='true' />" + "<label for='foo_post_active_true'>true</label>" + "<input checked='checked' id='foo_post_active_false' name='post[active]' type='radio' value='false' />" + - "<label for='foo_post_active_false'>false</label>" + - "<input type='hidden' name='post[active][]' value='' />" + "<label for='foo_post_active_false'>false</label>" end assert_dom_equal expected, output_buffer @@ -1689,11 +1689,11 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do + "<input type='hidden' name='post[1][active]' value='' />" + "<input id='post_1_active_true' name='post[1][active]' type='radio' value='true' />" + "<label for='post_1_active_true'>true</label>" + "<input checked='checked' id='post_1_active_false' name='post[1][active]' type='radio' value='false' />" + - "<label for='post_1_active_false'>false</label>" + - "<input type='hidden' name='post[1][active][]' value='' />" + "<label for='post_1_active_false'>false</label>" end assert_dom_equal expected, output_buffer @@ -1708,13 +1708,13 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do + "<input name='post[tag_ids][]' type='hidden' value='' />" + "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + "<label for='post_tag_ids_1'>Tag 1</label>" + "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" + "<label for='post_tag_ids_2'>Tag 2</label>" + "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" + - "<label for='post_tag_ids_3'>Tag 3</label>" + - "<input name='post[tag_ids][]' type='hidden' value='' />" + "<label for='post_tag_ids_3'>Tag 3</label>" end assert_dom_equal expected, output_buffer @@ -1732,6 +1732,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do + "<input name='post[tag_ids][]' type='hidden' value='' />" + "<label for='post_tag_ids_1'>" + "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + "Tag 1</label>" + @@ -1740,8 +1741,7 @@ class FormHelperTest < ActionView::TestCase "Tag 2</label>" + "<label for='post_tag_ids_3'>" + "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" + - "Tag 3</label>" + - "<input name='post[tag_ids][]' type='hidden' value='' />" + "Tag 3</label>" end assert_dom_equal expected, output_buffer @@ -1762,6 +1762,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post_1", "new_post") do + "<input name='post[tag_ids][]' type='hidden' value='' />"+ "<label for='post_tag_ids_1'>" + "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + "Tag 1</label>" + @@ -1771,7 +1772,6 @@ class FormHelperTest < ActionView::TestCase "<label for='post_tag_ids_3'>" + "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" + "Tag 3</label>" + - "<input name='post[tag_ids][]' type='hidden' value='' />"+ "<input id='post_id' name='post[id]' type='hidden' value='1' />" end @@ -1788,9 +1788,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "foo_new_post", "new_post") do + "<input name='post[tag_ids][]' type='hidden' value='' />" + "<input checked='checked' id='foo_post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" + - "<label for='foo_post_tag_ids_1'>Tag 1</label>" + - "<input name='post[tag_ids][]' type='hidden' value='' />" + "<label for='foo_post_tag_ids_1'>Tag 1</label>" end assert_dom_equal expected, output_buffer @@ -1806,9 +1806,9 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts", "new_post", "new_post") do + "<input name='post[1][tag_ids][]' type='hidden' value='' />" + "<input checked='checked' id='post_1_tag_ids_1' name='post[1][tag_ids][]' type='checkbox' value='1' />" + - "<label for='post_1_tag_ids_1'>Tag 1</label>" + - "<input name='post[1][tag_ids][]' type='hidden' value='' />" + "<label for='post_1_tag_ids_1'>Tag 1</label>" end assert_dom_equal expected, output_buffer diff --git a/activejob/MIT-LICENSE b/activejob/MIT-LICENSE index 0cef8cdda0..a3ffb46b3f 100644 --- a/activejob/MIT-LICENSE +++ b/activejob/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014-2015 David Heinemeier Hansson +Copyright (c) 2014-2016 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activejob/lib/active_job.rb b/activejob/lib/active_job.rb index eb8091a805..f7e05f7cd3 100644 --- a/activejob/lib/active_job.rb +++ b/activejob/lib/active_job.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2014-2015 David Heinemeier Hansson +# Copyright (c) 2014-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activemodel/MIT-LICENSE b/activemodel/MIT-LICENSE index 3ec7a617cf..8573eb1225 100644 --- a/activemodel/MIT-LICENSE +++ b/activemodel/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2015 David Heinemeier Hansson +Copyright (c) 2004-2016 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 4e1b3f7495..b95c174d6e 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE index 7c2197229d..1f496cf280 100644 --- a/activerecord/MIT-LICENSE +++ b/activerecord/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2015 David Heinemeier Hansson +Copyright (c) 2004-2016 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 264f869c68..f35e1d889e 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index fc12c3f45a..bac5a38a5d 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -331,15 +331,30 @@ module ActiveRecord validation_context = self.validation_context unless [:create, :update].include?(self.validation_context) unless valid = record.valid?(validation_context) if reflection.options[:autosave] + indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveRecord::Base.index_nested_attribute_errors) + record.errors.each do |attribute, message| - if index.nil? || (!reflection.options[:index_errors] && !ActiveRecord::Base.index_nested_attribute_errors) - attribute = "#{reflection.name}.#{attribute}" - else + if indexed_attribute attribute = "#{reflection.name}[#{index}].#{attribute}" + else + attribute = "#{reflection.name}.#{attribute}" end errors[attribute] << message errors[attribute].uniq! end + + record.errors.details.each_key do |attribute| + if indexed_attribute + reflection_attribute = "#{reflection.name}[#{index}].#{attribute}" + else + reflection_attribute = "#{reflection.name}.#{attribute}" + end + + record.errors.details[attribute].each do |error| + errors.details[reflection_attribute] << error + errors.details[reflection_attribute].uniq! + end + end else errors.add(reflection.name) end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 7ded96f8fb..04519f4dc3 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -124,7 +124,7 @@ module ActiveRecord def deserialize(value) return if value.nil? - mapping.key(value.to_i) + mapping.key(value) end def serialize(value) diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index ba238ea142..8c9eab436d 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -126,9 +126,9 @@ module ActiveRecord class PendingMigrationError < MigrationError#:nodoc: def initialize(message = nil) if !message && defined?(Rails.env) - super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rails db:migrate RAILS_ENV=#{::Rails.env}.") + super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rails db:migrate RAILS_ENV=#{::Rails.env}") elsif !message - super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rails db:migrate.") + super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rails db:migrate") else super end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 983bf019bc..b86cc094fc 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -649,8 +649,8 @@ module ActiveRecord # they must differ only by #where (if no #group has been defined) or #having (if a #group is # present). Neither relation may have a #limit, #offset, or #distinct set. # - # Post.where("id = 1").or(Post.where("id = 2")) - # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2')) + # Post.where("id = 1").or(Post.where("author_id = 3")) + # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'author_id = 3')) # def or(other) spawn.or!(other) diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 4e89ba4dd1..2bfc5ff7ae 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -213,7 +213,7 @@ module ActiveRecord end # TODO: Deprecate this - def quoted_id + def quoted_id # :nodoc: self.class.quote_value(@attributes[self.class.primary_key].value_for_database) end end diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 049ed1732e..3d90790367 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -146,8 +146,6 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase end setup do - enable_extension!('uuid-ossp', connection) - connection.create_table('pg_uuids', id: :uuid, default: 'uuid_generate_v1()') do |t| t.string 'name' t.uuid 'other_uuid', default: 'uuid_generate_v4()' @@ -172,7 +170,6 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase drop_table "pg_uuids" drop_table 'pg_uuids_2' connection.execute 'DROP FUNCTION IF EXISTS my_uuid_generator();' - disable_extension!('uuid-ossp', connection) end if ActiveRecord::Base.connection.supports_extensions? @@ -217,8 +214,6 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper setup do - enable_extension!('uuid-ossp', connection) - connection.create_table('pg_uuids', id: false) do |t| t.primary_key :id, :uuid, default: nil t.string 'name' @@ -227,7 +222,6 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase teardown do drop_table "pg_uuids" - disable_extension!('uuid-ossp', connection) end if ActiveRecord::Base.connection.supports_extensions? @@ -260,8 +254,6 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase end setup do - enable_extension!('uuid-ossp', connection) - connection.transaction do connection.create_table('pg_uuid_posts', id: :uuid) do |t| t.string 'title' @@ -276,7 +268,6 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase teardown do drop_table "pg_uuid_comments" drop_table "pg_uuid_posts" - disable_extension!('uuid-ossp', connection) end if ActiveRecord::Base.connection.supports_extensions? diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 0df8f1f798..3608063b01 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -433,6 +433,53 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib ActiveRecord::Base.index_nested_attribute_errors = old_attribute_config end + def test_errors_details_should_be_set + molecule = Molecule.new + valid_electron = Electron.new(name: 'electron') + invalid_electron = Electron.new + + molecule.electrons = [valid_electron, invalid_electron] + + assert_not invalid_electron.valid? + assert valid_electron.valid? + assert_not molecule.valid? + assert_equal [{error: :blank}], molecule.errors.details["electrons.name"] + end + + def test_errors_details_should_be_indexed_when_passed_as_array + guitar = Guitar.new + tuning_peg_valid = TuningPeg.new + tuning_peg_valid.pitch = 440.0 + tuning_peg_invalid = TuningPeg.new + + guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid] + + assert_not tuning_peg_invalid.valid? + assert tuning_peg_valid.valid? + assert_not guitar.valid? + assert_equal [{error: :not_a_number, value: nil}] , guitar.errors.details["tuning_pegs[1].pitch"] + assert_equal [], guitar.errors.details["tuning_pegs.pitch"] + end + + def test_errors_details_should_be_indexed_when_global_flag_is_set + old_attribute_config = ActiveRecord::Base.index_nested_attribute_errors + ActiveRecord::Base.index_nested_attribute_errors = true + + molecule = Molecule.new + valid_electron = Electron.new(name: 'electron') + invalid_electron = Electron.new + + molecule.electrons = [valid_electron, invalid_electron] + + assert_not invalid_electron.valid? + assert valid_electron.valid? + assert_not molecule.valid? + assert_equal [{error: :blank}], molecule.errors.details["electrons[1].name"] + assert_equal [], molecule.errors.details["electrons.name"] + ensure + ActiveRecord::Base.index_nested_attribute_errors = old_attribute_config + end + def test_valid_adding_with_nested_attributes molecule = Molecule.new valid_electron = Electron.new(name: 'electron') diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 8eb027d53f..b926a92849 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -80,12 +80,10 @@ module ActiveRecord end def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences - enable_extension!('uuid-ossp', connection) connection.create_table :cats, id: :uuid assert_nothing_raised { rename_table :cats, :felines } ActiveSupport::Deprecation.silence { assert connection.table_exists? :felines } ensure - disable_extension!('uuid-ossp', connection) connection.drop_table :cats, if_exists: true connection.drop_table :felines, if_exists: true end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index cee1e57c93..4963dd25d1 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,8 @@ +* Support extended grapheme clusters and UAX 29. + + *Adam Roben* + + ## Rails 5.0.0.beta1 (December 18, 2015) ## * Add petabyte and exabyte numeric conversion. diff --git a/activesupport/MIT-LICENSE b/activesupport/MIT-LICENSE index 7bffebb076..40235833ba 100644 --- a/activesupport/MIT-LICENSE +++ b/activesupport/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2005-2015 David Heinemeier Hansson +Copyright (c) 2005-2016 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb index 2019afeb00..94fe893149 100644 --- a/activesupport/lib/active_support.rb +++ b/activesupport/lib/active_support.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2005-2015 David Heinemeier Hansson +# Copyright (c) 2005-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb index 586002b03b..72b20fff06 100644 --- a/activesupport/lib/active_support/multibyte/unicode.rb +++ b/activesupport/lib/active_support/multibyte/unicode.rb @@ -87,19 +87,44 @@ module ActiveSupport pos += 1 previous = codepoints[pos-1] current = codepoints[pos] - if ( - # CR X LF - ( previous == database.boundary[:cr] and current == database.boundary[:lf] ) or - # L X (L|V|LV|LVT) - ( database.boundary[:l] === previous and in_char_class?(current, [:l,:v,:lv,:lvt]) ) or - # (LV|V) X (V|T) - ( in_char_class?(previous, [:lv,:v]) and in_char_class?(current, [:v,:t]) ) or - # (LVT|T) X (T) - ( in_char_class?(previous, [:lvt,:t]) and database.boundary[:t] === current ) or - # X Extend - (database.boundary[:extend] === current) - ) - else + + should_break = + # GB3. CR X LF + if previous == database.boundary[:cr] and current == database.boundary[:lf] + false + # GB4. (Control|CR|LF) ÷ + elsif previous and in_char_class?(previous, [:control,:cr,:lf]) + true + # GB5. ÷ (Control|CR|LF) + elsif in_char_class?(current, [:control,:cr,:lf]) + true + # GB6. L X (L|V|LV|LVT) + elsif database.boundary[:l] === previous and in_char_class?(current, [:l,:v,:lv,:lvt]) + false + # GB7. (LV|V) X (V|T) + elsif in_char_class?(previous, [:lv,:v]) and in_char_class?(current, [:v,:t]) + false + # GB8. (LVT|T) X (T) + elsif in_char_class?(previous, [:lvt,:t]) and database.boundary[:t] === current + false + # GB8a. Regional_Indicator X Regional_Indicator + elsif database.boundary[:regional_indicator] === previous and database.boundary[:regional_indicator] === current + false + # GB9. X Extend + elsif database.boundary[:extend] === current + false + # GB9a. X SpacingMark + elsif database.boundary[:spacingmark] === current + false + # GB9b. Prepend X + elsif database.boundary[:prepend] === previous + false + # GB10. Any ÷ Any + else + true + end + + if should_break unpacked << codepoints[marker..pos-1] marker = pos end diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index 8d4d9d736c..c1e0b19248 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -612,28 +612,54 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase ['abc', 3], ['こにちわ', 4], [[0x0924, 0x094D, 0x0930].pack('U*'), 2], + # GB3 [%w(cr lf), 1], + # GB4 + [%w(cr n), 2], + [%w(lf n), 2], + [%w(control n), 2], + [%w(cr extend), 2], + [%w(lf extend), 2], + [%w(control extend), 2], + # GB 5 + [%w(n cr), 2], + [%w(n lf), 2], + [%w(n control), 2], + [%w(extend cr), 2], + [%w(extend lf), 2], + [%w(extend control), 2], + # GB 6 [%w(l l), 1], [%w(l v), 1], [%w(l lv), 1], [%w(l lvt), 1], + # GB7 [%w(lv v), 1], [%w(lv t), 1], [%w(v v), 1], [%w(v t), 1], + # GB8 [%w(lvt t), 1], [%w(t t), 1], + # GB8a + [%w(r r), 1], + # GB9 [%w(n extend), 1], + # GB9a + [%w(n spacingmark), 1], + # GB10 [%w(n n), 2], + # Other [%w(n cr lf n), 3], - [%w(n l v t), 2] + [%w(n l v t), 2], + [%w(cr extend n), 3], ].each do |input, expected_length| if input.kind_of?(Array) str = string_from_classes(input) else str = input end - assert_equal expected_length, chars(str).grapheme_length + assert_equal expected_length, chars(str).grapheme_length, input.inspect end end @@ -698,7 +724,7 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase # Characters from the character classes as described in UAX #29 character_from_class = { :l => 0x1100, :v => 0x1160, :t => 0x11A8, :lv => 0xAC00, :lvt => 0xAC01, :cr => 0x000D, :lf => 0x000A, - :extend => 0x094D, :n => 0x64 + :extend => 0x094D, :n => 0x64, :spacingmark => 0x0903, :r => 0x1F1E6, :control => 0x0001 } classes.collect do |k| character_from_class[k.intern] diff --git a/activesupport/test/multibyte_grapheme_break_conformance.rb b/activesupport/test/multibyte_grapheme_break_conformance.rb new file mode 100644 index 0000000000..7d185e2cae --- /dev/null +++ b/activesupport/test/multibyte_grapheme_break_conformance.rb @@ -0,0 +1,76 @@ +# encoding: utf-8 + +require 'abstract_unit' + +require 'fileutils' +require 'open-uri' +require 'tmpdir' + +class Downloader + def self.download(from, to) + unless File.exist?(to) + $stderr.puts "Downloading #{from} to #{to}" + unless File.exist?(File.dirname(to)) + system "mkdir -p #{File.dirname(to)}" + end + open(from) do |source| + File.open(to, 'w') do |target| + source.each_line do |l| + target.write l + end + end + end + end + end +end + +class MultibyteGraphemeBreakConformanceTest < ActiveSupport::TestCase + TEST_DATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd/auxiliary" + TEST_DATA_FILE = '/GraphemeBreakTest.txt' + CACHE_DIR = File.join(Dir.tmpdir, 'cache') + + def setup + FileUtils.mkdir_p(CACHE_DIR) + Downloader.download(TEST_DATA_URL + TEST_DATA_FILE, CACHE_DIR + TEST_DATA_FILE) + end + + def test_breaks + each_line_of_break_tests do |*cols| + *clusters, comment = *cols + packed = ActiveSupport::Multibyte::Unicode.pack_graphemes(clusters) + assert_equal clusters, ActiveSupport::Multibyte::Unicode.unpack_graphemes(packed), comment + end + end + + protected + def each_line_of_break_tests(&block) + lines = 0 + max_test_lines = 0 # Don't limit below 21, because that's the header of the testfile + File.open(File.join(CACHE_DIR, TEST_DATA_FILE), 'r') do | f | + until f.eof? || (max_test_lines > 21 and lines > max_test_lines) + lines += 1 + line = f.gets.chomp! + next if (line.empty? || line =~ /^\#/) + + cols, comment = line.split("#") + # Cluster breaks are represented by ÷ + clusters = cols.split("÷").map{|e| e.strip}.reject{|e| e.empty? } + clusters = clusters.map do |cluster| + # Codepoints within each cluster are separated by × + codepoints = cluster.split("×").map{|e| e.strip}.reject{|e| e.empty? } + # codepoints are in hex in the test suite, pack wants them as integers + codepoints.map{|codepoint| codepoint.to_i(16)} + end + + # The tests contain a solitary U+D800 <Non Private Use High + # Surrogate, First> character, which Ruby does not allow to stand + # alone in a UTF-8 string. So we'll just skip it. + next if clusters.flatten.include?(0xd800) + + clusters << comment.strip + + yield(*clusters) + end + end + end +end diff --git a/activesupport/test/multibyte_normalization_conformance.rb b/activesupport/test/multibyte_normalization_conformance.rb new file mode 100644 index 0000000000..839aec7fa8 --- /dev/null +++ b/activesupport/test/multibyte_normalization_conformance.rb @@ -0,0 +1,129 @@ +# encoding: utf-8 + +require 'abstract_unit' +require 'multibyte_test_helpers' + +require 'fileutils' +require 'open-uri' +require 'tmpdir' + +class Downloader + def self.download(from, to) + unless File.exist?(to) + $stderr.puts "Downloading #{from} to #{to}" + unless File.exist?(File.dirname(to)) + system "mkdir -p #{File.dirname(to)}" + end + open(from) do |source| + File.open(to, 'w') do |target| + source.each_line do |l| + target.write l + end + end + end + end + end +end + +class MultibyteNormalizationConformanceTest < ActiveSupport::TestCase + include MultibyteTestHelpers + + UNIDATA_URL = "http://www.unicode.org/Public/#{ActiveSupport::Multibyte::Unicode::UNICODE_VERSION}/ucd" + UNIDATA_FILE = '/NormalizationTest.txt' + CACHE_DIR = File.join(Dir.tmpdir, 'cache') + + def setup + FileUtils.mkdir_p(CACHE_DIR) + Downloader.download(UNIDATA_URL + UNIDATA_FILE, CACHE_DIR + UNIDATA_FILE) + @proxy = ActiveSupport::Multibyte::Chars + end + + def test_normalizations_C + each_line_of_norm_tests do |*cols| + col1, col2, col3, col4, col5, comment = *cols + + # CONFORMANCE: + # 1. The following invariants must be true for all conformant implementations + # + # NFC + # c2 == NFC(c1) == NFC(c2) == NFC(c3) + assert_equal_codepoints col2, @proxy.new(col1).normalize(:c), "Form C - Col 2 has to be NFC(1) - #{comment}" + assert_equal_codepoints col2, @proxy.new(col2).normalize(:c), "Form C - Col 2 has to be NFC(2) - #{comment}" + assert_equal_codepoints col2, @proxy.new(col3).normalize(:c), "Form C - Col 2 has to be NFC(3) - #{comment}" + # + # c4 == NFC(c4) == NFC(c5) + assert_equal_codepoints col4, @proxy.new(col4).normalize(:c), "Form C - Col 4 has to be C(4) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col5).normalize(:c), "Form C - Col 4 has to be C(5) - #{comment}" + end + end + + def test_normalizations_D + each_line_of_norm_tests do |*cols| + col1, col2, col3, col4, col5, comment = *cols + # + # NFD + # c3 == NFD(c1) == NFD(c2) == NFD(c3) + assert_equal_codepoints col3, @proxy.new(col1).normalize(:d), "Form D - Col 3 has to be NFD(1) - #{comment}" + assert_equal_codepoints col3, @proxy.new(col2).normalize(:d), "Form D - Col 3 has to be NFD(2) - #{comment}" + assert_equal_codepoints col3, @proxy.new(col3).normalize(:d), "Form D - Col 3 has to be NFD(3) - #{comment}" + # c5 == NFD(c4) == NFD(c5) + assert_equal_codepoints col5, @proxy.new(col4).normalize(:d), "Form D - Col 5 has to be NFD(4) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col5).normalize(:d), "Form D - Col 5 has to be NFD(5) - #{comment}" + end + end + + def test_normalizations_KC + each_line_of_norm_tests do | *cols | + col1, col2, col3, col4, col5, comment = *cols + # + # NFKC + # c4 == NFKC(c1) == NFKC(c2) == NFKC(c3) == NFKC(c4) == NFKC(c5) + assert_equal_codepoints col4, @proxy.new(col1).normalize(:kc), "Form D - Col 4 has to be NFKC(1) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col2).normalize(:kc), "Form D - Col 4 has to be NFKC(2) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col3).normalize(:kc), "Form D - Col 4 has to be NFKC(3) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col4).normalize(:kc), "Form D - Col 4 has to be NFKC(4) - #{comment}" + assert_equal_codepoints col4, @proxy.new(col5).normalize(:kc), "Form D - Col 4 has to be NFKC(5) - #{comment}" + end + end + + def test_normalizations_KD + each_line_of_norm_tests do | *cols | + col1, col2, col3, col4, col5, comment = *cols + # + # NFKD + # c5 == NFKD(c1) == NFKD(c2) == NFKD(c3) == NFKD(c4) == NFKD(c5) + assert_equal_codepoints col5, @proxy.new(col1).normalize(:kd), "Form KD - Col 5 has to be NFKD(1) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col2).normalize(:kd), "Form KD - Col 5 has to be NFKD(2) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col3).normalize(:kd), "Form KD - Col 5 has to be NFKD(3) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col4).normalize(:kd), "Form KD - Col 5 has to be NFKD(4) - #{comment}" + assert_equal_codepoints col5, @proxy.new(col5).normalize(:kd), "Form KD - Col 5 has to be NFKD(5) - #{comment}" + end + end + + protected + def each_line_of_norm_tests(&block) + lines = 0 + max_test_lines = 0 # Don't limit below 38, because that's the header of the testfile + File.open(File.join(CACHE_DIR, UNIDATA_FILE), 'r') do | f | + until f.eof? || (max_test_lines > 38 and lines > max_test_lines) + lines += 1 + line = f.gets.chomp! + next if (line.empty? || line =~ /^\#/) + + cols, comment = line.split("#") + cols = cols.split(";").map{|e| e.strip}.reject{|e| e.empty? } + next unless cols.length == 5 + + # codepoints are in hex in the test suite, pack wants them as integers + cols.map!{|c| c.split.map{|codepoint| codepoint.to_i(16)}.pack("U*") } + cols << comment + + yield(*cols) + end + end + end + + def inspect_codepoints(str) + str.to_s.unpack("U*").map{|cp| cp.to_s(16) }.join(' ') + end +end diff --git a/guides/source/initialization.md b/guides/source/initialization.md index 7bf7eebb62..6232ef4c57 100644 --- a/guides/source/initialization.md +++ b/guides/source/initialization.md @@ -530,16 +530,17 @@ This file is responsible for requiring all the individual frameworks of Rails: require "rails" %w( - active_record - action_controller - action_view - action_mailer - active_job - rails/test_unit - sprockets -).each do |framework| + active_record/railtie + action_controller/railtie + action_view/railtie + action_mailer/railtie + active_job/railtie + action_cable/engine + rails/test_unit/railtie + sprockets/railtie +).each do |railtie| begin - require "#{framework}/railtie" + require "#{railtie}" rescue LoadError end end diff --git a/guides/source/testing.md b/guides/source/testing.md index a4b62955c5..84f80e3c37 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -654,7 +654,7 @@ The easiest way to see functional tests in action is to generate a controller scaffold: ```bash -$ bin/rails generate scaffold_controller article title:string body:test +$ bin/rails generate scaffold_controller article title:string body:text ... create app/controllers/articles_controller.rb ... diff --git a/railties/MIT-LICENSE b/railties/MIT-LICENSE index 7c2197229d..1f496cf280 100644 --- a/railties/MIT-LICENSE +++ b/railties/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2015 David Heinemeier Hansson +Copyright (c) 2004-2016 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb index ed6a1f82d3..4f1cc0703d 100644 --- a/railties/lib/rails/application/default_middleware_stack.rb +++ b/railties/lib/rails/application/default_middleware_stack.rb @@ -68,7 +68,7 @@ module Rails middleware.use ::ActionDispatch::Cookies unless config.api_only if !config.api_only && config.session_store - if config.force_ssl && !config.session_options.key?(:secure) + if config.force_ssl && config.ssl_options.fetch(:secure_cookies, true) && !config.session_options.key?(:secure) config.session_options[:secure] = true end middleware.use config.session_store, config.session_options diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index 064b109a6f..297ccb1dbf 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -281,6 +281,8 @@ module Rails return [] if options[:skip_sprockets] gems = [] + gems << GemfileEntry.version('sass-rails', '~> 5.0', + 'Use SCSS for stylesheets') gems << GemfileEntry.version('uglifier', '>= 1.3.0', diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.coffee b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.coffee index 49a7cfa69b..07934d026f 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.coffee +++ b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/cable.coffee @@ -1,4 +1,4 @@ -# Action Cable provides the framework to deal with WebSockets in Rails. +# Action Cable provides the framework to deal with WebSockets in Rails. # You can generate new channels where WebSocket features live using the rails generate channel command. # # Turn on the cable connection by removing the comments after the require statements (and ensure it's also on in config/routes.rb). diff --git a/railties/lib/rails/generators/rails/model/USAGE b/railties/lib/rails/generators/rails/model/USAGE index 11daa5c3cb..025bcf4774 100644 --- a/railties/lib/rails/generators/rails/model/USAGE +++ b/railties/lib/rails/generators/rails/model/USAGE @@ -8,14 +8,14 @@ Description: As a special case, specifying 'password:digest' will generate a password_digest field of string type, and configure your generated model and - tests for use with ActiveModel has_secure_password (assuming the default ORM + tests for use with Active Model has_secure_password (assuming the default ORM and test framework are being used). You don't have to think up every attribute up front, but it helps to sketch out a few so you can start working with the model immediately. This generator invokes your configured ORM and test framework, which - defaults to ActiveRecord and TestUnit. + defaults to Active Record and TestUnit. Finally, if --parent option is given, it's used as superclass of the created model. This allows you create Single Table Inheritance models. @@ -91,7 +91,7 @@ Available field types: Examples: `rails generate model account` - For ActiveRecord and TestUnit it creates: + For Active Record and TestUnit it creates: Model: app/models/account.rb Test: test/models/account_test.rb @@ -104,7 +104,7 @@ Examples: `rails generate model admin/account` - For ActiveRecord and TestUnit it creates: + For Active Record and TestUnit it creates: Module: app/models/admin.rb Model: app/models/admin/account.rb diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/models/application_record.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt index 8aa3de78f1..8aa3de78f1 100644 --- a/railties/lib/rails/generators/rails/plugin/templates/app/models/application_record.rb.tt +++ b/railties/lib/rails/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/models/.empty_directory b/railties/lib/rails/generators/rails/plugin/templates/app/models/.empty_directory deleted file mode 100644 index e69de29bb2..0000000000 --- a/railties/lib/rails/generators/rails/plugin/templates/app/models/.empty_directory +++ /dev/null diff --git a/railties/lib/rails/generators/rails/scaffold/USAGE b/railties/lib/rails/generators/rails/scaffold/USAGE index d2e495758d..c9283eda87 100644 --- a/railties/lib/rails/generators/rails/scaffold/USAGE +++ b/railties/lib/rails/generators/rails/scaffold/USAGE @@ -16,7 +16,7 @@ Description: As a special case, specifying 'password:digest' will generate a password_digest field of string type, and configure your generated model, - controller, views, and test suite for use with ActiveModel + controller, views, and test suite for use with Active Model has_secure_password (assuming they are using Rails defaults). Timestamps are added by default, so you don't have to specify them by hand diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb index 25eadfc387..f847e80471 100644 --- a/railties/test/application/middleware/session_test.rb +++ b/railties/test/application/middleware/session_test.rb @@ -20,12 +20,19 @@ module ApplicationTests @app ||= Rails.application end - test "config.force_ssl sets cookie to secure only" do + test "config.force_ssl sets cookie to secure only by default" do add_to_config "config.force_ssl = true" require "#{app_path}/config/environment" assert app.config.session_options[:secure], "Expected session to be marked as secure" end + test "config.force_ssl doesn't set cookie to secure only when changed from default" do + add_to_config "config.force_ssl = true" + add_to_config "config.ssl_options = { secure_cookies: false }" + require "#{app_path}/config/environment" + assert !app.config.session_options[:secure] + end + test "session is not loaded if it's not used" do make_basic_app diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 314d8c50b0..e5480180ce 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -361,6 +361,13 @@ class AppGeneratorTest < Rails::Generators::TestCase end end + def test_generator_has_assets_gems + run_generator + + assert_gem 'sass-rails' + assert_gem 'uglifier' + end + def test_generator_if_skip_sprockets_is_given run_generator [destination_root, "--skip-sprockets"] assert_no_file "config/initializers/assets.rb" |