13

Given the routes:

Example::Application.routes.draw do
  concern :commentable do
    resources :comments
  end

  resources :articles, concerns: :commentable

  resources :forums do
    resources :forum_topics, concerns: :commentable
  end
end

And the model:

class Comment < ActiveRecord::Base
  belongs_to :commentable, polymorphic: true
end

When I edit or add a comment, I need to go back to the "commentable" object. I have the following issues, though:

1) The redirect_to in the comments_controller.rb would be different depending on the parent object

2) The references on the views would differ as well

= simple_form_for comment do |form|

Is there a practical way to share views and controllers for this comment resource?

tereško
  • 58,060
  • 25
  • 98
  • 150
amencarini
  • 609
  • 6
  • 20

2 Answers2

19

In Rails 4 you can pass options to concerns. So if you do this:

# routes.rb
concern :commentable do |options|
  resources :comments, options
end


resources :articles do
  concerns :commentable, commentable_type: 'Article'
end

Then when you rake routes, you will see you get a route like

POST /articles/:id/comments, {commentable_type: 'Article'}

That will override anything the request tries to set to keep it secure. Then in your CommentsController:

# comments_controller.rb
class CommentsController < ApplicationController

  before_filter :set_commentable, only: [:index, :create]

  def create
    @comment = Comment.create!(commentable: @commentable)
    respond_with @comment
  end

  private
  def set_commentable
    commentable_id = params["#{params[:commentable_type].underscore}_id"]
    @commentable = params[:commentable_type].constantize.find(commentable_id)
  end

end

One way to test such a controller with rspec is:

require 'rails_helper'

describe CommentsController do

  let(:article) { create(:article) }

  [:article].each do |commentable|

    it "creates comments for #{commentable.to_s.pluralize} " do
      obj = send(commentable)
      options = {}
      options["#{commentable.to_s}_id"] = obj.id
      options["commentable_type".to_sym] = commentable.to_s.camelize
      options[:comment] = attributes_for(:comment)
      post :create, options
      expect(obj.comments).to eq [Comment.all.last]
    end

  end

end
Magne
  • 16,401
  • 10
  • 68
  • 88
chris
  • 6,653
  • 6
  • 41
  • 54
  • 1
    thanks for your answer, it was very helpful! Unfortunately I've found a mistake — in routes.rb you have to use a concern using 'concerns' method, like this: "concerns :commentable, commentable_type: 'Article'". – Alex Ponomarev Feb 22 '15 at 06:48
12

You can find the parent in a before filter like this:

comments_controller.rb

before_filter: find_parent

def find_parent
  params.each do |name, value|
    if name =~ /(.+)_id$/
      @parent = $1.classify.constantize.find(value)
    end
  end
end

Now you can redirect or do whatever you please depending on the parent type.

For example in a view:

= simple_form_for [@parent, comment] do |form|

Or in a controller

comments_controller.rb

redirect_to @parent # redirect to the show page of the commentable.
Arjan
  • 6,264
  • 2
  • 26
  • 42
  • Thanks for the idea; I'll give it a go although I could have more than one parent. For example I'll need `simple_form_for [forum, forum_topic, comment]`; or `redirect_to [forum, forum_topic]`. I'll play with splats and see where it gets me. – amencarini May 26 '13 at 12:25
  • @amencarini You could still do Arjan a favour and accept his answer! – Wukerplank Aug 02 '13 at 16:56