Ajax CSS Star Rating with acts_as_rateable

Plugins, Ruby on Rails Add comments

CSS Star Rating Systems are all the rage now. Komodo Media brought this idea to the web’s attention with their post CSS Star Rating Redux. This tutorial will show you how to bring this rating system to Ruby on Rails

Example

4.0/5 Stars (2 votes)

  • Currently 3.0/5 Stars.
  • 1
  • 2
  • 3
  • 4
  • 5

Several other websites have taken similar approaches:

So how do I get this to work with Rails?

I tried implementing David Naffis’ post “Ruby on Rails, Ajax & CSS Star Rating System”; however, I ran into several issues.

  1. Limited to one rating per page
  2. Limted to one type of object
  3. RJS didn’t seem to work
  4. Didn’t work with objects that were subclassed

Lets get started!

Note: where ever you see the text “Myobject“, you will want to replace this with the class name of the object that you are rating.

1. Install the acts_as_rateable plugin

Note: We use vendor branches for plugins, not script/plugin install so that we can make modifications to the plugin (if needed) and easily update the plugin to the latest revision while keeping the patches that we have created.

2. Create the Migration

file:db\migrate\xxx_add_acts_as_rating_plugin.rb

3. Update your existing Model

You will need to include the acts_as_rateable at the top of your class file.

Note: if you are using this with models that are subclassed you will want to put the acts_as_rateable in the parent class only.

4. Create the Ratings Controller

David Naffis’ post Ruby on Rails, Ajax & CSS Star Rating System started me off in the right direction; however, the ratings were not working for classes that were subclassed. Notice the function def get_class_by_name. This function allows the rating controller to find the base class and set the rating at the base class level.

Also note the before_filter :login_required. This verifies that the user is logged in before it runs the rate function. The function login_required is part of the acts_as_authenticated plugin and must be patched in order to work properly with the Ajax request. The patch for acts_as_authenticated is explained in step 8.

file: app\controller\ratings_controller.rb

5. Create the Rate Partial

Again David Naffis’ post Ruby on Rails, Ajax & CSS Star Rating System started my off in the right direction and Igvita’s post Rails 4-State Ajax & CSS Star Rating helped demonstrate out to keep this partial DRY. As usual, I have expanded on both.

file: app\views\rating\_rate.rhtml

Notice: the div is unique ids based on the object’s id (ex: star-ratings-block-1, star-ratings-block-2, …) so that you can include the partial multiple times on a page for multiple objects (currently must be of the same class). This works specifically for the “list” view.

6. Including the Partial

Now that you’ve created the partial, you can simply include it in the view of the object that you will be rating.

7. Make it pretty with CSS

At this point you will have a list of numbers that you can click on to set the rating. Using the star_rating.gif image and css from Komodo Media

Star Rating

8. Patch for acts_as_authenticated

As mentioned in Step 4 the acts_as_authenticated plugin must be patched to work with Ajax requests.

If you have already installed the acts_as_authenticated plugin you will need to patch it in 2 locations because the acts_as_authenticated plugin is a code generating plugin.

8A. First we’ll patch the plugin itself

The following code is inserted at line 70 into the access_denied method

file: vendor\plugins\acts_as_authenticated\generators\authenticated\templates\authenticated_system.rb

I have made a patch available for download: acts_as_authenticated Ajax Patch

8B. Second we’ll patch the lib file that the plugin generated

This is were we add the same patch to the existing lib file that was created by the acts_as_authenticated plugin if you have already installed it into your rails app. This is the exact same code as shown in 8A. In this case I will show you the whole code for the access_denied method.

file: lib\authenticated_system.rb

9. Test Cases

Finally it wouldn’t be a rails app if you didn’t include the test. Below you’ll find two test to confirm that the rating system works and that the user must be logged in to rate an object.

Out in the wild

Here’s where you can go check it out on the website Obsidian Portal http://www.obsidianportal.com/campaigns/list.

Note: As mentioned above the rating table uses a user_id to make sure that there is only one rating per user per object.

References

http://komodomedia.com/blog/index.php/2006/01/09/css-star-rating-part-deux/

http://komodomedia.com/blog/index.php/2007/01/20/css-star-rating-redux/

http://swik.net/Rails/Dave+Naffis+-+Rails,+Ruby,+Randomness/Ruby+on+Rails,+Ajax+&+CSS+Star+Rating+System/kqn0

http://www.igvita.com/blog/2006/09/02/4-state-rails-ajax-css-rating/

Alternatives Ideas

http://www.robarov.be/rate/

http://www.robarov.be/rate2/

Update 2007-11-30: All code corrections from the comments have been integrated into this article..


Share and Enjoy:
These icons link to social bookmarking sites where readers can share and discover new web pages.

  • Digg
  • del.icio.us
  • DZone
  • BlinkList
  • Furl
  • Reddit
  • StumbleUpon
  • Technorati

49 Responses to “Ajax CSS Star Rating with acts_as_rateable”

  1. Jonas Says:

    Thanks so much for this amazing and very clear tutorial !!

    I have tried to night the implementation on a small site I’m currently developing. All worked fine except at one point.
    For example, if I rate an object with 2, I can’t see after the two stars taking
    the orange color.

    What do you think it is ?

    Sorry if it’s a stupid question … I’m new to rails and css and I really appreciate your help guys :-)

    J.

  2. Ryan Says:

    Jonas,

    It appears to be to be an issue with the CSS source highlighting that I use: http://erik.range-it.de/wordpress/plugins/syntaxhighlighter/. It is replacing an the word bottom with bottombottom in the background-position element.

    I strongly recommend clicking on the “View Plain” link at the top of the source box to when you copy the source code so that you are coping exactly what I have typed.

    Hope this helps. When you get it up and running feel free to drop a link with the website that is implementing this post.

  3. Midnight Oil » Blog Archive » Find the top 5 highest rated objects with acts_as_rateable Says:

    [...] you’ve got the Ajax CSS Star Rating System with acts_as_rateable (A MUST READ). Now you want to list the top rated objects and how many votes they’ve [...]

  4. Alfred Toh Says:

    Just a heads up to anyone who is trying to implement this, the actual acts_as_rateable plugin to use is

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

    the blog post is using: svn://rubyforge.org/var/svn/rateableplugin/trunk

    which I think is wrong…

  5. Ryan Says:

    Alfred,

    Nice catch. They both are called acts_as_rateable. The correct one is from http://www.juixe.com/. I have update the post. Thanks for the feedback.

  6. Shawn Says:

    Hi, is there any way to make this plugin tally votes numerically instead, like reddit? Where users can +/- votes and they can reach negative numbers?

    Thanks for the great tutorial!

  7. Shawn Says:

    Never mind, I figured it out. If anyone else is interested in doing so, you can simply add a method called “total_rating” or something of the similar and define it as so:

    def total_rating
    total = 0
    ratings.each { |r|
    total = total + r.rating
    }
    total
    end

  8. Dave Says:

    To any noobs giving this a shot, there are a couple of small typos here. In section 4

    script/generate controller ratings rate

    Should be

    script/generate controller rating rate

    Also

    render :update do |page|
    page.replace_html “ratings-block-#{rateable.id}”, :partial => “rate”, :locals => { :asset => rateable }
    page.visual_effect :highlight, “ratings-block-#{rateable.id}”
    end

    Should be

    render :update do |page|
    page.replace_html “star-ratings-block-#{rateable.id}”, :partial => “rate”, :locals => { :asset => rateable }
    page.visual_effect :highlight, “star-ratings-block-#{rateable.id}”
    end

    Outside of those 2 small issues, great write-up!

  9. Micah Says:

    @Dave,

    Thanks for the typo catches. Ryan (the author) is out of town, but I’ll make sure he gets your comments. If he can confirm the typos we’ll fix them.

    Thanks again.

  10. Mitchell Says:

    I found another small error. In the css find .one-star and change it to .one-stars this will let let you rate something 1 star instead of the first star being two stars.

  11. Mitchell Says:

    ^edit^
    This is only if you use the downloaded css from komodo media.

  12. Ajax CSS Star Rating in Ruby on Rails with acts_as_rateable Says:

    [...] read the comments as well to pick up a couple of tips and get some typos in the main post corrected.read more | digg [...]

  13. Andrew Says:

    Help!
    Noob trying to get this working in a project.
    Second go around and I must say I appreciate everyones work getting this as clear (and correct) as possible.

    In _rate.rhtml should all “asset” references be changed to “myobject” basically?

    Thanks!!!
    Andrew

  14. Andrew Says:

    Sorry for that last post. I figured out my last question with a bit more perseverance and seem to be making progress. Sorry about the poor question. The answer to my question was “no”.

    I am still having trouble though. The data does not seem to be actually hooked to the backend though its displaying pretty dynamic stars in my view. Clicking on them just does not change anything. I am worried I need to do some include statements to use the javascript. I am using the latest version of InstantRails as my base.
    What else can I check for???

    As before. Any and all help is GREATLY appreciated.

    Andrew

  15. JJ Says:

    Anyway to do partial stars?
    instead of half or full..to show 4.3 of a star, possibly without making a billion different renders of the images?

  16. kaushik Says:

    Andrew,
    Make sure you have enabled javascript. The best way to check is to see the source of the web-page. You should see javascript tags there. If you don’t have it, put this in your application.rhtml layout:

    I am still not able to save the rankings into the database, but I can see in the log file that the view is collecting the right data. Post back your progress, if you can.

    Kaushik

  17. kaushik Says:

    Andrew,
    The form ate my javascript include command, but you should be able to google for it. It is a simple one-liner.

    Kasuhik

  18. Figure Cancellations » Blog Archive » links for 2007-08-06 Says:

    [...] Midnight Oil » Blog Archive » Ajax CSS Star Rating with acts_as_rateable (tags: rails ratings tutorial ajax cakephp) [...]

  19. Steve Says:

    Alright, noob question… what goes in the myobject spot where the partial is being rendered? Everything I try is undefined or wrong number of args.

  20. Ryan Says:

    Thanks everyone for the great feedback. Sorry for the delay in updates to this post. I have returned from my travels and will be updating this post shortly. Also, I will have a new revamped of this version of this post in the coming weeks.

  21. Seth Says:

    Hey there - really cool tute!

    Just wondering if in the revamped version you could add a ‘non-ajaxed’ version? Thanks :)

  22. Ryan Says:

    Seth,
    Thanks for the comment.

    With the revamp I plan on continuing with the Ajax version; however, I do plan to make it more non-Ajax friendly by adding a redirect_back_or_default if not Ajax.

    In the meantime, I think if you change the link_to_remote with a form_tag. Then add the following to the bottom of the rate function within RatingsController. (This is NOT tested).

    redirect_back_or_default(home_url)

    If you want to use it with the Ajax version, try something like this:

    respond_to do |format|
    format.html { redirect_back_or_default(home_url) }
    end

  23. Phillip McGill Says:

    Hey all, I’m glad someone asked about a non-ajaxed version, just what I was after!

    Thanks for the awesome tute - a real great walkthough on how to set it all up!

    Just curious though, I call quite a few times on a page (in the list action) and it’s making an SQL query ( SELECT count(*) AS count_all FROM ratings WHERE (ratings.rateable_id = 1 AND ratings.rateable_type = ‘Business’) ) for every (in my case) Business.

    I have just finished off a tute from Ryan Bates about counter_cache-ing and I tried implementing that into the business model BUT it didn’t work because of the polymorphic relationship…

    Just curious if anyone has implemented a counter_cache-ing approach?

    Thanks.

  24. Phillip McGill Says:

    Hello again, just another question :).

    Just wondering if there is a method to pull the acts_as_rateable model attributes for a specific user?

    My acts_as_rateable model is Businesses, so I can call @business.ratings.each do |b| b.user.login end - this will obviously return all the users that have rated that particular business… I would like to know how to do this in reverse…

    So pull a user and get the businesses that they have rated…

    So far I have this in the users controller which will only return the business id:
    @businesses = Rating.find(:all,
    :conditions => [”user_id = ?”, @user.id],
    :order => “created_at DESC”)

    Does anyone have any ideas?

    Thanks

  25. Ryan Says:

    Phillip,

    acts_as_rateable has 2 methods built in that you should check out:

    find_ratings_by_user
    rated_by_user?

    I suggest opening up your console (ruby script\console) and try the following:

    @user = User.find_by_id(1)
    @businesses = Business.find_ratings_by_user(@u)
    @businesses.each { |r| print “#{r.rating} | #{r.rateable_id}” }

    And…

    @user = User.find_by_id(1)
    @business = Business.find_by_id(1)
    @rated_by_user = @business.rated_by_user?(@user)

    I am working on a patch to acts_as_rateable to return the top rated objects with the number supplied by the user. So you can say Business.find_top_rated(10) to get the top 10 best rated Businesses. I haven’t completed it yet.

    Ryan

  26. Phillip McGill Says:

    Ryan,

    I have tried using find_ratings_by_user and it only gives back the rateable_id, I want to get back the properties of that id. In my case it’s the business model (it has a name column).

    @user = User.find_by_id(1)
    @businesses = Business.find_ratings_by_user(@u)
    @businesses.each { |r| print “#{r.rating} | #{r.rateable_id}” }

    I’d also like to be able to call: r.business.name, r.business.created_at et cetera.

    Thanks for replying so quick, you’ve been the greatest help!

  27. Phillip McGill Says:

    Okies - I still couldn’t figure it out so I wrote some SQL that seems to make it work pretty good. In the acts_as_rateable.rb file in the lib I added:

    # Helper class method to lookup model attributes associated with acts_as_rateable
    def find_businesses_ratings_by_user(user)
    @businesses = find_by_sql(”SELECT * FROM Businesses INNER JOIN Ratings ON Businesses.id = Ratings.rateable_id WHERE Ratings.user_id = #{user.id}”)
    end

    This lets me do:

    And I get all the info I need :) - if anyone knows a better way to do this I would be interested in knowing.

    Thanks

  28. Phillip McGill Says:

    Hmm problem 1 - the form ate my junk, it looked like
    @businesses.each do |r|
    r.name, r.email (all taken from the business model

    Problem 2 - this SQL is returning the ratings tables id as the businesses id…

    Any ideas? Thanks

  29. Phil Says:

    Awesome tutorial! I changed the images to BE NOT SO BIG :-) but otherwise used pretty much as is (with typo corrections).
    Thanks!

  30. samurails Says:

    hi this is an awesome tutorial!
    really helped me along
    now i have 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 an idea how to solve this?
    thx

  31. James O'Kelly Says:

    I am having the same routing issue as Sam. When I figure it out I will post.

  32. James O'Kelly Says:

    Doh :)

    You need to add this to your routes.

    map.resources :rating, :member => { :rate => :put }

    or just add map.resources :rating and then change the def rate to def new so it is restful.

  33. James O'Kelly Says:

    sorry, change the :put to :post.

  34. LAB Says:

    I keep getting a RJS error in all my browsers (Firefox, IE 6 & 7). The error says: TypeError ($element) has no properties. Then another popup comes up with the contents of the html replacement code that is trying to run. I’ve pin pointed the code that is causing it and it is this part in the controller:

    render :update do |page|
    page.replace_html “star-ratings-block-#{rateable.id}”, :partial => “rate”, :locals => { :asset => rateable }
    page.visual_effect :highlight, “star-ratings-block-#{rateable.id}”
    end

    If I remove that code, the error goes away, but I also do not get the immediate refresh to the page. Has anyone else seen this? It only happens when running the server in development mode, in production mode the error does not occur.

    The piece on the html page that I am trying to refresh, is inside a table, so I thought that might be a problem. But, after moving the code outside the table, I still see the errors.

    Any ideas? Thanks!!

  35. samurails Says:

    thanx james, it worked just fine

  36. ./script/generate migration England » Blog Archive » How to patch the acts_as_rateable plugin for Rails 2.0 Says:

    [...] For that, I started with the Midnight Oil tutorial [...]

  37. Marion Says:

    I am implementing this plugin on a site but need to be able to rate multiple things prior to saving. For example, if I wanted to rate the food(4 stars), service(5 stars) and atmosphere(4 stars). How should I approach this using the tutorial provided.

    Thanks in advance

  38. Ryan Says:

    Marion,

    Basically as mentioned in the post is that you just create a model, add that acts_as_rateable to the object. Then in the controller when you’re creating a new rating just make sure to specify the class_name and the id, which map to the rateable_type and rateable_type_id.

    Note, this post requires each item to be an model. If you make food, service, or atmosphere attributes of the Resturant model, this method will NOT work and you’ll have to come up with something clever.

    If food, service, or atmosphere are all separate models that belong_to a restaurant you can do this:

    class Restaurant < ActiveRecord::Base
    acts_as_rateable
    has_one :service
    ...
    end

    class Service < ActiveRecord::Base
    acts_as_rateable
    belongs_to : Restaurant
    ...
    end

    @restaurant = Restaurant.find(:first)

    < %= render :partial => “rating/rate”, :locals => { :asset => @restaurant } %>
    < %= render :partial => “rating/rate”, :locals => { :asset => @restaurant.service } %>

    Everything else is automatic, hope it helps…

  39. Marion Newman Says:

    Thank you for the response, it certainly got me much closer. Now I am getting stuck with making the updates to the controller and am getting an error. I am not quite sure what you mean when you say update class and id. Please understand that I am relatively new to ruby. Here’s what I have in a nutshell

    I am first trying to display a page with Host information that will allow users to rate multiple things.

    HostController
    def show
    @host = Host.find(params[:id])
    @hostRating = Host.find(:first)
    end

    I created a class called Support that belongs to a Host.

    class Support < ActiveRecord::Base
    acts_as_rateable
    belongs_to :Host

    @supportRating = Host.find(:first)
    end

    My partial sets the asset collection
    “rate”, :locals => { :asset => @supportRating.support } %>

    Now I have made no update to the ratings controller because I am a little confused. It looks like the _rate partial references the class that is passed via the collection. Does it look something like this: rateable = @host.support.find(params[:id])

    Any direction here would be great. I am trying to make sense of this and feel like I am getting close to truly understanding the logic.

    Btw…everything else is the same as what you have in your tutorial (ie. rate partial)

    Thanks again for your time and help!

    Marion

  40. David Pham Says:

    Hello!

    For some reason, when I click on a rating when I’m not logged in, it redirects me to the login page (as it should), however, when I am logged in and I click on a rating, it doesn’t work - it doesn’t rate the content.

    Please help!
    David Pham

  41. Ryan Says:

    David,

    That’s an existing issue that I know about.. I haven’t found a great way to solve it yet though. If you use the acts_as_authenticated store_location it only stores the javascript.. I’ll let you know if I get it figured out.

    Ryan

  42. PJ Says:

    Great post!

  43. LAB Says:

    Love the plugin! I would like to add an additional field to the table, called thing_id. I’ve tried passing another local variable called :thing_id => @thing.id, but when I render the partial in the view, I get this error from the controller:

    NameError (undefined local variable or method `thing_id’ for #):

    This is how i changed the render partial call in my view:
    “ratings/rate”, :locals => { :asset => current_user, :thing_id => @thing.id } %>

    why wouldn’t the local variable “thing_id” be available to me to use in the Ratings Controller in the Rate action?

    Thanks for any insight!!

  44. LAB Says:

    whoops … the error got cut off … here is the full text:

    NameError (undefined local variable or method `thing_id’ for #):

  45. Zach Wentz Says:

    Okay. Say you wanted to have multiple models to be rated in one view. Well you’ll find that it still rates correctly upon a page refresh, but the javascript only effects the first partial. This is easily rectified. You just have to change two pieces of code and your done.

    The first you’ll have to change is in your ratings/rate partial:

    change this…
    <div id=”star-ratings-block-” class=”star-ratings-block”>

    to this…
    <div id=”star-ratings-block-” class=”star-ratings-block”>

    And then change you rating controller:

    change this….
    render :update do |page|
    page.replace_html “star-ratings-block-#{rateable.id}”, :partial => “rate”, :locals => { :asset => rateable }
    page.visual_effect :highlight, “star-ratings-block-#{rateable.id}”
    end

    to this…
    render :update do |page|
    page.replace_html “star-ratings-block-#{@rateable_class}”, :partial => “rate”, :locals => { :asset => rateable }
    page.visual_effect :highlight, “star-ratings-block-#{@rateable_class}”
    end

  46. Zach Wentz Says:

    Wordpress ate my code in the portion of my comment. What you want to change is asset.id to asset.class. We do this so that the javascript finds the partial to update by it’s name rather than the id.

  47. Patrick Crowley Says:

    The current rating portion of the rating/rate partial assumes that your star container will always be 150px in length.

    If you use relative units here, designers can resize the star rater without touching your Rails app code.

    http://pastie.caboo.se/188152

  48. ravi Says:

    Hi,

    I have used this plugin in my application.functionality is working.
    But stars colours didn’t change with mouse over.and Current rating is not displaying above the stars..

    please give me solution for this.

    thanks,
    ravi.

  49. Nerb Says:

    I’ve been using this plugin for months now. whenever a user rates something, the browser shrinks the text to 150% smaller, after save???

    anyone else have that happen?

    So if i rate a product a 4 star, the ajax call will happen, the browser will refresh that one part and bammo… the text is written in 3 pt font. i refresh the page, and the call is made, and all that jazz, just it’s a css issue. obviously it’s something i did… but wanted to report, for others.

    Also, i can’t get those methods to work. I just want to print what my user rated, beside a review of the product. having trouble with the existing methods, and with find_all_by_user_id_and_rateable_type_and_rateable_id(params[:whatever]) isn’t working all that well.

    thoughts?

Leave a Reply

WP Theme & Icons by N.Design Studio
Entries RSS Comments RSS Log in