RankFu: Expressive user roles for Rails

Posted by on June 30th, 2008.

Almost every Rails developer has written an app with more than one user role, but when is_admin? isn’t enough, where do you go? RankFu aims to solve this by giving you a rich toolset for roles. Now it’s free to allow users to have more than one role, so you can have modifiers such as ‘Trusted’ and ‘New’ trivially. It’s also easy to have sets of roles, with administrators outranking moderators. Sets are optional though, so you’re free not to put Telephone Sanitizers in one due to their obvious lack of importance.

Throughout this piece, I’ll be talking about the ubiquitous User model, but you can use RankFu with any model you wish to.

Rankin’ Fu

When you install RankFu, you can add several new methods to your models:


User#has_role? 
User#has_role
User#"#{role}?" 
User#"#{role}_exactly?"    #This forces an exact comparison, useful if you want to test for a role which has a superset (e.g. moderators and  administrators)
User#"make_#{role}          
User#"remove_#{role}" 
User#rank                #Returns a string listing all roles, modifiers first eg: "Trusted Administrator" 
User#roles                #Returns an array of role names.

There is also some sugar for disabling users, to make your code more readable. These examples assume the existence of role with :disabled as its key:


User#disable_user 
User#enable_user
User#enabled?                       

Building on this, you can easily clean up your views. I DRY’d up many instances login check logic.


def logged_in_as_mod?
def logged_in_as?(user)  
def logged_in_as_friend_of?(user)  
def logged_in_as_or_as_friend_of?(user)   

There might have been a few others, but I don’t want to give any more ammunition to those who accuse me of pedantry.

InstallationFu

The easiest way to install is as a standard Rails plugin:

script/plugin install git://github.com/mjt/rank_fu.git

After installing this plugin, you’ll want to start by performing some admin:


script/generate rank_fu user
script/generate rank_fu roles         #create migration for roles 

After that, you’ll probably want to add roles to your user model, so just add this line:

knows_rank_fu

Then you need to create some roles. I suggest using the excellent Seed Fu plugin so you can do something like this:


Role.destroy_all
roles = [ {:id => 1, :key => "root",      :name=> "Superuser", :value => 2**22, :set => 1},
          {:id => 2, :key => "admin",     :name=> "Administrator", :value => 2**21, :set => 1},
          {:id => 3, :key => "moderator", :name=> "Editor", :value => 2**20, :set => 1},     
          {:id => 5, :key => "member",    :name=> "Member", :value => 2**10, :is_default => true}]

roles.each do |role|                          
  Role.create role
end

What’s going on here?

Not much.

Internally, bitwise arithmetic is used to store each model’s role-state. Once a model knows RankFu, you can assign and remove roles freely, knowing that RankFu is a good citizen and uses update_attribute internally.

You may also find that you can reduce your use of STI by separating users by roles.

Please feel free to leave any feedback in the comments.

Share:

Comment on this post (1 comment)


SubdomainFu: A New Way To Tame The Subdomain

An extremely common practice for Rails applications is to provide keyed access through subdomains (i.e. http://someaccount.awesomeapp.com/). However, there has never been a real unified convention for handling this functionality. DHH’s Account Location works for some circumstances but is more tailored for a Basecamp domain model (i.e. the app is on a separate domain from all other functionality, so you can always expect a subdomain) than the more common usage of one domain only.

SubdomainFu aims to provide a simple, generic toolset for dealing with subdomains in Rails applications. Rather than tie the functionality to something specific like an account, SubdomainFu simply provides a foundation upon which any subdomain-keyed system can easily be built.

Usage Fu

SubdomainFu works by riding on top of the URL Rewriting engine provided with Rails. This way you can use it anywhere you normally generate URLs: through url_for, in named routes, and in resources-based routes. There’s a small amount of configuration that is needed to get you running (though the defaults should work for most).

To set it up, you can modify any of these settings (the defaults are shown):

# in environment.rb

# These are the sizes of the domain (i.e. 0 for localhost, 1 for something.com)
# for each of your environments
SubdomainFu.tld_sizes = { :development => 0,
                          :test => 0,
                          :production => 1 }

# These are the subdomains that will be equivalent to no subdomain
SubdomainFu.mirrors = ["www"]

# This is the "preferred mirror" if you would rather show this subdomain
# in the URL than no subdomain at all.
SubdomainFu.preferred_mirror = "www"

Now when you’re in your application, you will have access to two useful features: a current_subdomain method and the URL Rewriting helpers. The current_subdomain method will give you the current subdomain or return nil if there is no subdomain or the current subdomain is a mirror:

# http://some_subdomain.myapp.com/
current_subdomain # => "some_subdomain" 

# http://www.myapp.com/ or http://myapp.com/
current_subdomain # => nil

# http://some.subdomain.myapp.com
current_subdomain # => "some.subdomain"

The URL rewriting features of SubdomainFu come through a :subdomain option passed to any URL generating method. Here are some examples (in these examples, the current page is considered to be ‘http://intridea.com/’):

url_for(:controller => "my_controller", 
  :action => "my_action", 
  :subdomain => "awesome") # => http://awesome.intridea.com/my_controller/my_action

users_url(:subdomain => false)  # => http://intridea.com/users

# The full URL will be generated if the subdomain is not the same as the
# current subdomain, regardless of whether _path or _url is used.
users_path(:subdomain => "fun") # => http://fun.intridea.com/users
users_path(:subdomain => false) # => /users

While this is just a simple set of tools, it can allow the easy creation of powerful subdomain-using tools. Note that the easiest way to locally test multiple subdomains on your app is to edit /etc/hosts and add subdomains like so:

127.0.0.1    localhost subdomain1.localhost subdomain2.localhost www.localhost

Adding an entry for each subdomain you want to use locally. Then you need to flush your local DNS cache to make sure your changes are picked up:

sudo dscacheutil -flushcache

Installation

SubdomainFu is available both as a traditional plugin and as a GemPlugin for Rails 2.1 and later. For a traditional plugin, install like so:

script/plugin install git://github.com/mbleigh/subdomain-fu.git

For a GemPlugin, add this dependency to your environment.rb:

config.gem 'mbleigh-subdomain-fu', :source => "http://gems.github.com/", :lib => "subdomain-fu"

Implementing A Simple Account Key System

Let’s take this functionality and implement a simple account-key system based off of the subdomain. We’ll start with some controller code (assuming that we have an Account model with a ‘subdomain’ field):

class ApplicationController < ActionController::Base
  protected

  # Will either fetch the current account or return nil if none is found
  def current_account
    @account ||= Account.find_by_subdomain(current_subdomain)
  end
  # Make this method visible to views as well
  helper_method :current_account

  # This is a before_filter we'll use in other controllers
  def account_required
    unless current_account
      flash[:error] = "Could not find the account '#{current_subdomain}'" 
      redirect_to :controller => "site", :action => "home", :subdomain => false
    end
  end
end

That’s really all we need for a basic setup, now let’s say we have a ProjectsController that you must specify an account to access:

class ProjectsController < ApplicationController
  # Redirect users away if no subdomain is specified
  before_filter :account_required
end

There’s lots more you can do with the plugin, but this is a simple use case that everyone can relate to.

Resources and Plans

A feature that I hoped would make it to the first release of SubdomainFu but is now a planned feature is subdomain-aware routing so that you can add conditional subdomain routes to your routes.rb file. Keep an eye out for more on that in the future.

In the meantime, the project will live at its home on Acts As Community for intermittent updates, is available on GitHub as always, and bugs/feature requests may be passed on through the Lighthouse.

Share:

Comment on this post (22 comments)


Announcing the Badger Rails Plugin

Badger is a simple Rails plugin that creates photo badges. A site often allows its users to upload a profile image. A profile image is just that, an image resized to fit in a predefined space to show up in the user’s profile.

With Badger, you can have something prettier – a badge that shows the user- uploaded image on top of another image that identifies the user as a part of the community. We have company badges, security badges, so why not web badges to have your users show off his/her affection for your site?

Badger works by accepting cropping parameters of the overlay image in a hash (x1, y1, width, height), which is used to crop the overlay image. It then resizes the cropped image to the size specified by composite_width and composite_height in badger.yml. Finally, it places the resized image on top of the background image at location specified by composite_x and composite_y in badger.yml. The resulting image is saved back to either the filesystem or Amazon S3, using attachment_fu.

Badger requires the attachment_fu plugin, ImageMagick, and MiniMagick. Also, the JavaScript Image Cropper UI can be used to obtain the cropping parameters from the users.

Configuration

When this plugin is installed , the badger.yml will be copied to the config directory. You need to specify the following:

  1. background : filename of the background image, searching from public/images
  2. composite_x : top left corner of the overlay image location in x
  3. composite_y : top left corner of the overlay image location in y
  4. composite_width : width of the overlay image in pixels
  5. composite_height : height of the overlay image in pixels


For example, I want to overlay an image on top of a background image (badge.jpg). The box for the overlay image should be 30 pixels in width and 20 pixels in height, and it should appear at (x, y) = (60, 80) of the background image. My badger.yml then looks like:

  development:
    background: badge.jpg
    composite_x: 60
    composite_y: 80
    composite_width: 30
    composite_height: 20

Example

In the model that you use to store attachments:

  class Photo < ActiveRecord::Base
    has_attachment :content_type => :image,
                   :storage => :s3,
                   :max_size => 1.megabytes,
                   :resize_to => '320x200>',
                   :thumbnails => { :thumb => '100x100>' },
                   :processor => :MiniMagick
    validates_as_attachment
    has_badge :storage => :s3
  end

In the controller:

  def create_my_awesome_badge
    @photo = Photo.find(params[:id])
    # params[:crop_coord] is a hash with indexes x1, y1, width, height
    @photo.create_badge(params[:crop_coord])
  end

Improvements Needed

Please feel free to submit patches for bug fixes and improvements. Specifically, I would like to:

1. Use something nicer than system(“convert blah…”), but couldn’t get it to work. I don’t think Minimagick supports compositing images, so RMagick may have to be used, but is it worth the heavy memory consumption?

2. Make it more flexible (i.e. accept background image and composite params dynamically instead of in badger.yml). Maybe pass them in the call to create_badge?

Share:

Comment on this post (2 comments)


Acts As Taggable On Grows Up

Acts As Taggable On (original post here), the tagging plugin with custom tag contexts, has gathered up some great new features over the past weeks thanks to the efforts of the community as well as fellow Intrideans Pradeep Elankumaran and Brendan Lim. I just wanted to take this opportunity to go over some of what’s new and interesting in the world of acts_as_taggable_on.

Community Fixes

First, Peter Cooper was kind enough to submit a patch that allows acts_as_taggable_on to work with Rails 2.1’s named_scope when using find_options_for_tag_counts.

Secondly, the much requested support for Single Table Inheritance is finally in! It was just a matter of using a class inheritable attribute instead of a class instance variable, and big thanks to slainer68 for hunting that down and taking the time to submit a patch.

If there’s anything you’ve hacked on to Acts As Taggable On, I urge you to submit a patch to the Lighthouse Project. I try to get new patches integrated into the codebase as quickly as possible, so please do submit anything!

During the Community Code Drive at RailsConf two great features were added: taggers and related objects.

Taggers

Tags can now have ownership, allowing for such things as User-tracked tags and more. This was a requested feature and something that I’d been looking forward to myself. Here’s the usage:

class User < ActiveRecord::Base
  acts_as_tagger
end

class Photo < ActiveRecord::Base
  acts_as_taggable_on :locations
end

@some_user.tag(@some_photo, :with => "paris, normandy", :on => :locations)
@some_user.owned_taggings
@some_user.owned_tags
@some_photo.locations_from(@some_user)

Find Related

Another request (and another great idea) is the ability to find related objects by similar tags. This is now available through the @object.find_related_on_tags syntax:

@bobby = User.find_by_name("Bobby")
@bobby.skill_list # => ["jogging", "diving"]

@frankie = User.find_by_name("Frankie")
@frankie.skill_list # => ["hacking"]

@tom = User.find_by_name("Tom")
@tom.skill_list # => ["hacking", "jogging", "diving"]

@tom.find_related_on_skills # => [<User name="Bobby">,<User name="Frankie">]
@bobby.find_related_on_skills # => [<User name="Tom">] 
@frankie.find_related_on_skills # => [<User name="Tom">] 

Gemified!

Acts As Taggable On now works as a GemPlugin in Rails. This is a new way (as of Rails 2.1) of distributing plugins as gems and having them still automatically link up and do their magic. To use it as a gem, add it to your config/environment.rb like so:

config.gem "mbleigh-acts-as-taggable-on", :source => "http://gems.github.com", :lib => "acts-as-taggable-on"

Now you should be able to get the latest version of the plugin just by running rake gems:install. However, this hasn’t been working for me so the alternative is just to install the gem directly:

gem install mbleigh-acts-as-taggable-on --source http://gems.github.com/

Now when you run your Rails app, even though it’s not in vendor/plugins it should be running! To make sure, look for this line on startup:

** acts_as_taggable_on: initialized properly

There are still a couple of issues outstanding in Rails regarding GemPlugins (if you unpack it, it will not run the initialization properly for some reason), but I wanted to give everyone the latest and greatest way to install the plugin possible. It will still work fine using the conventional methods as well.

Community and Future

I’ve been really happy with the response and support of the community, and I would like to do everything possible to cultivate future participation. To that end, I have created an Acts As Community Project for acts_as_taggable_on that will hopefully provide some casual communication about the project. Feel free to post on the wall or in the forums, and look out for additions soon.

Finally, the area of the plugin that still needs some work is tag caching. This is not a particular area of my expertise, so I’m hoping that someone from the community will write up some specs that flesh out the caching functionality in new and interesting ways.

Thanks for all of the patches, and I hope you continue to enjoy using Acts As Taggable On!

UPDATE (6/10/08): The improvements keep on rolling! After writing the post, I went off on a tangent and decided to make the plugin work both traditionally and as a gem. See more details above in the “Gemified” section.

Share:

Comment on this post (11 comments)