Managing Devise's current_user, current_admin and current_troll with CanCan

CanCan is awesome. It lets you manage user abilities easily and provides ways to define complex scenarios. I highly recommend using it for anyone who has more than one user type (like Troll).

Devise is great for authentication. When you have more than one user type as distinct classes, Devise will create current_* to be used in your controllers and views. So, User class corresponds to current_user. Admin class corresponds to current_admin. Troll class (used to identify Trolls under your application's bridge) corresponds to current_troll.

The problem

CanCan doesn't work with current_admin and current_troll out-of-the-box. It assumes that current_user is defined and current_user's abilities are defined in the Ability class. What if you want to break this paradigm? It turns out CanCan makes this pretty easy. Here are the current_user and Ability assumptions I am referring to:

def current_ability
@current_ability ||= ::Ability.new(current_user)
end

CanCan defines current_ability on your controller. This grabs an instance of the Ability class for the current user. So it assumes that you have current_user set and you have an Ability class defined. When your user types get more complex than what can be handled by one User model, it's time to make some changes.

Working with numerous Ability classes

Up front, your project might not require many different user types that vary greatly from one another. It might make sense to use Rail's nifty STI (Single Table Inheritance) and add all your abilities to one class. This can be nice in some respect. For instance, all users, no matter which type, can be reference by current_user.

When your user types get too complex to use one User model, your Ability class is too complex as well. In it's most simple form, say you have an Ability class that looks as follows:

class Ability
  include CanCan::Ability
 
  def initialize(user)
    user
||= User.new # guest user (not logged in)
    if user.is_a? Admin
      # Admin abilities
    elsif user.is_a? Troll
      # Troll abilities
    elsif user.new_record?
      # Guest abilities
    else
      # Basic user abilities
    end
  end
end

This structure gives you some flexibility in how you define your abilities but it's on it's way to Maintenance Hell, a deep dark place with no exit.

It would be best to define your abilities in different classes. Here we define UserAbility, AdminAbility, TrollAbility, and GuestAbility.

class UserAbility
  include CanCan::Ability
  def initialize(user)
    # Basic user abilities
  end
end
class AdminAbility
  include CanCan::Ability

  def initialize
    # Admin abilities
  end
end
class TrollAbility
  include CanCan::Ability
  def initialize(user)
   # Troll abilities
end
end
class GuestAbility
  include CanCan::Ability

  def initialize
    # Guest abilities
  end
end

Keep in mind that if your abilities are a subset of another user's abilities, you can inherit from other ability class. So in our case a Troll is a user who lives under a bridge. We don't want the Trolls to talk, so we limit their ability to post comments. Otherwise, they can do everything a User can do.

class TrollAbility < UserAbility
  def initialize(user)
    super(user)
    cannot :create, Comment     # More Troll abilities   end
end

Hooking up the Ability classes

When you use CanCan's "can? :create, Comment" method, it refers to current_ability to determine whether the given abilities include :create, Comment.

Since CanCan makes the assumption we're working with current_user and strictly Ability, we need to extend the built-in functionality. We do this by instantiating the new Ability classes based on the current user type (defined by Devise). CanCan has a brief wiki post on this topic.

def current_ability
  @current_ability ||= case
                       when current_user
                         UserAbility.new(current_user)
                       when current_admin
                         AdminAbility.new                        when current_troll                          TrollAbility.new(current_troll)
                       else
                         GuestAbility.new
                       end
end

 Now, when CanCan needs to check abilities (when you call "can? :create, Comment"), your current_ability method will return the appropriate Ability class.

Happy CanCaning!

 

Posted by Mike Pack on 07/21/2011 at 09:01PM

Tags: rails, devise, cancan


Tuesday Tricks - Regex Posix Shortcuts

Hate typing redundant regular expressions? Me too. How often have you typed the regex [a-zA-Z0-9]?

Posix character classes are here to save the day. You can replace a-zA-Z0-9 with [:alnum:]. [:alnum:] is the posix character class and there's a whole slew of them at your disposal. Use them in Ruby like so:

'-- I have 37 dollars --' =~ /[[:alnum:]]/ #=> 3
'-- I have 37 dollars --' =~ /[[:digit:]]/ #=> 10
'-- I have 37 dollars --' =~ /[[:space:]]/ #=> 2

Note: An expression with =~ returns the first position in the string which matches the regex.

Check out the full list of posix character classes and determine how you can prettify your expressions.

This Tuesday's Trick

Posix character classes won't prevent global warming but they sure can help make your regular expressions more readable.

Posted by Mike Pack on 06/28/2011 at 01:20PM

Tags: regex, ruby, posix


Testing Mobile Rails Apps with Capybara

Every web app should have a mobile version and every mobile version should be tested. Testing mobile web apps shouldn't be any more painful than testing desktop apps with the assumption that you're still serving up HTML, CSS, and JavaScript.

My Setup

For mobile detection, I use ActiveDevice, a User Agent sniffing library and some helper methods. While User Agent sniffing isn't the best approach for client-side (use feature detection with something like Modernizr), it's a reliable way to detect mobile devices in Rails.

For acceptance testing that doesn't need to be readily demonstrated to stakeholders, I use straight up Capybara with RSpec. Sometimes I use Steak.

The Pain of Testing Mobile

It's difficult to test mobile web apps because Capybara's default drivers are all desktop User Agents. You could acquire Capybara-iphone, but this solution didn't produce the expected results for me. I was given my mobile views but not my mobile layout. Plus, all this driver does is reset the User Agent for the Rack-Test driver. Further, what if you use Selenium for your default driver?

Platformatec wrote a nice blog post about mobile testing with user agents. The problem is it relies on Selenium. I was seeking a more concrete, driver agnostic way to serve up mobile views to my tests.

How I Test Mobile

This is a simple, straightforward way to invoke your mobile app from within your tests. I'm not convinced it's the most elegant way, but it's clean and simple.

Setup Your Application

In your ApplicationController, give some support for changing over to  your mobile app. This will also come in handy when you want to work on and test your mobile app on your desktop browser.

app/controllers/application_controller.rb

@@format = :html
cattr_accessor :format

Here we simply add a class attribute accessor with a default of :html. This will allow us to say ApplicationController.format

Next we need to add a before filter which will set the desired format upon each request.

app/controllers/application_controller.rb

before_filter :establish_format

private
def establish_format
  # If the request is from a true mobile device, don't set the format
request.format = self.format unless request.format == :mobile
end

Here's what a sample Rails 3 ApplicationController would look like:

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery
  include ActiveDevice

  # force the mobile version for development:
  #@@format = :mobile
  @@format = :html
  cattr_accessor :format
  before_filter :establish_format

private
  def establish_format
# If the request is from a true mobile device, don't set the format
request.format = self.format unless request.format == :mobile
end
end

Setup Your Tests

Now, in your tests, you can set the desired format for your application.

spec/integration/mobile/some_spec.rb

require 'spec_helper'

describe 'on a mobile device' do
  before do
    ApplicationController.format = :mobile
  end

  after do
    ApplicationController.format = :html
  end

  describe 'as a guest on the home page' do
    before do
      visit root_path
    end

    it 'does what I want' do
      page.should do_what_i_want
    end
  end
end

By setting ApplicationController.format = :mobile, we force the application to render the mobile version of files, for instance: index.mobile.erb. Your application will be invoked, your before filter will be run, and your are serving the mobile app to your tests.

Note: You need to reset your format to :html after your mobile tests are run so that tests which follow this in your suite are run under the default format, :html.

Happy testing!

Posted by Mike Pack on 06/16/2011 at 11:05AM

Tags: rails, testing, rspec, capybara, mobile