diff options
18 files changed, 201 insertions, 116 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 1083f4a92d..0134bf7cab 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -16,6 +16,14 @@ *David Ilizarov* +* Change filter on /rails/info/routes to use an actual path regexp from rails + and not approximate javascript version. Oniguruma supports much more + extensive list of features than javascript regexp engine. + + Fixes #18402. + + *Ravil Bayramgalin* + * Non-string authenticity tokens do not raise NoMethodError when decoding the masked token. diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb index e9df984c86..2b036796ab 100644 --- a/actionpack/lib/action_dispatch/journey/router.rb +++ b/actionpack/lib/action_dispatch/journey/router.rb @@ -121,7 +121,6 @@ module ActionDispatch end def match_head_routes(routes, req) - routes.delete_if { |route| route.verb == // } head_routes = match_routes(routes, req) if head_routes.empty? diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb index 24e44f31ac..6e995c85c1 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb @@ -4,13 +4,13 @@ <%= route[:name] %><span class='helper'>_path</span> <% end %> </td> - <td data-route-verb='<%= route[:verb] %>'> + <td> <%= route[:verb] %> </td> - <td data-route-path='<%= route[:path] %>' data-regexp='<%= route[:regexp] %>'> + <td data-route-path='<%= route[:path] %>'> <%= route[:path] %> </td> - <td data-route-reqs='<%= route[:reqs] %>'> - <%= route[:reqs] %> + <td> + <%=simple_format route[:reqs] %> </td> </tr> diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb index 5cee0b5932..429ea7057c 100644 --- a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb +++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb @@ -81,92 +81,87 @@ </table> <script type='text/javascript'> - // Iterates each element through a function - function each(elems, func) { - if (!elems instanceof Array) { elems = [elems]; } - for (var i = 0, len = elems.length; i < len; i++) { - func(elems[i]); - } - } - - // Sets innerHTML for an element - function setContent(elem, text) { - elem.innerHTML = text; - } + // support forEarch iterator on NodeList + NodeList.prototype.forEach = Array.prototype.forEach; // Enables path search functionality function setupMatchPaths() { - // Check if the user input (sanitized as a path) matches the regexp data attribute - function checkExactMatch(section, elem, value) { - var string = sanitizePath(value), - regexp = elem.getAttribute("data-regexp"); - - showMatch(string, regexp, section, elem); + // Check if there are any matched results in a section + function checkNoMatch(section, noMatchText) { + if (section.children.length <= 1) { + section.innerHTML += noMatchText; + } } - // Check if the route path data attribute contains the user input - function checkFuzzyMatch(section, elem, value) { - var string = elem.getAttribute("data-route-path"), - regexp = value; - - showMatch(string, regexp, section, elem); + // get JSON from url and invoke callback with result + function getJSON(url, success) { + var xhr = new XMLHttpRequest(); + xhr.open('GET', url); + xhr.onload = function() { + if (this.status == 200) + success(JSON.parse(this.response)); + }; + xhr.send(); } - // Display the parent <tr> element in the appropriate section when there's a match - function showMatch(string, regexp, section, elem) { - if(string.match(RegExp(regexp))) { - section.appendChild(elem.parentNode.cloneNode(true)); + function delayedKeyup(input, callback) { + var timeout; + input.onkeyup = function(){ + if (timeout) clearTimeout(timeout); + timeout = setTimeout(callback, 300); } } - // Check if there are any matched results in a section - function checkNoMatch(section, defaultText, noMatchText) { - if (section.innerHTML === defaultText) { - setContent(section, defaultText + noMatchText); - } - } - - // Ensure path always starts with a slash "/" and remove params or fragments + // remove params or fragments function sanitizePath(path) { - var path = path.charAt(0) == '/' ? path : "/" + path; - return path.replace(/\#.*|\?.*/, ''); + return path.replace(/[#?].*/, ''); } - var regexpElems = document.querySelectorAll('#route_table [data-regexp]'), - searchElem = document.querySelector('#search'), - exactMatches = document.querySelector('#exact_matches'), - fuzzyMatches = document.querySelector('#fuzzy_matches'); + var pathElements = document.querySelectorAll('#route_table [data-route-path]'), + searchElem = document.querySelector('#search'), + exactSection = document.querySelector('#exact_matches'), + fuzzySection = document.querySelector('#fuzzy_matches'); // Remove matches when no search value is present searchElem.onblur = function(e) { if (searchElem.value === "") { - setContent(exactMatches, ""); - setContent(fuzzyMatches, ""); + exactSection.innerHTML = ""; + fuzzySection.innerHTML = ""; } } // On key press perform a search for matching paths - searchElem.onkeyup = function(e){ - var userInput = searchElem.value, - defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + escape(sanitizePath(userInput)) +'):</th></tr>', - defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + escape(userInput) +'):</th></tr>', + delayedKeyup(searchElem, function() { + var path = sanitizePath(searchElem.value), + defaultExactMatch = '<tr><th colspan="4">Paths Matching (' + path +'):</th></tr>', + defaultFuzzyMatch = '<tr><th colspan="4">Paths Containing (' + path +'):</th></tr>', noExactMatch = '<tr><th colspan="4">No Exact Matches Found</th></tr>', noFuzzyMatch = '<tr><th colspan="4">No Fuzzy Matches Found</th></tr>'; - // Clear out results section - setContent(exactMatches, defaultExactMatch); - setContent(fuzzyMatches, defaultFuzzyMatch); + if (!path) + return searchElem.onblur(); - // Display exact matches and fuzzy matches - each(regexpElems, function(elem) { - checkExactMatch(exactMatches, elem, userInput); - checkFuzzyMatch(fuzzyMatches, elem, userInput); - }) + getJSON('/rails/info/routes?path=' + path, function(matches){ + // Clear out results section + exactSection.innerHTML = defaultExactMatch; + fuzzySection.innerHTML = defaultFuzzyMatch; - // Display 'No Matches' message when no matches are found - checkNoMatch(exactMatches, defaultExactMatch, noExactMatch); - checkNoMatch(fuzzyMatches, defaultFuzzyMatch, noFuzzyMatch); - } + // Display exact matches and fuzzy matches + pathElements.forEach(function(elem) { + var elemPath = elem.getAttribute('data-route-path'); + + if (matches['exact'].indexOf(elemPath) != -1) + exactSection.appendChild(elem.parentNode.cloneNode(true)); + + if (matches['fuzzy'].indexOf(elemPath) != -1) + fuzzySection.appendChild(elem.parentNode.cloneNode(true)); + }) + + // Display 'No Matches' message when no matches are found + checkNoMatch(exactSection, noExactMatch); + checkNoMatch(fuzzySection, noFuzzyMatch); + }) + }) } // Enables functionality to toggle between `_path` and `_url` helper suffixes @@ -174,19 +169,20 @@ // Sets content for each element function setValOn(elems, val) { - each(elems, function(elem) { - setContent(elem, val); + elems.forEach(function(elem) { + elem.innerHTML = val; }); } // Sets onClick event for each element function onClick(elems, func) { - each(elems, function(elem) { + elems.forEach(function(elem) { elem.onclick = func; }); } var toggleLinks = document.querySelectorAll('#route_table [data-route-helper]'); + onClick(toggleLinks, function(){ var helperTxt = this.getAttribute("data-route-helper"), helperElems = document.querySelectorAll('[data-route-name] span.helper'); diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index df5ebb6751..c513737fc2 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -28,23 +28,6 @@ module ActionDispatch super.to_s end - def regexp - __getobj__.path.to_regexp - end - - def json_regexp - str = regexp.inspect. - sub('\\A' , '^'). - sub('\\Z' , '$'). - sub('\\z' , '$'). - sub(/^\// , ''). - sub(/\/[a-z]*$/ , ''). - gsub(/\(\?#.+\)/ , ''). - gsub(/\(\?-\w+:/ , '('). - gsub(/\s/ , '') - Regexp.new(str).source - end - def reqs @reqs ||= begin reqs = endpoint @@ -117,11 +100,10 @@ module ActionDispatch end.reject(&:internal?).collect do |route| collect_engine_routes(route) - { name: route.name, - verb: route.verb, - path: route.path, - reqs: route.reqs, - regexp: route.json_regexp } + { name: route.name, + verb: route.verb, + path: route.path, + reqs: route.reqs } end end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 2fe37c5bd4..f7f898288b 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -388,16 +388,8 @@ module ActionDispatch APP_SESSIONS = {} - attr_reader :app - - def before_setup - super - @app = nil - @integration_session = nil - end - - def integration_session - @integration_session ||= create_session(app) + def app + @app ||= nil end # Reset the current session. This is useful for testing multiple sessions @@ -425,6 +417,8 @@ module ActionDispatch %w(get post patch put head delete cookies assigns xml_http_request xhr get_via_redirect post_via_redirect).each do |method| define_method(method) do |*args| + reset! unless integration_session + # reset the html_document variable, except for cookies/assigns calls unless method == 'cookies' || method == 'assigns' @html_document = nil @@ -456,16 +450,19 @@ module ActionDispatch # Copy the instance variables from the current session instance into the # test instance. def copy_session_variables! #:nodoc: + return unless integration_session @controller = @integration_session.controller @response = @integration_session.response @request = @integration_session.request end def default_url_options + reset! unless integration_session integration_session.default_url_options end def default_url_options=(options) + reset! unless integration_session integration_session.default_url_options = options end @@ -475,6 +472,7 @@ module ActionDispatch # Delegate unhandled messages to the current session instance. def method_missing(sym, *args, &block) + reset! unless integration_session if integration_session.respond_to?(sym) integration_session.__send__(sym, *args, &block).tap do copy_session_variables! @@ -483,6 +481,11 @@ module ActionDispatch super end end + + private + def integration_session + @integration_session ||= nil + end end end @@ -659,6 +662,7 @@ module ActionDispatch end def url_options + reset! unless integration_session integration_session.url_options end diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 5fbd19acdf..c61423dce4 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -254,6 +254,10 @@ class ResponseTest < ActiveSupport::TestCase end class ResponseIntegrationTest < ActionDispatch::IntegrationTest + def app + @app + end + test "response cache control from railsish app" do @app = lambda { |env| ActionDispatch::Response.new.tap { |resp| diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb index 3d3d4b74ae..3df022c64b 100644 --- a/actionpack/test/dispatch/routing/inspector_test.rb +++ b/actionpack/test/dispatch/routing/inspector_test.rb @@ -26,14 +26,6 @@ module ActionDispatch inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, options[:filter]).split("\n") end - def test_json_regexp_converter - @set.draw do - get '/cart', :to => 'cart#show' - end - route = ActionDispatch::Routing::RouteWrapper.new(@set.routes.first) - assert_equal "^\\/cart(?:\\.([^\\/.?]+))?$", route.json_regexp - end - def test_displaying_routes_for_engines engine = Class.new(Rails::Engine) do def self.inspect diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index d65d2e8e8f..b4502c19d6 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -3492,11 +3492,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest mount lambda { |env| [200, {}, [env['REQUEST_METHOD']]] }, at: '/' end - # HEAD request matches `get /home` rather than the lower-precedence - # Rack app mounted at `/` + # TODO: HEAD request should match `get /home` rather than the + # lower-precedence Rack app mounted at `/`. head '/home' - assert_response :success - assert_equal 'test#index', @response.body + assert_response :ok + #assert_equal 'test#index', @response.body + assert_equal 'HEAD', @response.body # But the Rack app can still respond to its own HEAD requests. head '/foobar' diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 86e20ad202..8f47171e62 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,3 +1,23 @@ +* Collection rendering automatically caches and fetches multiple partials. + + Collections rendered as: + + ```ruby + <%= render @notifications %> + <%= render partial: 'notifications/notification', collection: @notifications, as: :notification %> + ``` + + will now read several partials from cache at once, if the template starts with a cache call: + + ```ruby + # notifications/_notification.html.erb + <% cache notification do %> + <%# ... %> + <% end %> + ``` + + *Kasper Timm Hansen* + * Fixed a dependency tracker bug that caused template dependencies not count layouts as dependencies for partials. diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index b76b35bf3c..b0793d0b91 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -1246,7 +1246,7 @@ module ActionView # Admin: <%= person_form.check_box :admin %> # <% end %> # - # In the above block, the a +FormBuilder+ object is yielded as the + # In the above block, a +FormBuilder+ object is yielded as the # +person_form+ variable. This allows you to generate the +text_field+ # and +check_box+ fields by specifying their eponymous methods, which # modify the underlying template and associates the +@person+ model object @@ -1267,6 +1267,7 @@ module ActionView # ) # ) # end + # end # # The above code creates a new method +div_radio_button+ which wraps a div # around the new radio button. Note that when options are passed in, you diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index b66db6a754..8d76b4145b 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,10 @@ +* Correctly create through records when created on a has many through + association when using `where`. + + Fixes #19073. + + *Sean Griffin* + * Add `SchemaMigration.create_table` support any unicode charsets for MySQL. *Ryuta Kamizono* diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index f2b44913db..ee0bb8fafe 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -13,6 +13,19 @@ module ActiveRecord other == to_a end + def build(*args, &block) + scoping { @association.build(*args, &block) } + end + alias new build + + def create(*args, &block) + scoping { @association.create(*args, &block) } + end + + def create!(*args, &block) + scoping { @association.create!(*args, &block) } + end + private def exec_queries 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 6729a5a9fc..5f52c65412 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -595,6 +595,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert posts(:thinking).reload.people(true).collect(&:first_name).include?("Jeb") end + def test_through_record_is_built_when_created_with_where + assert_difference("posts(:thinking).readers.count", 1) do + posts(:thinking).people.where(first_name: "Jeb").create + end + end + def test_associate_with_create_and_no_options peeps = posts(:thinking).people.count posts(:thinking).people.create(:first_name => 'foo') diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index 0fbd6ed7e1..0594b701dc 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -727,7 +727,7 @@ NOTE: Defined in `active_support/core_ext/module/introspection.rb`. #### Qualified Constant Names -The standard methods `const_defined?`, `const_get` , and `const_set` accept +The standard methods `const_defined?`, `const_get`, and `const_set` accept bare constant names. Active Support extends this API to be able to pass relative qualified constant names. diff --git a/guides/source/command_line.md b/guides/source/command_line.md index 19ccdc5488..6f5a6b7957 100644 --- a/guides/source/command_line.md +++ b/guides/source/command_line.md @@ -338,6 +338,12 @@ You can specify the environment in which the `runner` command should operate usi $ bin/rails runner -e staging "Model.long_running_method" ``` +You can even execute ruby code written in a file with runner. + +```bash +$ bin/rails runner lib/code_to_be_run.rb +``` + ### `rails destroy` Think of `destroy` as the opposite of `generate`. It'll figure out what generate did, and undo it. diff --git a/railties/lib/rails/info_controller.rb b/railties/lib/rails/info_controller.rb index 49e5431a16..6e61cc3cb5 100644 --- a/railties/lib/rails/info_controller.rb +++ b/railties/lib/rails/info_controller.rb @@ -17,7 +17,28 @@ class Rails::InfoController < Rails::ApplicationController # :nodoc: end def routes - @routes_inspector = ActionDispatch::Routing::RoutesInspector.new(_routes.routes) - @page_title = 'Routes' + if path = params[:path] + path = URI.escape path + normalized_path = with_leading_slash path + render json: { + exact: match_route {|it| it.match normalized_path }, + fuzzy: match_route {|it| it.spec.to_s.match path } + } + else + @routes_inspector = ActionDispatch::Routing::RoutesInspector.new(_routes.routes) + @page_title = 'Routes' + end + end + + private + + def match_route + _routes.routes.select {|route| + yield route.path + }.map {|route| route.path.spec.to_s } + end + + def with_leading_slash(path) + ('/' + path).squeeze('/') end end diff --git a/railties/test/rails_info_controller_test.rb b/railties/test/rails_info_controller_test.rb index 25b0e761cb..d87b51d852 100644 --- a/railties/test/rails_info_controller_test.rb +++ b/railties/test/rails_info_controller_test.rb @@ -53,4 +53,29 @@ class InfoControllerTest < ActionController::TestCase assert_response :success end + test "info controller returns exact matches" do + exact_count = -> { JSON(response.body)['exact'].size } + + get :routes, path: 'rails/info/route' + assert exact_count.call == 0, 'should not match incomplete routes' + + get :routes, path: 'rails/info/routes' + assert exact_count.call == 1, 'should match complete routes' + + get :routes, path: 'rails/info/routes.html' + assert exact_count.call == 1, 'should match complete routes with optional parts' + end + + test "info controller returns fuzzy matches" do + fuzzy_count = -> { JSON(response.body)['fuzzy'].size } + + get :routes, path: 'rails/info' + assert fuzzy_count.call == 2, 'should match incomplete routes' + + get :routes, path: 'rails/info/routes' + assert fuzzy_count.call == 1, 'should match complete routes' + + get :routes, path: 'rails/info/routes.html' + assert fuzzy_count.call == 0, 'should match optional parts of route literally' + end end |