views:

687

answers:

3

Given the following resource definition:

map.resources :posts, :except => [:show]
map.post '/:year/:month/:slug, :controller => :posts, :action => :show

I can make url_for work for me, using this syntax:

<%= link_to @post.title, post_url(:year => '2010', :month => '02', :slug => 'test') %>

But is there a way to make this work?

<%= link_to @post.title, @post %>

Currently it throws this error:

No route matches {:year=>#<Post id: 1, title: "test", (...)>, :controller=>"posts", :action=>"show"}

Apparently it passes the @post object to the first route parameter (seems like a Rails bug...). But can I make this work for me? I'll add that messing with default_url_options is a dead end.

Solution working only in Rails 3.x is ok, but I would prefer not to use any plugins.

+2  A: 

Unfortunately, when you pass an ActiveRecord to link_to, it tries to use Polymorphic URL helpers to generate the URL for the link.

The Polymorphic URL helpers are only designed to generate RESTful URLs by using the record identifier of your ActiveRecord.

Since your route uses multiple attributes of the Post object, the Polymorphic URL helpers are not equipped to generate the correct URL... not so much a bug as a limitation :)

Delving into link_to, when you pass it a Hash, it doesn't use Polymorphic Routing, so you avoid the whole problem.

I suppose a hacky approach would be to define a method on Post called routing_hash which returns

(:year => post.year, :month => post.month, :slug => post.slug)

I appreciate that it's not a DRY approach, but it's the best I can come up with at the moment

Dancrumb
That's what I thought. That this is just a limitation of RESTful routing. Defining `routing_hash` on Post model doesn't help. Rails routing is not using it. I know I can use it like this: `<%= link_to @post.tile, @post_path(routing_hash) %>` but I'd like to be able to use just `url_for(@post)` method.
Paweł Gościcki
A: 

Override to_param. The default value for to_param is the record ID, but you can change that.

class Post
  def to_param
    "/#{year}/#{month}/#{slug}"
  end
end

(This assumes you have methods in the Post class for year, month and slug.)

Now when URLs are generated, your overridden to_param will be called.

<%= link_to @post.title, @post %> 
    => <a href="/2010/02/foobar">Foobar</a>

Obviously you must ensure your PostsController show action can find the record based on the parameters it is passed.

You may need to change your route definition and eliminate this route:

map.post '/:year/:month/:slug, :controller => :posts, :action => :show

However, with to_param overridden, the default RESTful post_path will work just fine for the URLs you want to generate.

Luke Francl
It almost works. Unfortunately it has a serious problem. In Rails3 the generated link is url_encoded. So links are generated like this: `url_for(@post) => "http://localhost:3000/posts/%2F2010%2F02%2Ffirst"`That's not exactly what I want. Btw: your `to_param` method should omit the first `/`.
Paweł Gościcki
Ah, bummer. Well, `to_param` is the usual way to accomplish custom paths like this but I guess the slashes are throwing it off. How about a helper method that returns the path you want? Then you could do something like this: `<%= link_to @post.title, post_url_helper(@post) %>`. Not quite as clean but it would work.
Luke Francl
The way I do it now is actually similar to your solution. I have a custom path method: `def path; "/#{year}/#{month}/#{slug}"; end` and I use it like this: `<%= link_to @post.title, @post.path %>` and it works. But @post variable unfortunately is not a resource in this case, so I cannot use all the other magic stuff that sits behind the RESTful routes.
Paweł Gościcki
What magic do you feel you're missing out on? Based on your code in the question, you can use RESTful routes for everything but the show URL...
Luke Francl
`atom_feed`, for one, requires you to pass a @post variable (in `feed_entry(@post)`, which should respond nicely to `url_for(@post)`. I presume the Rails 3 addition of `respond_with(@user)` will not work either.
Paweł Gościcki
+1  A: 

How about fixing url_for for the PostsController? May not be very elegant but it is DRY and things should just work.

# app/controllers/posts_controller.rb 
class PostsController < ApplicationController

  protected

    def url_for(options = {} )
      if options[:year].class.to_s == "Post"
        obj = options[:year]
        options[:year] = obj.year
        options[:month] = obj.month
        options[:slug] = obj.slug
      end
      super(options)
    end

end
anshul
Works! Very, very clever. Surely it's still just a hack, but so far everything works as expected. Both in Rails 2.3.5 and 3.0.0. I'm impressed. Btw: I've moved the `url_for` to the `application_controller` since I'm using `Post` model not only in the `Posts` controller but also in the `Site` controller.
Paweł Gościcki