diff --git a/.travis.yml b/.travis.yml index 53bca8b..ee737e9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,5 @@ language: ruby rvm: - - "1.9.2" - - "1.9.3" - - "2.0.0" - - rbx-18mode - - rbx-19mode + - "2.2.2" # uncomment this line if your project needs to run something other than `rake`: -# script: bundle exec rspec spec \ No newline at end of file +# script: bundle exec rspec spec diff --git a/Gemfile b/Gemfile index 64b3d4d..f48883f 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,10 @@ source "http://rubygems.org" -gem 'jquery-rails' +gem 'jquery-rails', '~> 4.3', '>= 4.3.1' + +ruby '2.2.2' + +# gem 'rspec' # Specify your gem's dependencies in letsrate.gemspec gemspec diff --git a/Gemfile.lock b/Gemfile.lock index 3180bfa..e03c845 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,49 +1,75 @@ PATH remote: . specs: - letsrate (1.0.9) + ratyrate (1.2.2.alpha) GEM remote: http://rubygems.org/ specs: - actionpack (4.0.2) - activesupport (= 4.0.2) - builder (~> 3.1.0) - erubis (~> 2.7.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) - i18n (0.6.9) - jquery-rails (3.0.4) - railties (>= 3.0, < 5.0) + actionpack (5.1.4) + actionview (= 5.1.4) + activesupport (= 5.1.4) + rack (~> 2.0) + rack-test (>= 0.6.3) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (5.1.4) + activesupport (= 5.1.4) + builder (~> 3.1) + erubi (~> 1.4) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activesupport (5.1.4) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (~> 0.7) + minitest (~> 5.1) + tzinfo (~> 1.1) + builder (3.2.3) + concurrent-ruby (1.0.5) + crass (1.0.2) + erubi (1.6.1) + i18n (0.8.6) + jquery-rails (4.3.1) + rails-dom-testing (>= 1, < 3) + railties (>= 4.2.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 (4.0.2) - actionpack (= 4.0.2) - activesupport (= 4.0.2) + loofah (2.1.1) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) + method_source (0.9.0) + mini_portile2 (2.3.0) + minitest (5.10.3) + nokogiri (1.8.1) + mini_portile2 (~> 2.3.0) + rack (2.0.3) + rack-test (0.7.0) + rack (>= 1.0, < 3) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) + rails-html-sanitizer (1.0.3) + loofah (~> 2.0) + railties (5.1.4) + actionpack (= 5.1.4) + activesupport (= 5.1.4) + method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) - rake (10.1.1) - thor (0.18.1) - thread_safe (0.1.3) - atomic - tzinfo (0.3.38) + rake (12.1.0) + thor (0.20.0) + thread_safe (0.3.6) + tzinfo (1.2.3) + thread_safe (~> 0.1) PLATFORMS ruby DEPENDENCIES - jquery-rails - letsrate! + jquery-rails (~> 4.3, >= 4.3.1) + ratyrate! + +RUBY VERSION + ruby 2.2.2p95 + +BUNDLED WITH + 1.15.4 diff --git a/README.md b/README.md index e9d049e..832f76e 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,97 @@ -# Letsrate Rating Gem +# RatyRate Stars Rating Gem -Provides the best way to add rating capabilites to your Rails application with jQuery Raty plugin. +A Ruby Gem that wrap the functionality of [jQuery Raty](https://github.com/wbotelhos/raty) library, and provides optional IMDB style rating. -[![Build Status](https://secure.travis-ci.org/muratguzel/letsrate.png)](http://travis-ci.org/muratguzel/letsrate) -[![Dependency Status](https://gemnasium.com/muratguzel/letsrate.png)](https://gemnasium.com/muratguzel/letsrate) -[![Code Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/muratguzel/letsrate) +[![Gem Version](https://badge.fury.io/rb/ratyrate.svg)](http://badge.fury.io/rb/ratyrate) +[![Build Status](https://travis-ci.org/wazery/ratyrate.svg)](http://travis-ci.org/wazery/ratyrate) +[![Dependency Status](https://gemnasium.com/wazery/ratyrate.svg)](https://gemnasium.com/wazery/ratyrate) +[![Code Climate](https://codeclimate.com/github/wazery/ratyrate.png)](https://codeclimate.com/github/wazery/ratyrate) +[![License](http://img.shields.io/license/MIT.png?color=green)](http://opensource.org/licenses/MIT) +[![Support jQuery Raty](http://img.shields.io/gittip/wbotelhos.svg)](https://www.gittip.com/wazery "Git Tip") +[![Stories in Ready](https://badge.waffle.io/wazery/ratyrate.png?label=ready&title=Ready)](https://waffle.io/wazery/ratyrate) +[![Code Triagers Badge](https://www.codetriage.com/wazery/ratyrate/badges/users.svg)](https://www.codetriage.com/wazery/ratyrate) ## Repository -Find it at [github.com/muratguzel/letsrate](https://github.com/muratguzel/letsrate) +This is a fork against the repository [muratguzel/letsrate](https://github.com/muratguzel/letsrate), the aim of this fork is to refresh the development in this Gem with a new scope and features, so please if you have any pull request issue it here, also I imported all the issues in the original repo. + +## Changelog from the main repository + +1. Exposed a lot of jQuery Raty plugin [features](http://wbotelhos.com/raty) + 1. Added cancel button that can be customized and inserted to left or right + 2. Added the ability to set custom star images for on, half or off state + 3. Added the ability to set a custom path for the images + 4. Added the ability to enable/disable half stars + 5. Added the ability to turn on/off half star +2. Their is a new option now to disable or enable the rating after the first rate +3. Added a star style to show just the score of the dimension, but this star is not editable +4. Added an overall average star just like IMDB style +5. Created a Heroku app to illustrate this Gem's purpose and features (MovieStore) +6. [Wrote a complete tutorial on SitePoint that illustrates how to use this gem](http://www.sitepoint.com/ratyrate-add-rating-rails-app/) +3. Nothing else +4. :wq + +## TODO + +1. Write RSpec tests for this Gem +3. Add option to show the number of users who gave rates +4. Add a share helper to Facebook, Twitter +5. Force refresh after rating when ***disable_after_rate*** and ***imdb_avg*** options is set to true. + +## Detailed view of the new features + +![Detailed view of the new featurews](http://i.imgur.com/m6n25ZF.png) + +## Complete tutorial for this Gem on SitePoint - Ruby + +I wrote a complete tutorial on SitePoint to cover this Gem's features in detail, if you are concerned to learn more about it just pay a visit to this [link](http://www.sitepoint.com/ratyrate-add-rating-rails-app/). ## Instructions ### Install -Add the letsrate gem into your Gemfile +Add the ratyrate gem into your Gemfile ```ruby -gem 'letsrate' +gem 'ratyrate' ``` ### Generate ``` -rails g letsrate User +rails g ratyrate user ``` The generator takes one argument which is the name of your existing devise user model UserModelName. This is necessary to bind the user and rating datas. -Also the generator copies necessary files (jquery raty plugin files, star icons and javascripts) +Also the generator copies necessary files (jQuery Raty plugin files, star icons and JavaScript files) Example: -Suppose you will have a devise user model which name is User. The devise generator and letsrate generator should be like below +Suppose you will have a devise user model which name is User. The devise generator and ratyrate generator should be like below ``` rails g devise:install rails g devise user -rails g letsrate user # => user is the model generated by devise +rails g ratyrate user # => user is the model generated by devise ``` -This generator will create Rate and RatingCache models and link to your user model. +This generator will create Rate and RatingCache models, +db/migrations, +and link to your user model. + +### Database Migration + +Run the migrations: +``` +rake db:migrate +``` + +### Javascript include +``` +//= require jquery.raty +//= require ratyrate +``` ### Prepare @@ -50,19 +101,19 @@ I suppose you have a car model rails g model car name:string ``` -You should add the letsrate_rateable function with its dimensions option. You can add multiple dimensions. +You should add the ratyrate_rateable function with its dimensions option. You can add multiple dimensions. ```ruby class Car < ActiveRecord::Base - letsrate_rateable "speed", "engine", "price" + ratyrate_rateable "speed", "engine", "price" end ``` -Then you need to add a call letsrate_rater in the user model. +Then you need to add a call ratyrate_rater in the user model. ```ruby class User < ActiveRecord::Base - letsrate_rater + ratyrate_rater end ``` @@ -74,25 +125,85 @@ new rating value from authenticated user. ```erb <%# show.html.erb -> /cars/1 %> -Speed : <%= rating_for @car, "speed" %> -Engine : <%= rating_for @car, "engine" %> -Price : <%= rating_for @car, "price" %> +Speed : <%= rating_for @car, 'speed' %> +Engine : <%= rating_for @car, 'engine' %> +Price : <%= rating_for @car, 'price' %> ``` -If you need to change the star number, you should use star option like below. +### Available Options +1- If you need to change the star number, you should use star option like below. ```erb -Speed : <%= rating_for @car, "speed", :star => 10 %> -Speed : <%= rating_for @car, "engine", :star => 7 %> -Speed : <%= rating_for @car, "price" %> +Speed : <%= rating_for @car, 'speed', star: 10 %> +Engine : <%= rating_for @car, 'engine', star: 7 %> +Price : <%= rating_for @car, 'price' %> ``` +2- If you want to disable/enable the rating after user's first rate use the new option *disable_after_rate* +```erb +Speed : <%= rating_for @car, 'speed', disable_after_rate: true %> +``` +To enable changes after first user rate set ```disable_after_rate``` to false -You can use the rating_for_user helper method to show the star rating for the user. +3- To enable half stars use the option *enable_half* +```erb +Speed : <%= rating_for @car, 'speed', enable_half: true %> +``` +4- To show or hide the half stars use *half_show* +```erb +Speed : <%= rating_for @car, 'speed', half_show: true %> +``` +5- To change the path in which the star images (star-on.png, star-off.png, star-half.png, ..etc) are located use +```erb +Speed : <%= rating_for @car, 'speed', star_path: true %> +``` + +To just change one of the star images choose from these options (star_on, star_off, star_half) + +6- To add the cancel button to the left, or right of the stars use **(default is false)** +```erb +Speed : <%= rating_for @car, 'speed', cancel: true %> +``` +7- To change the place of the cancel button (left, or right) use **(default is left)** +```erb +Speed : <%= rating_for @car, 'speed', cancel_place: left %> +``` +8- To change the hint on the cancel button use **(default is "Cancel current rating!" )** +```erb +Speed : <%= rating_for @car, 'speed', cancel_hint: 'Cancel this rating!' %> +``` +9- To change the image of the cancel on button use +```erb +Speed : <%= rating_for @car, 'speed', cancel_on: 'cancel-on2.png' %> +``` +10- To change the image of the cancel off use +```erb + Speed : <%= rating_for @car, 'speed', cancel_off: 'cancel-off2.png' %> +``` +### Other Helpers + +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 %> +``` + +And you can use the *imdb_style_rating_for* to show a similar to IMDB rating style. ```erb -Speed : <%= rating_for_user @car, current_user, "speed", :star => 10 %> +Speed : <%= imdb_style_rating_for @car, current_user %> ``` +## Semantic Versioning + +Ratyrate attempts to follow semantic versioning in the format of `x.y.z`, where: + +`x` stands for a major version (new features that are not backward-compatible). + +`y` stands for a minor version (new features that are backward-compatible). + +`z` stands for a patch (bug fixes). + +In other words: `Major.Minor.Patch`. ## Feedback -If you find bugs please open a ticket at [https://github.com/muratguzel/letsrate/issues](https://github.com/muratguzel/letsrate/issues) +If you find bugs please open a ticket at [https://github.com/wazery/ratyrate/issues](https://github.com/wazery/ratyrate/issues) diff --git a/letsrate.gemspec b/letsrate.gemspec deleted file mode 100644 index b073bdd..0000000 --- a/letsrate.gemspec +++ /dev/null @@ -1,24 +0,0 @@ -# -*- encoding: utf-8 -*- -$:.push File.expand_path("../lib", __FILE__) -require "letsrate/version" - -Gem::Specification.new do |s| - s.name = "letsrate" - s.version = Letsrate::VERSION - s.authors = ["Murat GUZEL"] - s.email = ["guzelmurat@gmail.com"] - s.homepage = "http://github.com/muratguzel/letsrate" - s.summary = %q{Provides the best solution to add rating functionality to your models.} - s.description = %q{Provides the best solution to add rating functionality to your models.} - - s.rubyforge_project = "letsrate" - - s.files = `git ls-files`.split("\n") - s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") - s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } - s.require_paths = ["lib"] - - # specify any dependencies here; for example: - # s.add_development_dependency "rspec" - # s.add_runtime_dependency "rest-client" -end diff --git a/lib/generators/letsrate/letsrate_generator.rb b/lib/generators/letsrate/letsrate_generator.rb deleted file mode 100644 index 825aef6..0000000 --- a/lib/generators/letsrate/letsrate_generator.rb +++ /dev/null @@ -1,40 +0,0 @@ -require 'rails/generators/migration' -require 'rails/generators/active_record' -class LetsrateGenerator < ActiveRecord::Generators::Base - include Rails::Generators::Migration - - 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.erb', 'app/assets/javascripts/letsrate.js.erb' - copy_file 'rater_controller.rb', 'app/controllers/rater_controller.rb' - end - - desc "model is creating..." - 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 'cache_model.rb', File.join('app/models', "rating_cache.rb") - end - - def add_rate_path_to_route - 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" - end -end diff --git a/lib/generators/letsrate/templates/jquery.raty.js b/lib/generators/letsrate/templates/jquery.raty.js deleted file mode 100644 index 26f53bf..0000000 --- a/lib/generators/letsrate/templates/jquery.raty.js +++ /dev/null @@ -1,464 +0,0 @@ -/*! - * jQuery Raty - A Star Rating Plugin - http://wbotelhos.com/raty - * ------------------------------------------------------------------- - * - * jQuery Raty is a plugin that generates a customizable star rating. - * - * Licensed under The MIT License - * - * @version 2.4.5 - * @since 2010.06.11 - * @author Washington Botelho - * @documentation wbotelhos.com/raty - * @twitter twitter.com/wbotelhos - * - * Usage: - * ------------------------------------------------------------------- - * $('#star').raty(); - * - *
- * - */ - -;(function($) { - - var methods = { - init: function(settings) { - return this.each(function() { - var self = this, - $this = $(self).empty(); - - self.opt = $.extend(true, {}, $.fn.raty.defaults, settings); - - $this.data('settings', self.opt); - - if (typeof self.opt.number == 'function') { - self.opt.number = self.opt.number.call(self); - } else { - self.opt.number = methods.between(self.opt.number, 0, 20) - } - - if (self.opt.path.substring(self.opt.path.length - 1, self.opt.path.length) != '/') { - self.opt.path += '/'; - } - - if (typeof self.opt.score == 'function') { - self.opt.score = self.opt.score.call(self); - } - - if (self.opt.score) { - self.opt.score = methods.between(self.opt.score, 0, self.opt.number); - } - - for (var i = 1; i <= self.opt.number; i++) { - $('', { - src : self.opt.path + ((!self.opt.score || self.opt.score < i) ? self.opt.starOff : self.opt.starOn), - alt : i, - title : (i <= self.opt.hints.length && self.opt.hints[i - 1] !== null) ? self.opt.hints[i - 1] : i - }).appendTo(self); - - if (self.opt.space) { - $this.append((i < self.opt.number) ? ' ' : ''); - } - } - - self.stars = $this.children('img:not(".raty-cancel")'); - self.score = $('', { type: 'hidden', name: self.opt.scoreName }).appendTo(self); - - if (self.opt.score && self.opt.score > 0) { - self.score.val(self.opt.score); - methods.roundStar.call(self, self.opt.score); - } - - if (self.opt.iconRange) { - methods.fill.call(self, self.opt.score); - } - - methods.setTarget.call(self, self.opt.score, self.opt.targetKeep); - - var space = self.opt.space ? 4 : 0, - width = self.opt.width || (self.opt.number * self.opt.size + self.opt.number * space); - - if (self.opt.cancel) { - self.cancel = $('', { src: self.opt.path + self.opt.cancelOff, alt: 'x', title: self.opt.cancelHint, 'class': 'raty-cancel' }); - - if (self.opt.cancelPlace == 'left') { - $this.prepend(' ').prepend(self.cancel); - } else { - $this.append(' ').append(self.cancel); - } - - width += (self.opt.size + space); - } - - if (self.opt.readOnly) { - methods.fixHint.call(self); - - if (self.cancel) { - self.cancel.hide(); - } - } else { - $this.css('cursor', 'pointer'); - - methods.bindAction.call(self); - } - - $this.css('width', width); - }); - }, between: function(value, min, max) { - return Math.min(Math.max(parseFloat(value), min), max); - }, bindAction: function() { - var self = this, - $this = $(self); - - $this.mouseleave(function() { - var score = self.score.val() || undefined; - - methods.initialize.call(self, score); - methods.setTarget.call(self, score, self.opt.targetKeep); - - if (self.opt.mouseover) { - self.opt.mouseover.call(self, score); - } - }); - - var action = self.opt.half ? 'mousemove' : 'mouseover'; - - if (self.opt.cancel) { - self.cancel.mouseenter(function() { - $(this).attr('src', self.opt.path + self.opt.cancelOn); - - self.stars.attr('src', self.opt.path + self.opt.starOff); - - methods.setTarget.call(self, null, true); - - if (self.opt.mouseover) { - self.opt.mouseover.call(self, null); - } - }).mouseleave(function() { - $(this).attr('src', self.opt.path + self.opt.cancelOff); - - if (self.opt.mouseover) { - self.opt.mouseover.call(self, self.score.val() || null); - } - }).click(function(evt) { - self.score.removeAttr('value'); - - if (self.opt.click) { - self.opt.click.call(self, null, evt); - } - }); - } - - self.stars.bind(action, function(evt) { - var value = parseInt(this.alt, 10); - - if (self.opt.half) { - var position = parseFloat((evt.pageX - $(this).offset().left) / self.opt.size), - diff = (position > .5) ? 1 : .5; - - value = parseFloat(this.alt) - 1 + diff; - - methods.fill.call(self, value); - - if (self.opt.precision) { - value = value - diff + position; - } - - methods.showHalf.call(self, value); - } else { - methods.fill.call(self, value); - } - - $this.data('score', value); - - methods.setTarget.call(self, value, true); - - if (self.opt.mouseover) { - self.opt.mouseover.call(self, value, evt); - } - }).click(function(evt) { - self.score.val((self.opt.half || self.opt.precision) ? $this.data('score') : this.alt); - - if (self.opt.click) { - self.opt.click.call(self, self.score.val(), evt); - } - }); - }, cancel: function(isClick) { - return $(this).each(function() { - var self = this, - $this = $(self); - - if ($this.data('readonly') === true) { - return this; - } - - if (isClick) { - methods.click.call(self, null); - } else { - methods.score.call(self, null); - } - - self.score.removeAttr('value'); - }); - }, click: function(score) { - return $(this).each(function() { - if ($(this).data('readonly') === true) { - return this; - } - - methods.initialize.call(this, score); - - if (this.opt.click) { - this.opt.click.call(this, score); - } else { - methods.error.call(this, 'you must add the "click: function(score, evt) { }" callback.'); - } - - methods.setTarget.call(this, score, true); - }); - }, error: function(message) { - $(this).html(message); - - $.error(message); - }, fill: function(score) { - var self = this, - number = self.stars.length, - count = 0, - $star , - star , - icon ; - - for (var i = 1; i <= number; i++) { - $star = self.stars.eq(i - 1); - - if (self.opt.iconRange && self.opt.iconRange.length > count) { - star = self.opt.iconRange[count]; - - if (self.opt.single) { - icon = (i == score) ? (star.on || self.opt.starOn) : (star.off || self.opt.starOff); - } else { - icon = (i <= score) ? (star.on || self.opt.starOn) : (star.off || self.opt.starOff); - } - - if (i <= star.range) { - $star.attr('src', self.opt.path + icon); - } - - if (i == star.range) { - count++; - } - } else { - if (self.opt.single) { - icon = (i == score) ? self.opt.starOn : self.opt.starOff; - } else { - icon = (i <= score) ? self.opt.starOn : self.opt.starOff; - } - - $star.attr('src', self.opt.path + icon); - } - } - }, fixHint: function() { - var $this = $(this), - score = parseInt(this.score.val(), 10), - hint = this.opt.noRatedMsg; - - if (!isNaN(score) && score > 0) { - hint = (score <= this.opt.hints.length && this.opt.hints[score - 1] !== null) ? this.opt.hints[score - 1] : score; - } - - $this.data('readonly', true).css('cursor', 'default').attr('title', hint); - - this.score.attr('readonly', 'readonly'); - this.stars.attr('title', hint); - }, getScore: function() { - var score = [], - value ; - - $(this).each(function() { - value = this.score.val(); - - score.push(value ? parseFloat(value) : undefined); - }); - - return (score.length > 1) ? score : score[0]; - }, readOnly: function(isReadOnly) { - return this.each(function() { - var $this = $(this); - - if ($this.data('readonly') === isReadOnly) { - return this; - } - - if (this.cancel) { - if (isReadOnly) { - this.cancel.hide(); - } else { - this.cancel.show(); - } - } - - if (isReadOnly) { - $this.unbind(); - - $this.children('img').unbind(); - - methods.fixHint.call(this); - } else { - methods.bindAction.call(this); - methods.unfixHint.call(this); - } - - $this.data('readonly', isReadOnly); - }); - }, reload: function() { - return methods.set.call(this, {}); - }, roundStar: function(score) { - var diff = (score - Math.floor(score)).toFixed(2); - - if (diff > this.opt.round.down) { - var icon = this.opt.starOn; // Full up: [x.76 .. x.99] - - if (diff < this.opt.round.up && this.opt.halfShow) { // Half: [x.26 .. x.75] - icon = this.opt.starHalf; - } else if (diff < this.opt.round.full) { // Full down: [x.00 .. x.5] - icon = this.opt.starOff; - } - - this.stars.eq(Math.ceil(score) - 1).attr('src', this.opt.path + icon); - } // Full down: [x.00 .. x.25] - }, score: function() { - return arguments.length ? methods.setScore.apply(this, arguments) : methods.getScore.call(this); - }, set: function(settings) { - this.each(function() { - var $this = $(this), - actual = $this.data('settings'), - clone = $this.clone().removeAttr('style').insertBefore($this); - - $this.remove(); - - clone.raty($.extend(actual, settings)); - }); - - return $(this.selector); - }, setScore: function(score) { - return $(this).each(function() { - if ($(this).data('readonly') === true) { - return this; - } - - methods.initialize.call(this, score); - methods.setTarget.call(this, score, true); - }); - }, setTarget: function(value, isKeep) { - if (this.opt.target) { - var $target = $(this.opt.target); - - if ($target.length == 0) { - methods.error.call(this, 'target selector invalid or missing!'); - } - - var score = value; - - if (!isKeep || score === undefined) { - score = this.opt.targetText; - } else { - if (this.opt.targetType == 'hint') { - score = (score === null && this.opt.cancel) - ? this.opt.cancelHint - : this.opt.hints[Math.ceil(score - 1)]; - } else { - score = this.opt.precision - ? parseFloat(score).toFixed(1) - : score; - } - } - - if (this.opt.targetFormat.indexOf('{score}') < 0) { - methods.error.call(this, 'template "{score}" missing!'); - } - - if (value !== null) { - score = this.opt.targetFormat.toString().replace('{score}', score); - } - - if ($target.is(':input')) { - $target.val(score); - } else { - $target.html(score); - } - } - }, showHalf: function(score) { - var diff = (score - Math.floor(score)).toFixed(1); - - if (diff > 0 && diff < .6) { - this.stars.eq(Math.ceil(score) - 1).attr('src', this.opt.path + this.opt.starHalf); - } - }, initialize: function(score) { - score = !score ? 0 : methods.between(score, 0, this.opt.number); - - methods.fill.call(this, score); - - if (score > 0) { - if (this.opt.halfShow) { - methods.roundStar.call(this, score); - } - - this.score.val(score); - } - }, unfixHint: function() { - for (var i = 0; i < this.opt.number; i++) { - this.stars.eq(i).attr('title', (i < this.opt.hints.length && this.opt.hints[i] !== null) ? this.opt.hints[i] : i); - } - - $(this).data('readonly', false).css('cursor', 'pointer').removeAttr('title'); - - this.score.attr('readonly', 'readonly'); - } - }; - - $.fn.raty = function(method) { - if (methods[method]) { - return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); - } else if (typeof method === 'object' || !method) { - return methods.init.apply(this, arguments); - } else { - $.error('Method ' + method + ' does not exist!'); - } - }; - - $.fn.raty.defaults = { - cancel : false, - cancelHint : 'cancel this rating!', - cancelOff : 'cancel-off.png', - cancelOn : 'cancel-on.png', - cancelPlace : 'left', - click : undefined, - half : false, - halfShow : true, - hints : ['bad', 'poor', 'regular', 'good', 'gorgeous'], - iconRange : undefined, - mouseover : undefined, - noRatedMsg : 'not rated yet', - number : 5, - path : 'img/', - precision : false, - round : { down: .25, full: .6, up: .76 }, - readOnly : false, - score : undefined, - scoreName : 'score', - single : false, - size : 16, - space : true, - starHalf : 'star-half.png', - starOff : 'star-off.png', - starOn : 'star-on.png', - target : undefined, - targetFormat : '{score}', - targetKeep : false, - targetText : '', - targetType : 'hint', - width : undefined - }; - -})(jQuery); diff --git a/lib/generators/letsrate/templates/letsrate.js.erb b/lib/generators/letsrate/templates/letsrate.js.erb deleted file mode 100644 index 83a4ff8..0000000 --- a/lib/generators/letsrate/templates/letsrate.js.erb +++ /dev/null @@ -1,36 +0,0 @@ -$.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/USAGE b/lib/generators/ratyrate/USAGE similarity index 100% rename from lib/generators/letsrate/USAGE rename to lib/generators/ratyrate/USAGE diff --git a/lib/generators/ratyrate/ratyrate_generator.rb b/lib/generators/ratyrate/ratyrate_generator.rb new file mode 100644 index 0000000..5e722c1 --- /dev/null +++ b/lib/generators/ratyrate/ratyrate_generator.rb @@ -0,0 +1,67 @@ +require 'rails/generators/migration' +require 'rails/generators/active_record' +class RatyrateGenerator < ActiveRecord::Generators::Base + include Rails::Generators::Migration + + source_root File.expand_path('../templates', __FILE__) + + desc "copying jquery.raty files to assets directory ..." + def copying + copy_file 'jquery.raty.js.erb', '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 'mid-star.png', 'app/assets/images/mid-star.png' + copy_file 'big-star.png', 'app/assets/images/big-star.png' + copy_file 'cancel-on.png', 'app/assets/images/cancel-on.png' + copy_file 'cancel-off.png', 'app/assets/images/cancel-off.png' + copy_file 'ratyrate.js.erb', 'app/assets/javascripts/ratyrate.js.erb' + copy_file 'rater_controller.rb', 'app/controllers/rater_controller.rb' + end + + desc "model is creating..." + 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 'cache_model.rb', File.join('app/models', "rating_cache.rb") + template 'average_cache_model.rb', File.join('app/models', "average_cache.rb") + template 'overall_average_model.rb', File.join('app/models', "overall_average.rb") + end + + def add_rate_path_to_route + 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", migration_version: migration_version + end + + desc "migration is creating ..." + def create_ratyrate_migration + migration_template "migration.rb", "db/migrate/create_rates.rb", migration_version: migration_version + end + + desc "average caches migration is creating ..." + def create_average_caches_migration + migration_template "average_cache_migration.rb", "db/migrate/create_average_caches.rb", migration_version: migration_version + end + + desc "overall averages migration is creating ..." + def create_overall_averages_migration + migration_template "overall_average_migration.rb", "db/migrate/create_overall_averages.rb", migration_version: migration_version + end + + def rails5? + Rails.version.start_with? '5' + end + + def migration_version + if rails5? + "[#{Rails::VERSION::MAJOR}.#{Rails::VERSION::MINOR}]" + end + end + +end diff --git a/lib/generators/ratyrate/templates/average_cache_migration.rb b/lib/generators/ratyrate/templates/average_cache_migration.rb new file mode 100644 index 0000000..abab7ef --- /dev/null +++ b/lib/generators/ratyrate/templates/average_cache_migration.rb @@ -0,0 +1,19 @@ +class CreateAverageCaches < ActiveRecord::Migration<%= migration_version %> + + def self.up + create_table :average_caches do |t| + t.belongs_to :rater + t.belongs_to :rateable, :polymorphic => true + t.float :avg, :null => false + t.timestamps + end + + add_index :average_caches, [:rater_id, :rateable_id, :rateable_type] + end + + def self.down + drop_table :average_caches + end + +end + diff --git a/lib/generators/letsrate/templates/model.rb b/lib/generators/ratyrate/templates/average_cache_model.rb similarity index 59% rename from lib/generators/letsrate/templates/model.rb rename to lib/generators/ratyrate/templates/average_cache_model.rb index fd148e2..b4d2da1 100644 --- a/lib/generators/letsrate/templates/model.rb +++ b/lib/generators/ratyrate/templates/average_cache_model.rb @@ -1,7 +1,4 @@ -class Rate < ActiveRecord::Base +class AverageCache < ActiveRecord::Base belongs_to :rater, :class_name => "<%= file_name.classify %>" belongs_to :rateable, :polymorphic => true - - #attr_accessible :rate, :dimension - -end \ No newline at end of file +end diff --git a/lib/generators/ratyrate/templates/big-star.png b/lib/generators/ratyrate/templates/big-star.png new file mode 100644 index 0000000..9304638 Binary files /dev/null and b/lib/generators/ratyrate/templates/big-star.png differ diff --git a/lib/generators/letsrate/templates/cache_migration.rb b/lib/generators/ratyrate/templates/cache_migration.rb similarity index 83% rename from lib/generators/letsrate/templates/cache_migration.rb rename to lib/generators/ratyrate/templates/cache_migration.rb index 8eb063a..6db8065 100644 --- a/lib/generators/letsrate/templates/cache_migration.rb +++ b/lib/generators/ratyrate/templates/cache_migration.rb @@ -1,4 +1,4 @@ -class CreateRatingCaches < ActiveRecord::Migration +class CreateRatingCaches < ActiveRecord::Migration<%= migration_version %> def self.up create_table :rating_caches do |t| diff --git a/lib/generators/letsrate/templates/cache_model.rb b/lib/generators/ratyrate/templates/cache_model.rb similarity index 100% rename from lib/generators/letsrate/templates/cache_model.rb rename to lib/generators/ratyrate/templates/cache_model.rb diff --git a/lib/generators/ratyrate/templates/cancel-off.png b/lib/generators/ratyrate/templates/cancel-off.png new file mode 100644 index 0000000..a3031f0 Binary files /dev/null and b/lib/generators/ratyrate/templates/cancel-off.png differ diff --git a/lib/generators/ratyrate/templates/cancel-on.png b/lib/generators/ratyrate/templates/cancel-on.png new file mode 100644 index 0000000..08f2493 Binary files /dev/null and b/lib/generators/ratyrate/templates/cancel-on.png differ diff --git a/lib/generators/ratyrate/templates/jquery.raty.js.erb b/lib/generators/ratyrate/templates/jquery.raty.js.erb new file mode 100644 index 0000000..0d7e491 --- /dev/null +++ b/lib/generators/ratyrate/templates/jquery.raty.js.erb @@ -0,0 +1,760 @@ +/*! + * jQuery Raty - A Star Rating Plugin + * + * The MIT License + * + * @author : Washington Botelho + * @doc : http://wbotelhos.com/raty + * @version : 2.7.0 + * + */ + +; +(function($) { + 'use strict'; + + var methods = { + init: function(options) { + return this.each(function() { + this.self = $(this); + + methods.destroy.call(this.self); + + this.opt = $.extend(true, {}, $.fn.raty.defaults, options); + + methods._adjustCallback.call(this); + methods._adjustNumber.call(this); + methods._adjustHints.call(this); + + this.opt.score = methods._adjustedScore.call(this, this.opt.score); + + if (this.opt.starType !== 'img') { + methods._adjustStarType.call(this); + } + + methods._adjustPath.call(this); + methods._createStars.call(this); + + if (this.opt.cancel) { + methods._createCancel.call(this); + } + + if (this.opt.precision) { + methods._adjustPrecision.call(this); + } + + methods._createScore.call(this); + methods._apply.call(this, this.opt.score); + methods._setTitle.call(this, this.opt.score); + methods._target.call(this, this.opt.score); + + if (this.opt.readOnly) { + methods._lock.call(this); + } else { + this.style.cursor = 'pointer'; + + methods._binds.call(this); + } + }); + }, + + _adjustCallback: function() { + var options = ['number', 'readOnly', 'score', 'scoreName', 'target']; + + for (var i = 0; i < options.length; i++) { + if (typeof this.opt[options[i]] === 'function') { + this.opt[options[i]] = this.opt[options[i]].call(this); + } + } + }, + + _adjustedScore: function(score) { + if (!score) { + return score; + } + + return methods._between(score, 0, this.opt.number); + }, + + _adjustHints: function() { + if (!this.opt.hints) { + this.opt.hints = []; + } + + if (!this.opt.halfShow && !this.opt.half) { + return; + } + + var steps = this.opt.precision ? 10 : 2; + + for (var i = 0; i < this.opt.number; i++) { + var group = this.opt.hints[i]; + + if (Object.prototype.toString.call(group) !== '[object Array]') { + group = [group]; + } + + this.opt.hints[i] = []; + + for (var j = 0; j < steps; j++) { + var + hint = group[j], + last = group[group.length - 1]; + + if (last === undefined) { + last = null; + } + + this.opt.hints[i][j] = hint === undefined ? last : hint; + } + } + }, + + _adjustNumber: function() { + this.opt.number = methods._between(this.opt.number, 1, this.opt.numberMax); + }, + + _adjustPath: function() { + this.opt.path = this.opt.path || ''; + + if (this.opt.path && this.opt.path.charAt(this.opt.path.length - 1) !== '/') { + this.opt.path += '/'; + } + }, + + _adjustPrecision: function() { + this.opt.half = true; + }, + + _adjustStarType: function() { + var replaces = ['cancelOff', 'cancelOn', 'starHalf', 'starOff', 'starOn']; + + this.opt.path = ''; + + for (var i = 0; i < replaces.length; i++) { + this.opt[replaces[i]] = this.opt[replaces[i]].replace('.', '-'); + } + }, + + _apply: function(score) { + methods._fill.call(this, score); + + if (score) { + if (score > 0) { + this.score.val(score); + } + + methods._roundStars.call(this, score); + } + }, + + _between: function(value, min, max) { + return Math.min(Math.max(parseFloat(value), min), max); + }, + + _binds: function() { + if (this.cancel) { + methods._bindOverCancel.call(this); + methods._bindClickCancel.call(this); + methods._bindOutCancel.call(this); + } + + methods._bindOver.call(this); + methods._bindClick.call(this); + methods._bindOut.call(this); + }, + + _bindClick: function() { + var that = this; + + that.stars.on('click.raty', function(evt) { + var + execute = true, + score = (that.opt.half || that.opt.precision) ? that.self.data('score') : (this.alt || $(this).data('alt')); + + if (that.opt.click) { + execute = that.opt.click.call(that, +score, evt); + } + + if (execute || execute === undefined) { + if (that.opt.half && !that.opt.precision) { + score = methods._roundHalfScore.call(that, score); + } + + methods._apply.call(that, score); + } + }); + }, + + _bindClickCancel: function() { + var that = this; + + that.cancel.on('click.raty', function(evt) { + that.score.removeAttr('value'); + + if (that.opt.click) { + that.opt.click.call(that, null, evt); + } + }); + }, + + _bindOut: function() { + var that = this; + + that.self.on('mouseleave.raty', function(evt) { + var score = +that.score.val() || undefined; + + methods._apply.call(that, score); + methods._target.call(that, score, evt); + methods._resetTitle.call(that); + + if (that.opt.mouseout) { + that.opt.mouseout.call(that, score, evt); + } + }); + }, + + _bindOutCancel: function() { + var that = this; + + that.cancel.on('mouseleave.raty', function(evt) { + var icon = that.opt.cancelOff; + + if (that.opt.starType !== 'img') { + icon = that.opt.cancelClass + ' ' + icon; + } + + methods._setIcon.call(that, this, icon); + + if (that.opt.mouseout) { + var score = +that.score.val() || undefined; + + that.opt.mouseout.call(that, score, evt); + } + }); + }, + + _bindOver: function() { + var that = this, + action = that.opt.half ? 'mousemove.raty' : 'mouseover.raty'; + + that.stars.on(action, function(evt) { + var score = methods._getScoreByPosition.call(that, evt, this); + + methods._fill.call(that, score); + + if (that.opt.half) { + methods._roundStars.call(that, score, evt); + methods._setTitle.call(that, score, evt); + + that.self.data('score', score); + } + + methods._target.call(that, score, evt); + + if (that.opt.mouseover) { + that.opt.mouseover.call(that, score, evt); + } + }); + }, + + _bindOverCancel: function() { + var that = this; + + that.cancel.on('mouseover.raty', function(evt) { + var + starOff = that.opt.path + that.opt.starOff, + icon = that.opt.cancelOn; + + if (that.opt.starType === 'img') { + that.stars.attr('src', starOff); + } else { + icon = that.opt.cancelClass + ' ' + icon; + + that.stars.attr('class', starOff); + } + + methods._setIcon.call(that, this, icon); + methods._target.call(that, null, evt); + + if (that.opt.mouseover) { + that.opt.mouseover.call(that, null); + } + }); + }, + + _buildScoreField: function() { + return $('', { name: this.opt.scoreName, type: 'hidden' }).appendTo(this); + }, + + _createCancel: function() { + var icon = this.opt.path + this.opt.cancelOff, + cancel = $('<' + this.opt.starType + ' />', { title: this.opt.cancelHint, 'class': this.opt.cancelClass }); + + if (this.opt.starType === 'img') { + cancel.attr({ src: icon, alt: 'x' }); + } else { + // TODO: use $.data + cancel.attr('data-alt', 'x').addClass(icon); + } + + if (this.opt.cancelPlace === 'left') { + this.self.prepend(' ').prepend(cancel); + } else { + this.self.append(' ').append(cancel); + } + + this.cancel = cancel; + }, + + _createScore: function() { + var score = $(this.opt.targetScore); + + this.score = score.length ? score : methods._buildScoreField.call(this); + }, + + _createStars: function() { + for (var i = 1; i <= this.opt.number; i++) { + var + name = methods._nameForIndex.call(this, i), + attrs = { alt: i, src: this.opt.path + this.opt[name] }; + + if (this.opt.starType !== 'img') { + attrs = { 'data-alt': i, 'class': attrs.src }; // TODO: use $.data. + } + + attrs.title = methods._getHint.call(this, i); + + $('<' + this.opt.starType + ' />', attrs).appendTo(this); + + if (this.opt.space) { + this.self.append(i < this.opt.number ? ' ' : ''); + } + } + + this.stars = this.self.children(this.opt.starType); + }, + + _error: function(message) { + $(this).text(message); + + $.error(message); + }, + + _fill: function(score) { + var hash = 0; + + for (var i = 1; i <= this.stars.length; i++) { + var + icon, + star = this.stars[i - 1], + turnOn = methods._turnOn.call(this, i, score); + + if (this.opt.iconRange && this.opt.iconRange.length > hash) { + var irange = this.opt.iconRange[hash]; + + icon = methods._getRangeIcon.call(this, irange, turnOn); + + if (i <= irange.range) { + methods._setIcon.call(this, star, icon); + } + + if (i === irange.range) { + hash++; + } + } else { + icon = this.opt[turnOn ? 'starOn' : 'starOff']; + + methods._setIcon.call(this, star, icon); + } + } + }, + + _getFirstDecimal: function(number) { + var + decimal = number.toString().split('.')[1], + result = 0; + + if (decimal) { + result = parseInt(decimal.charAt(0), 10); + + if (decimal.slice(1, 5) === '9999') { + result++; + } + } + + return result; + }, + + _getRangeIcon: function(irange, turnOn) { + return turnOn ? irange.on || this.opt.starOn : irange.off || this.opt.starOff; + }, + + _getScoreByPosition: function(evt, icon) { + var score = parseInt(icon.alt || icon.getAttribute('data-alt'), 10); + + if (this.opt.half) { + var + size = methods._getWidth.call(this), + percent = parseFloat((evt.pageX - $(icon).offset().left) / size); + + score = score - 1 + percent; + } + + return score; + }, + + _getHint: function(score, evt) { + if (score !== 0 && !score) { + return this.opt.noRatedMsg; + } + + var + decimal = methods._getFirstDecimal.call(this, score), + integer = Math.ceil(score), + group = this.opt.hints[(integer || 1) - 1], + hint = group, + set = !evt || this.move; + + if (this.opt.precision) { + if (set) { + decimal = decimal === 0 ? 9 : decimal - 1; + } + + hint = group[decimal]; + } else if (this.opt.halfShow || this.opt.half) { + decimal = set && decimal === 0 ? 1 : decimal > 5 ? 1 : 0; + + hint = group[decimal]; + } + + return hint === '' ? '' : hint || score; + }, + + _getWidth: function() { + var width = this.stars[0].width || parseFloat(this.stars.eq(0).css('font-size')); + + if (!width) { + methods._error.call(this, 'Could not get the icon width!'); + } + + return width; + }, + + _lock: function() { + var hint = methods._getHint.call(this, this.score.val()); + + this.style.cursor = ''; + this.title = hint; + + this.score.prop('readonly', true); + this.stars.prop('title', hint); + + if (this.cancel) { + this.cancel.hide(); + } + + this.self.data('readonly', true); + }, + + _nameForIndex: function(i) { + return this.opt.score && this.opt.score >= i ? 'starOn' : 'starOff'; + }, + + _resetTitle: function(star) { + for (var i = 0; i < this.opt.number; i++) { + this.stars[i].title = methods._getHint.call(this, i + 1); + } + }, + + _roundHalfScore: function(score) { + var integer = parseInt(score, 10), + decimal = methods._getFirstDecimal.call(this, score); + + if (decimal !== 0) { + decimal = decimal > 5 ? 1 : 0.5; + } + + return integer + decimal; + }, + + _roundStars: function(score, evt) { + var + decimal = (score % 1).toFixed(2), + name ; + + if (evt || this.move) { + name = decimal > 0.5 ? 'starOn' : 'starHalf'; + } else if (decimal > this.opt.round.down) { // Up: [x.76 .. x.99] + name = 'starOn'; + + if (this.opt.halfShow && decimal < this.opt.round.up) { // Half: [x.26 .. x.75] + name = 'starHalf'; + } else if (decimal < this.opt.round.full) { // Down: [x.00 .. x.5] + name = 'starOff'; + } + } + + if (name) { + var + icon = this.opt[name], + star = this.stars[Math.ceil(score) - 1]; + + methods._setIcon.call(this, star, icon); + } // Full down: [x.00 .. x.25] + }, + + _setIcon: function(star, icon) { + star[this.opt.starType === 'img' ? 'src' : 'className'] = this.opt.path + icon; + }, + + _setTarget: function(target, score) { + if (score) { + score = this.opt.targetFormat.toString().replace('{score}', score); + } + + if (target.is(':input')) { + target.val(score); + } else { + target.html(score); + } + }, + + _setTitle: function(score, evt) { + if (score) { + var + integer = parseInt(Math.ceil(score), 10), + star = this.stars[integer - 1]; + + star.title = methods._getHint.call(this, score, evt); + } + }, + + _target: function(score, evt) { + if (this.opt.target) { + var target = $(this.opt.target); + + if (!target.length) { + methods._error.call(this, 'Target selector invalid or missing!'); + } + + var mouseover = evt && evt.type === 'mouseover'; + + if (score === undefined) { + score = this.opt.targetText; + } else if (score === null) { + score = mouseover ? this.opt.cancelHint : this.opt.targetText; + } else { + if (this.opt.targetType === 'hint') { + score = methods._getHint.call(this, score, evt); + } else if (this.opt.precision) { + score = parseFloat(score).toFixed(1); + } + + var mousemove = evt && evt.type === 'mousemove'; + + if (!mouseover && !mousemove && !this.opt.targetKeep) { + score = this.opt.targetText; + } + } + + methods._setTarget.call(this, target, score); + } + }, + + _turnOn: function(i, score) { + return this.opt.single ? (i === score) : (i <= score); + }, + + _unlock: function() { + this.style.cursor = 'pointer'; + this.removeAttribute('title'); + + this.score.removeAttr('readonly'); + + this.self.data('readonly', false); + + for (var i = 0; i < this.opt.number; i++) { + this.stars[i].title = methods._getHint.call(this, i + 1); + } + + if (this.cancel) { + this.cancel.css('display', ''); + } + }, + + cancel: function(click) { + return this.each(function() { + var self = $(this); + + if (self.data('readonly') !== true) { + methods[click ? 'click' : 'score'].call(self, null); + + this.score.removeAttr('value'); + } + }); + }, + + click: function(score) { + return this.each(function() { + if ($(this).data('readonly') !== true) { + score = methods._adjustedScore.call(this, score); + + methods._apply.call(this, score); + + if (this.opt.click) { + this.opt.click.call(this, score, $.Event('click')); + } + + methods._target.call(this, score); + } + }); + }, + + destroy: function() { + return this.each(function() { + var self = $(this), + raw = self.data('raw'); + + if (raw) { + self.off('.raty').empty().css({ cursor: raw.style.cursor }).removeData('readonly'); + } else { + self.data('raw', self.clone()[0]); + } + }); + }, + + getScore: function() { + var score = [], + value ; + + this.each(function() { + value = this.score.val(); + + score.push(value ? +value : undefined); + }); + + return (score.length > 1) ? score : score[0]; + }, + + move: function(score) { + return this.each(function() { + var + integer = parseInt(score, 10), + decimal = methods._getFirstDecimal.call(this, score); + + if (integer >= this.opt.number) { + integer = this.opt.number - 1; + decimal = 10; + } + + var + width = methods._getWidth.call(this), + steps = width / 10, + star = $(this.stars[integer]), + percent = star.offset().left + steps * decimal, + evt = $.Event('mousemove', { pageX: percent }); + + this.move = true; + + star.trigger(evt); + + this.move = false; + }); + }, + + readOnly: function(readonly) { + return this.each(function() { + var self = $(this); + + if (self.data('readonly') !== readonly) { + if (readonly) { + self.off('.raty').children('img').off('.raty'); + + methods._lock.call(this); + } else { + methods._binds.call(this); + methods._unlock.call(this); + } + + self.data('readonly', readonly); + } + }); + }, + + reload: function() { + return methods.set.call(this, {}); + }, + + score: function() { + var self = $(this); + + return arguments.length ? methods.setScore.apply(self, arguments) : methods.getScore.call(self); + }, + + set: function(options) { + return this.each(function() { + $(this).raty($.extend({}, this.opt, options)); + }); + }, + + setScore: function(score) { + return this.each(function() { + if ($(this).data('readonly') !== true) { + score = methods._adjustedScore.call(this, score); + + methods._apply.call(this, score); + methods._target.call(this, score); + } + }); + } + }; + + $.fn.raty = function(method) { + if (methods[method]) { + return methods[method].apply(this, Array.prototype.slice.call(arguments, 1)); + } else if (typeof method === 'object' || !method) { + return methods.init.apply(this, arguments); + } else { + $.error('Method ' + method + ' does not exist!'); + } + }; + + $.fn.raty.defaults = { + cancel : false, + cancelClass : 'raty-cancel', + cancelHint : 'Cancel this rating!', + cancelOff : '<%= asset_path("cancel-off.png") %>', + cancelOn : '<%= asset_path("cancel-on.png") %>', + cancelPlace : 'left', + click : undefined, + half : false, + halfShow : true, + hints : ['bad', 'poor', 'regular', 'good', 'gorgeous'], + iconRange : undefined, + mouseout : undefined, + mouseover : undefined, + noRatedMsg : 'Not rated yet!', + number : 5, + numberMax : 20, + path : undefined, + precision : false, + readOnly : false, + round : { down: 0.25, full: 0.6, up: 0.76 }, + score : undefined, + scoreName : 'score', + single : false, + space : true, + starHalf : '<%= asset_path("star-half.png") %>', + starOff : '<%= asset_path("star-off.png") %>', + starOn : '<%= asset_path("star-on.png") %>', + starType : 'img', + target : undefined, + targetFormat : '{score}', + targetKeep : false, + targetScore : undefined, + targetText : '', + targetType : 'hint' + }; + +})(jQuery); diff --git a/lib/generators/ratyrate/templates/mid-star.png b/lib/generators/ratyrate/templates/mid-star.png new file mode 100644 index 0000000..aebb3cc Binary files /dev/null and b/lib/generators/ratyrate/templates/mid-star.png differ diff --git a/lib/generators/letsrate/templates/migration.rb b/lib/generators/ratyrate/templates/migration.rb similarity index 83% rename from lib/generators/letsrate/templates/migration.rb rename to lib/generators/ratyrate/templates/migration.rb index a15dc84..fe47af2 100644 --- a/lib/generators/letsrate/templates/migration.rb +++ b/lib/generators/ratyrate/templates/migration.rb @@ -1,4 +1,4 @@ -class CreateRates < ActiveRecord::Migration +class CreateRates < ActiveRecord::Migration<%= migration_version %> def self.up create_table :rates do |t| @@ -17,4 +17,4 @@ def self.down drop_table :rates end -end \ No newline at end of file +end diff --git a/lib/generators/ratyrate/templates/model.rb b/lib/generators/ratyrate/templates/model.rb new file mode 100644 index 0000000..d2bc34b --- /dev/null +++ b/lib/generators/ratyrate/templates/model.rb @@ -0,0 +1,7 @@ +class Rate < ActiveRecord::Base + belongs_to :rater, class_name: "<%= file_name.classify %>" + belongs_to :rateable, polymorphic: true + + #attr_accessible :rate, :dimension + +end diff --git a/lib/generators/ratyrate/templates/overall_average_migration.rb b/lib/generators/ratyrate/templates/overall_average_migration.rb new file mode 100644 index 0000000..cff5439 --- /dev/null +++ b/lib/generators/ratyrate/templates/overall_average_migration.rb @@ -0,0 +1,18 @@ +class CreateOverallAverages < ActiveRecord::Migration<%= migration_version %> + + def self.up + create_table :overall_averages do |t| + t.belongs_to :rateable, :polymorphic => true + t.float :overall_avg, :null => false + t.timestamps + end + + add_index :overall_averages, [:rateable_id, :rateable_type] + end + + def self.down + drop_table :overall_averages + end + +end + diff --git a/lib/generators/ratyrate/templates/overall_average_model.rb b/lib/generators/ratyrate/templates/overall_average_model.rb new file mode 100644 index 0000000..16acb68 --- /dev/null +++ b/lib/generators/ratyrate/templates/overall_average_model.rb @@ -0,0 +1,4 @@ +class OverallAverage < ActiveRecord::Base + belongs_to :rateable, :polymorphic => true +end + diff --git a/lib/generators/letsrate/templates/rater_controller.rb b/lib/generators/ratyrate/templates/rater_controller.rb similarity index 78% rename from lib/generators/letsrate/templates/rater_controller.rb rename to lib/generators/ratyrate/templates/rater_controller.rb index 7a4bd05..1a7a11b 100644 --- a/lib/generators/letsrate/templates/rater_controller.rb +++ b/lib/generators/ratyrate/templates/rater_controller.rb @@ -3,12 +3,11 @@ 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] + obj.rate params[:score].to_f, current_user, params[:dimension] render :json => true else render :json => false end end - end diff --git a/lib/generators/ratyrate/templates/ratyrate.js.erb b/lib/generators/ratyrate/templates/ratyrate.js.erb new file mode 100644 index 0000000..1b92549 --- /dev/null +++ b/lib/generators/ratyrate/templates/ratyrate.js.erb @@ -0,0 +1,67 @@ +$.fn.raty.defaults.half = false; +$.fn.raty.defaults.halfShow = true; +$.fn.raty.defaults.path = "/assets"; +$.fn.raty.defaults.cancel = false; + +function initstars(){ + $(".star").each(function() { + var $readonly = ($(this).attr('data-readonly') == 'true'); + var $half = ($(this).attr('data-enable-half') == 'true'); + var $halfShow = ($(this).attr('data-half-show') == 'true'); + var $single = ($(this).attr('data-single') == 'true'); + var $cancel = ($(this).attr('data-cancel') == 'true'); + $(this).raty({ + score: function() { + return $(this).attr('data-rating') + }, + number: function() { + return $(this).attr('data-star-count') + }, + half: $half, + halfShow: $halfShow, + single: $single, + path: $(this).attr('data-star-path'), + starOn: $(this).attr('data-star-on'), + starOff: $(this).attr('data-star-off'), + starHalf: $(this).attr('data-star-half'), + cancel: $cancel, + cancelPlace: $(this).attr('data-cancel-place'), + cancelHint: $(this).attr('data-cancel-hint'), + cancelOn: $(this).attr('data-cancel-on'), + cancelOff: $(this).attr('data-cancel-off'), + noRatedMsg: $(this).attr('data-no-rated-message'), + round: $(this).attr('data-round'), + space: $(this).attr('data-space'), + target: $(this).attr('data-target'), + targetText: $(this).attr('data-target-text'), + targetType: $(this).attr('data-target-type'), + targetFormat: $(this).attr('data-target-format'), + targetScoret: $(this).attr('data-target-score'), + readOnly: $readonly, + click: function(score, evt) { + var _this = this; + if (score == null) { score = 0; } + $(this).trigger('rated', score) + $.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 }); + } + } + }); + } + }); + }); +}; + +$(document).ready(initstars); +$(document).on('page:change',initstars); diff --git a/lib/generators/letsrate/templates/star-half.png b/lib/generators/ratyrate/templates/star-half.png similarity index 100% rename from lib/generators/letsrate/templates/star-half.png rename to lib/generators/ratyrate/templates/star-half.png diff --git a/lib/generators/letsrate/templates/star-off.png b/lib/generators/ratyrate/templates/star-off.png similarity index 100% rename from lib/generators/letsrate/templates/star-off.png rename to lib/generators/ratyrate/templates/star-off.png diff --git a/lib/generators/letsrate/templates/star-on.png b/lib/generators/ratyrate/templates/star-on.png similarity index 100% rename from lib/generators/letsrate/templates/star-on.png rename to lib/generators/ratyrate/templates/star-on.png diff --git a/lib/letsrate.rb b/lib/letsrate.rb deleted file mode 100644 index fd097a0..0000000 --- a/lib/letsrate.rb +++ /dev/null @@ -1,7 +0,0 @@ -require "letsrate/version" -require "letsrate/model" -require "letsrate/helpers" - -module Letsrate - -end \ No newline at end of file diff --git a/lib/letsrate/helpers.rb b/lib/letsrate/helpers.rb deleted file mode 100644 index c2130a0..0000000 --- a/lib/letsrate/helpers.rb +++ /dev/null @@ -1,45 +0,0 @@ -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 - - 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 diff --git a/lib/letsrate/model.rb b/lib/letsrate/model.rb deleted file mode 100644 index 4ae1e5b..0000000 --- a/lib/letsrate/model.rb +++ /dev/null @@ -1,113 +0,0 @@ -require 'active_support/concern' -module Letsrate - extend ActiveSupport::Concern - - 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 = user - end - if dirichlet_method - update_rate_average_dirichlet(stars, dimension) - else - update_rate_average(stars, dimension) - end - else - raise "User has already rated." - 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 - a = average(dimension) - a.qty = rates(dimension).count - a.avg = davg - a.save!(validate: false) - end - end - - def update_rate_average(stars, dimension=nil) - if average(dimension).nil? - RatingCache.create! do |avg| - avg.cacheable_id = self.id - avg.cacheable_type = self.class.name - avg.avg = stars - avg.qty = 1 - avg.dimension = dimension - end - else - a = average(dimension) - a.qty = rates(dimension).count - a.avg = rates(dimension).average(:stars) - a.save!(validate: false) - end - end - - def average(dimension=nil) - 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) - dimension ? self.send("#{dimension}_rates") : rates_without_dimension - end - - def raters(dimension=nil) - 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 - - def letsrate_rateable(*dimensions) - 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 - -class ActiveRecord::Base - include Letsrate -end diff --git a/lib/letsrate/version.rb b/lib/letsrate/version.rb deleted file mode 100644 index eed8328..0000000 --- a/lib/letsrate/version.rb +++ /dev/null @@ -1,3 +0,0 @@ -module Letsrate - VERSION = "1.0.9" -end \ No newline at end of file diff --git a/lib/ratyrate.rb b/lib/ratyrate.rb new file mode 100644 index 0000000..3fb3e97 --- /dev/null +++ b/lib/ratyrate.rb @@ -0,0 +1,7 @@ +require "ratyrate/version" +require "ratyrate/model" +require "ratyrate/helpers" + +module Ratyrate + +end diff --git a/lib/ratyrate/helpers.rb b/lib/ratyrate/helpers.rb new file mode 100644 index 0000000..a8cc94e --- /dev/null +++ b/lib/ratyrate/helpers.rb @@ -0,0 +1,149 @@ +module Ratyrate + 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 + enable_half = options[:enable_half] || false + half_show = options[:half_show] || true + star_path = options[:star_path] || '' + star_on = options[:star_on] || image_path('star-on.png') + star_off = options[:star_off] || image_path('star-off.png') + star_half = options[:star_half] || image_path('star-half.png') + cancel = options[:cancel] || false + cancel_place = options[:cancel_place] || 'left' + cancel_hint = options[:cancel_hint] || 'Cancel current rating!' + cancel_on = options[:cancel_on] || image_path('cancel-on.png') + cancel_off = options[:cancel_off] || image_path('cancel-off.png') + noRatedMsg = options[:noRatedMsg] || 'I\'am readOnly and I haven\'t rated yet!' + # round = options[:round] || { down: .26, full: .6, up: .76 } + space = options[:space] || false + single = options[:single] || false + target = options[:target] || '' + targetText = options[:targetText] || '' + targetType = options[:targetType] || 'hint' + targetFormat = options[:targetFormat] || '{score}' + targetScore = options[:targetScore] || '' + readOnly = options[:readonly] || false + + disable_after_rate = options[:disable_after_rate] && true + disable_after_rate = true if disable_after_rate == nil + + unless readOnly + if disable_after_rate + readOnly = !(current_user && rateable_obj.can_rate?(current_user, dimension)) + else + readOnly = !current_user || false + end + end + + if options[:imdb_avg] && readOnly + content_tag :div, '', :style => "background-image:url('#{image_path('mid-star.png')}');width:61px;height:57px;margin-top:10px;" do + content_tag :p, avg, :style => "position:relative;font-size:.8rem;text-align:center;line-height:60px;" + end + else + content_tag :div, '', "data-dimension" => dimension, :class => "star", "data-rating" => avg, + "data-id" => rateable_obj.id, "data-classname" => rateable_obj.class.name == rateable_obj.class.base_class.name ? rateable_obj.class.name : rateable_obj.class.base_class.name, + "data-disable-after-rate" => disable_after_rate, + "data-readonly" => readOnly, + "data-enable-half" => enable_half, + "data-half-show" => half_show, + "data-star-count" => star, + "data-star-path" => star_path, + "data-star-on" => star_on, + "data-star-off" => star_off, + "data-star-half" => star_half, + "data-cancel" => cancel, + "data-cancel-place" => cancel_place, + "data-cancel-hint" => cancel_hint, + "data-cancel-on" => cancel_on, + "data-cancel-off" => cancel_off, + "data-no-rated-message" => noRatedMsg, + # "data-round" => round, + "data-space" => space, + "data-single" => single, + "data-target" => target, + "data-target-text" => targetText, + "data-target-type" => targetType, + "data-target-format" => targetFormat, + "data-target-score" => targetScore + end + end + + def imdb_style_rating_for(rateable_obj, user, options = {}) + #TODO: add option to change the star icon + overall_avg = rateable_obj.overall_avg(user) + + content_tag :div, '', :style => "background-image:url('#{image_path('big-star.png')}');width:81px;height:81px;margin-top:10px;" do + content_tag :p, overall_avg, :style => "position:relative;line-height:85px;text-align:center;" + end + end + + def rating_for_user(rateable_obj, rating_user, dimension = nil, options = {}) + @object = rateable_obj + @user = rating_user + @rating = Rate.find_by rater: @user, rateable: @object, dimension: dimension + stars = @rating ? @rating.stars : 0 + + star = options[:star] || 5 + enable_half = options[:enable_half] || false + half_show = options[:half_show] || true + star_path = options[:star_path] || '' + star_on = options[:star_on] || image_path('star-on.png') + star_off = options[:star_off] || image_path('star-off.png') + star_half = options[:star_half] || image_path('star-half.png') + cancel = options[:cancel] || false + cancel_place = options[:cancel_place] || 'left' + cancel_hint = options[:cancel_hint] || 'Cancel current rating!' + cancel_on = options[:cancel_on] || image_path('cancel-on.png') + cancel_off = options[:cancel_off] || image_path('cancel-off.png') + noRatedMsg = options[:noRatedMsg] || 'I\'am readOnly and I haven\'t rated yet!' + # round = options[:round] || { down: .26, full: .6, up: .76 } + space = options[:space] || false + single = options[:single] || false + target = options[:target] || '' + targetText = options[:targetText] || '' + targetType = options[:targetType] || 'hint' + targetFormat = options[:targetFormat] || '{score}' + targetScore = options[:targetScore] || '' + readOnly = options[:readonly] || false + + disable_after_rate = options[:disable_after_rate] || false + + if disable_after_rate + readOnly = rating_user.present? ? !rateable_obj.can_rate?(rating_user, 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 == rateable_obj.class.base_class.name ? rateable_obj.class.name : rateable_obj.class.base_class.name, + "data-disable-after-rate" => disable_after_rate, + "data-readonly" => readOnly, + "data-enable-half" => enable_half, + "data-half-show" => half_show, + "data-star-count" => star, + "data-star-path" => star_path, + "data-star-on" => star_on, + "data-star-off" => star_off, + "data-star-half" => star_half, + "data-cancel" => cancel, + "data-cancel-place" => cancel_place, + "data-cancel-hint" => cancel_hint, + "data-cancel-on" => cancel_on, + "data-cancel-off" => cancel_off, + "data-no-rated-message" => noRatedMsg, + # "data-round" => round, + "data-space" => space, + "data-single" => single, + "data-target" => target, + "data-target-text" => targetText, + "data-target-format" => targetFormat, + "data-target-score" => targetScore + end + end +end + +class ActionView::Base + include Ratyrate::Helpers +end diff --git a/lib/ratyrate/model.rb b/lib/ratyrate/model.rb new file mode 100644 index 0000000..a695598 --- /dev/null +++ b/lib/ratyrate/model.rb @@ -0,0 +1,150 @@ +require 'active_support/concern' + +module Ratyrate + module Model + extend ActiveSupport::Concern + + 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 = user + end + if dirichlet_method + update_rate_average_dirichlet(stars, dimension) + else + update_rate_average(stars, dimension) + end + else + update_current_rate(stars, user, 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? + send("create_#{average_assoc_name(dimension)}!", { avg: davg, qty: 1, dimension: dimension }) + else + a = average(dimension) + a.qty = rates(dimension).count + a.avg = davg + a.save!(validate: false) + end + end + + def update_rate_average(stars, dimension=nil) + if average(dimension).nil? + send("create_#{average_assoc_name(dimension)}!", { avg: stars, qty: 1, dimension: dimension }) + else + a = average(dimension) + a.qty = rates(dimension).count + a.avg = rates(dimension).average(:stars) + a.save!(validate: false) + end + end + + def update_current_rate(stars, user, dimension) + current_rate = rates(dimension).where(rater_id: user.id).take + current_rate.stars = stars + current_rate.save!(validate: false) + + if rates(dimension).count > 1 + update_rate_average(stars, dimension) + else # Set the avarage to the exact number of stars + a = average(dimension) + a.avg = stars + a.save!(validate: false) + end + end + + def overall_avg(user) + # avg = OverallAverage.where(rateable_id: self.id) + # #FIXME: Fix the bug when the movie has no ratings + # unless avg.empty? + # return avg.take.avg unless avg.take.avg == 0 + # else # calculate average, and save it + # dimensions_count = overall_score = 0 + # user.ratings_given.select('DISTINCT dimension').each do |d| + # dimensions_count = dimensions_count + 1 + # unless average(d.dimension).nil? + # overall_score = overall_score + average(d.dimension).avg + # end + # end + # overall_avg = (overall_score / dimensions_count).to_f.round(1) + # AverageCache.create! do |a| + # a.rater_id = user.id + # a.rateable_id = self.id + # a.avg = overall_avg + # end + # overall_avg + # end + end + + # calculates the movie overall average rating for all users + def calculate_overall_average + rating = Rate.where(rateable: self).pluck('stars') + (rating.reduce(:+).to_f / rating.size).round(1) + end + + def average(dimension=nil) + send(average_assoc_name(dimension)) + end + + def average_assoc_name(dimension = nil) + dimension ? "#{dimension}_average" : 'rate_average_without_dimension' + end + + def can_rate?(user, dimension=nil) + rates(dimension).where(rater_id: user.id).size.zero? + end + + def rates(dimension=nil) + dimension ? self.send("#{dimension}_rates") : rates_without_dimension + end + + def raters(dimension=nil) + dimension ? self.send("#{dimension}_raters") : raters_without_dimension + end + + module ClassMethods + + def ratyrate_rater + has_many :ratings_given, class_name: 'Rate', foreign_key: :rater_id + end + + def ratyrate_rateable(*dimensions) + 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 Ratyrate::Model +end diff --git a/lib/ratyrate/version.rb b/lib/ratyrate/version.rb new file mode 100644 index 0000000..d02a044 --- /dev/null +++ b/lib/ratyrate/version.rb @@ -0,0 +1,3 @@ +module Ratyrate + VERSION = "1.2.2.alpha" +end diff --git a/ratyrate.gemspec b/ratyrate.gemspec new file mode 100644 index 0000000..89681aa --- /dev/null +++ b/ratyrate.gemspec @@ -0,0 +1,24 @@ +# -*- encoding: utf-8 -*- +$:.push File.expand_path("../lib", __FILE__) +require "ratyrate/version" + +Gem::Specification.new do |s| + s.name = "ratyrate" + s.version = Ratyrate::VERSION + s.authors = ["Islam Wazery"] + s.email = ["wazery@ubuntu.com"] + s.homepage = "http://github.com/wazery/ratyrate" + s.summary = %q{A Ruby Gem that wraps the functionality of jQuery Raty library, and adds IMDB style rating.} + s.description = %q{A Ruby Gem that wraps the functionality of jQuery Raty library, and adds IMDB style rating.} + + s.rubyforge_project = "ratyrate" + + s.files = `git ls-files`.split("\n") + s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") + s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } + s.require_paths = ["lib"] + + # specify any dependencies here; for example: + # s.add_development_dependency "rspec" + # s.add_runtime_dependency "rest-client" +end