开发者

How can I sort my records by average rating?

开发者 https://www.devze.com 2023-03-06 05:18 出处:网络
I have a table of venues which I\'m displaying on the venues index page as partials. I also have a table of reviews where one venue can have many reviews and each review has a rating 1-5.

I have a table of venues which I'm displaying on the venues index page as partials. I also have a table of reviews where one venue can have many reviews and each review has a rating 1-5.

I'm trying to get the venues to display on the index page with the ones with the highest average rating at the top and descending.

The controller code looks like this:

Venues controller

def index
    if
      @venues = Venue.with_type(params[:venuetypes]).with_area(params[:areas]).joins(:reviews).order("reviews.rating DESC")
    else
      @venues = Venue.all
    end
  end

This gives this kind of a result:

  • If venue 1 has a 5 star review it shows the venue partial at the top of the list.

  • If venue 2 has a 5 star review and a 1 star review it shows two partials, one at the top and one

    at the bottom of the list.

  • If venue 3 has a 5 star review, a 3 star review and a 1 star review it shows three partials, one at the top, one in the middle and one at the bottom of the list.

I just want one partial showing per venue but positioned in the list by the average rating, I feel theres a .average or something missing somewhere how can I acheive this?

Thanks for any help its much appreciated!

edit

Venue model

class Venue < ActiveRecord::Base
  attr_accessible :name, :addressline1, :addressline2, :addressline3, :addressline4, :postcode, :phonenumber, :about, :icontoppx, :iconleftpx, :area_id, :venuetype_id, :lat, :long, :venuephotos_attributes
  belongs_to :area
  belongs_to :venuetype
  has_many :reviews
  has_many :venuephotos

  accepts_nested_attributes_for :venuephotos, :allow_destroy => true

  scope :with_type, lambda { |types|
开发者_JS百科    types.present? ? where(:venuetype_id => types) : scoped }

  scope :with_area, lambda { |areas|
    areas.present? ? where(:area_id => areas) : scoped }

  def to_param
    "#{id}-#{name.gsub(/\W/, '-').downcase}"
  end

  def add_rating(rating_opts)
    @venue.add_rating(:rating => rating, :reviewer => params[:rating][:reviewer])
    self.reviews.create(rating_opts)
    self.update_rating!
  end

  def update_rating!
    s = self.reviews.sum(:rating)
    c = self.reviews.count
    self.update_attribute(:average_rating, s.to_f / c.to_f)
    self.save(:validate => false)
  end
end

Development log for adding a review

Started POST "/venues/44-rating-test-5/reviews" for 127.0.0.1 at 2011-05-18 09:24:24 +0100
  Processing by ReviewsController#create as JS
  Parameters: {"utf8"=>"✓", "authenticity_token"=>"GZWd67b5ocJOjwKI6z9nJInBXxvQahHrjUtUpdm9oJE=", "review"=>{"rating"=>"5", "title"=>"5 star review"}, "venue_id"=>"44-rating-test-5"}
  [1m[36mVenue Load (1.0ms)[0m  [1mSELECT `venues`.* FROM `venues` WHERE (`venues`.`id` = 44) LIMIT 1[0m
  [1m[35mUser Load (0.0ms)[0m  SELECT `users`.* FROM `users` WHERE (`users`.`id` = 3) LIMIT 1
  [1m[36mSQL (0.0ms)[0m  [1mBEGIN[0m
  [1m[35mSQL (2.0ms)[0m  describe `reviews`
  [1m[36mAREL (0.0ms)[0m  [1mINSERT INTO `reviews` (`title`, `created_at`, `updated_at`, `venue_id`, `user_id`, `rating`) VALUES ('5 star review', '2011-05-18 08:24:24', '2011-05-18 08:24:24', NULL, 3, 5)[0m
  [1m[35mSQL (27.0ms)[0m  COMMIT
  [1m[36mSQL (0.0ms)[0m  [1mBEGIN[0m
  [1m[35mAREL (0.0ms)[0m  UPDATE `reviews` SET `venue_id` = 44, `updated_at` = '2011-05-18 08:24:24' WHERE (`reviews`.`id` = 90)
  [1m[36mSQL (23.0ms)[0m  [1mCOMMIT[0m
  [1m[35mSQL (1.0ms)[0m  SELECT COUNT(*) FROM `reviews` WHERE (`reviews`.venue_id = 44)
  [1m[36mUser Load (0.0ms)[0m  [1mSELECT `users`.* FROM `users` WHERE (`users`.`id` = 3) LIMIT 1[0m
Rendered reviews/_review.html.erb (9.0ms)
Rendered reviews/create.js.erb (22.0ms)
Completed 200 OK in 220ms (Views: 56.0ms | ActiveRecord: 54.0ms)

edit create review method (reviews controller)

def create
    @review = current_user.reviews.create!(params[:review])
    @review.venue = @venue
    if @review.save
      flash[:notice] = 'Thank you for reviewing this venue!'
      respond_to do |format|
        format.html { redirect_to venue_path(@venue) }
        format.js
      end
    else
      render :action => :new
    end
  end


To add to NoICE's answer, by hooking into the :after_add and :after_remove association callbacks, you don't have to remember to call a special add_rating method.

class Venue < ActiveRecord::Base
  has_many :reviews, :after_add => :update_average_rating, :after_remove => :update_average_rating

  def update_average_rating(review=nil)
    s = self.reviews.sum(:rating)
    c = self.reviews.count
    self.update_attribute(:average_rating, c == 0 ? 0.0 : s / c.to_f)
  end

end

Also, you'll want to check count for 0 to prevent division by zero.

When you create the review, you must append with << or concat it to the venue object's reviews association so that the callback gets triggered. For example, this will associate the review to the venue, create the review (INSERT into the db), and trigger the callback:

@venue = Venue.find(params[:venue_id])
@venue.reviews << Review.new(params[:review])

This will create the review but won't trigger the callback, even if the venue_id is the params:

Review.create(params[:review])

If you really want to get your action to trigger the callback, you can change your code to:

def create
  @review = Review.new(params[:review].merge({ :user => current_user, :venue => @venue })
  if @review.valid? and @venue.reviews << @review
  ...

To fix it expediently, though, you could just add @review.venue.update_average_rating just before the line w/ flash[:notice].


If I understand correctly, you have model venue, which have has_many :reviews, and each review has collumn "rating".

I'm oferring alternative code for example given by Michael which should be a LOT MORE faster and millions-of-records ready, but it requires some processing then review is added (covered in this example), which give you huge performance boost when the records are selected, ordered and shown:

Create a migration which adds average_rating as float:

add_collumn :venues, :average_rating, :float, :default => 0.0, :null => false
add_index :venues, :average_rating

Now, in your controller:

# perhaps add paginate at the end instead of .all ...
@venues = Venue.with_type(params[:venuetypes]).with_area(params[:areas]).order("average_rating DESC").all

The model updated:

class Venue < ActiveRecord::Base
  has_many :reviews

  # you'll need to create ratings for this venue via this method, so everything is atomic
  # and transaction safe
  # parameter is hash, so you can pass as many review parameters as you wish, e.g.
  # @venue.add_rating(:rating => rating, :reviewer => params[:rating][:reviewer])
  # or
  # @venue.add_rating(params[:rating])
  # :)
  def add_rating(rating_opts)
    # you can of course add as 
    self.reviews.create(rating_opts)
    self.update_rating!
  end

  # let's update average rating of this venue
  def update_rating!
    s = self.reviews.sum(:rating)
    c = self.reviews.count
    self.average_rating = s.to_f / c.to_f
    self.save(:validate => false)
    # or you can use .update_attribute(:average_rating, s.to_f / c.to_f)
  end

end

Hope this helps. Please ask if you have any questions.

Regards, NoICE


You could create an average_rating method on Venue and then simply:

@venues = Venue.with_type(params[:venuetypes]).with_area(params[:areas]).includes(:reviews).sort_by(&:average_rating).reverse

The method:

class Venue
  def average_rating
    ratings = reviews.map(&:rating)
    ratings.sum.to_f / ratings.size
  end
end

This solution is probably not optimal if there's a huge number of records or if performance is critical, but it's very simple and it works.


Let us suppose that reviews are editable, then no answers work well. So I have done like below.

class Venue < ActiveRecord::Base
    has_many :reviews, :dependent => :delete_all
end

And now in Reviews model as below.

class Venue < ActiveRecord::Base
    belongs_to :venue

    # If reviews are editable
    after_save :update_average_rating 

    # If reviews are deletable
    before_destroy :update_average_rating 

    private
      def update_average_rating
       s = self.venue.reviews.sum(:rating)
       c = self.venue.reviews.count
       self.venue.update_attribute(:average_rating, c == 0 ? 0.0 : s / c.to_f)
  end
end
0

精彩评论

暂无评论...
验证码 换一张
取 消