diff options
author | David Lee <davidomundo@gmail.com> | 2011-05-06 14:03:55 -0700 |
---|---|---|
committer | David Lee <davidomundo@gmail.com> | 2012-02-22 08:47:10 -0800 |
commit | 002713c64568114f3754799acc0723ea0d442f7a (patch) | |
tree | bd01c3ded14a9a96b92fbb54b8e0da35a899e374 | |
parent | 66b7eb19279820b6464ad2b9e3f2efadb08f2ff2 (diff) | |
download | rails-002713c64568114f3754799acc0723ea0d442f7a.tar.gz rails-002713c64568114f3754799acc0723ea0d442f7a.tar.bz2 rails-002713c64568114f3754799acc0723ea0d442f7a.zip |
Add config.default_method_for_update to support PATCH
PATCH is the correct HTML verb to map to the #update action. The
semantics for PATCH allows for partial updates, whereas PUT requires a
complete replacement.
Changes:
* adds config.default_method_for_update you can set to :patch
* optionally use PATCH instead of PUT in resource routes and forms
* adds the #patch verb to routes to detect PATCH requests
* adds #patch? to Request
* changes documentation and comments to indicate support for PATCH
This change maintains complete backwards compatibility by keeping :put
as the default for config.default_method_for_update.
40 files changed, 402 insertions, 151 deletions
@@ -8,6 +8,7 @@ else gem 'arel' end +gem 'rack-test', :git => "https://github.com/brynary/rack-test.git" gem 'bcrypt-ruby', '~> 3.0.0' gem 'jquery-rails' diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb index 3d46163b74..b8b43ea9ef 100644 --- a/actionpack/lib/action_controller/metal/http_authentication.rb +++ b/actionpack/lib/action_controller/metal/http_authentication.rb @@ -279,7 +279,7 @@ module ActionController # # An implementation might choose not to accept a previously used nonce or a previously used digest, in order to # protect against a replay attack. Or, an implementation might choose to use one-time nonces or digests for - # POST or PUT requests and a time-stamp for GET requests. For more details on the issues involved see Section 4 + # POST, PUT, or PATCH requests and a time-stamp for GET requests. For more details on the issues involved see Section 4 # of this document. # # The nonce is opaque to the client. Composed of Time, and hash of Time with secret @@ -293,7 +293,7 @@ module ActionController end # Might want a shorter timeout depending on whether the request - # is a PUT or POST, and if client is browser or web service. + # is a PATCH, PUT, or POST, and if client is browser or web service. # Can be much shorter if the Stale directive is implemented. This would # allow a user to use new nonce without prompting user again for their # username and password. diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb index daa1ddd65f..ccda01ed44 100644 --- a/actionpack/lib/action_controller/metal/responder.rb +++ b/actionpack/lib/action_controller/metal/responder.rb @@ -53,7 +53,7 @@ module ActionController #:nodoc: # end # end # - # The same happens for PUT and DELETE requests. + # The same happens for PATCH/PUT and DELETE requests. # # === Nested resources # @@ -116,8 +116,9 @@ module ActionController #:nodoc: class Responder attr_reader :controller, :request, :format, :resource, :resources, :options - ACTIONS_FOR_VERBS = { + DEFAULT_ACTIONS_FOR_VERBS = { :post => :new, + :patch => :edit, :put => :edit } @@ -132,7 +133,7 @@ module ActionController #:nodoc: end delegate :head, :render, :redirect_to, :to => :controller - delegate :get?, :post?, :put?, :delete?, :to => :request + delegate :get?, :post?, :patch?, :put?, :delete?, :to => :request # Undefine :to_json and :to_yaml since it's defined on Object undef_method(:to_json) if method_defined?(:to_json) @@ -259,11 +260,11 @@ module ActionController #:nodoc: resource.respond_to?(:errors) && !resource.errors.empty? end - # By default, render the <code>:edit</code> action for HTML requests with failure, unless - # the verb is POST. + # By default, render the <code>:edit</code> action for HTML requests with errors, unless + # the verb was POST. # def default_action - @action ||= ACTIONS_FOR_VERBS[request.request_method_symbol] + @action ||= DEFAULT_ACTIONS_FOR_VERBS[request.request_method_symbol] end def resource_errors diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb index 1e226fc336..3509e74d5e 100644 --- a/actionpack/lib/action_controller/test_case.rb +++ b/actionpack/lib/action_controller/test_case.rb @@ -225,7 +225,7 @@ module ActionController # == Basic example # # Functional tests are written as follows: - # 1. First, one uses the +get+, +post+, +put+, +delete+ or +head+ method to simulate + # 1. First, one uses the +get+, +post+, +patch+, +put+, +delete+ or +head+ method to simulate # an HTTP request. # 2. Then, one asserts whether the current state is as expected. "State" can be anything: # the controller's HTTP response, the database contents, etc. @@ -392,6 +392,11 @@ module ActionController process(action, "POST", *args) end + # Executes a request simulating PATCH HTTP method and set/volley the response + def patch(action, *args) + process(action, "PATCH", *args) + end + # Executes a request simulating PUT HTTP method and set/volley the response def put(action, *args) process(action, "PUT", *args) diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb index 0a0ebe7fad..de014a9c00 100644 --- a/actionpack/lib/action_dispatch/http/request.rb +++ b/actionpack/lib/action_dispatch/http/request.rb @@ -97,6 +97,12 @@ module ActionDispatch HTTP_METHOD_LOOKUP[request_method] == :post end + # Is this a PATCH request? + # Equivalent to <tt>request.request_method == :patch</tt>. + def patch? + HTTP_METHOD_LOOKUP[request_method] == :patch + end + # Is this a PUT request? # Equivalent to <tt>request.request_method_symbol == :put</tt>. def put? diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb index 35f901c575..4135f3c142 100644 --- a/actionpack/lib/action_dispatch/railtie.rb +++ b/actionpack/lib/action_dispatch/railtie.rb @@ -23,6 +23,7 @@ module ActionDispatch ActionDispatch::Http::URL.tld_length = app.config.action_dispatch.tld_length ActionDispatch::Request.ignore_accept_header = app.config.action_dispatch.ignore_accept_header ActionDispatch::Response.default_charset = app.config.action_dispatch.default_charset || app.config.encoding + ActionDispatch::Routing::Mapper.default_method_for_update = app.config.default_method_for_update ActionDispatch::ExceptionWrapper.rescue_responses.merge!(config.action_dispatch.rescue_responses) ActionDispatch::ExceptionWrapper.rescue_templates.merge!(config.action_dispatch.rescue_templates) diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 107fe80d1f..38a0270151 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -182,10 +182,13 @@ module ActionDispatch # # == HTTP Methods # - # Using the <tt>:via</tt> option when specifying a route allows you to restrict it to a specific HTTP method. - # Possible values are <tt>:post</tt>, <tt>:get</tt>, <tt>:put</tt>, <tt>:delete</tt> and <tt>:any</tt>. - # If your route needs to respond to more than one method you can use an array, e.g. <tt>[ :get, :post ]</tt>. - # The default value is <tt>:any</tt> which means that the route will respond to any of the HTTP methods. + # Using the <tt>:via</tt> option when specifying a route allows you to + # restrict it to a specific HTTP method. Possible values are <tt>:post</tt>, + # <tt>:get</tt>, <tt>:patch</tt>, <tt>:put</tt>, <tt>:delete</tt> and + # <tt>:any</tt>. If your route needs to respond to more than one method you + # can use an array, e.g. <tt>[ :get, :post ]</tt>. The default value is + # <tt>:any</tt> which means that the route will respond to any of the HTTP + # methods. # # Examples: # @@ -198,7 +201,7 @@ module ActionDispatch # === HTTP helper methods # # An alternative method of specifying which HTTP method a route should respond to is to use the helper - # methods <tt>get</tt>, <tt>post</tt>, <tt>put</tt> and <tt>delete</tt>. + # methods <tt>get</tt>, <tt>post</tt>, <tt>patch</tt>, <tt>put</tt> and <tt>delete</tt>. # # Examples: # @@ -283,6 +286,6 @@ module ActionDispatch autoload :PolymorphicRoutes, 'action_dispatch/routing/polymorphic_routes' SEPARATORS = %w( / . ? ) #:nodoc: - HTTP_METHODS = [:get, :head, :post, :put, :delete, :options] #:nodoc: + HTTP_METHODS = [:get, :head, :post, :patch, :put, :delete, :options] #:nodoc: end end diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index cf9c0d7b6a..bb0441c100 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -7,6 +7,8 @@ require 'action_dispatch/routing/redirection' module ActionDispatch module Routing class Mapper + cattr_accessor(:default_method_for_update) {:put} + class Constraints #:nodoc: def self.new(app, constraints, request = Rack::Request) if constraints.any? @@ -465,7 +467,7 @@ module ActionDispatch # # Example: # - # get 'bacon', :to => 'food#bacon' + # get 'bacon', :to => 'food#bacon' def get(*args, &block) map_method(:get, args, &block) end @@ -475,17 +477,27 @@ module ActionDispatch # # Example: # - # post 'bacon', :to => 'food#bacon' + # post 'bacon', :to => 'food#bacon' def post(*args, &block) map_method(:post, args, &block) end + # Define a route that only recognizes HTTP PATCH. + # For supported arguments, see <tt>Base#match</tt>. + # + # Example: + # + # patch 'bacon', :to => 'food#bacon' + def patch(*args, &block) + map_method(:patch, args, &block) + end + # Define a route that only recognizes HTTP PUT. # For supported arguments, see <tt>Base#match</tt>. # # Example: # - # put 'bacon', :to => 'food#bacon' + # put 'bacon', :to => 'food#bacon' def put(*args, &block) map_method(:put, args, &block) end @@ -495,7 +507,7 @@ module ActionDispatch # # Example: # - # delete 'broccoli', :to => 'food#broccoli' + # delete 'broccoli', :to => 'food#broccoli' def delete(*args, &block) map_method(:delete, args, &block) end @@ -522,13 +534,13 @@ module ActionDispatch # This will create a number of routes for each of the posts and comments # controller. For <tt>Admin::PostsController</tt>, Rails will create: # - # GET /admin/posts - # GET /admin/posts/new - # POST /admin/posts - # GET /admin/posts/1 - # GET /admin/posts/1/edit - # PUT /admin/posts/1 - # DELETE /admin/posts/1 + # GET /admin/posts + # GET /admin/posts/new + # POST /admin/posts + # GET /admin/posts/1 + # GET /admin/posts/1/edit + # PUT/PATCH /admin/posts/1 + # DELETE /admin/posts/1 # # If you want to route /posts (without the prefix /admin) to # <tt>Admin::PostsController</tt>, you could use @@ -556,13 +568,13 @@ module ActionDispatch # not use scope. In the last case, the following paths map to # +PostsController+: # - # GET /admin/posts - # GET /admin/posts/new - # POST /admin/posts - # GET /admin/posts/1 - # GET /admin/posts/1/edit - # PUT /admin/posts/1 - # DELETE /admin/posts/1 + # GET /admin/posts + # GET /admin/posts/new + # POST /admin/posts + # GET /admin/posts/1 + # GET /admin/posts/1/edit + # PUT/PATCH /admin/posts/1 + # DELETE /admin/posts/1 module Scoping # Scopes a set of routes to the given default options. # @@ -651,13 +663,13 @@ module ActionDispatch # # This generates the following routes: # - # admin_posts GET /admin/posts(.:format) admin/posts#index - # admin_posts POST /admin/posts(.:format) admin/posts#create - # new_admin_post GET /admin/posts/new(.:format) admin/posts#new - # edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit - # admin_post GET /admin/posts/:id(.:format) admin/posts#show - # admin_post PUT /admin/posts/:id(.:format) admin/posts#update - # admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy + # admin_posts GET /admin/posts(.:format) admin/posts#index + # admin_posts POST /admin/posts(.:format) admin/posts#create + # new_admin_post GET /admin/posts/new(.:format) admin/posts#new + # edit_admin_post GET /admin/posts/:id/edit(.:format) admin/posts#edit + # admin_post GET /admin/posts/:id(.:format) admin/posts#show + # admin_post PUT/PATCH /admin/posts/:id(.:format) admin/posts#update + # admin_post DELETE /admin/posts/:id(.:format) admin/posts#destroy # # === Options # @@ -972,12 +984,12 @@ module ActionDispatch # the +GeoCoders+ controller (note that the controller is named after # the plural): # - # GET /geocoder/new - # POST /geocoder - # GET /geocoder - # GET /geocoder/edit - # PUT /geocoder - # DELETE /geocoder + # GET /geocoder/new + # POST /geocoder + # GET /geocoder + # GET /geocoder/edit + # PUT/PATCH /geocoder + # DELETE /geocoder # # === Options # Takes same options as +resources+. @@ -1002,8 +1014,10 @@ module ActionDispatch member do get :edit if parent_resource.actions.include?(:edit) get :show if parent_resource.actions.include?(:show) - put :update if parent_resource.actions.include?(:update) delete :destroy if parent_resource.actions.include?(:destroy) + if parent_resource.actions.include?(:update) + send default_method_for_update, :update + end end end @@ -1020,13 +1034,13 @@ module ActionDispatch # creates seven different routes in your application, all mapping to # the +Photos+ controller: # - # GET /photos - # GET /photos/new - # POST /photos - # GET /photos/:id - # GET /photos/:id/edit - # PUT /photos/:id - # DELETE /photos/:id + # GET /photos + # GET /photos/new + # POST /photos + # GET /photos/:id + # GET /photos/:id/edit + # PUT/PATCH /photos/:id + # DELETE /photos/:id # # Resources can also be nested infinitely by using this block syntax: # @@ -1036,13 +1050,13 @@ module ActionDispatch # # This generates the following comments routes: # - # GET /photos/:photo_id/comments - # GET /photos/:photo_id/comments/new - # POST /photos/:photo_id/comments - # GET /photos/:photo_id/comments/:id - # GET /photos/:photo_id/comments/:id/edit - # PUT /photos/:photo_id/comments/:id - # DELETE /photos/:photo_id/comments/:id + # GET /photos/:photo_id/comments + # GET /photos/:photo_id/comments/new + # POST /photos/:photo_id/comments + # GET /photos/:photo_id/comments/:id + # GET /photos/:photo_id/comments/:id/edit + # PUT/PATCH /photos/:photo_id/comments/:id + # DELETE /photos/:photo_id/comments/:id # # === Options # Takes same options as <tt>Base#match</tt> as well as: @@ -1104,13 +1118,13 @@ module ActionDispatch # # The +comments+ resource here will have the following routes generated for it: # - # post_comments GET /posts/:post_id/comments(.:format) - # post_comments POST /posts/:post_id/comments(.:format) - # new_post_comment GET /posts/:post_id/comments/new(.:format) - # edit_comment GET /sekret/comments/:id/edit(.:format) - # comment GET /sekret/comments/:id(.:format) - # comment PUT /sekret/comments/:id(.:format) - # comment DELETE /sekret/comments/:id(.:format) + # post_comments GET /posts/:post_id/comments(.:format) + # post_comments POST /posts/:post_id/comments(.:format) + # new_post_comment GET /posts/:post_id/comments/new(.:format) + # edit_comment GET /sekret/comments/:id/edit(.:format) + # comment GET /sekret/comments/:id(.:format) + # comment PUT/PATCH /sekret/comments/:id(.:format) + # comment DELETE /sekret/comments/:id(.:format) # # === Examples # @@ -1138,11 +1152,14 @@ module ActionDispatch get :new end if parent_resource.actions.include?(:new) + # TODO: Only accept patch or put depending on config member do get :edit if parent_resource.actions.include?(:edit) get :show if parent_resource.actions.include?(:show) - put :update if parent_resource.actions.include?(:update) delete :destroy if parent_resource.actions.include?(:destroy) + if parent_resource.actions.include?(:update) + send default_method_for_update, :update + end end end diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 0287e7728b..62b3a344f8 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -26,8 +26,8 @@ module ActionDispatch # object's <tt>@response</tt> instance variable will point to the same # response object. # - # You can also perform POST, PUT, DELETE, and HEAD requests with +#post+, - # +#put+, +#delete+, and +#head+. + # You can also perform POST, PATCH, PUT, DELETE, and HEAD requests with + # +#post+, +#patch+, +#put+, +#delete+, and +#head+. def get(path, parameters = nil, headers = nil) process :get, path, parameters, headers end @@ -38,6 +38,12 @@ module ActionDispatch process :post, path, parameters, headers end + # Performs a PATCH request with the given parameters. See +#get+ for more + # details. + def patch(path, parameters = nil, headers = nil) + process :patch, path, parameters, headers + end + # Performs a PUT request with the given parameters. See +#get+ for more # details. def put(path, parameters = nil, headers = nil) @@ -65,10 +71,10 @@ module ActionDispatch # Performs an XMLHttpRequest request with the given parameters, mirroring # a request from the Prototype library. # - # The request_method is +:get+, +:post+, +:put+, +:delete+ or +:head+; the - # parameters are +nil+, a hash, or a url-encoded or multipart string; - # the headers are a hash. Keys are automatically upcased and prefixed - # with 'HTTP_' if not already. + # The request_method is +:get+, +:post+, +:patch+, +:put+, +:delete+ or + # +:head+; the parameters are +nil+, a hash, or a url-encoded or multipart + # string; the headers are a hash. Keys are automatically upcased and + # prefixed with 'HTTP_' if not already. def xml_http_request(request_method, path, parameters = nil, headers = nil) headers ||= {} headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest' @@ -108,6 +114,12 @@ module ActionDispatch request_via_redirect(:post, path, parameters, headers) end + # Performs a PATCH request, following any subsequent redirect. + # See +request_via_redirect+ for more information. + def patch_via_redirect(path, parameters = nil, headers = nil) + request_via_redirect(:patch, path, parameters, headers) + end + # Performs a PUT request, following any subsequent redirect. # See +request_via_redirect+ for more information. def put_via_redirect(path, parameters = nil, headers = nil) @@ -318,7 +330,7 @@ module ActionDispatch @integration_session = Integration::Session.new(app) end - %w(get post put head delete options cookies assigns + %w(get post put patch head delete options cookies assigns xml_http_request xhr get_via_redirect post_via_redirect).each do |method| define_method(method) do |*args| reset! unless integration_session diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb index 23329d7f35..4641f10dc8 100644 --- a/actionpack/lib/action_view/base.rb +++ b/actionpack/lib/action_view/base.rb @@ -132,6 +132,8 @@ module ActionView #:nodoc: class Base include Helpers, ::ERB::Util, Context + cattr_accessor(:default_method_for_update) {:put} + # Specify the proc used to decorate input tags that refer to attributes with errors. cattr_accessor :field_error_proc @@field_error_proc = Proc.new{ |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe } diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb index cae345a1d6..33d509d968 100644 --- a/actionpack/lib/action_view/helpers/form_helper.rb +++ b/actionpack/lib/action_view/helpers/form_helper.rb @@ -250,7 +250,7 @@ module ActionView # # You can force the form to use the full array of HTTP verbs by setting # - # :method => (:get|:post|:put|:delete) + # :method => (:get|:post|:patch|:put|:delete) # # in the options hash. If the verb is not GET or POST, which are natively supported by HTML forms, the # form will be set to POST and a hidden input called _method will carry the intended verb for the server @@ -385,7 +385,7 @@ module ActionView object = convert_to_model(object) as = options[:as] - action, method = object.respond_to?(:persisted?) && object.persisted? ? [:edit, :put] : [:new, :post] + action, method = object.respond_to?(:persisted?) && object.persisted? ? [:edit, ActionView::Base.default_method_for_update] : [:new, :post] options[:html].reverse_merge!( :class => as ? "#{action}_#{as}" : dom_class(object, action), :id => as ? "#{action}_#{as}" : [options[:namespace], dom_id(object, action)].compact.join("_").presence, diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb index b5fc882e31..93f476926c 100644 --- a/actionpack/lib/action_view/helpers/url_helper.rb +++ b/actionpack/lib/action_view/helpers/url_helper.rb @@ -146,12 +146,12 @@ module ActionView # create an HTML form and immediately submit the form for processing using # the HTTP verb specified. Useful for having links perform a POST operation # in dangerous actions like deleting a record (which search bots can follow - # while spidering your site). Supported verbs are <tt>:post</tt>, <tt>:delete</tt> and <tt>:put</tt>. + # while spidering your site). Supported verbs are <tt>:post</tt>, <tt>:delete</tt>, <tt>:patch</tt>, and <tt>:put</tt>. # Note that if the user has JavaScript disabled, the request will fall back # to using GET. If <tt>:href => '#'</tt> is used and the user has JavaScript # disabled clicking the link will have no effect. If you are relying on the # POST behavior, you should check for it in your controller's action by using - # the request object's methods for <tt>post?</tt>, <tt>delete?</tt> or <tt>put?</tt>. + # the request object's methods for <tt>post?</tt>, <tt>delete?</tt>, <tt>:patch</tt>, or <tt>put?</tt>. # * <tt>:remote => true</tt> - This will allow the unobtrusive JavaScript # driver to make an Ajax request to the URL in question instead of following # the link. The drivers each provide mechanisms for listening for the @@ -272,7 +272,7 @@ module ActionView # # There are a few special +html_options+: # * <tt>:method</tt> - Symbol of HTTP verb. Supported verbs are <tt>:post</tt>, <tt>:get</tt>, - # <tt>:delete</tt> and <tt>:put</tt>. By default it will be <tt>:post</tt>. + # <tt>:delete</tt>, <tt>:patch</tt>, and <tt>:put</tt>. By default it will be <tt>:post</tt>. # * <tt>:disabled</tt> - If set to true, it will generate a disabled button. # * <tt>:confirm</tt> - This will use the unobtrusive JavaScript driver to # prompt with the question specified. If the user accepts, the link is @@ -329,7 +329,7 @@ module ActionView remote = html_options.delete('remote') method = html_options.delete('method').to_s - method_tag = %w{put delete}.include?(method) ? method_tag(method) : "" + method_tag = %w{put patch delete}.include?(method) ? method_tag(method) : "" form_method = method == 'get' ? 'get' : 'post' form_options = html_options.delete('form') || {} diff --git a/actionpack/lib/action_view/railtie.rb b/actionpack/lib/action_view/railtie.rb index 43371a1c49..b2926006b4 100644 --- a/actionpack/lib/action_view/railtie.rb +++ b/actionpack/lib/action_view/railtie.rb @@ -33,6 +33,7 @@ module ActionView end initializer "action_view.set_configs" do |app| + ActionView::Base.default_method_for_update = app.config.default_method_for_update ActiveSupport.on_load(:action_view) do app.config.action_view.each do |k,v| send "#{k}=", v diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 10f73c3da3..a42c68a628 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -180,7 +180,7 @@ class PageCachingTest < ActionController::TestCase end [:ok, :no_content, :found, :not_found].each do |status| - [:get, :post, :put, :delete].each do |method| + [:get, :post, :patch, :put, :delete].each do |method| unless method == :get && status == :ok define_method "test_shouldnt_cache_#{method}_with_#{status}_status" do send(method, status) diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 64c4682015..44f033119d 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -63,6 +63,12 @@ class SessionTest < ActiveSupport::TestCase @session.post_via_redirect(path, args, headers) end + def test_patch_via_redirect + path = "/somepath"; args = {:id => '1'}; headers = {"X-Test-Header" => "testvalue" } + @session.expects(:request_via_redirect).with(:patch, path, args, headers) + @session.patch_via_redirect(path, args, headers) + end + def test_put_via_redirect path = "/somepath"; args = {:id => '1'}; headers = {"X-Test-Header" => "testvalue" } @session.expects(:request_via_redirect).with(:put, path, args, headers) @@ -87,6 +93,12 @@ class SessionTest < ActiveSupport::TestCase @session.post(path,params,headers) end + def test_patch + path = "/index"; params = "blah"; headers = {:location => 'blah'} + @session.expects(:process).with(:patch,path,params,headers) + @session.patch(path,params,headers) + end + def test_put path = "/index"; params = "blah"; headers = {:location => 'blah'} @session.expects(:process).with(:put,path,params,headers) @@ -131,6 +143,16 @@ class SessionTest < ActiveSupport::TestCase @session.xml_http_request(:post,path,params,headers) end + def test_xml_http_request_patch + path = "/index"; params = "blah"; headers = {:location => 'blah'} + headers_after_xhr = headers.merge( + "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest", + "HTTP_ACCEPT" => "text/javascript, text/html, application/xml, text/xml, */*" + ) + @session.expects(:process).with(:patch,path,params,headers_after_xhr) + @session.xml_http_request(:patch,path,params,headers) + end + def test_xml_http_request_put path = "/index"; params = "blah"; headers = {:location => 'blah'} headers_after_xhr = headers.merge( @@ -228,7 +250,7 @@ class IntegrationTestUsesCorrectClass < ActionDispatch::IntegrationTest @integration_session.stubs(:generic_url_rewriter) @integration_session.stubs(:process) - %w( get post head put delete options ).each do |verb| + %w( get post head patch put delete options ).each do |verb| assert_nothing_raised("'#{verb}' should use integration test methods") { __send__(verb, '/') } end end diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb index 69a8f4f213..ae368842b5 100644 --- a/actionpack/test/controller/mime_responds_test.rb +++ b/actionpack/test/controller/mime_responds_test.rb @@ -770,6 +770,41 @@ class RespondWithControllerTest < ActionController::TestCase end end + def test_using_resource_for_patch_with_html_redirects_on_success + with_test_route_set do + patch :using_resource + assert_equal "text/html", @response.content_type + assert_equal 302, @response.status + assert_equal "http://www.example.com/customers/13", @response.location + assert @response.redirect? + end + end + + def test_using_resource_for_patch_with_html_rerender_on_failure + with_test_route_set do + errors = { :name => :invalid } + Customer.any_instance.stubs(:errors).returns(errors) + patch :using_resource + assert_equal "text/html", @response.content_type + assert_equal 200, @response.status + assert_equal "Edit world!\n", @response.body + assert_nil @response.location + end + end + + def test_using_resource_for_patch_with_html_rerender_on_failure_even_on_method_override + with_test_route_set do + errors = { :name => :invalid } + Customer.any_instance.stubs(:errors).returns(errors) + @request.env["rack.methodoverride.original_method"] = "POST" + patch :using_resource + assert_equal "text/html", @response.content_type + assert_equal 200, @response.status + assert_equal "Edit world!\n", @response.body + assert_nil @response.location + end + end + def test_using_resource_for_put_with_html_redirects_on_success with_test_route_set do put :using_resource diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb index e6d3fa74f2..64ed7f667f 100644 --- a/actionpack/test/controller/request_forgery_protection_test.rb +++ b/actionpack/test/controller/request_forgery_protection_test.rb @@ -114,6 +114,10 @@ module RequestForgeryProtectionTests assert_blocked { post :index, :format=>'xml' } end + def test_should_not_allow_patch_without_token + assert_blocked { patch :index } + end + def test_should_not_allow_put_without_token assert_blocked { put :index } end @@ -130,6 +134,10 @@ module RequestForgeryProtectionTests assert_not_blocked { post :index, :custom_authenticity_token => @token } end + def test_should_allow_patch_with_token + assert_not_blocked { patch :index, :custom_authenticity_token => @token } + end + def test_should_allow_put_with_token assert_not_blocked { put :index, :custom_authenticity_token => @token } end @@ -148,6 +156,11 @@ module RequestForgeryProtectionTests assert_not_blocked { delete :index } end + def test_should_allow_patch_with_token_in_header + @request.env['HTTP_X_CSRF_TOKEN'] = @token + assert_not_blocked { patch :index } + end + def test_should_allow_put_with_token_in_header @request.env['HTTP_X_CSRF_TOKEN'] = @token assert_not_blocked { put :index } @@ -232,7 +245,7 @@ class FreeCookieControllerTest < ActionController::TestCase end def test_should_allow_all_methods_without_token - [:post, :put, :delete].each do |method| + [:post, :patch, :put, :delete].each do |method| assert_nothing_raised { send(method, :index)} end end diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb index 73d72fe4d6..3c0a5d36ca 100644 --- a/actionpack/test/controller/resources_test.rb +++ b/actionpack/test/controller/resources_test.rb @@ -158,7 +158,7 @@ class ResourcesTest < ActionController::TestCase end def test_with_collection_actions - actions = { 'a' => :get, 'b' => :put, 'c' => :post, 'd' => :delete } + actions = { 'a' => :get, 'b' => :put, 'c' => :post, 'd' => :delete, 'e' => :patch } with_routing do |set| set.draw do @@ -167,6 +167,7 @@ class ResourcesTest < ActionController::TestCase put :b, :on => :collection post :c, :on => :collection delete :d, :on => :collection + patch :e, :on => :collection end end @@ -185,7 +186,7 @@ class ResourcesTest < ActionController::TestCase end def test_with_collection_actions_and_name_prefix - actions = { 'a' => :get, 'b' => :put, 'c' => :post, 'd' => :delete } + actions = { 'a' => :get, 'b' => :put, 'c' => :post, 'd' => :delete, 'e' => :patch } with_routing do |set| set.draw do @@ -195,6 +196,7 @@ class ResourcesTest < ActionController::TestCase put :b, :on => :collection post :c, :on => :collection delete :d, :on => :collection + patch :e, :on => :collection end end end @@ -241,7 +243,7 @@ class ResourcesTest < ActionController::TestCase end def test_with_collection_action_and_name_prefix_and_formatted - actions = { 'a' => :get, 'b' => :put, 'c' => :post, 'd' => :delete } + actions = { 'a' => :get, 'b' => :put, 'c' => :post, 'd' => :delete, 'e' => :patch } with_routing do |set| set.draw do @@ -251,6 +253,7 @@ class ResourcesTest < ActionController::TestCase put :b, :on => :collection post :c, :on => :collection delete :d, :on => :collection + patch :e, :on => :collection end end end @@ -270,7 +273,7 @@ class ResourcesTest < ActionController::TestCase end def test_with_member_action - [:put, :post].each do |method| + [:patch, :put, :post].each do |method| with_restful_routing :messages, :member => { :mark => method } do mark_options = {:action => 'mark', :id => '1'} mark_path = "/messages/1/mark" @@ -294,7 +297,7 @@ class ResourcesTest < ActionController::TestCase end def test_member_when_override_paths_for_default_restful_actions_with - [:put, :post].each do |method| + [:patch, :put, :post].each do |method| with_restful_routing :messages, :member => { :mark => method }, :path_names => {:new => 'nuevo'} do mark_options = {:action => 'mark', :id => '1', :controller => "messages"} mark_path = "/messages/1/mark" @@ -311,7 +314,7 @@ class ResourcesTest < ActionController::TestCase end def test_with_two_member_actions_with_same_method - [:put, :post].each do |method| + [:patch, :put, :post].each do |method| with_routing do |set| set.draw do resources :messages do @@ -564,7 +567,7 @@ class ResourcesTest < ActionController::TestCase end def test_singleton_resource_with_member_action - [:put, :post].each do |method| + [:patch, :put, :post].each do |method| with_routing do |set| set.draw do resource :account do @@ -586,7 +589,7 @@ class ResourcesTest < ActionController::TestCase end def test_singleton_resource_with_two_member_actions_with_same_method - [:put, :post].each do |method| + [:patch, :put, :post].each do |method| with_routing do |set| set.draw do resource :account do @@ -651,13 +654,17 @@ class ResourcesTest < ActionController::TestCase end end - def test_should_not_allow_delete_or_put_on_collection_path + def test_should_not_allow_delete_or_patch_or_put_on_collection_path controller_name = :messages with_restful_routing controller_name do options = { :controller => controller_name.to_s } collection_path = "/#{controller_name}" assert_raise(ActionController::RoutingError) do + assert_recognizes(options.merge(:action => 'update'), :path => collection_path, :method => :patch) + end + + assert_raise(ActionController::RoutingError) do assert_recognizes(options.merge(:action => 'update'), :path => collection_path, :method => :put) end diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb index ee9374cc91..807905c7b5 100644 --- a/actionpack/test/controller/routing_test.rb +++ b/actionpack/test/controller/routing_test.rb @@ -648,11 +648,12 @@ class LegacyRouteSetTests < ActiveSupport::TestCase match '/match' => 'books#get', :via => :get match '/match' => 'books#post', :via => :post match '/match' => 'books#put', :via => :put + match '/match' => 'books#patch', :via => :patch match '/match' => 'books#delete', :via => :delete end end - %w(GET POST PUT DELETE).each do |request_method| + %w(GET PATCH POST PUT DELETE).each do |request_method| define_method("test_request_method_recognized_with_#{request_method}") do setup_request_method_routes_for(request_method) params = rs.recognize_path("/match", :method => request_method) @@ -1035,6 +1036,7 @@ class RouteSetTest < ActiveSupport::TestCase post "/people" => "people#create" get "/people/:id" => "people#show", :as => "person" put "/people/:id" => "people#update" + patch "/people/:id" => "people#update" delete "/people/:id" => "people#destroy" end @@ -1047,6 +1049,9 @@ class RouteSetTest < ActiveSupport::TestCase params = set.recognize_path("/people/5", :method => :put) assert_equal("update", params[:action]) + params = set.recognize_path("/people/5", :method => :patch) + assert_equal("update", params[:action]) + assert_raise(ActionController::UnknownHttpMethod) { set.recognize_path("/people", :method => :bacon) } @@ -1059,6 +1064,10 @@ class RouteSetTest < ActiveSupport::TestCase assert_equal("update", params[:action]) assert_equal("5", params[:id]) + params = set.recognize_path("/people/5", :method => :patch) + assert_equal("update", params[:action]) + assert_equal("5", params[:id]) + params = set.recognize_path("/people/5", :method => :delete) assert_equal("destroy", params[:action]) assert_equal("5", params[:id]) @@ -1112,6 +1121,7 @@ class RouteSetTest < ActiveSupport::TestCase set.draw do get "people/:id" => "people#show", :as => "person" put "people/:id" => "people#update" + patch "people/:id" => "people#update" get "people/:id(.:format)" => "people#show" end @@ -1122,6 +1132,9 @@ class RouteSetTest < ActiveSupport::TestCase params = set.recognize_path("/people/5", :method => :put) assert_equal("update", params[:action]) + params = set.recognize_path("/people/5", :method => :patch) + assert_equal("update", params[:action]) + params = set.recognize_path("/people/5.png", :method => :get) assert_equal("show", params[:action]) assert_equal("5", params[:id]) diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 8f0ac5310e..6c8b22c47f 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -325,14 +325,14 @@ class RequestTest < ActiveSupport::TestCase end test "String request methods" do - [:get, :post, :put, :delete].each do |method| + [:get, :post, :patch, :put, :delete].each do |method| request = stub_request 'REQUEST_METHOD' => method.to_s.upcase assert_equal method.to_s.upcase, request.method end end test "Symbol forms of request methods via method_symbol" do - [:get, :post, :put, :delete].each do |method| + [:get, :post, :patch, :put, :delete].each do |method| request = stub_request 'REQUEST_METHOD' => method.to_s.upcase assert_equal method, request.method_symbol end @@ -346,7 +346,7 @@ class RequestTest < ActiveSupport::TestCase end test "allow method hacking on post" do - %w(GET OPTIONS PUT POST DELETE).each do |method| + %w(GET OPTIONS PATCH PUT POST DELETE).each do |method| request = stub_request "REQUEST_METHOD" => method.to_s.upcase assert_equal(method == "HEAD" ? "GET" : method, request.method) end @@ -360,7 +360,7 @@ class RequestTest < ActiveSupport::TestCase end test "restrict method hacking" do - [:get, :put, :delete].each do |method| + [:get, :patch, :put, :delete].each do |method| request = stub_request 'REQUEST_METHOD' => method.to_s.upcase, 'action_dispatch.request.request_parameters' => { :_method => 'put' } assert_equal method.to_s.upcase, request.method @@ -375,6 +375,13 @@ class RequestTest < ActiveSupport::TestCase assert request.head? end + test "post masquerading as patch" do + request = stub_request 'REQUEST_METHOD' => 'PATCH', "rack.methodoverride.original_method" => "POST" + assert_equal "POST", request.method + assert_equal "PATCH", request.request_method + assert request.patch? + end + test "post masquerading as put" do request = stub_request 'REQUEST_METHOD' => 'PUT', "rack.methodoverride.original_method" => "POST" assert_equal "POST", request.method diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb index d072d3bce0..e4cb7e02a0 100644 --- a/actionpack/test/template/form_helper_test.rb +++ b/actionpack/test/template/form_helper_test.rb @@ -2195,6 +2195,15 @@ class FormHelperTest < ActionView::TestCase assert_equal expected, output_buffer end + def test_form_for_with_default_method_as_patch + ActionView::Base.default_method_for_update = :patch + form_for(@post) {} + expected = whole_form("/posts/123", "edit_post_123", "edit_post", "patch") + assert_dom_equal expected, output_buffer + ensure + ActionView::Base.default_method_for_update = :put + end + def test_fields_for_returns_block_result output = fields_for(Post.new) { |f| "fields" } assert_equal "fields", output diff --git a/actionpack/test/template/form_tag_helper_test.rb b/actionpack/test/template/form_tag_helper_test.rb index 809102e5c2..6ef4cf4dd2 100644 --- a/actionpack/test/template/form_tag_helper_test.rb +++ b/actionpack/test/template/form_tag_helper_test.rb @@ -78,6 +78,12 @@ class FormTagHelperTest < ActionView::TestCase assert_dom_equal expected, actual end + def test_form_tag_with_method_patch + actual = form_tag({}, { :method => :patch }) + expected = whole_form("http://www.example.com", :method => :patch) + assert_dom_equal expected, actual + end + def test_form_tag_with_method_put actual = form_tag({}, { :method => :put }) expected = whole_form("http://www.example.com", :method => :put) diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb index 49ea894150..a10fdefd1a 100644 --- a/activemodel/lib/active_model/lint.rb +++ b/activemodel/lib/active_model/lint.rb @@ -62,9 +62,9 @@ module ActiveModel # # Returns a boolean that specifies whether the object has been persisted yet. # This is used when calculating the URL for an object. If the object is - # not persisted, a form for that object, for instance, will be POSTed to the - # collection. If it is persisted, a form for the object will be PUT to the - # URL for the object. + # not persisted, a form for that object, for instance, will route to the + # create action. If it is persisted, a form for the object will routes to + # the update action. def test_persisted? assert model.respond_to?(:persisted?), "The model should respond to persisted?" assert_boolean model.persisted?, "persisted?" diff --git a/activeresource/lib/active_resource/connection.rb b/activeresource/lib/active_resource/connection.rb index 46060b6f74..8a8dc3146d 100644 --- a/activeresource/lib/active_resource/connection.rb +++ b/activeresource/lib/active_resource/connection.rb @@ -15,6 +15,7 @@ module ActiveResource HTTP_FORMAT_HEADER_NAMES = { :get => 'Accept', :put => 'Content-Type', :post => 'Content-Type', + :patch => 'Content-Type', :delete => 'Accept', :head => 'Accept' } @@ -86,6 +87,12 @@ module ActiveResource with_auth { request(:delete, path, build_request_headers(headers, :delete, self.site.merge(path))) } end + # Executes a PATCH request (see HTTP protocol documentation if unfamiliar). + # Used to update resources. + def patch(path, body = '', headers = {}) + with_auth { request(:patch, path, body.to_s, build_request_headers(headers, :patch, self.site.merge(path))) } + end + # Executes a PUT request (see HTTP protocol documentation if unfamiliar). # Used to update resources. def put(path, body = '', headers = {}) diff --git a/activeresource/lib/active_resource/custom_methods.rb b/activeresource/lib/active_resource/custom_methods.rb index a0eb28ed13..095dcd2a8e 100644 --- a/activeresource/lib/active_resource/custom_methods.rb +++ b/activeresource/lib/active_resource/custom_methods.rb @@ -11,10 +11,10 @@ module ActiveResource # # This route set creates routes for the following HTTP requests: # - # POST /people/new/register.json # PeopleController.register - # PUT /people/1/promote.json # PeopleController.promote with :id => 1 - # DELETE /people/1/deactivate.json # PeopleController.deactivate with :id => 1 - # GET /people/active.json # PeopleController.active + # POST /people/new/register.json # PeopleController.register + # PUT/PATCH /people/1/promote.json # PeopleController.promote with :id => 1 + # DELETE /people/1/deactivate.json # PeopleController.deactivate with :id => 1 + # GET /people/active.json # PeopleController.active # # Using this module, Active Resource can use these custom REST methods just like the # standard methods. @@ -63,6 +63,10 @@ module ActiveResource connection.post(custom_method_collection_url(custom_method_name, options), body, headers) end + def patch(custom_method_name, options = {}, body = '') + connection.patch(custom_method_collection_url(custom_method_name, options), body, headers) + end + def put(custom_method_name, options = {}, body = '') connection.put(custom_method_collection_url(custom_method_name, options), body, headers) end @@ -98,6 +102,10 @@ module ActiveResource end end + def patch(method_name, options = {}, body = '') + connection.patch(custom_method_element_url(method_name, options), body, self.class.headers) + end + def put(method_name, options = {}, body = '') connection.put(custom_method_element_url(method_name, options), body, self.class.headers) end diff --git a/activeresource/lib/active_resource/http_mock.rb b/activeresource/lib/active_resource/http_mock.rb index 666b961f87..820b178d4b 100644 --- a/activeresource/lib/active_resource/http_mock.rb +++ b/activeresource/lib/active_resource/http_mock.rb @@ -15,7 +15,7 @@ module ActiveResource # # mock.http_method(path, request_headers = {}, body = nil, status = 200, response_headers = {}) # - # * <tt>http_method</tt> - The HTTP method to listen for. This can be +get+, +post+, +put+, +delete+ or + # * <tt>http_method</tt> - The HTTP method to listen for. This can be +get+, +post+, +patch+, +put+, +delete+ or # +head+. # * <tt>path</tt> - A string, starting with a "/", defining the URI that is expected to be # called. @@ -55,7 +55,7 @@ module ActiveResource @responses = responses end - [ :post, :put, :get, :delete, :head ].each do |method| + [ :post, :put, :patch, :get, :delete, :head ].each do |method| # def post(path, request_headers = {}, body = nil, status = 200, response_headers = {}) # @responses[Request.new(:post, path, nil, request_headers)] = Response.new(body || "", status, response_headers) # end @@ -217,7 +217,7 @@ module ActiveResource end # body? methods - { true => %w(post put), + { true => %w(post patch put), false => %w(get delete head) }.each do |has_body, methods| methods.each do |method| # def post(path, body, headers) diff --git a/activeresource/test/cases/format_test.rb b/activeresource/test/cases/format_test.rb index 30342ecc74..315f9db1eb 100644 --- a/activeresource/test/cases/format_test.rb +++ b/activeresource/test/cases/format_test.rb @@ -11,11 +11,15 @@ class FormatTest < ActiveSupport::TestCase end def test_http_format_header_name - header_name = ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[:get] - assert_equal 'Accept', header_name + [:get, :head].each do |verb| + header_name = ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[verb] + assert_equal 'Accept', header_name + end - headers_names = [ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[:put], ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[:post]] - headers_names.each{ |name| assert_equal 'Content-Type', name } + [:patch, :put, :post].each do |verb| + header_name = ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES[verb] + assert_equal 'Content-Type', header_name + end end def test_formats_on_single_element diff --git a/activeresource/test/cases/http_mock_test.rb b/activeresource/test/cases/http_mock_test.rb index d2fd911314..d13d9258ce 100644 --- a/activeresource/test/cases/http_mock_test.rb +++ b/activeresource/test/cases/http_mock_test.rb @@ -8,7 +8,7 @@ class HttpMockTest < ActiveSupport::TestCase FORMAT_HEADER = ActiveResource::Connection::HTTP_FORMAT_HEADER_NAMES - [:post, :put, :get, :delete, :head].each do |method| + [:post, :patch, :put, :get, :delete, :head].each do |method| test "responds to simple #{method} request" do ActiveResource::HttpMock.respond_to do |mock| mock.send(method, "/people/1", { FORMAT_HEADER[method] => "application/json" }, "Response") @@ -193,7 +193,7 @@ class HttpMockTest < ActiveSupport::TestCase end def request(method, path, headers = {}, body = nil) - if method.in?([:put, :post]) + if method.in?([:patch, :put, :post]) @http.send(method, path, body, headers) else @http.send(method, path, headers) diff --git a/railties/guides/source/action_controller_overview.textile b/railties/guides/source/action_controller_overview.textile index bc85f07ecc..52d134ace5 100644 --- a/railties/guides/source/action_controller_overview.textile +++ b/railties/guides/source/action_controller_overview.textile @@ -563,7 +563,7 @@ The request object contains a lot of useful information about the request coming |domain(n=2)|The hostname's first +n+ segments, starting from the right (the TLD).| |format|The content type requested by the client.| |method|The HTTP method used for the request.| -|get?, post?, put?, delete?, head?|Returns true if the HTTP method is GET/POST/PUT/DELETE/HEAD.| +|get?, post?, patch?, put?, delete?, head?|Returns true if the HTTP method is GET/POST/PATCH/PUT/DELETE/HEAD.| |headers|Returns a hash containing the headers associated with the request.| |port|The port number (integer) used for the request.| |protocol|Returns a string containing the protocol used plus "://", for example "http://".| diff --git a/railties/guides/source/ajax_on_rails.textile b/railties/guides/source/ajax_on_rails.textile index 3a0ccfe9b2..5dbfc41e9f 100644 --- a/railties/guides/source/ajax_on_rails.textile +++ b/railties/guides/source/ajax_on_rails.textile @@ -146,7 +146,8 @@ link_to_remote "Add new item", :position => :bottom </ruby> -** *:method* Most typically you want to use a POST request when adding a remote link to your view so this is the default behavior. However, sometimes you'll want to update (PUT) or delete/destroy (DELETE) something and you can specify this with the +:method+ option. Let's see an example for a typical AJAX link for deleting an item from a list: +** *:method* Most typically you want to use a POST request when adding a remote +link to your view so this is the default behavior. However, sometimes you'll want to update (PUT/PATCH) or delete/destroy (DELETE) something and you can specify this with the +:method+ option. Let's see an example for a typical AJAX link for deleting an item from a list: <ruby> link_to_remote "Delete the item", diff --git a/railties/guides/source/configuring.textile b/railties/guides/source/configuring.textile index 451235d41d..3f360e5022 100644 --- a/railties/guides/source/configuring.textile +++ b/railties/guides/source/configuring.textile @@ -76,6 +76,8 @@ NOTE. The +config.asset_path+ configuration is ignored if the asset pipeline is * +config.consider_all_requests_local+ is a flag. If true then any error will cause detailed debugging information to be dumped in the HTTP response, and the +Rails::Info+ controller will show the application runtime context in +/rails/info/properties+. True by default in development and test environments, and false in production mode. For finer-grained control, set this to false and implement +local_request?+ in controllers to specify which requests should provide debugging information on errors. +* +config.default_method_for_update+ tells Rails which HTTP method to use for update actions by default. Set this to +:patch+ for proper HTTP update semantics. The default is +:put+ for backwards compatibility. + * +config.dependency_loading+ is a flag that allows you to disable constant autoloading setting it to false. It only has effect if +config.cache_classes+ is true, which it is by default in production mode. This flag is set to false by +config.threadsafe!+. * +config.eager_load_paths+ accepts an array of paths from which Rails will eager load on boot if cache classes is enabled. Defaults to every folder in the +app+ directory of the application. @@ -214,7 +216,7 @@ Every Rails application comes with a standard set of middleware which it uses in * +ActionDispatch::Session::CookieStore+ is responsible for storing the session in cookies. An alternate middleware can be used for this by changing the +config.action_controller.session_store+ to an alternate value. Additionally, options passed to this can be configured by using +config.action_controller.session_options+. * +ActionDispatch::Flash+ sets up the +flash+ keys. Only available if +config.action_controller.session_store+ is set to a value. * +ActionDispatch::ParamsParser+ parses out parameters from the request into +params+. -* +Rack::MethodOverride+ allows the method to be overridden if +params[:_method]+ is set. This is the middleware which supports the PUT and DELETE HTTP method types. +* +Rack::MethodOverride+ allows the method to be overridden if +params[:_method]+ is set. This is the middleware which supports the PUT, PATCH, and DELETE HTTP method types. * +ActionDispatch::Head+ converts HEAD requests to GET requests and serves them as so. * +ActionDispatch::BestStandardsSupport+ enables "best standards support" so that IE8 renders some elements correctly. @@ -346,7 +348,8 @@ h4. Configuring Action Dispatch h4. Configuring Action View -There are only a few configuration options for Action View, starting with four on +ActionView::Base+: +There are only a few configuration options for Action View, starting with six on +ActionView::Base+: + * +config.action_view.field_error_proc+ provides an HTML generator for displaying errors that come from Active Record. The default is diff --git a/railties/guides/source/form_helpers.textile b/railties/guides/source/form_helpers.textile index 2de4d49cf2..31201db844 100644 --- a/railties/guides/source/form_helpers.textile +++ b/railties/guides/source/form_helpers.textile @@ -303,6 +303,11 @@ Rails will also automatically set the +class+ and +id+ of the form appropriately WARNING: When you're using STI (single-table inheritance) with your models, you can't rely on record identification on a subclass if only their parent class is declared a resource. You will have to specify the model name, +:url+, and +:method+ explicitly. +NOTE: Rails can use the +PATCH+ method instead of +PUT+ for update forms if you set the following option in your +config/application.rb+: +<ruby> +config.default_method_for_update = :patch +</ruby> + h5. Dealing with Namespaces If you have created namespaced routes, +form_for+ has a nifty shorthand for that too. If your application has an admin namespace then @@ -320,14 +325,14 @@ form_for [:admin, :management, @article] For more information on Rails' routing system and the associated conventions, please see the "routing guide":routing.html. -h4. How do forms with PUT or DELETE methods work? +h4. How do forms with PATCH, PUT, or DELETE methods work? -The Rails framework encourages RESTful design of your applications, which means you'll be making a lot of "PUT" and "DELETE" requests (besides "GET" and "POST"). However, most browsers _don't support_ methods other than "GET" and "POST" when it comes to submitting forms. +The Rails framework encourages RESTful design of your applications, which means you'll be making a lot of "PATCH" and "DELETE" requests (besides "GET" and "POST"). However, most browsers _don't support_ methods other than "GET" and "POST" when it comes to submitting forms. Rails works around this issue by emulating other methods over POST with a hidden input named +"_method"+, which is set to reflect the desired method: <ruby> -form_tag(search_path, :method => "put") +form_tag(search_path, :method => "patch") </ruby> output: @@ -335,14 +340,14 @@ output: <html> <form accept-charset="UTF-8" action="/search" method="post"> <div style="margin:0;padding:0"> - <input name="_method" type="hidden" value="put" /> + <input name="_method" type="hidden" value="patch" /> <input name="utf8" type="hidden" value="✓" /> <input name="authenticity_token" type="hidden" value="f755bb0ed134b76c432144748a6d4b7a7ddf2b71" /> </div> ... </html> -When parsing POSTed data, Rails will take into account the special +_method+ parameter and acts as if the HTTP method was the one specified inside it ("PUT" in this example). +When parsing POSTed data, Rails will take into account the special +_method+ parameter and acts as if the HTTP method was the one specified inside it ("PATCH" in this example). h3. Making Select Boxes with Ease diff --git a/railties/guides/source/routing.textile b/railties/guides/source/routing.textile index 0823fb14e3..6f1677e85a 100644 --- a/railties/guides/source/routing.textile +++ b/railties/guides/source/routing.textile @@ -50,7 +50,7 @@ Resource routing allows you to quickly declare all of the common routes for a gi h4. Resources on the Web -Browsers request pages from Rails by making a request for a URL using a specific HTTP method, such as +GET+, +POST+, +PUT+ and +DELETE+. Each method is a request to perform an operation on the resource. A resource route maps a number of related requests to actions in a single controller. +Browsers request pages from Rails by making a request for a URL using a specific HTTP method, such as +GET+, +POST+, +PATCH+, +PUT+ and +DELETE+. Each method is a request to perform an operation on the resource. A resource route maps a number of related requests to actions in a single controller. When your Rails application receives an incoming request for @@ -82,11 +82,10 @@ creates seven different routes in your application, all mapping to the +Photos+ |POST |/photos |create |create a new photo | |GET |/photos/:id |show |display a specific photo | |GET |/photos/:id/edit |edit |return an HTML form for editing a photo | -|PUT |/photos/:id |update |update a specific photo | +|PUT/PATCH |/photos/:id |update |update a specific photo | |DELETE |/photos/:id |destroy |delete a specific photo | - -NOTE: Rails routes are matched in the order they are specified, so if you have a +resources :photos+ above a +get 'photos/poll'+ the +show+ action's route for the +resources+ line will be matched before the +get+ line. To fix this, move the +get+ line *above* the +resources+ line so that it is matched first. +NOTE: The HTTP Verb for the +update+ action defaults to +PUT+ for backwards compatibility. Set +config.default_method_for_update+ to +:patch+ to use +PATCH+. Rails routes are matched in the order they are specified, so if you have a +resources :photos+ above a +get 'photos/poll'+ the +show+ action's route for the +resources+ line will be matched before the +get+ line. To fix this, move the +get+ line *above* the +resources+ line so that it is matched first. h4. Paths and URLs @@ -138,10 +137,10 @@ creates six different routes in your application, all mapping to the +Geocoders+ |POST |/geocoder |create |create the new geocoder | |GET |/geocoder |show |display the one and only geocoder resource | |GET |/geocoder/edit |edit |return an HTML form for editing the geocoder | -|PUT |/geocoder |update |update the one and only geocoder resource | +|PUT/PATCH |/geocoder |update |update the one and only geocoder resource | |DELETE |/geocoder |destroy |delete the geocoder resource | -NOTE: Because you might want to use the same controller for a singular route (+/account+) and a plural route (+/accounts/45+), singular resources map to plural controllers. +NOTE: The HTTP Verb for the +update+ action defaults to +PUT+ for backwards compatibility. Set +config.default_method_for_update+ to +:patch+ to use +PATCH+. Because you might want to use the same controller for a singular route (+/account+) and a plural route (+/accounts/45+), singular resources map to plural controllers. A singular resourceful route generates these helpers: @@ -169,9 +168,11 @@ This will create a number of routes for each of the +posts+ and +comments+ contr |POST |/admin/posts |create | admin_posts_path | |GET |/admin/posts/:id |show | admin_post_path(:id) | |GET |/admin/posts/:id/edit |edit | edit_admin_post_path(:id) | -|PUT |/admin/posts/:id |update | admin_post_path(:id) | +|PUT/PATCH |/admin/posts/:id |update | admin_post_path(:id) | |DELETE |/admin/posts/:id |destroy | admin_post_path(:id) | +NOTE: The HTTP Verb for the +update+ action defaults to +PUT+ for backwards compatibility. Set +config.default_method_for_update+ to +:patch+ to use +PATCH+. + If you want to route +/posts+ (without the prefix +/admin+) to +Admin::PostsController+, you could use <ruby> @@ -208,9 +209,11 @@ In each of these cases, the named routes remain the same as if you did not use + |POST |/admin/posts |create | posts_path | |GET |/admin/posts/:id |show | post_path(:id) | |GET |/admin/posts/:id/edit|edit | edit_post_path(:id)| -|PUT |/admin/posts/:id |update | post_path(:id) | +|PUT/PATCH |/admin/posts/:id |update | post_path(:id) | |DELETE |/admin/posts/:id |destroy | post_path(:id) | +NOTE: The HTTP Verb for the +update+ action defaults to +PUT+ for backwards compatibility. Set +config.default_method_for_update+ to +:patch+ to use +PATCH+. + h4. Nested Resources It's common to have resources that are logically children of other resources. For example, suppose your application includes these models: @@ -235,15 +238,16 @@ end In addition to the routes for magazines, this declaration will also route ads to an +AdsController+. The ad URLs require a magazine: -|_.HTTP Verb |_.Path |_.action |_.used for | -|GET |/magazines/:id/ads |index |display a list of all ads for a specific magazine | -|GET |/magazines/:id/ads/new |new |return an HTML form for creating a new ad belonging to a specific magazine | -|POST |/magazines/:id/ads |create |create a new ad belonging to a specific magazine | +|_.HTTP Verb |_.Path |_.action |_.used for | +|GET |/magazines/:id/ads |index |display a list of all ads for a specific magazine | +|GET |/magazines/:id/ads/new |new |return an HTML form for creating a new ad belonging to a specific magazine | +|POST |/magazines/:id/ads |create |create a new ad belonging to a specific magazine | |GET |/magazines/:id/ads/:id |show |display a specific ad belonging to a specific magazine | |GET |/magazines/:id/ads/:id/edit |edit |return an HTML form for editing an ad belonging to a specific magazine | -|PUT |/magazines/:id/ads/:id |update |update a specific ad belonging to a specific magazine | +|PUT/PATCH |/magazines/:id/ads/:id |update |update a specific ad belonging to a specific magazine | |DELETE |/magazines/:id/ads/:id |destroy |delete a specific ad belonging to a specific magazine | +NOTE: The HTTP Verb for the +update+ action defaults to +PUT+ for backwards compatibility. Set +config.default_method_for_update+ to +:patch+ to use +PATCH+. This will also create routing helpers such as +magazine_ads_url+ and +edit_magazine_ad_path+. These helpers take an instance of Magazine as the first parameter (+magazine_ads_url(@magazine)+). @@ -323,7 +327,7 @@ end This will recognize +/photos/1/preview+ with GET, and route to the +preview+ action of +PhotosController+. It will also create the +preview_photo_url+ and +preview_photo_path+ helpers. -Within the block of member routes, each route name specifies the HTTP verb that it will recognize. You can use +get+, +put+, +post+, or +delete+ here. If you don't have multiple +member+ routes, you can also pass +:on+ to a route, eliminating the block: +Within the block of member routes, each route name specifies the HTTP verb that it will recognize. You can use +get+, +patch+, +put+, +post+, or +delete+ here. If you don't have multiple +member+ routes, you can also pass +:on+ to a route, eliminating the block: <ruby> resources :photos do @@ -642,7 +646,7 @@ will recognize incoming paths beginning with +/photos+ but route to the +Images+ |POST |/photos |create | photos_path | |GET |/photos/:id |show | photo_path(:id) | |GET |/photos/:id/edit |edit | edit_photo_path(:id) | -|PUT |/photos/:id |update | photo_path(:id) | +|PUT/PATCH |/photos/:id |update | photo_path(:id) | |DELETE |/photos/:id |destroy | photo_path(:id) | NOTE: Use +photos_path+, +new_photo_path+, etc. to generate paths for this resource. @@ -686,9 +690,11 @@ will recognize incoming paths beginning with +/photos+ and route the requests to |POST |/photos |create | images_path | |GET |/photos/:id |show | image_path(:id) | |GET |/photos/:id/edit |edit | edit_image_path(:id) | -|PUT |/photos/:id |update | image_path(:id) | +|PUT/PATCH |/photos/:id |update | image_path(:id) | |DELETE |/photos/:id |destroy | image_path(:id) | +NOTE: The HTTP Verb for the +update+ action defaults to +PUT+ for backwards compatibility. Set +config.default_method_for_update+ to +:patch+ to use +PATCH+. + h4. Overriding the +new+ and +edit+ Segments The +:path_names+ option lets you override the automatically-generated "new" and "edit" segments in paths: @@ -790,9 +796,11 @@ Rails now creates routes to the +CategoriesController+. |POST |/kategorien |create | categories_path | |GET |/kategorien/:id |show | category_path(:id) | |GET |/kategorien/:id/bearbeiten |edit | edit_category_path(:id) | -|PUT |/kategorien/:id |update | category_path(:id) | +|PUT/PATCH |/kategorien/:id |update | category_path(:id) | |DELETE |/kategorien/:id |destroy | category_path(:id) | +NOTE: The HTTP Verb for the +update+ action defaults to +PUT+ for backwards compatibility. Set +config.default_method_for_update+ to +:patch+ to use +PATCH+. + h4. Overriding the Singular Form If you want to define the singular form of a resource, you should add additional rules to the +Inflector+. diff --git a/railties/guides/source/testing.textile b/railties/guides/source/testing.textile index 1e6b92f45c..c367f532ae 100644 --- a/railties/guides/source/testing.textile +++ b/railties/guides/source/testing.textile @@ -483,10 +483,11 @@ Now you can try running all the tests and they should pass. h4. Available Request Types for Functional Tests -If you're familiar with the HTTP protocol, you'll know that +get+ is a type of request. There are 5 request types supported in Rails functional tests: +If you're familiar with the HTTP protocol, you'll know that +get+ is a type of request. There are 6 request types supported in Rails functional tests: * +get+ * +post+ +* +patch+ * +put+ * +head+ * +delete+ @@ -638,6 +639,7 @@ In addition to the standard testing helpers, there are some additional helpers a |+request_via_redirect(http_method, path, [parameters], [headers])+ |Allows you to make an HTTP request and follow any subsequent redirects.| |+post_via_redirect(path, [parameters], [headers])+ |Allows you to make an HTTP POST request and follow any subsequent redirects.| |+get_via_redirect(path, [parameters], [headers])+ |Allows you to make an HTTP GET request and follow any subsequent redirects.| +|+patch_via_redirect(path, [parameters], [headers])+ |Allows you to make an HTTP PATCH request and follow any subsequent redirects.| |+put_via_redirect(path, [parameters], [headers])+ |Allows you to make an HTTP PUT request and follow any subsequent redirects.| |+delete_via_redirect(path, [parameters], [headers])+ |Allows you to make an HTTP DELETE request and follow any subsequent redirects.| |+open_session+ |Opens a new session instance.| @@ -810,7 +812,7 @@ class PostsControllerTest < ActionController::TestCase end test "should update post" do - put :update, :id => @post.id, :post => { } + patch :update, :id => @post.id, :post => { } assert_redirected_to post_path(assigns(:post)) end diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 825ea985fc..0297c41274 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -11,7 +11,7 @@ module Rails :force_ssl, :helpers_paths, :logger, :log_tags, :preload_frameworks, :railties_order, :relative_url_root, :secret_token, :serve_static_assets, :ssl_options, :static_cache_control, :session_options, - :time_zone, :reload_classes_only_on_change + :time_zone, :reload_classes_only_on_change, :default_method_for_update attr_writer :log_level attr_reader :encoding @@ -40,6 +40,7 @@ module Rails @reload_classes_only_on_change = true @file_watcher = ActiveSupport::FileUpdateChecker @exceptions_app = nil + @default_method_for_update = :put @assets = ActiveSupport::OrderedOptions.new @assets.enabled = false diff --git a/railties/lib/rails/generators/active_model.rb b/railties/lib/rails/generators/active_model.rb index 4b828340d2..f51028b657 100644 --- a/railties/lib/rails/generators/active_model.rb +++ b/railties/lib/rails/generators/active_model.rb @@ -37,7 +37,7 @@ module Rails # GET show # GET edit - # PUT update + # PUT/PATCH update # DELETE destroy def self.find(klass, params=nil) "#{klass}.find(#{params})" @@ -58,13 +58,13 @@ module Rails "#{name}.save" end - # PUT update + # PUT/PATCH update def update_attributes(params=nil) "#{name}.update_attributes(#{params})" end # POST create - # PUT update + # PUT/PATCH update def errors "#{name}.errors" end diff --git a/railties/lib/rails/generators/rails/app/templates/config/application.rb b/railties/lib/rails/generators/rails/app/templates/config/application.rb index acf47a03e5..c970c2cf44 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/application.rb +++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb @@ -31,6 +31,9 @@ module <%= app_const_base %> # Activate observers that should always be running. # config.active_record.observers = :cacher, :garbage_collector, :forum_observer + # Use PATCH as default method for update actions + # config.default_method_for_update = :patch + # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. # config.time_zone = 'Central Time (US & Canada)' diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb index 4ff15fd288..566d78c6eb 100644 --- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb +++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb @@ -54,8 +54,8 @@ class <%= controller_class_name %>Controller < ApplicationController end end - # PUT <%= route_url %>/1 - # PUT <%= route_url %>/1.json + # PUT/PATCH <%= route_url %>/1 + # PUT/PATCH <%= route_url %>/1.json def update @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index 3dffd1c74c..bf44a8b10b 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -146,7 +146,7 @@ module ApplicationTests test "frameworks are not preloaded by default" do require "#{app_path}/config/environment" - assert ActionController.autoload?(:RecordIdentifier) + assert ActionController.autoload?(:Caching) end test "frameworks are preloaded with config.preload_frameworks is set" do @@ -156,7 +156,7 @@ module ApplicationTests require "#{app_path}/config/environment" - assert !ActionController.autoload?(:RecordIdentifier) + assert !ActionController.autoload?(:Caching) end test "filter_parameters should be able to set via config.filter_parameters" do @@ -246,6 +246,49 @@ module ApplicationTests assert last_response.body =~ /csrf\-param/ end + test "default method for update can be changed" do + app_file 'app/models/post.rb', <<-RUBY + class Post + extend ActiveModel::Naming + def to_key; [1]; end + def persisted?; true; end + end + RUBY + + app_file 'app/controllers/posts_controller.rb', <<-RUBY + class PostsController < ApplicationController + def show + render :inline => "<%= begin; form_for(Post.new) {}; rescue => e; e.to_s; end %>" + end + + def update + render :text => "update" + end + end + RUBY + + add_to_config <<-RUBY + config.default_method_for_update = :patch + routes.prepend do + resources :posts + end + RUBY + + require "#{app_path}/config/environment" + + assert_equal ActionView::Base.default_method_for_update, :patch + assert_equal ActionDispatch::Routing::Mapper.default_method_for_update, :patch + + get "/posts/1" + assert_match /patch/, last_response.body + + patch "/posts/1" + assert_match /update/, last_response.body + + put "/posts/1" + assert_equal 404, last_response.status + end + test "request forgery token param can be changed" do make_basic_app do app.config.action_controller.request_forgery_protection_token = '_xsrf_token_here' diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index a3c24c392b..55c7ce1047 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -325,6 +325,11 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_file "config/application.rb", /#\s+require\s+["']active_record\/railtie["']/ end + def test_default_method_for_update_is_not_patch + run_generator [destination_root, "--skip-test-unit", "--skip-active-record"] + assert_file "config/application.rb", /#\s+config\.default_method_for_update\s+=\s+:patch/ + end + def test_new_hash_style run_generator [destination_root] assert_file "config/initializers/session_store.rb" do |file| |