huihuihui
huihuihui

Reputation: 189

Ruby on Rails: Can't access User Attributes From a Comment

In this application, I have a Recipe model, User model(Devise), and a Comment model.

class User < ActiveRecord::Base
  has_many :recipes
  has_many :comments
end

class Recipe < ActiveRecord::Base
  belongs_to :user
  has_many :comments, :dependent => :destroy
end

class Comment < ActiveRecord::Base
  belongs_to :user
  belongs_to :recipe
end

This is what my Comments Controller looks like.

class CommentsController < ApplicationController

  def create
    @recipe = Recipe.find(params[:recipe_id])
    @comment = @recipe.comments.build(comment_params)
    @comment.user = current_user
    if @comment.save
      redirect_to root_path #change this the the appropriate path later
    end
  end

  private
  def comment_params
    params.require(:comment).permit(:content)
  end
end

This is the error that I am getting

ActionView::Template::Error (undefined method `first_name' for nil:NilClass):
68:     <p><%= @recipe.comments.count %> Comments</p>
69:     <% @recipe.comments.each do |comment| %>
70:       <p><%= comment.content %><p>
71:       <%= comment.user.first_name %>
72:     <% end %>
73:   </div>
74: </div>

So the problem occurs when I try accessing the first_name attribute. If I do just 'comment.user' it spits out # User:0x007f8f4f9b6780.

I looked in the Rails Console and saw that my comments are saving. In the Rails Console, I am able to do

@recipe = Recipe.first
@recipe.comments.first.user.first_name => "John"

This is the actual code that's failing

<% @recipe.comments.each do |comment| %>
  <% byebug %>
  <p><%= comment.content %><p>
  <p><%= comment.user.first_name %></p>
<% end %>

I tried using byebug to debug, and I'm able to do 'comment.user.first_name' => John

I'm not sure what I have wrong here and would appreciate the help. Rails 4.2.0 btw

Edit: RecipeController#Show

class RecipesController < ApplicationController
  before_action :find_user, only: [:edit, :update, :show, :destroy]
  before_action :find_recipe, only: [:edit, :update, :show, :destroy]


  ...

  def show

  end

  private

  def find_user
    @user = User.find(params[:user_id])
  end

  def find_recipe
    @recipe = Recipe.find(params[:id])
  end
end

Comment Form Partial

<div class="container">
  <% if user_signed_in? %>
  <%= form_for([@recipe.user, @recipe, @recipe.comments.build]) do |f| %>
    <%= f.label :content %><br>
    <%= f.text_area :content %><br>
    <br>
    <%= f.submit class: "btn btn-default" %>
  <% end %>
<% end %>
</div>

Upvotes: 1

Views: 490

Answers (3)

Paweł Dawczak
Paweł Dawczak

Reputation: 9649

Because you're iterating over the collection of Comments

@recipe.comments.each

The error you've mentioned occurs, because ONE of comments doesn't have user set (which causes first_name call on nil, and the error you've mentioned).

Try modifying your template as follows, to track the "problematic" comment:

<% @recipe.comments.each do |comment| %>
  <p><%= comment.content %><p>
  <% if comment.user.nil? %>
    Anonymous
  <% else %>
    <%= comment.user.first_name %>
  <% end %>
<% end %>

Hope that helps!

Update

Try updating RecipesController:

class RecipesController < ApplicationController
  def show
    @comment = Comment.new
  end
end

And replace @recipe.comments.build in partial:

<div class="container">
  <% if user_signed_in? %>
    <%= form_for([@recipe.user, @recipe, @comment]) do |f| %>
      <%= f.label :content %><br>
      <%= f.text_area :content %><br>
      <br>
      <%= f.submit class: "btn btn-default" %>
    <% end %>
  <% end %>
</div>

You don't need "linking" @recipe and Comment at that point, as it will be properly handled in CommentsController#create.

That was nice exercise!

Upvotes: 2

Mohammad AbuShady
Mohammad AbuShady

Reputation: 42909

A better way to handle that is using delegation and allowing nil, let me explain

class Comment < ActiveRecord::Base
  belongs_to :user
  belongs_to :recipe

  delegate :first_name, to: :user, prefix: true, allow_nil: true
end

This will create a method called user_first_name (user is the prefix), and allow_nil means if user doesn't exist return nil

The view code will be like this

<% @recipe.comments.each do |comment| %>
  <p><%= comment.content %><p>
  <p><%= comment.user_first_name %></p> # notice it's 1 dot
<% end %>

When the loop reaches a comment that doesn't have a user, it will return nil which will turn into an empty string,
While when the user exists, the comment will go ask the user to return it's first_name

Upvotes: 0

Chambeur
Chambeur

Reputation: 1619

Technically, you never check if the current_user exists. So when you iterate in each comments, if a someone, who's not log in, posts a comment, the user will be nil.

To prevents this error, you can add this Devise Helper in your controller :

class CommentsController < ApplicationController
  before_filter :authenticate_pro_user!

It will automaticaly redirect to the sign in view if someone try to access the comments controller.

Upvotes: 0

Related Questions