Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rails: URL after validation fails when creating new records via form

Lets say I am creating a new Foo using a form and a standard Rails restful controller, which looks something like this:

class FoosController < ApplicationController
  ...
  def index
    @foos = Foo.all
  end

  def new
    @foo = Foo.new
  end

  def create
    @foo = Foo.create(params[:foo])
    if @foo.save
      redirect_to foos_path, :notice => 'Created a foo.'
    else
      render 'new'
    end
  end
  ...
end

So, if I use the standard restful controller (as above), then when I'm creating the Foo I am at example.com/foos/new, and if I submit the form and it saves correctly I'm at example.com/foos showing the index action. However, if the form is not filled correctly the form is rendered again and error messages are shown. This is all plain vanilla.

However, if errors are shown, the form page will be rendered but the URL will be example.com/foos, because the CREATE action posts to that url. However, one would expect to find Foos#index at example.com/foos, not the form they just submitted now with error messages added.

This seems to be Rails standard behavior, but it doesn't make a lot of sense to me. Obviously I could redirect back to new instead of rendering new from the create action, but the problem with that is the error messages etc. would be lost along with the partially complete Foos in memory.

Is there a clean solution for this problem, a way to send people back to example.com/foos/new when there are errors in the new Foo form they submitted?

Thanks!

like image 713
Andrew Avatar asked Mar 18 '11 04:03

Andrew


4 Answers

To answer your comment on another answer:

I'm wondering if there's a way, without rewriting the controller at all, to tell rails that you want the URL to match the rendered template, rather than the controller action that called it.

I don't think so; URLs are tied directly to routing, which is tied into a controller and action pair--the rendering layer doesn't touch it at all.

To answer your original question, here's information from another similar question I answered.


As you've found, by default when you specify resources :things, the POST path for creating a new thing is at /things. Here's the output for rake routes:

    things GET    /things(.:format)          {:action=>"index", :controller=>"things"}
           POST   /things(.:format)          {:action=>"create", :controller=>"things"}
 new_thing GET    /things/new(.:format)      {:action=>"new", :controller=>"things"}
edit_thing GET    /things/:id/edit(.:format) {:action=>"edit", :controller=>"things"}
     thing GET    /things/:id(.:format)      {:action=>"show", :controller=>"things"}
           PUT    /things/:id(.:format)      {:action=>"update", :controller=>"things"}
           DELETE /things/:id(.:format)      {:action=>"destroy", :controller=>"things"}

It sounds like you want something more like this:

create_things POST   /things/new(.:format)      {:action=>"create", :controller=>"things"}
       things GET    /things(.:format)          {:action=>"index", :controller=>"things"}
    new_thing GET    /things/new(.:format)      {:action=>"new", :controller=>"things"}
   edit_thing GET    /things/:id/edit(.:format) {:action=>"edit", :controller=>"things"}
        thing GET    /things/:id(.:format)      {:action=>"show", :controller=>"things"}
              PUT    /things/:id(.:format)      {:action=>"update", :controller=>"things"}
              DELETE /things/:id(.:format)      {:action=>"destroy", :controller=>"things"}

Although not recommended, you can get this result with the following route:

resources :things, :except => [ :create ] do
  post "create" => "things#create", :as => :create, :path => 'new', :on => :collection
end

You would also need to modify your forms to make them POST to the correct path.

like image 75
Michelle Tilley Avatar answered Nov 13 '22 20:11

Michelle Tilley


You could hook into rails routing by adding this in an initializer: https://gist.github.com/903411

Then just put the regular resources in your routes.rb:

resources :users

It should create the routes and behaviour you are looking for.

like image 35
Marian André Avatar answered Nov 13 '22 22:11

Marian André


You can set up the routing manually, if you're that concerned about what URL is going to show. For what you want, you can have a GET to /foos/new render your form, and a POST to the same URL do the creation:

map.with_options :controller => :foos do |foo|
    foo.new_foo    '/foos/new', :conditions => {:method => :get},  :action => :new
    foo.create_foo '/foos/new', :conditions => {:method => :post}, :action => :create
    foo.foos       '/foos',     :conditions => {:method => :get},  :action => :index
end

This should work without requiring any changes to your controller (yay!) - all three actions from your example are taken care of. The few disclaimers:

  1. This is based on my routing for a 2.3.8 app - some syntax (semantics?) changes are probably required to get it into Rails 3 routing style.
  2. My attempts to mix this style of routing with map.resources have failed horribly - unless you're more familiar with this than me, or Rails 3 routing is better (both easily possible), you'll have to do this for every route to the controller.
  3. And finally, don't forget to add /:id, (.:format), etc. to the routes that need them (none in this example, but see #2).

Hope this helps!

Edit: One last thing - you'll need to hard-code the URL in your form_for helper on /foos/new.html.erb. Just add :url => create_foo_path, so Rails doesn't try to post to /foos, which it will by default (there might be a way to change the creation URL in the model, but I don't know of it, if there is one).

like image 2
Xavier Holt Avatar answered Nov 13 '22 22:11

Xavier Holt


You could use Rack::Flash to store the parameters you wanted in the user's session and then redirect to your form url.

def create
  @foo = Foo.new(params[:foo])
  if @foo.save
    redirect_to foos_path, :notice => 'Created a foo.'
  else
    flash[:foo] = params[:foo]
    flash[:errors] = @foo.errors
    redirect_to new_foo_path #sorry - can't remember the Rails convention for this route
  end
end

def new
  # in your view, output the contents of flash[:foo]
  @foo = Foo.new(flash[:foo])
end
like image 1
stef Avatar answered Nov 13 '22 22:11

stef