diff --git a/db/up/reviews.sql b/db/up/reviews.sql index 50ae9df5f..89a30ee32 100644 --- a/db/up/reviews.sql +++ b/db/up/reviews.sql @@ -3,7 +3,7 @@ CREATE TABLE reviews ( user_id VARCHAR(64) NOT NULL REFERENCES users(id) ON DELETE CASCADE, target_id VARCHAR(64) NOT NULL, target_type VARCHAR(32) NOT NULL, - description VARCHAR(8000), + description VARCHAR, rating INT NOT NULL, deleted_by_user_id VARCHAR(64) REFERENCES users(id) ON DELETE SET NULL, deleted_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NULL, diff --git a/ruby/lib/jam_ruby/models/review.rb b/ruby/lib/jam_ruby/models/review.rb index cbd0648d3..157790e43 100644 --- a/ruby/lib/jam_ruby/models/review.rb +++ b/ruby/lib/jam_ruby/models/review.rb @@ -1,67 +1,93 @@ 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("*")} + scope :all, -> { select("*") } - validates :rating, presence:true, numericality: {only_integer: true, minimum:1, maximum:5} + 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, 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."} - class << self - def index(options={}) - if(options.key?(:include_deleted)) - arel = Review.all - else - arel = Review.available - end + after_save :reduce - 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 + def self.index(options={}) + if options.key?(:include_deleted) + arel = Review.all + else + arel = Review.available end - # Create review_summary records by grouping reviews - def reduce() - 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 # each - end # transaction - end # reduce - - def 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) + if options.key?(:target_id) + arel = arel.where("target_id=?", options[:target_id]) end - end # self + 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).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") + .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) + 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 end end \ No newline at end of file diff --git a/ruby/spec/jam_ruby/models/review_spec.rb b/ruby/spec/jam_ruby/models/review_spec.rb index a8cc13cc7..7a5851948 100644 --- a/ruby/spec/jam_ruby/models/review_spec.rb +++ b/ruby/spec/jam_ruby/models/review_spec.rb @@ -57,8 +57,6 @@ describe Review do Review.index.should have(2).items # Reduce and check: - ReviewSummary.should have(0).items - Review.reduce() ReviewSummary.should have(1).items ReviewSummary.first.avg_rating.should eq(4.0) @@ -68,8 +66,7 @@ describe Review do # Create some more and verify: 5.times {Review.create(target:target, rating:5, user:FactoryGirl.create(:user))} Review.index.should have(7).items - - Review.reduce() + ReviewSummary.should have(1).items ReviewSummary.first.wilson_score.should > ws_orig ReviewSummary.first.avg_rating.should > avg_orig @@ -127,9 +124,6 @@ describe Review do review2.valid?.should be_true Review.should have(2).items - # Reduce and check: - ReviewSummary.should have(0).items - Review.reduce() ReviewSummary.should have(1).items ReviewSummary.first.avg_rating.should eq(4.0) @@ -139,7 +133,6 @@ describe Review do # Create some more and verify: 5.times {Review.create(target:target, rating:5, user:FactoryGirl.create(:user))} - Review.reduce() ReviewSummary.should have(1).items ReviewSummary.first.wilson_score.should > ws_orig ReviewSummary.first.avg_rating.should > avg_orig @@ -150,7 +143,6 @@ describe Review do 5.times {Review.create(target:target2, rating:5, user:FactoryGirl.create(:user))} Review.index.should have(12).items Review.index(target_id: target2).should have(5).items - Review.reduce() summaries = ReviewSummary.index() summaries.should have(2).items summaries[0].wilson_score.should > summaries[1].wilson_score