Skip to content

Commit

Permalink
Use PATCH instead of PUT; Fixes issue rails#348
Browse files Browse the repository at this point in the history
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 the #patch verb to routes to detect PATCH requests
* adds #patch? to Request
* adds the PATCH -> update mapping in the #resource(s) routes.
* changes default form helpers to prefer :patch instead of :put for updates
* changes documentation and comments to indicate the preference for PATCH

This change tries to maintain complete backwards compatibility by keeping the
original PUT -> update mapping. Users using the #resource(s) routes should not
notice a change in behavior since both PUT and PATCH requests get mapped to
update.
  • Loading branch information
dlee committed May 10, 2011
1 parent fa187ec commit 3f9a09c
Show file tree
Hide file tree
Showing 36 changed files with 350 additions and 137 deletions.
4 changes: 2 additions & 2 deletions actionpack/lib/action_controller/metal/http_authentication.rb
Expand Up @@ -276,7 +276,7 @@ def secret_token(request)
#
# 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
Expand All @@ -290,7 +290,7 @@ def nonce(secret_key, time = Time.now)
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.
Expand Down
5 changes: 3 additions & 2 deletions actionpack/lib/action_controller/metal/responder.rb
Expand Up @@ -53,7 +53,7 @@ module ActionController #:nodoc:
# end
# end
#
# The same happens for PUT and DELETE requests.
# The same happens for PATCH and DELETE requests.
#
# === Nested resources
#
Expand Down Expand Up @@ -118,6 +118,7 @@ class Responder

ACTIONS_FOR_VERBS = {
:post => :new,
:patch => :edit,
:put => :edit
}

Expand All @@ -133,7 +134,7 @@ def initialize(controller, resources, options={})
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)
Expand Down
7 changes: 6 additions & 1 deletion actionpack/lib/action_controller/test_case.rb
Expand Up @@ -224,7 +224,7 @@ def exists?
# == 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.
Expand Down Expand Up @@ -369,6 +369,11 @@ def post(action, parameters = nil, session = nil, flash = nil)
process(action, parameters, session, flash, "POST")
end

# Executes a request simulating PATCH HTTP method and set/volley the response
def patch(action, parameters = nil, session = nil, flash = nil)
process(action, parameters, session, flash, "PATCH")
end

# Executes a request simulating PUT HTTP method and set/volley the response
def put(action, parameters = nil, session = nil, flash = nil)
process(action, parameters, session, flash, "PUT")
Expand Down
6 changes: 6 additions & 0 deletions actionpack/lib/action_dispatch/http/request.rb
Expand Up @@ -105,6 +105,12 @@ def post?
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 == :put</tt>.
def put?
Expand Down
15 changes: 9 additions & 6 deletions actionpack/lib/action_dispatch/routing.rb
Expand Up @@ -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:
#
Expand All @@ -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:
#
Expand Down Expand Up @@ -284,7 +287,7 @@ module Routing
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:

# A helper module to hold URL related helpers.
module Helpers #:nodoc:
Expand Down
27 changes: 24 additions & 3 deletions actionpack/lib/action_dispatch/routing/mapper.rb
Expand Up @@ -484,6 +484,16 @@ 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>.
#
Expand All @@ -494,7 +504,7 @@ def put(*args, &block)
map_method(:put, *args, &block)
end

# Define a route that only recognizes HTTP PUT.
# Define a route that only recognizes HTTP DELETE.
# For supported arguments, see <tt>Base#match</tt>.
#
# Example:
Expand Down Expand Up @@ -532,6 +542,7 @@ def map_method(method, *args, &block)
# POST /admin/posts
# GET /admin/posts/1
# GET /admin/posts/1/edit
# PATCH /admin/posts/1
# PUT /admin/posts/1
# DELETE /admin/posts/1
#
Expand Down Expand Up @@ -566,6 +577,7 @@ def map_method(method, *args, &block)
# POST /admin/posts
# GET /admin/posts/1
# GET /admin/posts/1/edit
# PATCH /admin/posts/1
# PUT /admin/posts/1
# DELETE /admin/posts/1
module Scoping
Expand Down Expand Up @@ -661,6 +673,7 @@ def controller(controller, options={})
# new_admin_post GET /admin/posts/new(.:format) {:action=>"new", :controller=>"admin/posts"}
# edit_admin_post GET /admin/posts/:id/edit(.:format) {:action=>"edit", :controller=>"admin/posts"}
# admin_post GET /admin/posts/:id(.:format) {:action=>"show", :controller=>"admin/posts"}
# admin_post PATCH /admin/posts/:id(.:format) {:action=>"update", :controller=>"admin/posts"}
# admin_post PUT /admin/posts/:id(.:format) {:action=>"update", :controller=>"admin/posts"}
# admin_post DELETE /admin/posts/:id(.:format) {:action=>"destroy", :controller=>"admin/posts"}
#
Expand Down Expand Up @@ -974,14 +987,15 @@ def resources_path_names(options)
#
# resource :geocoder
#
# creates six different routes in your application, all mapping to
# creates seven different routes in your application, all mapping to
# the GeoCoders controller (note that the controller is named after
# the plural):
#
# GET /geocoder/new
# POST /geocoder
# GET /geocoder
# GET /geocoder/edit
# PATCH /geocoder
# PUT /geocoder
# DELETE /geocoder
#
Expand All @@ -1008,6 +1022,7 @@ def resource(*resources, &block)
member do
get :edit if parent_resource.actions.include?(:edit)
get :show if parent_resource.actions.include?(:show)
patch :update if parent_resource.actions.include?(:update)
put :update if parent_resource.actions.include?(:update)
delete :destroy if parent_resource.actions.include?(:destroy)
end
Expand All @@ -1023,13 +1038,15 @@ def resource(*resources, &block)
#
# resources :photos
#
# creates seven different routes in your application, all mapping to
# creates eight 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
# PATCH /photos/:id
# PUT /photos/:id
# DELETE /photos/:id
#
Expand All @@ -1041,10 +1058,12 @@ def resource(*resources, &block)
#
# This generates the following comments routes:
#
# GET /photos/:id/comments
# GET /photos/:id/comments/new
# POST /photos/:id/comments
# GET /photos/:id/comments/:id
# GET /photos/:id/comments/:id/edit
# PATCH /photos/:id/comments/:id
# PUT /photos/:id/comments/:id
# DELETE /photos/:id/comments/:id
#
Expand Down Expand Up @@ -1102,6 +1121,7 @@ def resource(*resources, &block)
# new_post_comment GET /sekret/posts/:post_id/comments/new(.:format)
# edit_comment GET /sekret/comments/:id/edit(.:format)
# comment GET /sekret/comments/:id(.:format)
# comment PATCH /sekret/comments/:id(.:format)
# comment PUT /sekret/comments/:id(.:format)
# comment DELETE /sekret/comments/:id(.:format)
#
Expand Down Expand Up @@ -1134,6 +1154,7 @@ def resources(*resources, &block)
member do
get :edit if parent_resource.actions.include?(:edit)
get :show if parent_resource.actions.include?(:show)
patch :update if parent_resource.actions.include?(:update)
put :update if parent_resource.actions.include?(:update)
delete :destroy if parent_resource.actions.include?(:destroy)
end
Expand Down
26 changes: 19 additions & 7 deletions actionpack/lib/action_dispatch/testing/integration.rb
Expand Up @@ -27,8 +27,8 @@ module RequestHelpers
# 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
Expand All @@ -39,6 +39,12 @@ def post(path, parameters = nil, headers = nil)
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)
Expand All @@ -60,10 +66,10 @@ def head(path, parameters = nil, headers = nil)
# 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'
Expand Down Expand Up @@ -103,6 +109,12 @@ def post_via_redirect(path, parameters = nil, headers = nil)
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)
Expand Down Expand Up @@ -316,7 +328,7 @@ def reset!
@integration_session = Integration::Session.new(app)
end

%w(get post put head delete cookies assigns
%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
Expand Down
8 changes: 4 additions & 4 deletions actionpack/lib/action_view/helpers/form_helper.rb
Expand Up @@ -190,7 +190,7 @@ def convert_to_model(object)
#
# is equivalent to something like:
#
# <%= form_for @post, :as => :post, :url => post_path(@post), :method => :put, :html => { :class => "edit_post", :id => "edit_post_45" } do |f| %>
# <%= form_for @post, :as => :post, :url => post_path(@post), :method => :patch, :html => { :class => "edit_post", :id => "edit_post_45" } do |f| %>
# ...
# <% end %>
#
Expand Down Expand Up @@ -245,7 +245,7 @@ def convert_to_model(object)
#
# 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
Expand Down Expand Up @@ -273,7 +273,7 @@ def convert_to_model(object)
#
# <form action='http://www.example.com' method='post' data-remote='true'>
# <div style='margin:0;padding:0;display:inline'>
# <input name='_method' type='hidden' value='put' />
# <input name='_method' type='hidden' value='patch' />
# </div>
# ...
# </form>
Expand Down Expand Up @@ -381,7 +381,7 @@ def apply_form_for_options!(object_or_array, options) #:nodoc:
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, :patch] : [:new, :post]
options[:html].reverse_merge!(
:class => as ? "#{as}_#{action}" : dom_class(object, action),
:id => as ? "#{as}_#{action}" : dom_id(object, action),
Expand Down
6 changes: 3 additions & 3 deletions actionpack/lib/action_view/helpers/form_tag_helper.rb
Expand Up @@ -23,7 +23,7 @@ module FormTagHelper
# ==== Options
# * <tt>:multipart</tt> - If set to true, the enctype is set to "multipart/form-data".
# * <tt>:method</tt> - The method to use when submitting the form, usually either "get" or "post".
# If "put", "delete", or another verb is used, a hidden input with name <tt>_method</tt>
# If "patch", "delete", or another verb is used, a hidden input with name <tt>_method</tt>
# is added to simulate the verb over post.
# * <tt>:authenticity_token</tt> - Authenticity token to use in the form. Use only if you need to
# pass custom authenticity token string, or to not add authenticity_token field at all
Expand All @@ -36,8 +36,8 @@ module FormTagHelper
# form_tag('/posts')
# # => <form action="/posts" method="post">
#
# form_tag('/posts/1', :method => :put)
# # => <form action="/posts/1" method="put">
# form_tag('/posts/1', :method => :patch)
# # => <form action="/posts/1" method="patch">
#
# form_tag('/upload', :multipart => true)
# # => <form action="/upload" method="post" enctype="multipart/form-data">
Expand Down
8 changes: 4 additions & 4 deletions actionpack/lib/action_view/helpers/url_helper.rb
Expand Up @@ -146,12 +146,12 @@ def url_for(options = {})
# 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
Expand Down Expand Up @@ -272,7 +272,7 @@ def link_to(*args, &block)
#
# 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
Expand Down Expand Up @@ -319,7 +319,7 @@ def button_to(name, options = {}, html_options = {})
convert_boolean_attributes!(html_options, %w( disabled ))

method_tag = ''
if (method = html_options.delete('method')) && %w{put delete}.include?(method.to_s)
if (method = html_options.delete('method')) && %w{patch put delete}.include?(method.to_s)
method_tag = tag('input', :type => 'hidden', :name => '_method', :value => method.to_s)
end

Expand Down
2 changes: 1 addition & 1 deletion actionpack/test/controller/caching_test.rb
Expand Up @@ -140,7 +140,7 @@ def test_should_cache_ok_at_custom_path
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 and status == :ok
define_method "test_shouldnt_cache_#{method}_with_#{status}_status" do
send(method, status)
Expand Down

0 comments on commit 3f9a09c

Please sign in to comment.