开发者

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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜