module JamRuby class Review < ActiveRecord::Base include HtmlSanitize html_sanitize strict: [:description] attr_accessible :target, :rating, :description, :user, :user_id, :target_id, :target_type belongs_to :target, polymorphic: true belongs_to :user, foreign_key: 'user_id', class_name: "JamRuby::User" belongs_to :deleted_by_user, foreign_key: 'deleted_by_user_id', class_name: "JamRuby::User" scope :available, -> { where("deleted_at iS NULL") } scope :all, -> { select("*") } validates :description, length: {maximum: 16000}, no_profanity: true, :allow_blank => true validates :rating, presence: true, numericality: {only_integer: true, minimum: 1, maximum: 5} validates :target, presence: true validates :user_id, presence: true validates :target_id, uniqueness: {scope: :user_id, message: "There is already a review for this User and Target."} validate :requires_lesson after_save :reduce def requires_lesson if target_type == 'JamRuby::User' # you are rating a student lesson = LessonSession.joins(:music_session).where('music_sessions.user_id = ?', target.id).where(teacher_id: user.id).first if lesson.nil? errors.add(:target, "You must have at least scheduled or been in a lesson with this student") end elsif target_type == "JamRuby::Teacher" # you are rating a teacher lesson = LessonSession.joins(:music_session).where('music_sessions.user_id = ?', user.id).where(teacher_id: target.user.id).first if lesson.nil? errors.add(:target, "You must have at least scheduled or been in a lesson with this teacher") end end end def self.create_or_update(params) review = Review.where(user_id: params[:user].id).where(target_id: params[:target].id).where(target_type: params[:target].class.to_s).first if review review.description = params[:description] review.rating = params[:rating] review.save else review = Review.create(params) end review end def self.create(params) review = Review.new review.target = params[:target] review.user = params[:user] review.rating = params[:rating] review.description = params[:description] review.target_type = params[:target].class.to_s review.save review end def self.index(options={}) if options.key?(:include_deleted) arel = Review.all else arel = Review.available end if options.key?(:target_id) arel = arel.where("target_id=?", options[:target_id]) end if options.key?(:user_id) arel = arel.where("user_id=?", options[:user_id]) end arel end # Create review_summary records by grouping reviews def self.reduce_all ReviewSummary.transaction do ReviewSummary.destroy_all Review.select("target_id, target_type AS target_type, AVG(rating) as avg_rating, count(*) as review_count, SUM(CASE WHEN rating>=3.0 THEN 1 ELSE 0 END) AS pos_count") .where("deleted_at IS NULL") .group("target_type, target_id") .each do |r| wilson_score = ci_lower_bound(r.pos_count, r.review_count) ReviewSummary.create!( target_id: r.target_id, target_type: r.target_type, avg_rating: r.avg_rating, wilson_score: wilson_score, review_count: r.review_count ) end end end # http://www.evanmiller.org/how-not-to-sort-by-average-rating.html def self.ci_lower_bound(pos, n, confidence=0.95) pos=pos.to_f n=n.to_f return 0 if n == 0 z = 1.96 # Statistics2.pnormaldist(1-(1-confidence)/2) phat = 1.0*pos/n (phat + z*z/(2*n) - z * Math.sqrt((phat*(1-phat)+z*z/(4*n))/n))/(1+z*z/n) end def reduce ReviewSummary.transaction do ReviewSummary.where(target_type: target_type, target_id: target_id).delete_all Review.select("target_id, target_type AS target_type, AVG(rating) as avg_rating, count(*) as review_count, SUM(CASE WHEN rating>=3.0 THEN 1 ELSE 0 END) AS pos_count") .where("deleted_at IS NULL") .where(target_type: target_type, target_id: target_id) .group("target_type, target_id") .each do |r| wilson_score = Review.ci_lower_bound(r.pos_count, r.review_count) summary = ReviewSummary.create( target_id: r.target_id, target_type: r.target_type, avg_rating: r.avg_rating, wilson_score: wilson_score, review_count: r.review_count ) if summary.errors.any? puts "review summary unable to be created #{summary.errors.inspect}" raise "review summary unable to be created #{summary.errors.inspect}" end end end return true end end end