Wolfmans Howlings

A programmers Blog about Programming solutions and a few other issues

Developing a social networking site part 2 - rating stars

Posted by Jim Morris on Sat Jun 23 13:49:25 -0700 2007

In part 1 I outlined my project to implement snowdogsr.us a social networking site for snow dogs.

I am pleased to announce that version 1 of this site is up, however I had to make some trade-offs to get it up this far. I needed to prioritize my goals and just get the essentials implemented.

So the ability to create Posts, Events and Places, and add photos of your dogs was goal number 1. The linking to friends was postponed, although most of the logic was implemented, I couldn't figure out how to expose that in an intuitive UI.

The first thing I wanted was to be able to rate the various Posts and Places people entered, and how better to do that than to copy the Netflix and Google groups method of showing 5 stars and clicking on them to rate the item.

To that end some googling found an excellent CSS method to do this, all I had to do was integrate it into Rails. I had some extra business rules I needed to deal with

  • Only people logged in can rate an article
  • The creator of the article can not rate their own article
  • If the rating is readonly then it still needs to be displayed as stars, just not clickable ones.

Out of the box the Stars were always clickable, so I needed a way to show readonly stars when the user was not allowed to rate the article, luckily the above referenced blog explained how to do that in the blogs comments.

I'm using the acts_as_rated plugin, which works fine so long as you don't try using the optimized method of creating a stats table, that method didn't seem to work, and for now I'll just let the SQL do the math.

As I have many models that acts_as_rated I needed to write a helper to display the stars, and handle the rating clicks, and the business logic.

The helpers look a little ugly as you need to figure out what the parent model is.

So here is the code that goes in application_helper.rb

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
def star_rating(rating, obj_type, id, allow_rate= true)
  per= rating > 0 ? (rating/5.0)*100 : 0;
  url_meth= "rate_#{obj_type}_path".to_sym
  if allow_rate
    links= [
      link_to('1', send(url_meth, {:id => id, :rate => 1}), :method => :put, :class => "one-star", :title =>"1 star out of 5"),
      link_to('2', send(url_meth, {:id => id, :rate => 2}), :method => :put, :class => "two-stars", :title =>"2 stars out of 5"),
      link_to('3', send(url_meth, {:id => id, :rate => 3}), :method => :put, :class => "three-stars", :title =>"3 stars out of 5"),
      link_to('4', send(url_meth, {:id => id, :rate => 4}), :method => :put, :class => "four-stars", :title =>"4 stars out of 5"),
      link_to('5', send(url_meth, {:id => id, :rate => 5}), :method => :put, :class => "five-stars", :title =>"5 stars out of 5")]
  end
 
  r= " "
  r += "<span class=\"inline-rating\">  <ul class=\"star-rating\"> <li class=\"current-rating\" style=\"width:#{per}%;\"></li>"
 
  # if we can't rate then just show stars
  if allow_rate
    (0..4).each do |i|
      r += "<li>#{links[i]}</li>"
    end
  end
 
  r += "</ul></span>"
  r += '<span class="prompt">Click star to rate</span>' if allow_rate
  r
end
 
def render_rating(o, allow_rating=true
  can_rate= allow_rating && logged_in? && ! o.created_by?(current_user)
  o_type= o.class.to_s.downcase
 
  if o.rated?
    r= "Rating #{o.rating_average}/5 by #{pluralize(o.rated_count, 'person')}"
    r += star_rating(o.rating_average, o_type, o.id, can_rate)
  elsif can_rate
    r= "Not yet Rated"
    r += star_rating(0, o_type, o.id, can_rate)
  else
    r= ""
  end
  r
end

The render_rating method is what you call from your view, the star_rating method generates the HTML.

(I presume you included the star_rating.css which is available from the above mentioned blog, and the acts_as_rated plugin.)

The render_rating method takes the model object being rated, and an optional override to allow_rating or not (which determines if the stars are clickable or not).

I include some text saying what the rating is and how many people have rated the article, followed by the stars showing the rating.

I have all my ratings set to 1 to 5, YMMV.

In my view (HAML) code I have...

= render_rating(place)

Which will render the rating for the current place article.

the Place model has this code...

acts_as_rated(:rating_range => 1..5, :rater_class => 'Person')

because I have a Person class as the rater rather than the default User.

The rating percentage is calculated using..

  per= rating > 0 ? (rating/5.0)*100 : 0;

Which is used by the CSS to highlight the stars correctly.

I use RESTful routing so the links are generated using named routes, however as the named route will be different depending on the Model object being rated I need to use this code...

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# added to routes.rb to provide a rate method
map.resources :places, :member => { :rate => :put }
 
# added to places_controller.rb to handle the new route
 def rate
    @place = Place.find(params[:id])
    rating= params[:rate].to_i
    @place.rate(rating, current_user.person)
 
    respond_to do |format|
      format.html { redirect_to :back }
      format.xml  { head :ok }
    end
 
  end
 
# code snippets from the application helper above
# get the named route to use in this case rate_place_path
url_meth= "rate_#{obj_type}_path".to_sym
 
...
 
# generate the named route, equivalent to
# rate_place_path(:id => id, :rate => 1)
 
send(url_meth, {:id => id, :rate => 1})

One last thing slightly on topic, I added the following to the acts_as_rated.rb file so I could get a list of the top rated articles, seemed like a logical thing that anyone would want to do ;)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Find the top rated items, ordered by average rating
def find_top_rated(limit=nil)
  rating_class = acts_as_rated_options[:rating_class].constantize
  base_sql = <<-EOS
    select #{table_name}.*,COALESCE(average,0) AS rating_average from #{table_name} left outer join
      (select avg(rating) as average, rated_id 
         from #{rating_class.table_name}
         where rated_type = '#{class_name}'
         group by rated_id) as rated
         on rated_id=id
       order by rating_average DESC
  EOS
  base_sql += " LIMIT #{limit}" if limit
  find_by_sql base_sql
end

I hope this helps others trying the same thing, as always comments and code improvements always welcome.

Posted in Rails  |  Tags rails,acts_as_rated,stars,rating  |  11 comments

Comments

  1. topfunky said on Sat Jun 23 16:04:28 -0700 2007
    I have to take this opportunity to mention one of the worst implementations of star rating I've seen.

    Someone felt that the basic 5 stars wasn't good enough and decided to make a spectrum that reflects the percentage of votes for each rating. The result is a very confusing graphic.

    http://www.summize.com/about.html

    In _Information Dashboard Design_, Stephen Few mentions the kinds of graphical differentiation our minds can understand at first glance. Comparing several widths spread horizontally isn't one of them.

    Sometimes it's better to stick with a widely-used meme that people understand!
  2. wolfmanjm said on Sat Jun 23 16:23:49 -0700 2007
    Thats interesting, the engineer in me likes the concept ;) it would be nice to combine the rating and the number of people who rated it in one graphic, but then I never was any good at UI design.
  3. topfunky said on Sun Jun 24 11:39:42 -0700 2007
    Maybe you could overlay a line graph on top of the stars.

    Or, just display the two graphs side-by-side.
  4. Jonathan said on Mon Jun 25 21:09:47 -0700 2007
    Dude, I Love You!!!

    I've been slacking on adding the stars rating to my rails project, but you just gave me a butt kick on getting that rolling. Thanks!
  5. s01ipsist said on Tue Jun 26 22:32:36 -0700 2007
    Great work!
    If the object hasn't been rated it can't have an average rating.
    Changing` o.rating_average` to 0 in the below code saves me a sql call (I'm using the stats table)
    r= "Not yet Rated"
    r += `star_rating(o.rating_average, o_type, o.id, can_rate)`
  6. wolfmanjm said on Tue Jun 26 22:57:01 -0700 2007
    Good catch I'll update the code above
    Thanks
  7. abenamer@yahoo.com said on Mon Feb 25 15:23:29 -0800 2008
    Hi there, I'm using this same code but I keep getting this error:

    "the rater object must be the one used when defining acts_as_rated (or a descendent of it). other objects are not acceptable"

    Did you encounter this error when you were writing this example?
  8. wolfmanjm said on Mon Feb 25 15:29:06 -0800 2008
    no, but then the code is pretty old, and newer versions of rails and/or acts_as_rated may have changed something, and I haven't upgraded either as the current versions work well enough.
    If you find the solution please post here.

    thanks
  9. Zach said on Fri May 23 12:36:12 -0700 2008
    OH HAI! Awesome little tutorial/plugin/I-don't-know here :) I solved abenamer@yahoo.com's problem. It originates in the controller in your rate definition, specifically:
    <code>
    @landlord.rate(rating, current_user.person)
    </code>

    As you said, you used person instead of user, but it might have been vague. Regardless, you need to use something like:
    <code>
    @landlord.rate(rating, current_user)
    </code>
    if you are using the standard user system.

    That is all.
  10. Tom said on Wed Jul 09 13:25:39 -0700 2008
    Has anyone tried to cache the ratings?
    I have a product search screen, and recalculating the rates is expensive, so I was thinking either caching the rates in the database, or fragment cache.

    Any cool ideas?

  11. wolfmanjm said on Wed Jul 09 13:37:42 -0700 2008
    I think the `act_as_rated` plugin has several schemes for the ratings, and one is to keep them in a table rather than calculate them each time, It should be easy to use that option instead of the calculate every time I use here.

(leave email ยป)


Next Article โ†’