return to list of articles

Adding a 5 star ratings feature to a Rails 4 model

A quick tutorial on how to go about adding a star-rating feature to Rails using raty.js

This is a quick tutorial on how to go about adding a simple 5 star rating system that updates a comment’s rating using Ajax to Rails 4 model using the raty.js library. Here we’ll just add ratings to comments for users so go ahead and check out Raty first. Download the raty.min.js file and added it to assets/javascripts/raty.mis.js and add the following to your application.js manifest:

//= require raty.min.js

Also download the 2 star images and add them to your images path:

# assets/images/star-on.png
# assets/images/star-off.png

In your Ruby on Rails application, fist add a Rating model:

rails g model Rating comment:references user:references score:integer default: 0

Run the migrations and prepare your test database

rake db:migrate db:test:prepare

Add your associations:

# Rating.rb
class Rating < ActiveRecord::Base
  belongs_to :comment
  belongs_to :user
end

# Comment.rb
class Comment < ActiveRecord::Base
  has_many :ratings
  belongs_to :user
end

# User.rb
class User < ActiveRecord::Base
  has_many :ratings
  has_many :comments
end

You’ll want to add the route for updating ratings. Only the update action is required.

# Routes
resources :ratings, only: :update

In your view, you’ll want to display the average ratings for a comment and the user’s rating seperately:

<p>Averate rating</p>
<div id="star"></div>

<p>Your rating</p>
<div id="user_star"></div>

Now add the JavaScript needed to display both ratings and update the current user’s rating. @comment is assumed to be defined in the current view’s action. In the current action, you need to define @rating, you may want to do it like so:

@rating = Rating.where(comment_id: @comment.id, user_id: @current_user.id).first
unless @rating
  @rating = Rating.create(comment_id: @comment.id, user_id: @current_user.id, score: 0)
end

The path is added to ensure the images are loaded. The first rating block is set to read-only so the user can’t click on that and change the score. The average rating is loaded from the method which is defined below.

<script>
  $('#star').raty({
    readOnly: true,
    score: <%= @comment.average_rating %>,
    path: '/assets'
  });

  $('#user_star').raty({
    score: <%= @rating.score %>,
    path: '/assets',
    click: function(score, evt) {
      $.ajax({
        url: '/ratings/' + <%= @rating.id %>,
        type: 'PATCH',
        data: { score: score }
      });
    }
  });
</script>

Now add controllers/ratings_controller.rb and you can create the update action and assign the instance variables we added above.

class RatingsController < ApplicationController

  def update
    @rating = Rating.find(params[:id])
    @comment = @rating.comment
    if @rating.update_attributes(score: params[:score])
      respond_to do |format|
        format.js
      end
    end
  end

end

The comment model houses the method that fetches the average rating for any one comment:

def average_rating
  ratings.sum(:score) / ratings.size
end

Finally inside of app/views/ratings/update.js.erb add some JavaScript to run after the rating’s score has been updated in the controller. This and the above JavaScript can be DRYed up.

$('#star').raty({
  readOnly: true,
  score: <%= @comment.average_rating %>,
  path: '/assets'
});

$('#user_star').raty({
  score: <%= @rating.score %>,
  path: '/assets',
  click: function(score, evt) {
    $.ajax({
      url: '/ratings/' + <%= @rating.id %>,
      type: 'PATCH',
      data: { score: score }
    }).done(function (data){});
  }
});

If you spot a mistake here, please shout at me on Twitter and I’ll fix it.


Get notified when Pawel releases new posts, guides, or projects