Вопрос

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
Это было полезно?

Решение

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.

Другие советы

In the example by @muffinista you should probably do it more 'Ruby-ish' and do it in a single line:

def update_average_rating
  update_attributes(average_rating: self.ratings.collect(&:value).avg)
end

If you expect nil's you can do .compact.avg.

You need to extend Array with #avg:

class Array
  def avg
    sum.to_f/size
  end
end
Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top