Restful polymorphic

In working on a RESTful website, I’ve encountered a stumbling block
involving polymorphic associations.

For example, given:

map.resources :posts do |post|
post.resources :comments, :name_prefix => ‘post_’
end
map.resources :photos do |photo|
photo.resources :comments, :name_prefix => ‘photo_’
end

Where comments is polymorphic. The acts_as_commentable plugin fits the
bill.

So we have routes like:

/posts/678/comments
/photos/185/comments

If we want to create a new comment for photo 678 we would send a POST to
/posts/678/comments. Nice and RESTful.

But… how do I generate the path for the form target if the comment
form is in a shared partial (DRY)?

I did find one post about this on Rails Weenie and the answer was more
of a workaround… an ugly helper that uses link_to. Is there any way to
do this gracefully, such as *_path?

Ideally:

<% form_for(comment_path(@photo)) %>

…would be smart enough to determine the object type of @photo and
generate the correct /photos/@photo.id/comments route automatically.

Any thoughts? Anyone run into this, and if so how did you solve it?

Thanks!

Funny, moments before you posted this I was struggling with the exact
same thing and was looking at the exact same Techno Weenie article.

I opted to go with a modified version of the before_filter approach like
so. Read below for my observations.

POST /comments

POST /comments.xml

def create
@comment = Comment.new(params[:comment])

@commentable = find_commentable
@commentable.comments << @comment

respond_to do |format|
  if @comment.save
    flash[:notice] = 'Comment was successfully created.'
    format.html { eval("redirect_to 

#{@commentable.class.to_s.underscore}_url(@commentable)") }
format.js # render create.rjs; update comment listing
format.xml { head :created, :location =>
eval("#{@commentable.class.to_s.underscore}_url(@commentable)") }
else
format.html { render :controller =>
@commentable.class.to_s.underscore, :id => @commentable.id, :action =>
:show }
format.js { :render :js => @comment.errors.to_json } # XXX
format.xml { render :xml => @comment.errors.to_xml }
end
end
end

private

def find_commentable
# lazy regex, just finds instances of /controller/id
sections = request.env[‘REQUEST_URI’].scan(%r{/(\w+)/(\d*)})

sections.map! do |controller_name, id|
  [controller_name.singularize.camelize, id]
end

klass, id = sections.pop
eval("#{klass}.find(id)")

end

I’ve got the entire thing working brilliantly as long as it’s a Ajaxed
or if the non-Ajax submission is valid. What I haven’t figured out yet
to do properly is handle invalid non-Ajax submissions and still populate
the input fields.

I see two possible paths:

  1. Perform a redirection after doing some session storage. Implement a
    before_filter that checks for session-stored form submissions and
    errors.

  2. Move all views that are commentable into partials.

Though #2 is “kinda OK”, it remains a bit of a stretch.
Interested to hear your thoughts and any code improvements.

  • Roderick

I’m glad I’m not the only one struggling with this problem! Frankly, I’m
a bit surprised Rails 1.2 is going to be released with open questions
like this.

How are you generating your form destinations in your comment form(s)?

Carl J. wrote:

How are you generating your form destinations in your comment form(s)?

Here’s what I have, let me know if you work up something better.

In news_items/show.rhtml:

<%= render :partial => ‘shared/comment_form’, :locals => {
:commentable => @news_item } %>

shared/_comment_form.rhtml:

New comment

<%= error_messages_for :comment %>

<% remote_form_for(:comment, :url => comments_path(commentable)) do 

|f| -%>


Comment

<%= f.text_area :body %>

  <p>
    <%= submit_tag 'Create' %>
  </p>
<% end -%>
<% remote_form_for(:comment, :url => comments_path(commentable)) do 

|f| -%>

I am not able to simply use comments_path because I have overlapping
routes. Like this:

map.resources :pictures do |picture|
picture.resources :comments, :name_prefix => ‘picture_’

map.resources :posts do |post|
post.resources :comments, :name_prefix => ‘post_’

So in order for my New Comment form to POST to the correct URL I have to
use either picture_comments_path or post_comments_path. In the routing
above the name_prefixes are required, otherwise one overwrites the
other.

It seems like your solution will work for you as long as you have only
one parent defined in the routing for comments, but as soon as you want
to add comments for something like :blog_items, comments_path is not
going to know what to do. Unless I’m missing something?

Carl

You are right. I hadn’t tackled that issue yet. However, it should be
solvable by something (untested and hand-coded) like:

pictures/show.rhtml:

<%= render :partial => ‘shared/comment_form’, :locals => {
:commentable => @picture, :name_prefix => ‘picture_’ } %>

Yeah that’s pretty simple. I’ll probably do the same too… been putting
off converting my comment submission to RESTful so far.

I still think that the _path generators should be able to do a little
magic and determine the correct route for polymorphic targets. Hopefully
that will eventually be built-in. I will try to tackle it myself in the
Rails code but it will take me quite a while, if ever! :slight_smile:

Carl J. wrote:

It seems like your solution will work for you as long as you have only
one parent defined in the routing for comments, but as soon as you want
to add comments for something like :blog_items, comments_path is not
going to know what to do. Unless I’m missing something?

You are right. I hadn’t tackled that issue yet. However, it should be
solvable by something (untested and hand-coded) like:

pictures/show.rhtml:

<%= render :partial => ‘shared/comment_form’, :locals => {
:commentable => @picture, :name_prefix => ‘picture_’ } %>

shared/_comment_form.rhtml:

<% eval(“remote_form_for(:comment, :url =>
#{name_prefix}comments_path(commentable))”) do
|f| -%>

Roderick van Domburg wrote:

You are right. I hadn’t tackled that issue yet. However, it should be
solvable by something (untested and hand-coded) like:

pictures/show.rhtml:

<%= render :partial => ‘shared/comment_form’, :locals => {
:commentable => @picture, :name_prefix => ‘picture_’ } %>

shared/_comment_form.rhtml:

<% eval(“remote_form_for(:comment, :url =>
#{name_prefix}comments_path(commentable))”) do
|f| -%>

FYI, this inspired me to convert to RESTful comment submissions and
here’s what I did. I tried to limit what I pass to each partial since I
already have to pass a bunch of pagination stuff.

show.rhtml:

<%= render(:partial => ‘shared/comments’, :locals => {:comment_pages =>
@comment_pages, :count => @comment_count, :item => @picture})%>

shared/_comments.rhtml:

<% form_for(:comment, :url =>
eval("#{item.class.to_s.downcase}_comments_path(#{item.id})")) do |f| %>

On 1/16/07, Carl J. [email protected] wrote:

<% form_for(:comment, :url =>
eval(“#{item.class.to_s.downcase}_comments_path(#{item.id})”)) do |f| %>

We do a lot of polymorphic stuff like this where I work. Just a
suggestion:

instead of
eval(“#{item.class.to_s.downcase}_comments_path(#{item.id})”)

try
send(“#{item.class.to_s.downcase}_comments_path”, item.id))

Or, wrap it up:
def polymorphic_comments_path(item)
send(“#{item.class.to_s.underscore}_comments_path”, item))
end

If you find yourself using eval, there’s usually another way to do it in
Ruby.


Chris W.

hi,
so far, so good, I’m having the same problems.

say now I can create Comment for an Article, rendering comment form
partial directly from /app/views/articles/show.rhtml

<%= render(:partial => ‘shared/comment_form’, :locals => {:item =>
@article, :name_prefix => ‘article_’})%>

#/app/views/shared/_comment_form.rhtml
<%= error_messages_for :comment %>

<% form_for(:comment, :url => polymorphic_comments_path(item)) do |f| %>

Title
Please be concise.
<%= f.text_field 'title' %>
Comment
Write your comment here.
<%= f.text_area 'comment', :class => 'articleintroduction' %>

<%= submit_tag "Create" %>

<% end %>

this form will generate

which works well.

routes.rb

map.resources :articles do |post|
post.resources :comments, :name_prefix => ‘article_’
end

all done.

controller using simply_commentable

POST /comments

POST /comments.xml

def create
@commentable = find_commentable
@comment = @commentable.comment_by!(current_user,params[:comment] )
respond_to do |format|
if @comment.save
flash[:notice] = ‘Comment was successfully created.’
format.html { eval(“redirect_to
#{@commentable.class.to_s.underscore}_url(@commentable)”) }
format.js # render create.rjs; update comment listing
format.xml { head :created, :location =>
eval("#{@commentable.class.to_s.underscore}_url(@commentable)") }
else
format.html { render :controller =>
@commentable.class.to_s.underscore, :id => @commentable.id, :action =>
:show }
format.js { render :js => @comment.errors.to_json } # to check
format.xml { render :xml => @comment.errors.to_xml }
end
end
end

private
def find_commentable
sections = request.env[‘REQUEST_URI’].scan(%r{/(\w+)/(\d*)})
sections.map! do |controller_name, id|
[controller_name.singularize.camelize, id]
end
klass, id = sections.pop
eval("#{klass}.find(id)")
end

as suggested. I’m posting the entire thing just for reference to other
users.
anyway a question arises, say I want to edit a comment, we should use
the controller Comments, how is the link_to built? with which routes and
params? what should we put in the views/partials?

thanks

Have you made any progress on this? I’ve been searching for the same
answers.

Claudio P. wrote:

hi,
so far, so good, I’m having the same problems.

say now I can create Comment for an Article, rendering comment form
partial directly from /app/views/articles/show.rhtml

…snip…

as suggested. I’m posting the entire thing just for reference to other
users.
anyway a question arises, say I want to edit a comment, we should use
the controller Comments, how is the link_to built? with which routes and
params? what should we put in the views/partials?

thanks