John Topley
John Topley

Reputation: 115342

Use custom route upon model validation failure

I've just added a contact form to my Rails application so that site visitors can send me a message. The application has a Message resource and I've defined this custom route to make the URL nicer and more obvious:

map.contact '/contact', :controller => 'messages', :action => 'new'

How can I keep the URL as /contact when the model fails validation? At the moment the URL changes to /messages upon validation failure.

This is the create method in my messages_controller:

def create
  @message = Message.new(params[:message])

  if @message.save
    flash[:notice] = 'Thanks for your message etc...'
    redirect_to contact_path
  else
    render 'new', :layout => 'contact'
  end
end

Thanks in advance.

Upvotes: 4

Views: 2027

Answers (4)

Pieter Jongsma
Pieter Jongsma

Reputation: 3373

One solution would be to make two conditional routes with the following code:

map.contact 'contact', :controller => 'messages', :action => 'new', :conditions => { :method => :get }
map.connect 'contact', :controller => 'messages', :action => 'create', :conditions => { :method => :post } # Notice we are using 'connect' here, not 'contact'! See bottom of answer for explanation

This will make all get request (direct requests etc.) use the 'new' action, and the post request the 'create' action. (There are two other types of requests: put and delete, but these are irrelevant here.)

Now, in the form where you are creating the message object change

<%= form_for @message do |f| %>

to

<%= form_for @message, :url => contact_url do |f| %>

(The form helper will automatically choose the post request type, because that is default when creating new objects.)

Should solve your troubles.

(This also won't cause the addressbar to flicker the other address. It never uses another address.)

.

  • Explanation why using connect is not a problem here The map.name_of_route references JUST THE PATH. Therefore you don't need a new named route for the second route. You can use the original one, because the paths are the same. All the other options are used only when a new request reaches rails and it needs to know where to send it.

.

EDIT

If you think the extra routes make a bit of a mess (especially when you use it more often) you could create a special method to create them. This method isn't very beautiful (terrible variable names), but it should do the job.

def map.connect_different_actions_to_same_path(path, controller, request_types_with_actions) # Should really change the name...
  first = true # There first route should be a named route
  request_types_with_actions.each do |request, action|
    route_name = first ? path : 'connect'
    eval("map.#{route_name} '#{path}', :controller => '#{controller}', :action => '#{action}', :conditions => { :method => :#{request.to_s} }")
    first = false
  end
end

And then use it like this

map.connect_different_actions_to_same_path('contact', 'messages', {:get => 'new', :post => 'create'})

I prefer the original method though...

Upvotes: 8

Pieter Jongsma
Pieter Jongsma

Reputation: 3373

I just came up with a second solution, guided by Omar's comments on my first one.

If you write this as your resources route

map.resources :messages, :as => 'contact'

This gives (amongst others) the following routes

/contact # + GET = controller:messages action:index
/contact # + POST = controller:messages action:create

So when you move your 'new' action code into your 'index' action, you will have the same result. No flicker and an easier to read routes file. However, your controller will make no more sense.

I, however, think it is a worse solution because you'll soon forget why you put your 'new' code into the index action.

Btw. If you want to keep a kind of index action, you could do this

map.resources :messages, :as => 'contact', :collection => { :manage => :get }

This will give you the following route

manage_messages_path # = /contact/manage controller:messages action:manage

You could then move your index action code into the manage action.

Upvotes: 1

Omar Qureshi
Omar Qureshi

Reputation: 9093

Not to my knowledge, no. Since im assuming you want to render so that you keep the @message object as is with the errors attached.

There is a horrible solution that I have which will let you do it, but, its so horrible, I wouldn't recommend it:

before_filter :find_message_in_session, :only => [:new]

def new
  @message ||= Message.new
end

def create
  @message = Message.new(params[:message])
  if @message.save
    flash[:notice] = 'Thanks for your message etc...'
    redirect_to contact_path
  else
    flash[:notice] = 'Sorry there was a problem with your message'
    store_message_in_session
    redirect_to contact_path
  end
end

private

def find_message_in_session
  @message = session[:message]; session[:message] = nil
end

def store_message_in_session
  session[:message] = @message
end

Upvotes: 0

DanSingerman
DanSingerman

Reputation: 36502

I suspect you are posting to '/messages' from the form which creates the message which explains why you see that in your URL.

Any reason why this won't work:

def create
  @message = Message.new(params[:message])

  if @message.save
    flash[:notice] = 'Thanks for your message etc...'
    redirect_to contact_path
  else
     flash[:notice] = 'Sorry there was a problem with your message'
    redirect_to contact_path
  end
end

Upvotes: 0

Related Questions