diff --git a/.travis.yml b/.travis.yml index bd227bb..53bca8b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,8 @@ language: ruby -rvm: +rvm: - "1.9.2" - - "1.9.3" - - jruby-19mode # JRuby in 1.9 mode + - "1.9.3" + - "2.0.0" - rbx-18mode - rbx-19mode # uncomment this line if your project needs to run something other than `rake`: diff --git a/Gemfile b/Gemfile index 7c23576..64b3d4d 100644 --- a/Gemfile +++ b/Gemfile @@ -1,4 +1,4 @@ -source "http://rubygems.org" +source "http://rubygems.org" gem 'jquery-rails' diff --git a/Gemfile.lock b/Gemfile.lock index 9a4df5a..3180bfa 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,60 +1,45 @@ PATH remote: . specs: - letsrate (1.0.8) + letsrate (1.0.9) GEM remote: http://rubygems.org/ specs: - actionpack (3.2.2) - activemodel (= 3.2.2) - activesupport (= 3.2.2) - builder (~> 3.0.0) + actionpack (4.0.2) + activesupport (= 4.0.2) + builder (~> 3.1.0) erubis (~> 2.7.0) - journey (~> 1.0.1) - rack (~> 1.4.0) - rack-cache (~> 1.1) - rack-test (~> 0.6.1) - sprockets (~> 2.1.2) - activemodel (3.2.2) - activesupport (= 3.2.2) - builder (~> 3.0.0) - activesupport (3.2.2) - i18n (~> 0.6) - multi_json (~> 1.0) - builder (3.0.0) + rack (~> 1.5.2) + rack-test (~> 0.6.2) + activesupport (4.0.2) + i18n (~> 0.6, >= 0.6.4) + minitest (~> 4.2) + multi_json (~> 1.3) + thread_safe (~> 0.1) + tzinfo (~> 0.3.37) + atomic (1.1.14) + builder (3.1.4) erubis (2.7.0) - hike (1.2.1) - i18n (0.6.0) - journey (1.0.3) - jquery-rails (2.0.1) - railties (< 5.0, >= 3.2.0) - thor (~> 0.14) - json (1.6.5) - multi_json (1.1.0) - rack (1.4.1) - rack-cache (1.2) - rack (>= 0.4) - rack-ssl (1.3.2) - rack - rack-test (0.6.1) + i18n (0.6.9) + jquery-rails (3.0.4) + railties (>= 3.0, < 5.0) + thor (>= 0.14, < 2.0) + minitest (4.7.5) + multi_json (1.8.2) + rack (1.5.2) + rack-test (0.6.2) rack (>= 1.0) - railties (3.2.2) - actionpack (= 3.2.2) - activesupport (= 3.2.2) - rack-ssl (~> 1.3.2) + railties (4.0.2) + actionpack (= 4.0.2) + activesupport (= 4.0.2) rake (>= 0.8.7) - rdoc (~> 3.4) - thor (~> 0.14.6) - rake (0.9.2.2) - rdoc (3.12) - json (~> 1.4) - sprockets (2.1.2) - hike (~> 1.2) - rack (~> 1.0) - tilt (!= 1.3.0, ~> 1.1) - thor (0.14.6) - tilt (1.3.3) + thor (>= 0.18.1, < 2.0) + rake (10.1.1) + thor (0.18.1) + thread_safe (0.1.3) + atomic + tzinfo (0.3.38) PLATFORMS ruby diff --git a/README.md b/README.md index db582d0..e9d049e 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,12 @@ Speed : <%= rating_for @car, "engine", :star => 7 %> Speed : <%= rating_for @car, "price" %> ``` +You can use the rating_for_user helper method to show the star rating for the user. + +```erb +Speed : <%= rating_for_user @car, current_user, "speed", :star => 10 %> +``` + ## Feedback If you find bugs please open a ticket at [https://github.com/muratguzel/letsrate/issues](https://github.com/muratguzel/letsrate/issues) diff --git a/lib/generators/letsrate/letsrate_generator.rb b/lib/generators/letsrate/letsrate_generator.rb index ec35dab..c9bd0eb 100644 --- a/lib/generators/letsrate/letsrate_generator.rb +++ b/lib/generators/letsrate/letsrate_generator.rb @@ -2,39 +2,39 @@ require 'rails/generators/active_record' class LetsrateGenerator < ActiveRecord::Generators::Base include Rails::Generators::Migration - - source_root File.expand_path('../templates', __FILE__) - + + source_root File.expand_path('../templates', __FILE__) + desc "copying jquery.raty files to assets directory ..." def copying copy_file 'jquery.raty.js', 'app/assets/javascripts/jquery.raty.js' copy_file 'star-on.png', 'app/assets/images/star-on.png' copy_file 'star-off.png', 'app/assets/images/star-off.png' copy_file 'star-half.png', 'app/assets/images/star-half.png' - copy_file 'letsrate.js', 'app/assets/javascripts/letsrate.js.erb' + copy_file 'letsrate.js.erb', 'app/assets/javascripts/letsrate.js.erb' copy_file 'rater_controller.rb', 'app/controllers/rater_controller.rb' - end - + end + desc "model is creating..." - def create_model + def create_model model_file = File.join('app/models', "#{file_path}.rb") raise "User model (#{model_file}) must exits." unless File.exists?(model_file) class_collisions 'Rate' - template 'model.rb', File.join('app/models', "rate.rb") + template 'model.rb', File.join('app/models', "rate.rb") template 'cache_model.rb', File.join('app/models', "rating_cache.rb") - end - + end + def add_rate_path_to_route - route "match '/rate' => 'rater#create', :as => 'rate'" + route "post '/rate' => 'rater#create', :as => 'rate'" end desc "cacheable rating average migration is creating ..." def create_cacheable_migration migration_template "cache_migration.rb", "db/migrate/create_rating_caches.rb" end - + desc "migration is creating ..." - def create_migration - migration_template "migration.rb", "db/migrate/create_rates.rb" + def create_letsrate_migration + migration_template "migration.rb", "db/migrate/create_rates.rb" end -end \ No newline at end of file +end diff --git a/lib/generators/letsrate/templates/cache_migration.rb b/lib/generators/letsrate/templates/cache_migration.rb index db29d55..8eb063a 100644 --- a/lib/generators/letsrate/templates/cache_migration.rb +++ b/lib/generators/letsrate/templates/cache_migration.rb @@ -1,9 +1,9 @@ class CreateRatingCaches < ActiveRecord::Migration - + def self.up create_table :rating_caches do |t| t.belongs_to :cacheable, :polymorphic => true - t.float :avg, :null => false + t.float :avg, :null => false t.integer :qty, :null => false t.string :dimension t.timestamps @@ -14,6 +14,6 @@ def self.up def self.down drop_table :rating_caches - end - + end + end \ No newline at end of file diff --git a/lib/generators/letsrate/templates/cache_model.rb b/lib/generators/letsrate/templates/cache_model.rb index 74d9e0a..1a2b2eb 100644 --- a/lib/generators/letsrate/templates/cache_model.rb +++ b/lib/generators/letsrate/templates/cache_model.rb @@ -1,3 +1,3 @@ class RatingCache < ActiveRecord::Base - belongs_to :cacheable, :polymorphic => true + belongs_to :cacheable, :polymorphic => true end \ No newline at end of file diff --git a/lib/generators/letsrate/templates/jquery.raty.js b/lib/generators/letsrate/templates/jquery.raty.js index 53d369b..26f53bf 100644 --- a/lib/generators/letsrate/templates/jquery.raty.js +++ b/lib/generators/letsrate/templates/jquery.raty.js @@ -27,7 +27,7 @@ return this.each(function() { var self = this, $this = $(self).empty(); - + self.opt = $.extend(true, {}, $.fn.raty.defaults, settings); $this.data('settings', self.opt); @@ -47,7 +47,7 @@ } if (self.opt.score) { - self.opt.score = methods.between(self.opt.score, 0, self.opt.number); + self.opt.score = methods.between(self.opt.score, 0, self.opt.number); } for (var i = 1; i <= self.opt.number; i++) { @@ -71,7 +71,7 @@ } if (self.opt.iconRange) { - methods.fill.call(self, self.opt.score); + methods.fill.call(self, self.opt.score); } methods.setTarget.call(self, self.opt.score, self.opt.targetKeep); @@ -186,7 +186,7 @@ }); }, cancel: function(isClick) { return $(this).each(function() { - var self = this, + var self = this, $this = $(self); if ($this.data('readonly') === true) { @@ -424,7 +424,7 @@ return methods.init.apply(this, arguments); } else { $.error('Method ' + method + ' does not exist!'); - } + } }; $.fn.raty.defaults = { diff --git a/lib/generators/letsrate/templates/letsrate.js b/lib/generators/letsrate/templates/letsrate.js deleted file mode 100644 index 901f29f..0000000 --- a/lib/generators/letsrate/templates/letsrate.js +++ /dev/null @@ -1,27 +0,0 @@ -$.fn.raty.defaults.path = "/assets"; -$.fn.raty.defaults.half_show = true; - -$(function(){ - $(".star").raty({ - score: function(){ - return $(this).attr('data-rating') - }, - number: function() { - return $(this).attr('data-star-count') - }, - click: function(score, evt) { - $.post('<%= Rails.application.class.routes.url_helpers.rate_path %>', - { - score: score, - dimension: $(this).attr('data-dimension'), - id: $(this).attr('data-id'), - klass: $(this).attr('data-classname') - }, - function(data) { - if(data) { - // success code goes here ... - } - }); - } - }); -}); \ No newline at end of file diff --git a/lib/generators/letsrate/templates/letsrate.js.erb b/lib/generators/letsrate/templates/letsrate.js.erb new file mode 100644 index 0000000..83a4ff8 --- /dev/null +++ b/lib/generators/letsrate/templates/letsrate.js.erb @@ -0,0 +1,36 @@ +$.fn.raty.defaults.path = "/assets"; +$.fn.raty.defaults.half_show = true; + +$(function(){ + $(".star").each(function() { + var $readonly = ($(this).attr('data-readonly') == 'true'); + $(this).raty({ + score: function(){ + return $(this).attr('data-rating') + }, + number: function() { + return $(this).attr('data-star-count') + }, + readOnly: $readonly, + click: function(score, evt) { + var _this = this; + $.post('<%= Rails.application.class.routes.url_helpers.rate_path %>', + { + score: score, + dimension: $(this).attr('data-dimension'), + id: $(this).attr('data-id'), + klass: $(this).attr('data-classname') + }, + function(data) { + if(data) { + // success code goes here ... + + if ($(_this).attr('data-disable-after-rate') == 'true') { + $(_this).raty('set', { readOnly: true, score: score }); + } + } + }); + } + }); + }); +}); \ No newline at end of file diff --git a/lib/generators/letsrate/templates/migration.rb b/lib/generators/letsrate/templates/migration.rb index 20aca0d..a15dc84 100644 --- a/lib/generators/letsrate/templates/migration.rb +++ b/lib/generators/letsrate/templates/migration.rb @@ -1,5 +1,5 @@ class CreateRates < ActiveRecord::Migration - + def self.up create_table :rates do |t| t.belongs_to :rater @@ -15,6 +15,6 @@ def self.up def self.down drop_table :rates - end - + end + end \ No newline at end of file diff --git a/lib/generators/letsrate/templates/model.rb b/lib/generators/letsrate/templates/model.rb index 6ffa934..fd148e2 100644 --- a/lib/generators/letsrate/templates/model.rb +++ b/lib/generators/letsrate/templates/model.rb @@ -1,7 +1,7 @@ class Rate < ActiveRecord::Base belongs_to :rater, :class_name => "<%= file_name.classify %>" belongs_to :rateable, :polymorphic => true - - attr_accessible :rate, :dimension - + + #attr_accessible :rate, :dimension + end \ No newline at end of file diff --git a/lib/generators/letsrate/templates/rater_controller.rb b/lib/generators/letsrate/templates/rater_controller.rb index dc84c4b..7a4bd05 100644 --- a/lib/generators/letsrate/templates/rater_controller.rb +++ b/lib/generators/letsrate/templates/rater_controller.rb @@ -1,19 +1,14 @@ -class RaterController < ApplicationController - - def create - if current_user.present? - obj = eval "#{params[:klass]}.find(#{params[:id]})" - if params[:dimension].present? - obj.rate params[:score].to_i, current_user.id, "#{params[:dimension]}" - else - obj.rate params[:score].to_i, current_user.id - end - - render :json => true +class RaterController < ApplicationController + + def create + if user_signed_in? + obj = params[:klass].classify.constantize.find(params[:id]) + obj.rate params[:score].to_i, current_user, params[:dimension] + + render :json => true else - render :json => false + render :json => false end - end - - -end \ No newline at end of file + end + +end diff --git a/lib/letsrate/helpers.rb b/lib/letsrate/helpers.rb index 119046b..c2130a0 100644 --- a/lib/letsrate/helpers.rb +++ b/lib/letsrate/helpers.rb @@ -1,29 +1,45 @@ -module Helpers - def rating_for(rateable_obj, dimension=nil, options={}) - - if dimension.nil? - klass = rateable_obj.average - else - klass = rateable_obj.average "#{dimension}" - end - - if klass.nil? - avg = 0 - else - avg = klass.avg - end - +module Helpers + def rating_for(rateable_obj, dimension=nil, options={}) + + cached_average = rateable_obj.average dimension + + avg = cached_average ? cached_average.avg : 0 + star = options[:star] || 5 - - content_tag :div, "", "data-dimension" => dimension, :class => "star", "data-rating" => avg, - "data-id" => rateable_obj.id, "data-classname" => rateable_obj.class.name, - "data-star-count" => star - - + + disable_after_rate = options[:disable_after_rate] || true + + readonly = !(current_user && rateable_obj.can_rate?(current_user, dimension)) + + content_tag :div, '', "data-dimension" => dimension, :class => "star", "data-rating" => avg, + "data-id" => rateable_obj.id, "data-classname" => rateable_obj.class.name, + "data-disable-after-rate" => disable_after_rate, + "data-readonly" => readonly, + "data-star-count" => star end - + + def rating_for_user(rateable_obj, rating_user, dimension = nil, options = {}) + @object = rateable_obj + @user = rating_user + @rating = Rate.find_by_rater_id_and_rateable_id_and_dimension(@user.id, @object.id, dimension) + stars = @rating ? @rating.stars : 0 + + disable_after_rate = options[:disable_after_rate] || false + + readonly=false + if disable_after_rate + readonly = current_user.present? ? !rateable_obj.can_rate?(current_user.id, dimension) : true + end + + content_tag :div, '', "data-dimension" => dimension, :class => "star", "data-rating" => stars, + "data-id" => rateable_obj.id, "data-classname" => rateable_obj.class.name, + "data-disable-after-rate" => disable_after_rate, + "data-readonly" => readonly, + "data-star-count" => stars + end + end class ActionView::Base include Helpers -end \ No newline at end of file +end diff --git a/lib/letsrate/model.rb b/lib/letsrate/model.rb index 8dd319d..d1ab93e 100644 --- a/lib/letsrate/model.rb +++ b/lib/letsrate/model.rb @@ -1,101 +1,116 @@ require 'active_support/concern' module Letsrate extend ActiveSupport::Concern - - def rate(stars, user_id, dimension=nil) - if can_rate? user_id, dimension - rates(dimension).build do |r| + + def rate(stars, user, dimension=nil, dirichlet_method=false) + dimension = nil if dimension.blank? + + if can_rate? user, dimension + rates(dimension).create! do |r| r.stars = stars - r.rater_id = user_id - r.save! - end + r.rater = user + end + else + previous_rate = rates(dimension).where(:rater_id => user_id).first + previous_rate.stars = stars + previous_rate.save! + end + + if dirichlet_method + update_rate_average_dirichlet(stars, dimension) + else update_rate_average(stars, dimension) + end + end + + def update_rate_average_dirichlet(stars, dimension=nil) + ## assumes 5 possible vote categories + dp = {1 => 1, 2 => 1, 3 => 1, 4 => 1, 5 => 1} + stars_group = Hash[rates(dimension).group(:stars).count.map{|k,v| [k.to_i,v] }] + posterior = dp.merge(stars_group){|key, a, b| a + b} + sum = posterior.map{ |i, v| v }.inject { |a, b| a + b } + davg = posterior.map{ |i, v| i * v }.inject { |a, b| a + b }.to_f / sum + + if average(dimension).nil? + RatingCache.create! do |avg| + avg.cacheable_id = self.id + avg.cacheable_type = self.class.name + avg.qty = 1 + avg.avg = davg + avg.dimension = dimension + end else - raise "User has already rated." + a = average(dimension) + a.qty = rates(dimension).count + a.avg = davg + a.save!(validate: false) end - end - + end + def update_rate_average(stars, dimension=nil) if average(dimension).nil? - RatingCache.create do |avg| + RatingCache.create! do |avg| avg.cacheable_id = self.id avg.cacheable_type = self.class.name avg.avg = stars avg.qty = 1 avg.dimension = dimension - avg.save! - end + end else a = average(dimension) - a.avg = (a.avg*a.qty + stars) / (a.qty+1) - a.qty = a.qty + 1 - a.save! - end - end - + a.qty = rates(dimension).count + a.avg = rates(dimension).average(:stars) + a.save!(validate: false) + end + end + def average(dimension=nil) - if dimension.nil? - self.send "rate_average_without_dimension" - else - self.send "#{dimension}_average" - end - end - - def can_rate?(user_id, dimension=nil) - val = self.connection.select_value("select count(*) as cnt from rates where rateable_id=#{self.id} and rateable_type='#{self.class.name}' and rater_id=#{user_id} and dimension='#{dimension}'").to_i - if val == 0 - true - else - false - end - end - + dimension ? self.send("#{dimension}_average") : rate_average_without_dimension + end + + def can_rate?(user, dimension=nil) + user.ratings_given.where(dimension: dimension, rateable_id: id, rateable_type: self.class.name).size.zero? + end + def rates(dimension=nil) - if dimension.nil? - self.send "rates_without_dimension" - else - self.send "#{dimension}_rates" - end + dimension ? self.send("#{dimension}_rates") : rates_without_dimension end - + def raters(dimension=nil) - if dimension.nil? - self.send "raters_without_dimension" - else - self.send "#{dimension}_raters" - end + dimension ? self.send("#{dimension}_raters") : raters_without_dimension end module ClassMethods - + def letsrate_rater - has_many :ratings_given, :class_name => "Rate", :foreign_key => :rater_id - end - + has_many :ratings_given, :class_name => "Rate", :foreign_key => :rater_id + end + def letsrate_rateable(*dimensions) - has_many :rates_without_dimension, :as => :rateable, :class_name => "Rate", :dependent => :destroy, :conditions => {:dimension => nil} - has_many :raters_without_dimension, :through => :rates_without_dimension, :source => :rater - - has_one :rate_average_without_dimension, :as => :cacheable, :class_name => "RatingCache", - :dependent => :destroy, :conditions => {:dimension => nil} - - - dimensions.each do |dimension| - has_many "#{dimension}_rates", :dependent => :destroy, - :conditions => {:dimension => dimension.to_s}, - :class_name => "Rate", - :as => :rateable - - has_many "#{dimension}_raters", :through => "#{dimension}_rates", :source => :rater - - has_one "#{dimension}_average", :as => :cacheable, :class_name => "RatingCache", - :dependent => :destroy, :conditions => {:dimension => dimension.to_s} - end + has_many :rates_without_dimension, -> { where dimension: nil}, :as => :rateable, :class_name => "Rate", :dependent => :destroy + has_many :raters_without_dimension, :through => :rates_without_dimension, :source => :rater + + has_one :rate_average_without_dimension, -> { where dimension: nil}, :as => :cacheable, + :class_name => "RatingCache", :dependent => :destroy + + + dimensions.each do |dimension| + has_many "#{dimension}_rates".to_sym, -> {where dimension: dimension.to_s}, + :dependent => :destroy, + :class_name => "Rate", + :as => :rateable + + has_many "#{dimension}_raters".to_sym, :through => "#{dimension}_rates", :source => :rater + + has_one "#{dimension}_average".to_sym, -> { where dimension: dimension.to_s }, + :as => :cacheable, :class_name => "RatingCache", + :dependent => :destroy + end end end - -end + +end class ActiveRecord::Base include Letsrate -end \ No newline at end of file +end diff --git a/lib/letsrate/version.rb b/lib/letsrate/version.rb index 6425c87..eed8328 100644 --- a/lib/letsrate/version.rb +++ b/lib/letsrate/version.rb @@ -1,3 +1,3 @@ module Letsrate - VERSION = "1.0.8" + VERSION = "1.0.9" end \ No newline at end of file