Ruby on Rails, Ajax & CSS Star Rating System
August 31st, 2006
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.
Washington DC Ruby on Rails Users Group
August 28th, 2006
I’m creating a Washington DC area Ruby on Rails Users Group. If you’re interested in getting involved check out www.dcrug.org for more information.
RejectMail.com - Free Receive Only Email
August 20th, 2006
RejectMail.com is a free receive only email service.
Give out any address @rejectmail.com and then come to rejectmail.com to check your email or use the RSS feed provided for updates.
It’s a bit ugly right now but hopefully when I have some time I’ll clean it up and add a few more features.
Again, this was written entirely with Ruby on Rails.
Adzmo.com - Location Based Shopping Tool
August 15th, 2006
Adzmo.com is a location based shopping tool. By sending a short text message, like the one you see below, adzmo.com can tell you where the closest shops and restaurants are.
Just sent a text message to any category of product, like pizza@adzmo.com. In the body of your message include the phone number of any nearby store, restaurant, or merchant. Within seconds you’ll receive a detailed list of nearby merchants matching the category your provided.
This one was also created with RoR.
Receiving Emails and Attachments with Rails
August 14th, 2006
Does your Rails app need to handle incoming emails with the attachments? All of the examples I’ve seen so far show you how to insert email attachments into the DB. Here’s a quick example that uses RailsCron to poll a POP3 account every minute for new emails and stores the attachments on the filesystem. If you need help using or running RailsCron see my previous posts about the topic.
The Agile book has a good example that kicks off a runner script but I think this method is far more efficient than having your mail system kick off a separate runner every time a new email is received, especially if you’re dealing in high volume.
It also handles non-responsive or slow responding POP3 servers by setting a high timeout length and retrying a handful of times before it gives up.
This class will check for and hand off any incoming emails:
require 'net/pop'
class EmailQueue < ActiveRecord::Base
background :poll_mail, :every => 1.minute, :concurrent => false
def self.poll_mail
retrycount = 0
begin
timeout(600) do
Net::POP3.start("yourdomain.com", nil, "username", "password") do |pop|
if pop.mails.empty?
logger.info "NO MAIL"
else
pop.mails.each do |email|
begin
logger.info "receiving mail..."
AssetSubmitHandler.receive(email.pop)
email.delete
rescue Exception => e
logger.error "Error receiving email at " + Time.now.to_s + "::: " + e.message
end
end
end
end
end
rescue TimeoutError
if(retrycount < 5)
retrycount+=1
retry
else
logger.info("ERROR Timeout error in poll_mail attempt #" + retrycount.to_s)
nil
end
end
rescue Exception => exception
SystemNotifier.deliver_exception_notification(exception)
logger.info("Error in poll_mail")
logger.info(exception.class.to_s + " " + exception.message.to_s + " " + exception.backtrace.to_s)
end
end
And here’s the code that handles the email:
class AssetSubmitHandler < ActionMailer::Base
# content type should be validated to image/gif, image/jpg, or image/jpeg
def receive(email)
if email.has_attachments?
email.attachments.each do |attachment|
asset = Asset.new
asset.submitter = email.from.first
asset.name = base_part_of(attachment.original_filename)
asset.content_type = attachment.content_type.chomp
base_dir = "/home/someapp/www/"
# save original file
asset.original = "assets/o_#{Time.now.utc.to_i}#{rand(1000000)}."+asset.name
File.open(base_dir+asset.original,File::CREAT|File::TRUNC|File::WRONLY,0666){ |f|
f.write(attachment.read)
}
asset.save
end
end
end
def base_part_of(file_name)
name = File.basename(file_name)
name.gsub(/[^W._-]/, '')
sanitize_filename(name)
end
# Fixes a 'feature' of IE where it passes the entire path instead of just the filename
def sanitize_filename(value)
#get only the filename (not the whole path)
just_filename = value.gsub(/^.*(\\|\/)/, '')
just_filename.gsub(/[^\w\.\-]/,'_')
end
end
My site in an O'Reilly book
August 10th, 2006
I just found out recently that one of my webpages was featured in an O’Reilly book:
Google Maps Hacks: Tips & Tools for Geographic Searching and Remixing
Ok, so it’s a pretty simple page but nonetheless I think it’s pretty cool.
Rails Security Issue and Fix
August 9th, 2006
If you’re running Ruby on Rails you should do an update immediately to get the fix for a newly found security issue.
Here’s some more info about it.
To upgrade run:gem install rails --include-dependencies
Rails 1.0 and earlier versions as well as Rails 1.1.3 aren’t affected.
If you want to freeze your particular version of Rails for an app go to the root directory of that app and run:
rake rails:freeze:gems
Note: The last release of rails (1.1.5) only solves part of the security problem. Update rails today to get 1.1.6.