Skip to main content

68 Comments With Polymorphic Associations

Episode 36 · December 19, 2014

Learn how to set up polymorphic associations, add comments to your app, and think about the structure of your Rails application

ActiveRecord


Transcripts

In this episode, we're going to talk about polymorphic associations and what they are. I've got a movie database with films and actors, and I want to add comments on both. When you click on a movie, you should be able to see comments down here and when you click on actors, you should be able to see comments underneath their profile as well.

If we were to do this normally, we would probably add comments and then we would have a Film ID, and that would be an integer and then we'd have an actor ID and that would be an integer, and we'd have a belongs_to :film and belongs_to :actor and so on, but that's not actually how we want to do this. We want to use this comment model a little bit more generically so we don't have to specify each and every individual association between the two, and luckily for us, Rails provides polymorphic associations, that allow you to create a generic association like in this case imageable and when you belongs_to :imageable you can add polymorphic: true and that lets you say that employees can have many pictures and products can have many pictures and they share the same association.

We can make comments, also polymorphic and we'll just make our things, that we want to comment on commentable. So let's go and do that here

rails g model Comment commentable_type:string commentable_id:integer

Rails is going to store the string "film" or "actor" in the commentable_type and to know which record it's going to be associated with, it's going to use the commentable_id.

We will combine the two to join the association and look up the record, and it will also assign both of those when you assign the commentable to the comment.

We'll also add a regular user_id and our body which will be text and we run

rake db:migrate

to add these to our app. in the film and in the actor model, we add:

app/models/actor.rb
app/models/film.rb
has_many :comments, as: :commentable

and in:

app/models/comment.rb
belongs_to :commentable, polymorphic: true

This is going to allow us to assign films or objects like that to commentable on a comment. In the rails console, we look up the comments of the first film with

Film.first.comments

and you'll see that whenit queries for it, it plugs in the commentable_id is one, and then film is the type searched against the commentable type. Rails automatically handles filling out these two columns instead of the single ID like it normally does. Here we can just say, if we want to create a comment inside the controller or the console here, we can do it through the association or we can do it directly through building a new comment where the commentable object is the first film and we're going to leave the user_id blank for now and the body will be "testing".

When we create that, you can see that it does the same thing, it inserts the commentable attributes there, and now when we grab the first film's comments, we receive this comment back. So it's properly associated and this is the way that you probably want to test most of your associations in the console because it's a lot less work to make sure they're correct than trying to do that in your Rails app immediately.

If we go into our films, and to the show view, we can add:

app/views/films/show.html.erb

<h3>Comments</h3>
<% @film.comments.each do |comment| %>
    <div class="well">
        <%= comment.body %>
    </div>
<% end %>

Once we refresh our movie database page for 'The Matrix', we can see that there's the comment there. Now we have a working polymorphic association and we can go do the same thing in our console to create a comment for the first actor. This past time, I've created the comment in the console and assigning commentable, and if we do it the other way around, with Actor.first.comments.create(), this will automatically set the commentable_type for us and we just need to say body: "Whatever". It does the same thing, and sets the body but then it also set the commentable type to actor and the id to 1 because it's the first actor.

We can refesh this page and we don't get to see the comments but we want to have the same generic comments section there.

We can pull the bit of code on top into a comment's partial and we'll render the partial and add in the film show page

<%= render partial: "comments/comments", locals: {commentable: @film} %>

We will also add this last code to our actors show page passing in @actor as the commentable object instead of the film

We'll create the app/views/comments directory and inside of there we will create a comments partial that will be used for the commentable objects. We will render the exact same view for every different type. The reason why we passed in the commentable local variable into this partial is because we can take @film and pass that in and use commentable here, and then that means we can take the same render partial and go into the actors show action and paste that in but this time we pass in actor as the local variable instead and we never have to change our partial for the comments and it will properly work. We are going to use the exact same partial, we'll pass in the different commentable object, it will get that object's comments and display them. So we have the exact same views, same code. It's reused in both places and it's really really flexible. That is basically the simplest way that you can get comments shared across multiple things, so we can add users, if users have a profile of their favorite movies or whatever, we could add comments to that or lists or anything like that. If you even wanted individual pictures to have comments you could do that as well and all you would have to do is to have this line and replace the object here that's commentable and this partial will magically work for you.

Now you're probably thinking that's cool and all but how do I make these connected put it in the controllers and all of that, there's a lot of work left to do, right?

The way that I designed it, it actually turns out that it's not too much work to implement controllers to handle this. So going to our actors/show action, what if we just render a partial:

`<%= render partial: "comments/form", locals: {commentable: @actor} %>

We will also have commentable as our local variable here and we save it, and we go to films/show and do the same thing (passing the film object), this way we can render a form that is for a commentable object. That way we can use the same form and reuse it between the two and then hopefully we'll be able to design our routes to separate the two out and share all that code magically between them.

Now that we have this, we actually need to go create that comments form. I’m going to add the

app/views/comments/_form.html.erb

<%= form_for [commentable, Comment.new] do |f| %>
<% end %> 

We use the square bracket in the form_for to create an array and pass in the commentable and the new comment that we want to create. This is going to generate an interesting form for us, what it's going to do is to make a post request when commentable is a film, it's going to go to /film/1/comments but if you have an actor passed in, it's going to go to /actor/1/comments, so this is nifty, it's going to pass in to two separate controllers, those controllers can set the commentable first and then we can have a generic CREATE action that will just add the new comment to that commentable. This is an interesting way of structuring your controller, and you don't have to do that, you can use the generic comments controller, and then that could have the comment's for that and it could be just generic and you could add the comment.new and then in a hidden field:

<%= f.hidden_field :commentable_type, value: "Film" %>

You could do that if you wanted, but the way that I want to design this, I think you'll find interesting separating out your controllers:

app/views/comments/_form.html.erb

<%= form_for [commentable, Comment.new] do |f| %>
    <div class="form-group">
        <%= f.text_area :body, class: "form-control", placeholder: "Add a comment" %>
    </div>
    <%= f.submit class: "btn btn-primary" %> 
<% end %> 

Since we have our routes, we have our resources for actors and films and the typical way you'll see a lot of people do this is add resources for comments. This is going to have to deal with all the different things of what is the commentable thing and do we redirect to different locations afterwards and all of that.

I don't want to deal with that and I actually would like our code in the file structure to represent that actors have comments and films have comments and have that run through our application. What we're going to do, is that we're going to add a resources :comments to the resources :actors as a nested route, and that will be what will wire up the form that we just created, and then we're going to add the module as actors, so this is going to go to the actors/comments controller and we'll do the same thing for films

config/routes

resources :actors do
    resources :comments, module: actors
end

resources :films do
    resources :comments, module :films
end

Before creating these individual actors comments comments controller and the films comments controller, let's actually create a generic one in our

app/controllers/comments_controller.rb

class CommentsController < ApplicationController
    before_action :authenticate_user!

    def create
        @comment = @commentable.comments.new comment_params
        @comment.user = current_user
        @comment.save
            redirect_to @commentable, notice: "Your comment was successfully posted."
    end

    private

        def comment_params
            params.require(:comment).permit(:body)
        end
end

When we write the create action, we set up the user manually to the current user and then we'll save the comment and redirect them to @commentable.

The private method for comment_params will just need to permit the body because the commentable type will be set beforehand and if you were to do this in a generic controller and accept the hidden fields, you could accept the commentable type and ID.

Because we're setting it up this way, we've set up this generic controller but never did we set commentable, and the reason for that is because now we can go to the comments/controllers that are specific to films and actors. We can start creating this if we add:

app/controllers/films/comments_controller.rb

class Films::CommentsController < CommentsController
    before_action :set_commentable

    private 
        def set_commentable
            @commentable = Film.find(params[:film_id])
        end
end

since the module is films, we have to put the module and the class here, and it's going to be the CommentsController, but instead of inheriting from application controller we can inherit from comments controller, doing this will allow us to inherit this create and comment_params methods, and then we can set our before action to :set_commentable, we can define a private set_commebtable and this will automatically set the commentable object to the film before the create action gets called, which is inherited, and all of this flows now; and this form_submit will talk to the films::CommentsController which then sets the commentable object, and then it goes to the CommentsController for the create action, creates a new one, sets the user, saves it, and then redirects you back. This doesn't really have much in it, but all of the stuff that's very specific to film comments, such as setting the commentable film is there, and it's very easy for anybody to jump in and understand that, and they can see that really there's not much custom logic for film comments, and if there ever becomes that, we're already sort of ready.

Devise takes a very similar approach in the way that you override their controllers if you've ever dove into that, you can override just the actions and the pieces of functionality that you need to override, and I think that's very useful for making a very clean controller like this.

Now we can edit the actors controller and do the same thing for their comments

app/controllers/actors/comments_controller.rb

We can take the same code that we did for films and replace the instances of Films for Actors.

Both of these controllers now have a little bit of duplication but in a sense, it's a good piece to duplicate because if you start to move this out so that it's a little bit more flexible, one of the problems with creating those hidden fields and permitting content on commentable_type and ID, is that anybody can submit in other data. This is not going to be automatically handled by Rails and protected, unless you do it this way with the specific URL's for it. That's a good reason to do this, you can also update these find actions to add in Can-Can or Pundit so that a user can only comment on them that are publishable or the ones that they're allowed to do. You can add in permissions on a model by model benefit basis on the comments section. So you may or may not need to do that, but you're already kind of set up and ready to go for it. Let's check this out in our application.

When I refresh the Matrix page, we've got our new "Add a comment" here, and the moment of truth. We'll create a comment and the comment was created.

That is all functional, and you know that it's posting to the right film because it showed up there, but you can verify that by scrolling down into our logs and see the Started POST "/films/1/comments", and that it was processed by the Films::CommentsController#create, and it inserted into the comments table, which means that it had to have gone into the generic comments controller. That has properly worked, and we can test it on actors as well.

If we create a comment on Keanu Reeves, we should see that it saved here as well, and there's no way for us to edit this html and pass in a hidden field that comments in here but let's actually send it somewhere else. If anyone is trying to do some malicious things to your comments, it's already set up to handle those things appropriately as it should.

One thing that I don't think a lot of people put much attention in, is viewing your Rails application as a design piece of your architecture of your app. The way this works is that we have to duplicate those routes each time, and have the comments controller duplicate it each time, but that's not a bad thing. If you look at our controller’s folder, we have actors, we have comments and we have films that gives us a higher level overview of it but now we can dive in to just actors and we can see the actors have comments.

So if we're just browsing these folders in our Rails application we can actually get an idea of what functionality belongs to films and what belongs to actors just by looking at the files that exist and where they exist. If you didn't do that, then you wouldn't have these folders and you wouldn't have the comments controllers inside of them. You just have comments controller here, and if you glanced at that: Imagine the films and actors folders don't exist and you would see that you have actors, comments and films, but you don't really know where the comments belong to. This structure allows us to visualize that just from looking at the Rails app. I think that's a very important piece of functionality and that's why Rails is designed with the assets, controllers, helpers, mailers, models and views.

This is an important piece of your application, for someone new to jump in, or for you in six months to return to the app and really just in a moment's notice remember everything that it did.

That's something to definitely take into account when you're designing the way that your applications work. Same thing goes with views (you can see that there's comments), but if you go into films, you don't actually know that there are comments there. That's the example, or the counterexample to doing this. And because you want these views to be generic, you want to actually set this up, and you don't need to copy the comments partial into each of the actors and films folders because that's quite a bit of stuff.

These are organized in a different manner than the actual Rails application itself, so internally in the views folder you want stuff just specific to actors to be in there, just specific to comments to be its own thing.

I hope that's useful, it's definitely an interesting level of looking at an applications design that most people don't go into, so I hope that was helpful for you, and there's a billion different ways to do this, so by all means, take this with a grain of salt, see if you like the structure or not, and as your applications grows determine if this is useful for you or not.

Transcript written by Miguel

Discussion


Gravatar
Neeraj Kumar (30 XP) on

Awesome Video. I really liked and refreshed my concepts about Polymorphic Associations. Thanks :)


Gravatar
Dan Tappin (860 XP) on

Here is a Pro episode suggestion - take this and add nested comments and some AJAX to it :)

Gravatar
Damien Hogan (120 XP) on

Id be sure to subscribe for that. Not found a decent example of nested before.

Gravatar
Dan Tappin (860 XP) on

It took me forever to build but I did it from scratch a few years back on a project that I still have yet to finish. I works slick but I am sure it could be done better than the way I did it. I used the ancestry gem and a few Railscasts episodes to cobble it together.

Gravatar
Saint on

Maybe you should make a video on this. Some of us are still struggling through it.

Gravatar
Chris Oliver (167,500 XP) on

I'm more than a little behind, but I added this to my short list!

Gravatar
Gravatar
Gravatar
John Athayde (920 XP) on
+1 on the suggestion. Daniel's link is helpful, but I'm getting caught up on the polymorphic_url generation to use the _comment partial as a shared file between mutliple models. 

Gravatar
Nick Chernyshev on

Polymorphism is the main anti-pattern in rails(


Gravatar
Jonathan Denney on

How would you suggest getting all the comments a user's posts collectively have? For example @user.comments.all throws the error
ActiveRecord::HasManyThroughAssociationNotFoundError at / Could not find the association :commentable in model User


Gravatar
Kaspar on

Is this something to consider in a social networking site? If I would have say comments polymorphically associated with various models, wouldn't it be a major hit on one table all the time?

Gravatar
Chris Oliver (167,500 XP) on

Since they are reads, I think that so long as your database server can handle it, it doesn't matter how many connections read from the same table. Reads don't lock the table/row like writes do since a read cannot cause data loss. You shouldn't experience any loss of speed if many things are reading the same table.


Gravatar
Chris Habgood (40 XP) on

The problem with polymorphic models and tables is there is no way to keep the database from becoming corrupt due to no FK constraints.


Gravatar
wbs on

I would suggest that you please either put in minimal security on these screen casts or at least mention that the implementation is very insecure and to check out the rails security guidelines. I know the case can be made where security is out of scope for this discussion but a, at one point you reference how this method is "more secure" when talking about the hidden form field and b, you leave xss/injection etc all wide open so that body may be used to by malicious users to extend reach.

Gravatar
Jay Killeen (1,580 XP) on

Can you please elaborate on this part `you leave xss/injection etc all wide open so that body may be used to by malicious users to extend reach`? I'd like to understand the security risk a little more.


Gravatar
Martin Ferretti (100 XP) on

How would this be handled if we only want the user to see their own comments, but not anyone else's?

Gravatar
Chris Oliver (167,500 XP) on

You could do that by scoping the comments further by adding ".where(user: current_user)" to the query in the controller.


Gravatar
Jay Killeen (1,580 XP) on

I found some of this information useful on how to test these polymorphic comment features at https://github.com/thoughtb...


Gravatar
BronzeTax on

Interesting way to do this. I created an app that does comments, but I did it by the whole post has_many :comments, comments belongs_to post, and resource nesting. Took me hours to figure out how to do display comments. What's the pros and cons of the that way versus this way?

One great thing about this episode is that the whole polymorphic thing finally clicks for me, because for the longest time I still didn't really understand it despite reading on it repeatedly. In all of my albiet smallish projects I never used it, now that I understand it finally, I have some idea when to use polymorphic association.

Thanks so much! I signed up for GoRails based on this episode and the omniauth twitter episode!

Gravatar
Chris Oliver (167,500 XP) on

The main difference is that you probably have tied your comments to the Post model. With polymorphism, you could have comments on the Post model, the User model, or any other model you've got.

And thank you a bunch for subscribing!!

Gravatar
BronzeTax on

Ah I see, what would be the pros and cons of each solution in your opinion?

Gravatar
Chris Oliver (167,500 XP) on

If you don't go with polymorphism and you want comments on multiple models, you will need to have two different tables for comments, like PostComments and UserComments, but why do that when you could combine them into just Comments? Really that's the main difference. You reduce duplication there.

Gravatar
BronzeTax on

Ah I see that makes sense. What about foreign key and rails integrity? Is that ever an issue?

Gravatar
Chris Oliver (167,500 XP) on

You shouldn't have any trouble. Rails handles it well. But you're right that with polymorphism you don't get the same database level enforcement like you would with the individual associations.


Gravatar
Michael Langat on

How do you add a destroy method to this


Gravatar
Melanie (1,520 XP) on

I am trying to add a delete comment button to this. But it is sending me to the articles controller destroy action. Please can you add how to add a delete button to the comment? I'm stuck. Thanks very much!

Gravatar
Chris Oliver (167,500 XP) on

Hey Melanie!

For deleting comments, it might be useful to add a "resources :comments" to your routes that isn't inside another resources block. Then you can create a regular CommentsController with a destroy action like normal. That way you can delete any comment as long as you know the ID of it and not worry about whether it's a film or actor comment because that doesn't really matter when you're deleting a comment.

To add the delete link to each comment in the view, you can say: <%= link_to "Delete", comment, method: :delete %> and that will make a DELETE request to the /comments/1 url, which will trigger the destroy action.

That should do it! If you want to go over and above, you can also make it as a "remote: true" link so that you can return some JS to remove the item from the page to make it AJAXy and nicer to use.

Gravatar
Melanie (1,520 XP) on

Hi Chris. Thanks very much for the response. I tried adding this to my routes:

resources :comments, only: [ :destroy]

and this to my existing comments controller (which only had the create method in it - per your tutorial)

def destroy
@comment.destroy
respond_to do |format|
format.html { redirect_to data_url }
format.json { head :no_content }
end
end.

When I try this, I get an error that says:
undefined method `destroy' for nil:NilClass

Any ideas on what's gone awry?

Gravatar
Chris Oliver (167,500 XP) on

You might be needing to add a before_action for the destroy action called set_comment to set the @comment variable. It's saying that @comment is nil there so that would be it. You can just set @comment = Comment.find(params[:id]) in the before action and you should be set.

Gravatar
Melanie (1,520 XP) on

Thanks so much Chris.


Gravatar
Melanie (1,520 XP) on

Hi Chris, I'm still struggling along in trying to get this set up. My current issue is with my comments policy update function. Your tutorial shows how to define update as set out below (which I have tried in both my article policy and my comment policy:

Article Policy:

def update?

#user && user.article.exists?(article.id) -- I have also tried this on this suggestion of someone on stack overflow).

user.present? && user == article.user

end

Comment Policy:

def update?

user.present? && user == comment.user

end

I keep getting a controller action error which says: undefined method `user' for #<class:0x007f9e24fa7cf0>

It highlights the update definition. Has something changed in pundit that requires a different form of expression for this update function? Can you see what might have gone wrong? Thanks very much

Gravatar
Chris Oliver (167,500 XP) on

That looks correct. It sounds like the comment object is potentially the class and not the instance of the comment.

Does your controller have " authorize @comment" in it? And is your @comment variable set to an individual record?

Gravatar
Melanie (1,520 XP) on

Yes - my articles controller update action has:

def update
# before_action :authenticate_user!
authorize @article
respond_to do |format|
# if @article.update(article_params)
# format.json { render :show, status: :ok, location: @article }
# else
# format.html { render :edit }
# format.json { render json: @article.errors, status: :unprocessable_entity }
# end
# end
if @article.update(article_params)
format.json { render :show, status: :ok, location: @article }
else
format.json { render json: @article.errors, status: :unprocessable_entity }
end
format.html { render :edit }
end
end

Gravatar
Chris Oliver (167,500 XP) on

Do you have the part that sets @article?

Gravatar
Melanie (1,520 XP) on

Hi - what part would that be?
def set_article

@article = Article.find(params[:id])

authorize @article

end

Gravatar
Chris Oliver (167,500 XP) on

That's exactly what I need. I might suggest removing the authorize @article in this function and keeping the one in your update action instead. Everything else looks right, so you might need to also send me the full error logs to see where exactly it broke.

Gravatar
Melanie (1,520 XP) on

The error log says:
Completed 500 Internal Server Error in 56ms (ActiveRecord: 28.7ms)

ActionView::Template::Error (undefined method `user' for #<class:0x007f9e1a734af0>):

22: <%= comment.created_at.try(:strftime, '%e %B %Y') %>

23: </div>

24:

25: <% if policy(Comment).update? %>

26: <%= button_to 'Edit', polymorphic_path([commentable, comment]), :class => 'btn btn-large btn-primary' %>

27: <% end %>

28: <% if policy(Comment).destroy? %>

app/policies/comment_policy.rb:13:in `update?'

app/views/comments/_display.html.erb:25:in `block in _app_views_comments__display_html_erb___34367520595054411_70158511210960'

app/views/comments/_display.html.erb:12:in `_app_views_comments__display_html_erb___34367520595054411_70158511210960'

app/views/articles/show.html.erb:68:in `_app_views_articles_show_html_erb__2619839868106814513_70158550563960'

Gravatar
Chris Oliver (167,500 XP) on

Ah ha! So your errors is in the view, not your controller. :)

Change line 25 to say <% if policy(@comment).update? %>. Like I first guessed, you were referencing the class Comment, and not the individual record "@comment". That should do it!

Gravatar
Melanie (1,520 XP) on

Hi Chris - when I try that, I get this error: Pundit::NotDefinedError in Articles#show

Showing //app/views/comments/_display.html.erb where line #25 raised:

unable to find policy of nil

Also, It's strange that the form of expression the way I had it works in delete, but not in update

Gravatar
Chris Oliver (167,500 XP) on

It sounds like you're passing in a variable that's nil then. Are you sure you're before_action :set_comment is being called?

Gravatar
Melanie (1,520 XP) on

Not sure how to check that. It's in the controller. I've followed the steps as set out in your tutorial. I'm not sure what I've messed up. I'll go back and watch the video again (take 30 might do it).

Gravatar
Chris Oliver (167,500 XP) on

I think you're passing in that variable into a partial actually(?), so that wouldn't actually solve your problem. You may need to make sure you're passing the right comment variable into your pundit stuff in each case.

Gravatar
Melanie (1,520 XP) on

If I change the view so that it's:

<% commentable.comments.each do | comment | %>

<div class="well">

<%= comment.opinion %>

<div class="commentattributionname">

<%= comment.user.full_name %>

</div>

<div class="commentattributiontitle">

<%= comment.user.formal_title %>

</div>

<div class="commentattributiondate">

<%= comment.created_at.try(:strftime, '%e %B %Y') %>

</div>

<% if policy(comment).update? %>

<%= button_to 'Edit', polymorphic_path([commentable, comment]), :class => 'btn btn-large btn-primary' %>

<% end %>

<% if policy(comment).destroy? %>

<%= button_to 'Delete', polymorphic_path([commentable, comment]), method: :delete, :class => 'btn btn-large btn-primary' %>

<% end %>

</div>

then the delete still works (it did with 'Comment' instead of 'comment', but the update shows an error as:

No route matches [POST] "/articles/10/comments/29"

In my routes, I have:

resources :articles do

collection do

get 'search'

end

resources :comments, module: :articles

end

resources :comments, only: [ :update, :destroy]

Gravatar
Chris Oliver (167,500 XP) on

You must link to the route that matches the destroy action. You're linking to a nested route, but you want to link to comments route directly.

Replace the polymorphic_path([commentable, comment]) in your button_to's to simply be comments_path(comment)

Gravatar
Melanie (1,520 XP) on

When I try:

<% if policy(comment).update? %>

<%= button_to 'Edit', comments_path(comment), :class => 'btn btn-large btn-primary' %>

<% end %>

I get this error:
Routing Error

No route matches [POST] "/articles/8/comments/30"

I'm baffled by the idea that update is defined in the same comments controller as delete. So far delete is working (using the polymorphic nested path), but update has errors.

Gravatar
Chris Oliver (167,500 XP) on

Well, that's a button_to, which is a POST, but update is actually an UPDATE request. You'd need to add the :method => :update to the button_to here.

Your destroy action should be put inside the regular CommentsController and should be in the same place as these. The only reason you need the nested routes and controllers is for helpers that make creating the comments (and referencing the original object you're commenting) on a little simpler. To destroy any comment, it doesn't matter what the original object was, you can just delete the comment itself if that makes sense.

Gravatar
Melanie (1,520 XP) on

Hi, still no good. Each of the create, update and destroy methods are in the ordinary comments controller. When I try:

<% if policy(comment).update? %>

<%= button_to 'Edit', comments_path(comment), :method => :update, :class => 'btn btn-large btn-primary' %>

<% end %>

<% if policy(comment).destroy? %>

<%= button_to 'Delete', polymorphic_path([commentable, comment]), method: :delete, :class => 'btn btn-large btn-primary' %>

I get this error:

undefined method `comments_path' for #<#<class:0x007f9e26697c30>:0x007f9e1a6daff0>

On top of that, now my delete action doesnt work either (it did prior to this set of changes).

Gravatar
Melanie (1,520 XP) on

Should I go back to capital 'C' in comment for destroy? e.g.: <% if policy(Comment).destroy? %>

Gravatar
Chris Oliver (167,500 XP) on

No, you'll still want that to reference the variable and not the class.

Do you have a github repo I can show you some fixes for this? It's kinda hard to give guidance in the comments. :)

Gravatar
Melanie (1,520 XP) on

Hi Chris - I appreciate your efforts to help. The repo is private so I can't share it. Thanks anyway. Ill keep trying. It's been 3 years trying to grasp the basics and I'm still waiting for the penny to drop. Thanks anyway.

Gravatar
Chris Oliver (167,500 XP) on

You'll get there! I think you can add me as a collaborator to the private project temporarily if you wanted, but no worries if not.

I would also recommend playing a lot with plain Ruby because that will help wrap your head around all these things in Rails. It's mostly just regular old Ruby code just connected in various ways. The Ruby Pickaxe book and Metaprogramming Ruby are both really good.

Feel free to shoot me some emails as well! My email's on the about page I believe.


Gravatar
heart_and_me on

Chris, Fantastic episode here. Thank you very much.

I got it to work - mostly - in my set-up but I encounter a little redirect problem.

In my case my association is a "noteable" - referring to Notes.

My resources and namespace profile is:

namespace :navigate do
--resources :boks, :only => [:show] do
----resources :tools, :only => [:show, :index]
------resources :notes, module: :processus
----resources :processus do
------resources :notes, module: :processus

The problem I have is with the NotesController for the create/update of the notes. When I try to do a redirect I am finding that I am missing the information about the "bok" entity.

In my controller I have this:

def create
@note = @noteable.notes.new note_params
@note.user = current_user
@note.save
redirect_to [:navigate, @bok, @noteable], notice: "Your note was succesfully created."
end

Really the redirect_to should send me back to the right place, however the @bok entity is not present at all...mostly because at this stage I don't actually need it.

What would be the recommended approach to dealing with this nested situation?

Thanks!


Gravatar
Sz M (2,710 XP) on

Chris, awesome episode! I'm trying to implement something similar, but before I dive deep into it I'd like to make sure I go down the right path. So I will do the exactly same, except comments are gonna have replies. My guess is if I have the right polymorphic setup for the comments, then I can just setup a simple `has_many + belongs_to` relationship between the comment model and the reply model, so from the reply's perspective it doesn't matter if the comment is polymorphic or not since every comment will have its unique id. Is this right?

Gravatar
Chris Oliver (167,500 XP) on

Yeah, you can have a comment as a commentable, allowing you to have Comments with comments if you like. That's how you would normally setup threaded comments. You might want to put some limits on the nesting so you don't get threads that are too far nested in. Facebook limits it to one layer for example.


Gravatar
LuxDG on

In the comments controller when I try to do

@comment.user = current_user

Throws an error (no user method for user class) unless I do

@comment.user_id = current_user.id

Problem is, when I try to show the username associated to a comment (I need to do queries instead of accessing the object). Any idea on how to fix this?

Gravatar
LuxDG on

Oops, I never did the belongs_to :user association :$


Gravatar
Stan Smith (1,850 XP) on

Question Chris - I'm trying to show the last 5 comments in the show view. I can't get the query string right. Thanks for your help!

Gravatar
Chris Oliver (167,500 XP) on

You could change it to the following:

<% commentable.comments.order(created_at: :desc).limit(5).each do |comment| %>

Gravatar
Stan Smith (1,850 XP) on

I should have mentioned that I'm showing the comments on an dashboard view that shows info from multiple models. So what you provided throws a 'undefined local variable or method `commentable' error.

For example I have a contacts model that is commentable and I would like to show the comments from that model on the dashboard(index)

Gravatar
Chris Oliver (167,500 XP) on

Just ignore that part and add the order and limit functions to your call when you retrieve comments. That's the important bit there.

Gravatar
Stan Smith (1,850 XP) on

That works.
I overlooked that their is a comments model to pull from.
Thanks!


Gravatar
Daniela Correa Orozco on

Hi Chris!
Awesome video!
I'm having problems with current_user being nil in the base CommentsController where we set
@comment.user_id = current_user

current_user seems to return the right user_id in my other controllers. I have looked around but I haven't been able to pin point the problem (cookie problems maybe?)
Thank you in advance and for the really helpful videos!

Gravatar
Chris Oliver (167,500 XP) on

Is the user not currently signed in by chance when you submit a comment?

Gravatar
Daniela Correa Orozco on

yes, the user is signed in when I post a comment. I also have the before_action :authenticate_user! to make sure there is a logged in user available.

Gravatar
Chris Oliver (167,500 XP) on

Hmm, I was hoping it wasn't. Usually it's just that the user isn't signed in. Otherwise...I'm not entirely sure. There aren't a whole lot of places to go check to make sure things are correct aside from that.

Possibly just a typo in your comment, but make sure you've got:

@comment.user_id = current_user.id

If you specify user_id, then you need to specify ID on the user, or you can just assign the object to the association alternatively:

@comment.user = current_user
Gravatar
Daniela Correa Orozco on

That was it!

if I use
@comment.user = current_user I get a undefined method user so I changed it to .user_id but never specified ID of current user.

Thank you very much!

Gravatar
Chris Oliver (167,500 XP) on

Awesome! You're welcome! :D


Gravatar
Stan Smith (1,850 XP) on

I love this nifty bit of code. I'm wondering how would you go about using will_paginate to paginate the comments?


Gravatar
Melanie (1,520 XP) on

I forgot about this. I watched it a year ago and used in in my last attempt at implementing these associations. And then I forgot about it. Im back on track again - thanks for this (again). I'm struggling to figure out how to filter the index of the comments resource by an attribute (say :status == 'published'). I can't do that in the regular way because the route is looking for a prefix (of film/actor)

Gravatar
Chris Oliver (167,500 XP) on

Hey Melanie! :)

If you wanted a route to get all published comments (not ones scoped to a commentable type) you could add a resources :comments that was not nested in your routes and use that.

  resources :comments

resources :actors do
resources :comments, module: :actors
end
resources :films do
resource :comments, module: :films
end

And then you could make a comments_controller.rb that worked for all comments for any object. Is that what you're looking for?

Gravatar
Melanie (1,520 XP) on

Hi Chris, I'm trying to make an index view (for comments) that shows all the comments on a specific film that are published. If comments wasn't a polymorphic resource, I could add published: true to the index path. But, since the comments view belongs to both actor and film I can't prefix the index with the parent name in the path. So I'm a bit stuck for what to do.

Gravatar
Chris Oliver (167,500 XP) on

Ah, I gotcha. So in your films/comments_controller.rb you could say:

def index
@comments = @film.comments.where(status: 'published')
end

And then you would want to build the index.html.erb to display all those comments. Is that what you're looking for?

Gravatar
Melanie (1,520 XP) on

thank you :)


Gravatar

Gravatar
Zulhilmi Zainudin (270 XP) on

Thank you Chris!


Gravatar
Saint on

Just coming back here to say thanks! I watched this several times and it eventually sunk in. I managed to set it up a few months ago and it's been working quite well. I will admit it did take me some time to grasp the concept though.

Gravatar
Chris Oliver (167,500 XP) on

That's great to hear! :D And I agree, it's a tough one to wrap your head around the first time.


Gravatar
Khemlall Mangal on

Hi all, i am getting issue with my routes when i follow this methodology. I get uninitialized constant Squeals.
SQUEAL Models

class Squeal < ActiveRecord::Base
has_many :comments, as: :commentable
end
comment.rb

class Comment < ActiveRecord::Base
belongs_to:commentable, polymorphic:true
end
/squeal/comments_controller.rb

class Squeals::CommentsController <commentscontroller before_action="" :set_commentable="" private="" def="" set_commentable="" @commentable="Squeal.find(params[:squeal_id])" end="" end="" comments="" controller="" class="" commentscontroller="" <="" applicationcontroller="" before_action:authenticate_user!="" def="" create="" @comment="@commentable.comments.new" comment_params="" @user.user="current_user" comment.save="" redirect_to="" @commentable,="" notice:="" "your="" comment="" was="" posted"="" end="" private="" def="" comment_params="" params.require(:comment).permit(:body)="" end="" end="" routes="" resources="" :squeals="" do="" resources="" :comments,="" module:="" :squeals="" end="">


Gravatar
yawar sultan (10 XP) on

Hi Chris, I want to add comments to multiple films at the same time. Suppose I want to add comments section on index page of films. Can you please help me how can i do that.


Gravatar
Dinesh Pallapa on

thanks,It's really help full.If we want to delete the comment from actor or film we need to define a method as destroy or else using _destroy for nested attributes.Can anyone help me out.


Gravatar
Nick Noble (440 XP) on

@excid3:disqus
How would you need to modify this to work with deeply nested resources?

i.e:



resources :projects do
resources :project_users, path: :users, module: :projects
resources :posts do
resources :comments, module: :posts
end
end


Gravatar
Kick buttowski (100 XP) on

I am trying to use commentable with actioncable , but I keep getting the following error
d6f951d17) from Async(default) in 20.73ms: ActionView::Template::Error (undefined method `comments' for #<class:0x007f5cec08b880>):
someone advice me to look for value of comments and I realized is not define
but my question is Is it necessary to define relationship between comment and lets say article class ?

class Comment < ApplicationRecord
belongs_to :commentable, polymorphic: true #, optional: true
belongs_to :article, optional: true <--- this point

validates :body, presence: true, length: {minimum: 5, maximimum: 1000 }
after_create_commit {CommentBroadcastJob.perform_later self}
end


Login or create an account to join the conversation.