brg
brg

Reputation: 3953

Rails-4, CanCan returning undefined method for nil:NilClass, for a user model attribute that exists

I am using devise and cancan in my app. My Product model has a belong_to :user and the User table has a column seller:boolean. So in my ability class I have this line product.try(:user).seller as shown in the Ability Class pasted in the question, that line throws NoMethodError in ProductsController#create, undefined method `seller' for nil:NilClass any time I try to create a new product. But in the controllers create action, the user object is not nil because when I inspect the log after the error, I see SELECT "users".* FROM "users" WHERE "users"."id" = 11.

Also, in the rails console if I do:

   a = Product.find(4)    
   a.user.seller  will return  => true   

My ability class

 class Ability
   include CanCan::Ability

   def initialize(user)
     user ||= User.new 

     can :manage, Product do | product |
      product.try(:user_id) == user.id
      product.try(:user).seller == true 
     end
   end
 end   

The Productscontroller:

class ProductsController < ApplicationController
  before_filter :authenticate_user!, except: [:index, :show]
  load_and_authorize_resource only: [:create, :edit, :destroy]
  respond_to :html, :json

  def index
    @products = Product.all
    respond_with @products
  end

  def new
    @product = Product.new    
  end

  def create 
    @user = User.find_by(id: current_user.id)
    @product = @user.products.build(product_params)
    @product.save 
  end

end  

To make CanCan work with rails 4, my application controller has

class ApplicationController < ActionController::Base
  #temprary work-around for cancan gem to work with rails-4
  #source https://github.com/ryanb/cancan/issues/835#issuecomment-18663815
  before_filter do
    resource = controller_path.singularize.gsub('/', '_').to_sym
    method = "#{resource}_params"
    params[resource] &&= send(method) if respond_to?(method, true)
  end
end

To give clarity to the screenshots below, here is a shortened version of products/index.html.erb

<% @products.each do |product| %>     
  <% if user_signed_in? %>      
    <% if can? :update, product %>
      <span class="bottomcentre"><%= link_to 'edit', edit_product_path(product), class: "btn btn-primary"  %></span>
    <% end %>

    <% if can? :destroy, product %>
      <span class="bottomright"><%= link_to "Delete", product, data: {confirm: 'Are u sure?'}, method: :delete, class: "btn btn-danger" %></span>
    <% end %>

   <% end %><!-- closes user_signed_in -->

  <% end %>

  <br/>
  <% if user_signed_in? %> 
    <% if can? :create, Product %>
      <%= link_to 'Create a new Product', new_product_path %>
    <% end %>
  <% end %>

An additional effect is that the edit and destroy links are shown to a seller for products that are not theirs unless I comment out the line raising the error, that is this product.try(:user).seller == true in the CanCan's Ability Class shown earlier on. So when commented out, I get this screenshot 1 with edit links hidden fro products not belongs to the seller and when uncommented you get screenshot 2, with all product edit link displayed to a seller, even if the products are not theirs.

screenshot 1 with product.try(:user).seller == true commented out in the CanCan Ability Class, and it shows the edit link for only the first two product which belongs to the signed_in seller enter image description here

screenshot 2 with product.try(:user).seller == true left intact in the CanCan Ability Class. see edit links being displayed for the lower products that is shirt and cufflinks and they don't belong to the signed_in seller. enter image description here

Upvotes: 1

Views: 2492

Answers (1)

Shadwell
Shadwell

Reputation: 34784

Your can :manage block definition is slightly wrong. It is the return value of the block that determines whether the user has the ability or not. In your block you have two statements of which only the second will have any bearing on whether the user has the ability. You need to join your statements with &&.

Also, your error seems to be when a product has no user as opposed to when the current user is nil. You need to use try on the return of product.try(:user) too as it will be nil if there is no product.

So, all told I think your block needs to be:

can :manage, Product do | product |
  product.try(:user_id) == user.id &&
  product.try(:user).try(:seller) == true 
end

Upvotes: 2

Related Questions