From f59984cc81bd1a64a53a2480a9b4e05fe7357d7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Sat, 1 Aug 2009 15:29:39 +0200 Subject: Add nagivational behavior to respond_with. --- .../lib/action_controller/metal/conditional_get.rb | 7 +- .../lib/action_controller/metal/mime_responds.rb | 157 +++++++++++++++------ .../routing/generation/polymorphic_routes.rb | 21 ++- 3 files changed, 124 insertions(+), 61 deletions(-) (limited to 'actionpack/lib/action_controller') diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb index 6d35137428..8575d30335 100644 --- a/actionpack/lib/action_controller/metal/conditional_get.rb +++ b/actionpack/lib/action_controller/metal/conditional_get.rb @@ -55,14 +55,15 @@ module ActionController elsif args.empty? raise ArgumentError, "too few arguments to head" end - options = args.extract_options! - status = args.shift || options.delete(:status) || :ok + options = args.extract_options! + status = args.shift || options.delete(:status) || :ok + location = options.delete(:location) options.each do |key, value| headers[key.to_s.dasherize.split(/-/).map { |v| v.capitalize }.join("-")] = value.to_s end - render :nothing => true, :status => status + render :nothing => true, :status => status, :location => location end # Sets the etag and/or last_modified on the response and checks it against diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 9a6c8aa58b..837496e477 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -198,11 +198,11 @@ module ActionController #:nodoc: end # respond_with allows you to respond an action with a given resource. It - # requires that you set your class with a :respond_to method with the + # requires that you set your class with a respond_to method with the # formats allowed: # # class PeopleController < ApplicationController - # respond_to :xml, :json + # respond_to :html, :xml, :json # # def index # @people = Person.find(:all) @@ -210,14 +210,43 @@ module ActionController #:nodoc: # end # end # - # When a request comes with format :xml, the respond_with will first search - # for a template as person/index.xml, if the template is not available, it - # will see if the given resource responds to :to_xml. + # When a request comes, for example with format :xml, three steps happen: + # + # 1) respond_with searches for a template at people/index.xml; + # + # 2) if the template is not available, it will check if the given + # resource responds to :to_xml. + # + # 3) if a :location option was provided, redirect to the location with + # redirect status if a string was given, or render an action if a + # symbol was given. + # + # If all steps fail, a missing template error will be raised. + # + # === Supported options + # + # [status] + # Sets the response status. + # + # [head] + # Tell respond_with to set the content type, status and location header, + # but do not render the object, leaving the response body empty. This + # option only has effect if the resource is being rendered. If a + # template was found, it's going to be rendered anyway. + # + # [location] + # Sets the location header with the given value. It accepts a string, + # representing the location header value, or a symbol representing an + # action name. + # + # === Builtin HTTP verb semantics # - # If neither are available, it will raise an error. + # respond_with holds semantics for each HTTP verb. Depending on the verb + # and the resource status, respond_with will automatically set the options + # above. # - # respond_with holds semantics for each HTTP verb. The example above cover - # GET requests. Let's check a POST request example: + # Above we saw an example for GET requests, where actually no option is + # configured. A create action for POST requests, could be written as: # # def create # @person = Person.new(params[:person]) @@ -225,34 +254,40 @@ module ActionController #:nodoc: # respond_with(@person) # end # - # Since the request is a POST, respond_with will check wether @people - # resource have errors or not. If it has errors, it will render the error - # object with unprocessable entity status (422). + # respond_with will inspect the @person object and check if we have any + # error. If errors are empty, it will add status and location to the options + # hash. Then the create action in case of success, is equivalent to this: # - # If no error was found, it will render the @people resource, with status - # created (201) and location header set to person_url(@people). + # respond_with(@person, :status => :created, :location => @person) # - # If you also want to provide html behavior in the method above, you can - # supply a block to customize it: + # From them on, the lookup happens as described above. Let's suppose a :xml + # request and we don't have a people/create.xml template. But since the + # @person object responds to :to_xml, it will render the newly created + # resource and set status and location. # - # class PeopleController < ApplicationController - # respond_to :html, :xml, :json # Add :html to respond_to definition - # - # def create - # @person = Person.new(params[:pe]) - # - # respond_with(@person) do |format| - # if @person.save - # flash[:notice] = 'Person was successfully created.' - # format.html { redirect_to @person } - # else - # format.html { render :action => "new" } - # end - # end + # However, if the request is :html, a template is not available and @person + # does not respond to :to_html. But since a :location options was provided, + # it will redirect to it. + # + # In case of failures (when the @person could not be saved and errors are + # not empty), respond_with can be expanded as this: + # + # respond_with(@person.errors, :status => :unprocessable_entity, :location => :new) + # + # In other words, respond_with(@person) for POST requests is expanded + # internally into this: + # + # def create + # @person = Person.new(params[:person]) + # + # if @person.save + # respond_with(@person, :status => :created, :location => @person) + # else + # respond_with(@person.errors, :status => :unprocessable_entity, :location => :new) # end # end # - # It works similarly for PUT requests: + # For an update action for PUT requests, we would have: # # def update # @person = Person.find(params[:id]) @@ -260,10 +295,23 @@ module ActionController #:nodoc: # respond_with(@person) # end # - # In case of failures, it works as POST requests, but in success failures - # it just reply status ok (200) to the client. + # Which, in face of success and failure scenarios, can be expanded as: # - # A DELETE request also works in the same way: + # def update + # @person = Person.find(params[:id]) + # @person.update_attributes(params[:person]) + # + # if @person.save + # respond_with(@person, :status => :ok, :location => @person, :head => true) + # else + # respond_with(@person.errors, :status => :unprocessable_entity, :location => :edit) + # end + # end + # + # Notice that in case of success, we just need to reply :ok to the client. + # The option :head ensures that the object is not rendered. + # + # Finally, we have the destroy action with DELETE verb: # # def destroy # @person = Person.find(params[:id]) @@ -271,21 +319,30 @@ module ActionController #:nodoc: # respond_with(@person) # end # - # It just replies with status ok, indicating the record was successfuly - # destroyed. + # Which is expanded as: + # + # def destroy + # @person = Person.find(params[:id]) + # @person.destroy + # respond_with(@person, :status => :ok, :location => @person, :head => true) + # end + # + # In this case, since @person.destroyed? returns true, polymorphic urls will + # redirect to the collection url, instead of the resource url. # def respond_with(resource, options={}, &block) respond_to(&block) rescue ActionView::MissingTemplate => e format = self.formats.first resource = normalize_resource_options_by_verb(resource, options) + action = options.delete(:location) if options[:location].is_a?(Symbol) if resource.respond_to?(:"to_#{format}") - if options.delete(:no_content) - head options - else - render options.merge(format => resource) - end + options.delete(:head) ? head(options) : render(options.merge(format => resource)) + elsif action + render :action => action + elsif options[:location] + redirect_to options[:location] else raise e end @@ -296,16 +353,22 @@ module ActionController #:nodoc: # Change respond with behavior based on the HTTP verb. # def normalize_resource_options_by_verb(resource_or_array, options) - resource = resource_or_array.is_a?(Array) ? resource_or_array.last : resource_or_array - has_errors = resource.respond_to?(:errors) && !resource.errors.empty? + resource = resource_or_array.is_a?(Array) ? resource_or_array.last : resource_or_array - if has_errors && (request.post? || request.put?) - options.reverse_merge!(:status => :unprocessable_entity) + if resource.respond_to?(:errors) && !resource.errors.empty? + options[:status] ||= :unprocessable_entity + options[:location] ||= :new if request.post? + options[:location] ||= :edit if request.put? return resource.errors - elsif request.post? - options.reverse_merge!(:status => :created, :location => resource_or_array) elsif !request.get? - options.reverse_merge!(:status => :ok, :no_content => true) + options[:location] ||= resource_or_array + + if request.post? + options[:status] ||= :created + else + options[:status] ||= :ok + options[:head] = true unless options.key?(:head) + end end return resource diff --git a/actionpack/lib/action_controller/routing/generation/polymorphic_routes.rb b/actionpack/lib/action_controller/routing/generation/polymorphic_routes.rb index 159d869a54..ee44160274 100644 --- a/actionpack/lib/action_controller/routing/generation/polymorphic_routes.rb +++ b/actionpack/lib/action_controller/routing/generation/polymorphic_routes.rb @@ -86,17 +86,16 @@ module ActionController else [ record_or_hash_or_array ] end - inflection = - case - when options[:action].to_s == "new" - args.pop - :singular - when record.respond_to?(:new_record?) && record.new_record? - args.pop - :plural - else - :singular - end + inflection = if options[:action].to_s == "new" + args.pop + :singular + elsif (record.respond_to?(:new_record?) && record.new_record?) || + (record.respond_to?(:destroyed?) && record.destroyed?) + args.pop + :plural + else + :singular + end args.delete_if {|arg| arg.is_a?(Symbol) || arg.is_a?(String)} -- cgit v1.2.3