I’m sure everyone by now has seen those oh-so-Web 2.0 star rating features on hundreds of websites. Well I needed to implement one for a site I’m working on and I couldn’t find a complete example anywhere (not in RoR). So here it is. A complete Rails based Ajax and CSS star ratings sytem with some RJS thrown in for good measure.

I used Rogie’s very elegant CSS only star rating system found here CSS Star Rating Part Deux. I also used Chris Ingrassia’s acts_as_rateable plugin.

So here we go.

Get the CSS and change the image url’s

First figure out which version of the CSS ratings you like. I used this example.

/*             styles for the star rater                */    
    .star-rating{
        list-style:none;
        margin: 0px;
        padding:0px;
        width: 150px;
        height: 30px;
        position: relative;
        background: url(/images/star_rating.gif) top left repeat-x;        
    }
    .star-rating li{
        padding:0px;
        margin:0px;
        /*\*/
        float: left;
        /* */
    }
    .star-rating li a{
        display:block;
        width:30px;
        height: 30px;
        text-decoration: none;
        text-indent: -9000px;
        z-index: 20;
        position: absolute;
        padding: 0px;
    }
    .star-rating li a:hover{
        background: url(/images/star_rating.gif) left center;
        z-index: 2;
        left: 0px;
        border:none;
    }
    .star-rating a.one-star{
        left: 0px;
    }
    .star-rating a.one-star:hover{
        width:30px;
    }
    .star-rating a.two-stars{
        left:30px;
    }
    .star-rating a.two-stars:hover{
        width: 60px;
    }
    .star-rating a.three-stars{
        left: 60px;
    }
    .star-rating a.three-stars:hover{
        width: 90px;
    }
    .star-rating a.four-stars{
        left: 90px;
    }    
    .star-rating a.four-stars:hover{
        width: 120px;
    }
    .star-rating a.five-stars{
        left: 120px;
    }
    .star-rating a.five-stars:hover{
        width: 150px;
    }
    .star-rating li.current-rating{
        background: url(/images/star_rating.gif) left bottom;
        position: absolute;
        height: 30px;
        display: block;
        text-indent: -9000px;
        z-index: 1;
    }

Make sure you change your image url’s so that your Rails app can find them.

Get the images for your CSS

Grab the images used in your CSS and put them in your images directory. Here are both

.

Install the acts_as_rateable plugin.

Run the following from the root of your Rails app to install the plugin.

script/plugin install http://juixe.com/svn/acts_as_rateable

Create the tables used by acts_as_rateable

Create a file db/migrate/xxx_create_ratings.rb (xxx is 001 if it’s the first migration file you have).

class CreateRatings< ActiveRecord::Migration

  def self.up
    create_table :ratings, :force => true do |t|
      t.column :rating, :integer, :default => 0
      t.column :created_at, :datetime, :null => false
      t.column :rateable_type, :string, :limit => 15,
      :default => "", :null => false
      t.column :rateable_id, :integer, :default => 0, :null => false
      t.column :user_id, :integer, :default => 0, :null => false
    end

    add_index :ratings, ["user_id"], :name => "fk_ratings_user"
  end

  def self.down
    drop_table :ratings
  end

end

Run your migration.

rake migrate

You should now have the appropriate tables.

Make one of your models rateable

I was trying to add a rating system for the model Asset. Yours can obviously be whatever you like but from here on out I’ll be using Asset. So add acts_as_rateable to your model.

class Asset < ActiveRecord::Base
  acts_as_rateable
  ...
end

Create a controller to handle the rating submissions

Create the file /controllers/rating_controller.rb

class RatingController < ApplicationController

  def rate
    @asset = Asset.find(params[:id])
    Rating.delete_all(["rateable_type = 'Asset' AND rateable_id = ? AND user_id = ?", 
      @asset.id, current_user.id])
    @asset.add_rating Rating.new(:rating => params[:rating], 
      :user_id => current_user.id)
  end

end

Two things to note here. First I’m associating ratings to users. I’ve already implemented a user/permission system for my site using the model User. Use whatever is appropriate for you. You can modify this whole example to work without associating ratings to users, the acts_as_rateable plugin will handle it just fine. However, I’m not going to get into that here.

Since I am associating ratings to users it would be bad to have a user skew the results by storing multiple ratings for a single Asset. Hence the delete. I’m telling it to delete all ratings for the rateable_type ‘Asset’ and the id (rateable_id) of the Asset. The rateable_type of Asset is handled by the plugin and stored in the ratings table.

Create your views

Create the partial /views/rating/_rating.rhtml

<%= number_with_precision(asset.rating, 1) %>/5 Stars<br>
<ul class='star-rating'>
    <li class='current-rating' style='width:<%= (asset.rating * 30).to_i -%>px;'>
          Currently <%= number_with_precision(asset.rating, 1) %>/5 Stars.
        </li>
    <li>
        <%= link_to_remote( "1", {:url => { :controller => "rating_demo", 
            :action => "rate", :id => asset.id, :rating => 1}},
            :class => 'one-star', :name => '1 star out of 5') %>
    </li>
    <li>
        <%= link_to_remote( "2", {:url => { :controller => "rating_demo", 
            :action => "rate", :id => asset.id, :rating => 2}},
            :class => 'two-stars', :name => '2 stars out of 5') %>    
    </li>
    <li>
        <%= link_to_remote( "3", {:url => { :controller => "rating_demo", 
            :action => "rate", :id => asset.id, :rating => 3}},
            :class => 'three-stars', :name => '3 stars out of 5') %>
    </li>
    <li>
        <%= link_to_remote( "4", {:url => { :controller => "rating_demo", 
            :action => "rate", :id => asset.id, :rating => 4}},
            :class => 'four-stars', :name => '4 stars out of 5') %>    
    </li>
    <li>
        <%= link_to_remote( "5", {:url => { :controller => "rating_demo", 
            :action => "rate", :id => asset.id, :rating => 5}},
            :class => 'five-stars', :name => '5 stars out of 5') %>
    </li>
</ul>

Obviously it’s using Ajax with the prototype helper link_to_remote to submit the user’s rating. One thing to note. Where you see width:<= (asset.rating * 30).to_i ->px;’ you’ll have to modify this to correspond with the images you chose to use. The one I’m using has images which are 30px wide. If you chose the smaller star images then you’ll have to modify this calculation to correspond to your image width. By the way, this is the line that handles the display of the current rating.

And now a little RJS

Create the file /views/rating/rate.rjs

page.replace_html "star-ratings-block", :partial => 'rating/rating', :locals => { :asset => @asset }

This will replace the star ratings with the partial we created previously in order to reflect any rating changes made by the submission.

And finally put it on your page

Render the partial in one of your views.

<div id="star-ratings-block">
    <%= render :partial => "rating/rating", :locals => { :asset => @asset } %>
</div>

This needs @asset (or whatver you’re going to be using) in order to function.

Done

Now wasn’t that easy? Gotta love rails. 10 minutes of coding and you have a complete Ajax and CSS star rating system just like the pros use. Here’s a demo.

I could very well have skipped something so let me know if you have any problems.