Ruby on Rails, Ajax & CSS Star Rating System

Posted 08/31/2006 by dnaffis 64 CommentsAdd Your Comment

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.

64 Responses to “Ruby on Rails, Ajax & CSS Star Rating System”

  1. Ruby on Rails Blog Says:
    This is great :) This will definetely be useful in rating based apps. Nicely done
  2. Christopher Aycock Says:
    Great info. I'll be adding this to del.icio.us, which is were I found it.
  3. Dave Says:
    This won't fallback to a non-Ajax version if the user has js turned off. I'll add some code in the next few days for that.
  4. Juixe Says:
    This is kewl!! Just as an FYI, there is more information regarding the Acts As Rateable plugin here.
  5. Ilya Grigorik Says:
    Nice how-to David! I did a very similar implementation for my project just a couple of days ago! I would suggest collapsing the partial into a for loop though to avoid code duplication. You can take a look at my code here: http://www.igvita.com/blog/2006/09/02/4-state-rails-ajax-css-rating/ That, and I chose not to give the user the ability to re-rate the item. Minor modifications, otherwise our code is almost identical.
  6. Aníbal Rojas Says:
    This code looks great, maybe we will try it at RubyCorner for rating the registered blogs. Off Topic: Please feel free to register at RubyCorner.com, a meeting place for people interested in the Ruby Programming Language or any of the related technologies.
  7. cman man Says:
    Has any one been able to make this work with multiple ratings on a single page? It starts to go a bit haywire because this piece: page.replace_html "star-ratings-block", :partial => 'rating/rating', :locals => { :asset => @asset } doesn't know which star-ratings-block to refresh Thanks!
  8. Dave Says:
    cman: You need to name each 'star-ratings-block' uniquely. I guess the easiest way to name them would be by the asset id. So in your view you would have
     
    Then in your rjs you can do something like:
    page.replace_html "star-ratings-block-#{@asset.id}", :partial => ‘rating/rating’, :locals => { :asset => @asset }
    
    That should do it.
  9. cman Says:
    dave: this worked like a charm! one this make sure you use id.to_s when appending to the id name. one additional note - the rating response time seems a bit slower on my hosted environment as opposed to my like machine, which I suppose is to be expected. does anyone have any tips to increase response times?
  10. cman Says:
    uhh - i meant LOCAL machine, not like machine, but i suppose i do like my machine :)
  11. Nice article Says:
    You should make comments <%= h %> to kill the spam
  12. Pratiksha Gajjar Says:
    Very good Article Dave really! It is very helpful to me in my project. In my project i want that when user gives rating, the new rating should be available to user instatly on same page. Curretly i have to refresh the page to get the new rating. So pls can u help me.
  13. Brian Says:
    Pratiksha: I noticed some browsers don't update the rating when voting (like Opera), and others do (like Firefox). Adding ":update" does not work in this case for some reason.
  14. Nitin Says:
    Great works, Realy Good plugin. Its Just 10 minute task to add ratings in RoR. Is there any other plugin for same?
  15. Al Brown Says:
    But how do I rate different qualities for a particular entity? I think I can do it by creating a model relating each entity to all of the qualities and then rating those relationships instead of the entity directly. Does anybody have any ideas on how to do this more elegantly and without modifying/duplicating acts_as_rateable?
  16. B.Jade Hess Says:
    Thanks for this excellent HOWTO. It's gotten me most of the way to where I need to be. "This won’t fallback to a non-Ajax version if the user has js turned off. I’ll add some code in the next few days for that." Just wondering if you happened to figure this out yet? Also, has anyone noticed display problems in IE 6? The overlayed, filled-in rating stars are not overlaying over top of the blank stars. The overlay is starting about 2.5 stars right of where it should.
  17. JGeiger Says:
    It seems that the plugins you mention in the article aren't the same. http://rubyforge.org/projects/rateableplugin/ http://juixe.com/svn/acts_as_rateable They're by different authors and have different APIs. You're using the juixe.com version in your code.
  18. Matte Says:
    It would be nice if a user can write also a comment rather than only a star vote. Thanks for the code
  19. Bazzel Says:
    Thanks for the article. Very well explained and usefull. After installing the numbersToWords plugin (http://www.recentrambles.com/pragmatic/view/53) I replaced the list in the rating view with:
    ...
        <% 1.upto(5) do |i| %>
        
  20. <%= link_to_remote( i, {:url => { :action => "rate", :id => movie.id, :rating => i}}, :class => "#{i.to_english}-#{pluralize(i, 'star')}", :name => "#{i} star out of 5") %>
  21. <% end %> ...
  22. http://www.scrawlers.com Says:
    Anyone resolve this IE incompatibility issue?
  23. bjhess Says:
    Sorry - I messed up my entry above. This is bjhess again...
  24. Al Brown Says:
    Figured out how to rate multiple attributes for a given entity. Just added a column in the ratings table for an attribute ID.
  25. jney Says:
    Can it be used without this table :"create_ratings". What should i do to adapt it to my own tables?
  26. Ashish Says:
    removing the position: absolute from the css file seems to solve the IE issue.
  27. Daniel Says:
    I'm using on my model Manual, I have replaced Asset by Manual in all the code, for example: class RatingController < ApplicationController def rate @manual = Manual.find(params[:id]) Rating.delete_all(["rateable_type = 'Manual' AND rateable_id = ? AND user_id = ?", @manual.id, current_user.id]) @manual.add_rating Rating.new(:rating => params[:rating], :user_id => current_user.id) end end But, dont work!!! help me please
  28. bjhess Says:
    "removing the position: absolute from the css file seems to solve the IE issue." Thanks! Specifically, remove the position: absolute from the .star-rating li.current-rating definition.
  29. Daniel Says:
    Any help to me please!!!
  30. Tom Says:
    How would you change this link_to_remote call to be restful? Does this work with restful resources if you have to send 2 params? Or do I need a form
  31. Tom Says:
    How would you change this link_to_remote call to be restful? Does this work with restful resources if you have to send 2 params? Or do I need a form
  32. Help Says:
    I'm having the same problem as Daniel. Please help
  33. John Says:
    I'm having the same problem as Daniel. Please help
  34. Sean Says:
    I can't seem to get this to work properly, it's the first plugin I've tried to install so I think my mistake might be something simple I overlooked. Whenever I add "acts_as_rateable" to my story model I get the following error on any action that uses the story model (which is just about all of them): "undefined local variable or method `acts_as_rateable' for Story:Class". I installed the plugin and it is present inside the vendor/plugins directory and I created the migration and ran rake on it. If I had to guess it seems like rails isn't finding the plugin. Is there something else I need to do?
  35. Brian Says:

    Yeah me too. I am new to Ruby on Rails and got the same exact error as everyone else…

    The Rails debug information isn’t very helpful either!

  36. prash Says:

    i am a newly converted java to rails programmer and there’s something wrong here! things are NOT supposed to be THIS easy. took me 10 minutes to do the whole thing! what the heck am i to do with the rest of my day? thanks for screwing up my schedule! :-)

  37. A Different John Says:

    I am having the same problem as Daniel. The model I have made ratable is called Document. I am getting a null pointer error in the first line of _rating.rhtml:

    You have a nil object when you didn’t expect it! The error occurred while evaluating nil.rating <%= numberwithprecision(document.rating, 1) %>/5 Stars
    Should document contain a has_many ratings? Or am I missing something else somewhere? I am certain I have all of the code in the example and I have replaced asset with document everywhere. Dang! Wasn’t a 10 minute job for this tard wagon. prash, maybe you can use the rest of your day helping me :-)

  38. Guillaume Says:

    Hi,

    Will it be possible to not show other user ratings when a current user is trying to rate ? So only show to a user his rating if it exists, or no star if not…

    Thanks for the help

    Guillaume.

  39. Chris Says:

    To everyone with tutorial problems,

    This might sound obvious but make sure your files are named correctly (ie. rate.rjs NOT rating.rjs - one of the problems I spend hours on, this will solve the null pointer error people are getting). Also, take a look at rating.rhtml and make sure the controller is pointed to “rating” not “ratingdemo”. In addition make sure <%= javascriptincludetag :defaults %> (since this is ajax at work), I ashame to admit but I forgot about this too. After installing the plugin make sure you restart your server (Mongrel or webrick), this is solve the undefineded actas_rateable error. I’m using Aptana’s RadRail and tried using the built in script console thing, for some reason it doesn’t plugin install doesn’t work (FYI: Don’t install the actas_rateable plugin on the plugin tab it IS NOT the SAME). Just go to your directory and do it the plugin install on your console. Hope that helps some of you guys other there.

    P.S. - Great tutorial. Keep up the good work Dave! RSS feed me “fo sho”!

  40. kaushik Says:

    Further info on tutorial problems:

    Make sure in Rating controller, you use the find method on your particular rateable model. I have a Product model that is rateable, and so I use Product.find(params[:id]), NOT Asset.find(params[:id]). Noob mistake!

  41. Yash Says:

    Nice code…

  42. FB Says:

    Like many people here, I am having trouble with _rating.rhtml seemingly, keep getting “You have a nil object when you didn’t expect it! The error occurred while evaluating nil.rating”!! I have fixed all re-namings and took Chris’ advice, still not getting anywhere… Has anyone figured out why this error occurs in the end? Many thanks!!!

  43. Aaron Says:

    Anyone tried implementing this code without having logged in users? Any tips would be much appreciated.

  44. Joseph Says:

    Hi,

    FB: did you restart the server?

    I also have a question. The code is working fine for me except for one thing: The code does not get recorded to the database. Any ideas?

    Thank you so much for this code!!

  45. Joseph Says:

    After looking at your code, figured out that you were missing parentheses:

    @asset.add_rating(Rating.new(:rating => params[:rating], :userid => currentuser.id))

    for add_rating method.

  46. ynw Says:

    Thanks for your time writing this article. Its nice to see knowledge shared

  47. Michael Hendrickx Says:

    Very nice solution indeed… Thank you for this!

  48. Tim Says:

    That’s for the great tutorial - works real well.

    Just wondering if anyone has figured out the non-ajax’d version?

  49. Nirmit Patel Says:

    For those of you experiencing the unexpected nil problem…you should first verify that you have followed all the naming conventions. If after all this you seen to still be getting this error (and most of the people have said its in the partial – _rating.rhtml) you should verify that the object that your trying to get does indeed have ratings. The reason it might be evaluating to nil is because the object.ratings has nothing since they have not been rated. If you add test data and make sure to provide the correct data for the ratings table for a particular object or product you should be able to view the partial.

    For those of you experiencing problems with the stars not working correctly…well I was one of them…took me 4 hours till I read Chris’s comment. What you have to do is if your using a layout, (and if you dont know what that is you should read up on it) go to the layout your using in your current view controller and put this ”<%= javascriptincludetag :defaults %>” in between the <head> and </head>. It allows javascript/ajax to work. This worked for me and now it updates without a hitch.

    Hope this helped and sorry for ranting on.

  50. jer Says:

    demo link errors out

  51. kiwi Says:

    I think some code is already outdated if you use the latest version. I had do some to implement the one without user.

      def rate
       @asset = Asset.find(params[:id])
    
       # add_rating() could not found in the plugin
       #@asset.add_rating Rating.new(:rating => params[:rating])
    
       #use this, should validate the params as well
       @asset.rating = params[:rating] 
      end
    

    basically what I do is generate a scaffold of asset (model) and rating_demo (controller). add this line into create method (rating demo)

    @asset = Asset.new(params[:asset])
    #add this line to initialize rating, else won't work
    @asset.rating = 0
    

    then change the appl/view/layout/rating_demo.rhtml to include javascript and star.css.

    hope this help.

  52. kiwi Says:

    please omit the comment above

       # add_rating() could not found in the plugin
       #@asset.add_rating Rating.new(:rating => params[:rating])
    

    I had use the wrong plugin. Note that this plugin from Chris Ingrassia is difference from juxie

    the one with addrating()_ is available from juxie.

    you should see the project pat is vendor/plugin/actsasrateable, but not vendor/plugin/rateableplugin if u follow this tutorials.

  53. samurails Says:

    hi thanks for the great toot, it was just what i needed! just one question left: as i am using restful routings i keep getting this routing error when i call link_to_remote: ActionController::RoutingError (no route found to match “/ratings_controller/rat e/33” with {:method=>:post}): doew anyone have a clue how to solve this? thx

  54. APRaj Says:

    HI This is a gr8 tutorial. Thanx a lot.

  55. rubier Says:

    Does anyone know how to add additional stars to the left of the first star to make negative ratings possible? How can you get the hover stars to move from the first star in the left hand direction? Thanks.

  56. Alex Says:

    Great solution! I’ll use it for my project ASAP.

  57. Rabbi Says:

    Thanks for the Great Tutorial !!!

  58. Photios Says:

    Nice!

  59. Lee Says:

    Hi Dave, I appreciate the time you took to write this article.

    I have went through each step in the tutorial but am getting this error “The :dependent option expects either :destroy, :delete_all, or :nullify (true)”

    I am a noob to RoR so I’m a bit lost. I don’t have a user login (therefore no user id at present). Can you point me in the right direction?

  60. mtraven Says:

    I’m a noob also, but I figured out that replacing :dependent => :true with :dependent => :destroy vendor/plugins/actsas_rateable/lib/actsas_rateable.rb line 12

    seems to fix things.

  61. pierrederome Says:

    tx a lot. took me 2 hours to implement on my project. including 1 hour to realise most of my problems were solved by Chris on 19 Jul 06:48.

    Super, these are the kind of blogs that just make it enjoyable to work !

  62. SKP Says:

    the demo does not work.

  63. johnny72k Says:

    didn’t quite get there in 10 min, but with some work…

    1. had to get “ratings_demo” out of the partial, and make sure I switched out “assets” for my model everywhere
    2. added the parenthesis mentioned by Joseph above
    3. had to change currentuser.id to @currentuser.id

    I got there in about 30 min. thanks so much!

  64. tgere Says:

    Don’t give up if you don’t get it working right away, the demo does work! If you are wondering if this demo works on newer environments, mine is Ruby 1.84 and Rails 2.02. Works as advertised! Many thanks to Dave Naffis for this great tutorial, Chris Ingrassia for the acts_as_rateable plugin, and rogie for the CSS. If you are running into problems, be sure to read the comments left by other for things to check for, especially if you are just copy and pasting from the demo (like I did ;-)

    I think because of the Ajax it’s a bit difficult to find the errors. I was using WEBrick and was not getting any useful debugging out of Terminal ruby. I switched over to Lighttpd and Terminal ruby had just the information that I needed to debug. Hope this helps someone.

  65. Sergio Says:

    Wow! This is amazing! Thank you!

  66. Paolo Says:

    The demo on this site doesn’t work anymore:

    TypeError in Rating_demo#index

    Showing app/views/ratingdemo/rating.rhtml where line #1 raised:

    nil can’t be coerced into Float

Sorry, comments are closed for this article.

Dave is the cofounder of Intridea and leads Intridea's product development efforts.

Before Intridea, Dave spent years at both AOL and IMAKE and received a Masters in Systems Engineering from the University of Virginia.