timroman
timroman

Reputation: 1394

Problems with CanCan Methods with Rails3 / Devise

After two days, I've not been able to solve this problem on my own. It seems like it should be pretty simple, but I'm missing something. I'm creating a simple blog with Posts and Authors. Authors have a boolean admin column.

The line that is giving me an error right now is where I check permissions to show the edit button in a post.Current error is:

NoMethodError in Posts#show

Showing .../posts/show.html.erb where line #18 raised:

undefined method `stringify_keys' for #

posts/show.html.rb

          <% if @author.can? :update, @post %>
          <p><%= link_to 'Edit', edit_post_path(@post), :class => 'btn' %> &nbsp; <%= link_to 'Destroy', @post, confirm: 'Are you sure?', method: :delete %></p>
          <% end %>

application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery  
  rescue_from CanCan::AccessDenied do |exception|
    redirect_to root_url, :alert => exception.message
  end
  helper_method :current_author
  def current_user
    @current_ability ||= Author.new(current_author)
  end
end

ability.rb

class Ability
  include CanCan::Ability
  def initialize(author)
    author ||= Author.new # guest user (not logged in)   
    if author.admin?
      can :manage, :all
    else
      can :read, :all
    end  
  end
end

Also, from what I can tell, CanCan is included in the gem file correctly.

Upvotes: 0

Views: 284

Answers (1)

pdu
pdu

Reputation: 10423

Two things.

First, you need to have a current_user method in your controller, on which cancan relies. If you don't have one, you may

  • alias current_user to your current_whatever method or
  • manually instantiate the ability like @ability = Ability.new(current_whatever) and call your can?'s on that generated ability in your views (like @ability.can? :edit, @post).

Second, your Ability uses current_author on both line 4 and 5, however you don't have a current_author in your initialize method. You have author, though. If no Author object is available/given to the ability's initializer, you use a non-persisted author instead (and not a AuthorAbility, unless your AuthorAbility is what current_user returns or initialize in your ability gets as argument). Something like this:

class Ability
  include CanCan::Ability
  def initialize(author)
    author ||= Author.new # guest user (not logged in)
    if author.admin?
      can :manage, :all
    else
      can :read, :all
    end  
  end
end

Edit based on the comments to keep it simpler:

Ideally you put a current_user method inside your application controller and also make it available as helper in your views (because you may want to conditionally show/hide/change things in your view based on a logged-in user).

class ApplicationController < ActionController::Base
  helper_method :current_user

  def current_user
    # return the currently logged in user record
  end
end

If this is new to you, I suggest to have a look at an authentication gem. Devise also introduces this and authlogic describes this in its how-to and the example application. If you're doing authentication from scratch, you just need to return the user based on the session.


edit 2. You actually need to understand what you do, which IMHO is not the case at the moment. You're doing a bit of a mess here ;-)

Problem 1: current_user needs to return the current author / user logged in (not an abilty nor fallback user nor something else) or nil if no author is logged in. So you can e.g. do <% if current_user %> in your view. @current_ability ||= Author.new(current_author) is plain wrong. The fallback from the ability class needs to stay in the ability class because cancan's methods can only be applied to an object and not to nil. So with author ||= Author.new in your ability, you make sure that there is an object, even if no author is logged in (in which case current_author returns nil).

Problem 2: helper_method :current_author actually does nothing because there is no current_author method in your application controller. You need to somehow define current_author.

Problem 3: In your view, you're calling can? on an instance of Author which is wrong. can? is a method of Ability. So you'd need to use @my_ability.can? where @my_ability is an instance of e.g. Ability.new(Author.first). This is used if you need to work with multiple abilities or customized something, which is not the case here, so just use can? directly without a receiver (like @author.can?).

For testing purposes, I'd create the following:

class ApplicationController < ActionController::Base
  helper_method :current_user # as defined below...

  def current_user
    # Return a static user here, for testing purposes,
    # like @current_user = User.first or Author.first
  end
end

So your current_user returns a valid user (I hope, you need to at least have one in your database stored, though) and then can sort out the ability issues. If your ability works, you implement your authentication. As a beginner, I'd either stick to authlogic or devise.

Upvotes: 1

Related Questions