views:

138

answers:

1

I've created an app that has several models (say A, B) that are polymorphically associated with a Comment model. When one views a page associated with A controller, show action, comments associated with the A object are displayed as is a form to create a new object. All of this works and is similar to Ryan Bates' 15 minute blog posted on the rails website. However, if I add validation to ensure that the user does not submit a blank comment I'm unsure how to render that. Here's what I have in my Comments controller:

before_filter :load_resources, :only => [:create]
def create
  if @comment.save
    redirect_to @back
  else
    render @action
  end
end

private

def load_resources
  @comment = Comment.new(params[:comment])
  case @comment.commentable_type
  when 'A'
    @a = A.find(params[:a_id]
    @comments = @a.comments
    @back = a_url(@comment.commentable_id)
    @resource = @a
    @action = 'as/show'
  when 'B'
    ...
  end
end

View partial for comments and form (using Haml):

=render :partial => 'comments/comment', :collection => @comments

%h3 Leave a comment:
-form_for [@resource, Comment.new] do |f|
  =f.error_messages
  =f.hidden_field :commentable_type, :value => params[:controller].singularize.titleize
  =f.hidden_field :commentable_id, :value => params[:id]
  =f.hidden_field :editor_id, :value  => @current_user.id
  =f.hidden_field :creator_id, :value => @current_user.id
%fieldset
  =f.label :subject, 'Subject', :class => 'block'
  =f.text_field :subject, :class => 'block'
  =f.label :text, 'Comment', :class => 'block'
  =f.text_area :text, :class => 'block'
  .clear_thick
 =f.submit 'Submit', :id => 'submit'

What I can seem to figure out is how to deal with validation errors. When validation errors are triggered it doesn't seem to trigger the f.error_messages. Furthermore, when render is triggered it will take the user to a page with the following url: a/2/comments, when I'd like it to render a/2.

newest solution:

def create
  subject = ""
  if [email protected]
    subject = "?subject=#{@comment.subject}"
  end
  redirect_to @back + subject
end

Then in the A controller show action:

if params.has_key?('subject')
  @comment = Comment.create(:subject => params[:subject])
else
  @comment = Comment.new
end

This works, but feels kind of ugly...

+2  A: 

It's kind of difficult to wrap your head around it because you don't know what kind of object you're going to be receiving in the comments controller.

It's much simpler when it's not a polymorphic relationship. Before we understand how to do that, we need to understand the best way to do the singular version.

I should note that this assumes that you have your resources/routes defined correctly:

map.resources :posts, :has_many => [ :comments ] map.resources :pages, :has_many => [ :comments ]

Let's say that we have the simple example of a Post has many Comments. Here is a sample way of doing this:

class CommentsController < ApplicationController
  before_filter => :fetch_post 

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

    if @comment.save
      success_message_here
      redirect post_path(@post)
    else
      error_message_here
      redirect_to post_path(@post)
    end
  end

  protected
    def fetch_post
      @post = Post.find(params[:post_id])
    end
end

Now we want to use this in a polymorphic relationship, so we have to setup a couple of things. Let's say that we have Pages and Posts that have comments now. Here is a sample way of doing this:

From you Posts and Pages show pages:

<%= render 'comments/new' %>

In the posts controller:

before_filter :fetch_post

    def show
      @comment = @commentable.comments.build
    end

    protected
      def fetch_post
        @post = @commentable = Post.find(params[:id])
      end

This sets up your form to be simple: <% error_messsages_for :comment %>

<% form_for [ @commentable, @comment ] do |f| %>
  #Your form fields here (DO NOT include commentable_type and or commentable_id also don't include editor and creator id's here either. They will created in the controller.)
<% end %>

In your comments controller:

def create
  @commentable = find_commentable
  # Not sure what the relationship between the base parent and the creator and editor are so I'm going to merge in params in a hacky way
  @comment = @commentable.comments.build(params[:comment]).merge({:creator => current_user, :editor => current_user})

  if @comment.save
    success message here
    redirect_to url_for(@commentable)
  else
    failure message here
    render :controller => @commentable.class.downcase.pluralize, :action => :show
  end
end

  protected
    def find_commentable
      params.each do |name, value|
        if name =~ /(.+)_id$/
          return $1.classify.constantize.find(value)
        end
      end
      nil
    end
Carlos
Very nice solution. Out of curiosity, why not include creator and editor_id in the form directly? It seems to me that it is an easy way to reduce complexity in the controller. With that said, should views be cleaner than controllers?
LDK
LDK, The general rule is that you shouldn't trust user input. Even with hidden fields, a malicious user could inject whatever IDs they wanted into the form and then pass that along and therefore impersonate another user.By removing that option, you're making the app more secure.
Carlos