Search code examples
ruby-on-railsaveragerating

Table column that automatically calculates average rating


I'm following tutorial and have models user, hotel and rating. Users can create hotels, and users can rate them. Users rating value is recorded to table rating together with user_id and hotel_id. When I render partial <%= render "hotels/hotels_list", :@hotels => Hotel.all %> it shows list of hotels with their average rating that calculates in model hotel Model Hotel.rb :

class Hotel < ActiveRecord::Base
  attr_accessible :user_id
  belongs_to :user
  has_many :ratings
  has_many :raters, :through => :ratings, :source => :users

  def average_rating
    @value = 0
    self.ratings.each do |rating|
      @value = @value + rating.value
    end
    @total = self.ratings.size
    '%.2f' % (@value.to_f / @total.to_f)
  end
end

Model User.rb :

class User < ActiveRecord::Base
  has_many :hotels
  has_many :ratings
  has_many :rated_hotels, :through => :ratings, :source => :hotels
end

Model Rating.rb :

class Rating < ActiveRecord::Base
  attr_accessible :value
  belongs_to :user
  belongs_to :hotel
end

I need to sort list of hotels by average rating, maybe need to add some column average_rating that will be at once calculate average value like that average_rating method in hotel model, so than I can easily access to it. How can I solve this issue? RatingsController.rb

class RatingsController < ApplicationController

      before_filter :authenticate_user!
      def create
        @hotel = Hotel.find_by_id(params[:hotel_id])
        @rating = Rating.new(params[:rating])
        @rating.hotel_id = @hotel.id
        @rating.user_id = current_user.id
        if @rating.save
          respond_to do |format|
            format.html { redirect_to hotel_path(@hotel), :notice => "Your rating has been saved" }
            format.js
          end
        end
      end

      def update
        @hotel = Hotel.find_by_id(params[:hotel_id])
        @rating = current_user.ratings.find_by_hotel_id(@hotel.id)
        if @rating.update_attributes(params[:rating])
          respond_to do |format|
            format.html { redirect_to hotel_path(@hotel), :notice => "Your rating has been updated" }
            format.js
          end
        end
      end 
    end

Solution

  • Very simple. First, you would add the average_rating column to your Hotel model with a migration. Then, you would add a callback to your Rating model which updates the value in the Hotel model. Basically, every time a rating is created, destroyed, or updated, you need to update the average rating. It would look something like this:

    class Hotel < ActiveRecord::Base
      [ code snipped ]
    
      def update_average_rating
        @value = 0
        self.ratings.each do |rating|
          @value = @value + rating.value
        end
        @total = self.ratings.size
    
    
        update_attributes(average_rating: @value.to_f / @total.to_f)
      end
    end
    
    class Rating
      belongs_to :hotel
      after_create :update_hotel_rating
    
      def update_hotel_rating
        hotel.update_average_rating
      end
    end
    

    Now you can easily sort by rating. I'm leaving some details out but I think you can get the general idea here.