9

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.

Paweł Gościcki
  • 9,066
  • 5
  • 70
  • 81

4 Answers4

8

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
  • 31,028
  • 18
  • 69
  • 91
  • 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 Feb 18 '10 at 14:37
  • 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 Feb 18 '10 at 16:50
  • 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 Feb 18 '10 at 20:20
  • 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 Feb 18 '10 at 22:14
  • `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 Feb 19 '10 at 00:01
7

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
  • 5,945
  • 5
  • 24
  • 29
  • 1
    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 Feb 24 '10 at 16:42
  • Cool hack. You could also simplify the first line to `options[:year].class == Post`. – mahemoff Jan 14 '14 at 13:30
3

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
  • 26,597
  • 10
  • 74
  • 130
  • 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 Feb 18 '10 at 14:49
0

I was trying out TempoDB and extending their class with the below worked for me. I had the route "resources :series" and then could use url_for with no problem.

class TempoDB::Series
  def self.model_name
    ActiveModel::Name.new(TempoDB::Series,nil,'series')
  end
  def to_param
    self.key
  end
end
Chris Hobbs
  • 495
  • 6
  • 5