Rails 3 - Displaying submit errors on polymorphic comment model
Fairly new to Rails 3 and have been Googling every which way to no avail to solve the following problem, with most tutorials stopping short of handling errors.
I have created a Rails 3 project with multiple content types/models, such as Articles, Blogs, etc. Each content type has comments, all stored in a single Comments table as a nested resource and with polymorphic associations. There is only one action for comments, the 'create' action, because there is no need for the show, etc as it belongs to the parent content type and should simply redisplay that page on submit.
Now I have most of this working and comments submit and post just fine, but the last remaining issue is displaying errors when the user doesn't fill out a required field. If the fields aren't filled out, it should return to the parent page and display validation errors like Rails typically does with an MVC.
The create action of my Comments controller looks like this, and this is what I first tried...
def create
@commentable = find_commentable
@comment = @commentable.comments.build(params[:comment])
respond_to do |format|
if @comment.save
format.html { redirect_to(@commentable, :notice => 'Comment was successfully created.') }
else
format.html { redirect_to @commentable }
format.xml { render :xml => @commentable.errors, :status => :unprocessable_entity }
end
end
end
When you fill nothing out and submit the comments form, the page does redirect back to it's appropriate parent, but no flash or nothing is displayed. Now I figured out why, from what I understand, the flash won't persist on a redirect_to, only on a render. Now here's where the trouble lies.
There is only the 'create' action in the comment controller, so I needed to point the render towards 'blogs/show' (NOTE: I know this isn't polymorphic, but once I get thi开发者_如何学编程s working I'll worry about that then). I tried this in the "else" block of the above code...
else
format.html { render 'blogs/show' }
format.xml { render :xml => @commentable.errors, :status => :unprocessable_entity }
end
Anyway, when I try to submit an invalid comment on a blog, I get an error message saying "Showing [...]/app/views/blogs/show.html.erb where line #1 raised: undefined method `title' for nil:NilClass."
Looking at the URL, I think I know why...instead of directing to /blogs/the-title-of-my-article (I'm using friendly_id), it's going to /blogs/the-title-of-my-article/comments. I figure that extra "comments" is throwing the query off and returning it nil.
So how can I get the page to render without throwing that extra 'comments' on there? Or is there a better way to go about this issue?
Not sure if it matters or helps, but the route.rb for comments / blogs looks like this...
resources :blogs, :only => [:show] do
resources :comments, :only => [:create]
end
I've been plugging away at this over the last few weeks and I think I've finally pulled it off, errors/proper direction on render, filled out fields remain filled in and all. I did consider AJAX, however I would prefer to do it with graceful degradation if at all possible.
In addition, I admit I had to go about this a very hacky-sack way, including pulling in a way to pluralize the parent model to render the appropriate content type's show action, and at this stage I need the code to simply work, not necessarily look pretty doing it.
I KNOW it can be refactored way better, and I hope to do so as I get better with Rails. Or, anyone else who thinks they can improve this is welcomed to have at it. Anyway, here is all my code, just wanted to share back and hope this helps someone in the same scenario.
comments_controller.rb
class CommentsController < ApplicationController
# this include will bring all the Text Helper methods into your Controller
include ActionView::Helpers::TextHelper
def create
@commentable = find_commentable
@comment = @commentable.comments.build(params[:comment])
respond_to do |format|
if @comment.save
format.html { redirect_to(@commentable, :notice => 'Comment was successfully created.') }
else
# Transform class of commentable into pluralized content type
content_type = find_commentable.class.to_s.downcase.pluralize
# Choose appropriate instance variable based on @commentable, rendered page won't work without it
if content_type == 'blogs'
@blog = @commentable
elsif content_type == 'articles'
@article = @commentable
end
format.html { render "#{content_type}/show" }
format.xml { render :xml => @commentable.errors, :status => :unprocessable_entity }
end
end
end
private
# Gets the ID/type of parent model, see Comment#create in controller
def find_commentable
params.each do |name, value|
if name =~ /(.+)_id$/
return $1.classify.constantize.find(value)
end
end
end
end
articles_controller.rb
class ArticlesController < ApplicationController
def show
@article = Article.where(:status => 1).find_by_cached_slug(params[:id])
@comment = Comment.new
# On another content type like blogs_controller.rb, replace with appropriate instance variable
@content = @article
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => @article }
end
end
end
show.html.erb for articles (change appropriate variables for blog or whatever)
<h1><%= @article.title %></h1>
<%= @article.body.html_safe %>
<%= render :partial => 'shared/comments', :locals => { :commentable => @article } %>
shared/_comments.html.erb (I'm leaving out the displaying of posted comments here for simplification, just showing the form to submit them)
<%= form_for([commentable, @comment]) do |f| %>
<h3>Post a new comment</h3>
<%= render :partial => 'shared/errors', :locals => { :content => @comment } %>
<div class="field">
<%= f.label :name, :value => params[:name] %>
<%= f.text_field :name, :class => 'textfield' %>
</div>
<div class="field">
<%= f.label :mail, :value => params[:mail] %>
<%= f.text_field :mail, :class => 'textfield' %>
</div>
<div class="field">
<%= f.text_area :body, :rows => 10, :class => 'textarea full', :value => params[:body] %>
</div>
<%= f.submit :class => 'button blue' %>
<% end %>
shared/_errors.html.erb (I refactored this as a partial to reuse for articles, blogs, comments, etc, but this is just a standard error code)
<% if content.errors.any? %>
<div class="flash error">
<p><strong><%= pluralize(content.errors.count, "error") %> prohibited this page from being saved:</strong></p>
<ul>
<% content.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
I slightly refactored @Shannon answer to make it more dynamic. In my 'find_parent' method I'm grabbing the url path and fetching the controller name. In the 'create' method I'm creating an 'instance_variable_set' which creates a dynamic variable for either Articles (@article) or Blogs (@blog) or what ever it may be.
Hopefully you'll like what I've done? Please let me know if you have any doubts or if something can be improved?
def create
@comment = @commentable.comments.new(params[:comment])
if @comment.save
redirect_to @commentable, notice: "Comment created."
else
content_type = find_parent
instance_variable_set "@#{content_type.singularize}".to_sym, @commentable
@comments = @commentable.comments
render "#{content_type}/show"
end
end
def find_parent
resource = request.path.split('/')[1]
return resource.downcase
end
You're getting an error because the blogs/show
view likely refers to the @blog
object, which isn't present when you render it in the comments controller.
You should go back to using the redirect_to
rather than render. It wasn't displaying a flash when you made an invalid comment because you weren't telling it to set a flash if the comment wasn't saved. A flash will persist till the next request.
精彩评论