mongoid_rating 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3619dc0eda67a2cfd21622ac78b7100d788be907
4
+ data.tar.gz: 3c000c1c31a4cefba53f948934fdcf568cb73326
5
+ SHA512:
6
+ metadata.gz: edfbbd674d7a882ba979593ffb53d7e4d6d5a31636f0a268cc0c527ecd67edd14735b22802d346738063335e700387ba0e09d37223d0df296b85bfa49f784e8c
7
+ data.tar.gz: de68bfcc7cac056ae41782164acecbe61439359a08c0d4784b10a8f50c2bbac3743955cd59c0e66f75eb7aa8f1cb63121fb9bd4865e82c69e0bf328e621c88ec
data/.coveralls.yml ADDED
@@ -0,0 +1 @@
1
+ service_name: travis-ci
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/.ruby-gemset ADDED
@@ -0,0 +1 @@
1
+ mongoid_rating
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.0.0
data/.travis.yml ADDED
@@ -0,0 +1,22 @@
1
+ services: mongodb
2
+
3
+ notifications:
4
+ email: false
5
+
6
+ language: ruby
7
+ rvm:
8
+ - 1.9.3
9
+ - 2.0.0
10
+ - jruby-19mode
11
+ - rbx-19mode
12
+
13
+ gemfile:
14
+ - Gemfile
15
+ - gemfiles/mongoid-3.0.gemfile
16
+ - gemfiles/mongoid-3.1.gemfile
17
+ - gemfiles/mongoid-4.0.gemfile
18
+
19
+ matrix:
20
+ exclude:
21
+ - rvm: rbx-19mode
22
+ gemfile: gemfiles/mongoid-4.0.gemfile
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in mongoid_rating.gemspec
4
+ gem "mongoid", github: "mongoid/mongoid"
5
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,25 @@
1
+ Copyright (c) 2013 glebtv
2
+
3
+ Copyright (c) 2010-2011 Vinova Pte Ltd
4
+ Alex Nguyen - Author
5
+
6
+ MIT License
7
+
8
+ Permission is hereby granted, free of charge, to any person obtaining
9
+ a copy of this software and associated documentation files (the
10
+ "Software"), to deal in the Software without restriction, including
11
+ without limitation the rights to use, copy, modify, merge, publish,
12
+ distribute, sublicense, and/or sell copies of the Software, and to
13
+ permit persons to whom the Software is furnished to do so, subject to
14
+ the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be
17
+ included in all copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
20
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
21
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
22
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
23
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
24
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
25
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,86 @@
1
+ ## Star rating for Mongoid 4 - MongoidRating
2
+
3
+ [![Gem Version](https://badge.fury.io/rb/mongoid_rating.png)](http://badge.fury.io/rb/mongoid_rating)
4
+ [![Dependency Status](https://gemnasium.com/rs-pro/mongoid_rating.png)](https://gemnasium.com/rs-pro/mongoid_rating)
5
+ [![Build Status](https://travis-ci.org/rs-pro/mongoid_rating.png?branch=master)](https://travis-ci.org/rs-pro/mongoid_rating)
6
+ [![Coverage Status](https://coveralls.io/repos/rs-pro/mongoid_rating/badge.png)](https://coveralls.io/r/rs-pro/mongoid_rating)
7
+
8
+ ## Currenty this gem supports only Mongoid 4
9
+
10
+ ## Features
11
+
12
+ - Multiple rating fields per model
13
+ - Float rating marks (users can give 4.5 stars)
14
+ - Accurate concurrent rating updates with db.eval
15
+
16
+ ## Installation
17
+
18
+ Add this line to your application's Gemfile:
19
+
20
+ gem 'mongoid_rating'
21
+
22
+ And then execute:
23
+
24
+ $ bundle
25
+
26
+ Or install it yourself as:
27
+
28
+ $ gem install mongoid_rating
29
+
30
+ ## Usage
31
+
32
+ make model rateable:
33
+
34
+ class Post
35
+ include Mongoid::Document
36
+ rateable :rate
37
+ end
38
+ ps = Post.create()
39
+ user = User.create()
40
+
41
+ rate and unrate:
42
+
43
+ ps.rate 5, user
44
+ ps.unrate, user
45
+
46
+ Get current rating
47
+
48
+ ps.rate
49
+ => 5.0
50
+ ps.rate_by(user)
51
+ => 5
52
+
53
+ Check if user rated:
54
+
55
+ ps.rate_by?(user)
56
+ => true
57
+
58
+ Scopes:
59
+
60
+ Post.rate_in(2..5)
61
+ Post.rate_in(2..5).first
62
+ => #<Post rate_count: 1, rate_sum: 5.0, rate_average: 5.0>
63
+ Post.rate_in(2..3).first
64
+ => nil
65
+
66
+ Posts rated by user:
67
+
68
+ Post.rate_by(user).first
69
+ => #<Post rate_count: 1, rate_sum: 5.0, rate_average: 5.0>
70
+
71
+ ## Credits
72
+
73
+ (c) 2013 glebtv, MIT license
74
+
75
+ Partially based on
76
+ (mongoid-rateable)[https://github.com/proton/mongoid_rateable]
77
+ which is Copyright (c) 2011 Peter Savichev (proton), MIT license
78
+
79
+ ## Contributing
80
+
81
+ 1. Fork it
82
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
83
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
84
+ 4. Push to the branch (`git push origin my-new-feature`)
85
+ 5. Create new Pull Request
86
+
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+
3
+ require 'rspec/core/rake_task'
4
+
5
+ desc 'Default: run specs.'
6
+ task :default => :spec
7
+
8
+ desc "Run specs"
9
+ RSpec::Core::RakeTask.new
@@ -0,0 +1,5 @@
1
+ source "https://rubygems.org"
2
+
3
+ gem "mongoid", github: "mongoid/mongoid", branch: "master"
4
+
5
+ gemspec path: "../"
@@ -0,0 +1,168 @@
1
+ module Mongoid
2
+ module Rating
3
+ module Model
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+ # Make model rateable
8
+ #
9
+ # @param [Hash] options a hash containings:
10
+ #
11
+ # rateable :overall, range: -5..5
12
+ #
13
+ # Disable atomic eval (in case it's disabled on the database side)
14
+ # rateable :design, range: -5..5, average: false
15
+ #
16
+ # Disable average completely
17
+ # rateable :quality, range: -5..5, average: true
18
+ def rateable(field, options = {})
19
+ options = {
20
+ range: 1..5,
21
+ rerate: true,
22
+ eval: true,
23
+ counters: true,
24
+ float: true
25
+ }.merge(options)
26
+ field = field.to_sym
27
+ sfield = field.inspect
28
+
29
+ # total rates count
30
+ field "#{field}_count", type: Integer, default: 0
31
+
32
+ # rates data
33
+ embeds_many "#{field}_data", as: :rateable, class_name: 'Mongoid::Rating::Rate', counter_cache: true
34
+
35
+ # sum of all rates to calculate average
36
+ field "#{field}_sum".to_sym, type: options[:float] ? Float : Integer
37
+
38
+ # average rate value
39
+ avg = "#{field}_average".to_sym
40
+ field avg, type: Float
41
+ savg = avg.inspect
42
+
43
+ class_eval <<-RUBY, __FILE__, __LINE__+1
44
+ scope :#{field}_by, ->(rater) {
45
+ where("#{field}_data.rater_id" => rater.id, "#{field}_data.rater_type" => rater.class.to_s)
46
+ }
47
+ scope :#{field}_in, ->(range) {
48
+ where(#{savg}.gte => range.begin, #{savg}.lte => range.end)
49
+ }
50
+ scope :highest_#{field}, -> {
51
+ where(#{savg}.ne => nil).order_by([#{savg}, :desc])
52
+ }
53
+
54
+ def #{field}!(value, rater)
55
+ value = value.to_i unless #{options[:float]}
56
+ unless (#{options[:range]}).include?(value)
57
+ raise "bad vote value"
58
+ end
59
+ raise "can't rate" unless can_#{field}?(rater)
60
+ if #{options[:eval]}
61
+ #{field}_data.where(rater_id: rater.id).destroy_all
62
+ #{field}_data.create!(rater: rater, value: value)
63
+ collection.database.session.cluster.with_primary do
64
+ doc = collection.database.command({
65
+ eval: 'function(id) {
66
+ var oid = ObjectId(id);
67
+ var doc = db.' + collection.name + '.findOne( { _id : oid } );
68
+ if (doc) {
69
+ doc.#{field}_count = 0
70
+ doc.#{field}_sum = 0;
71
+ doc.#{field}_data.forEach(function(fd) {
72
+ doc.#{field}_sum += fd.value;
73
+ doc.#{field}_count += 1;
74
+ })
75
+ doc.#{field}_average = doc.#{field}_sum /doc.#{field}_count;
76
+ db.' + collection.name + '.save(doc);
77
+ return doc;
78
+ } else {
79
+ return false;
80
+ }
81
+ }',
82
+ args: [ id.to_s ]
83
+ })
84
+ self.#{field}_count = doc[:retval]["#{field}_count"]
85
+ self.#{field}_sum = doc[:retval]["#{field}_sum"]
86
+ self.#{field}_average = doc[:retval]["#{field}_average"]
87
+ remove_change(:#{field}_count)
88
+ remove_change(:#{field}_sum)
89
+ remove_change(:#{field}_average)
90
+ end
91
+ else
92
+ un#{field}!(rater)
93
+ atomically do
94
+ inc("#{field}_count" => 1, "#{field}_sum" => value)
95
+ #{field}_data.create!(rater: rater, value: value)
96
+ set("#{field}_average" => calc_#{field}_avg)
97
+ end
98
+ end
99
+ end
100
+ def calc_#{field}_avg
101
+ if #{field}_count < 1
102
+ nil
103
+ else
104
+ #{field}_sum.to_f / #{field}_count.to_f
105
+ end
106
+ end
107
+ def un#{field}!(rater)
108
+ r = #{field}_data.where(rater_id: rater.id).first
109
+ if r.nil?
110
+ # not rated before
111
+ else
112
+ atomically do
113
+ inc("#{field}_count" => -1, "#{field}_sum" => -r.value)
114
+ set("#{field}_average" => calc_#{field}_avg)
115
+ r.destroy
116
+ end
117
+ end
118
+ end
119
+ alias_method :un#{field}, :un#{field}!
120
+
121
+ def did_#{field}?(rater)
122
+ !raw_#{field}_by(rater).nil?
123
+ end
124
+
125
+ def can_#{field}?(rater)
126
+ if #{options[:rerate]}
127
+ true
128
+ else
129
+ !did_#{field}?(rater)
130
+ end
131
+ end
132
+
133
+ def raw_#{field}_by(rater)
134
+ #{field}_data.select do |rate|
135
+ rate[:rater_id] == rater.id && rate[:rater_type] == rater.class.name
136
+ end.first
137
+ end
138
+
139
+ def #{field}(value=nil, rater=nil)
140
+ if rater.nil? && value.nil?
141
+ #{field}_count.nil? ? nil : #{field}_average
142
+ else
143
+ #{field}!(value, rater)
144
+ end
145
+ end
146
+
147
+ def #{field}_values
148
+ #{field}_data.map(&:value)
149
+ end
150
+
151
+ def #{field}_by(rater)
152
+ rate = raw_#{field}_by(rater)
153
+ if rate.nil?
154
+ nil
155
+ else
156
+ rate.value
157
+ end
158
+ end
159
+ def #{field}_by?(rater)
160
+ !#{field}_by(rater).nil?
161
+ end
162
+ RUBY
163
+ end
164
+ end
165
+ end
166
+ end
167
+ end
168
+
@@ -0,0 +1,13 @@
1
+ module Mongoid::Rating
2
+ class Rate
3
+ include Mongoid::Document
4
+ include Mongoid::Timestamps::Short
5
+
6
+ embedded_in :rateable, polymorphic: true
7
+ belongs_to :rater, polymorphic: true, inverse_of: nil
8
+
9
+ # rate can be integer or float so we don't force class here
10
+ field :value
11
+ end
12
+ end
13
+
@@ -0,0 +1,3 @@
1
+ module MongoidRating
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,6 @@
1
+ require "mongoid_rating/version"
2
+ require 'mongoid_rating/rate'
3
+ require 'mongoid_rating/model'
4
+
5
+ Mongoid::Document.send :include, Mongoid::Rating::Model
6
+
@@ -0,0 +1,28 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mongoid_rating/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "mongoid_rating"
8
+ spec.version = MongoidRating::VERSION
9
+ spec.authors = ["glebtv"]
10
+ spec.email = ["glebtv@gmail.com"]
11
+ spec.description = %q{Star rating for Mongoid}
12
+ spec.summary = %q{Star rating for Mongoid}
13
+ spec.homepage = "https://github.com/rs-pro/mongoid_rating"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_dependency "mongoid", [">= 4.0", "< 5.0"]
22
+
23
+ spec.add_development_dependency "bundler", "~> 1.3"
24
+ spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "rspec"
26
+ spec.add_development_dependency "database_cleaner"
27
+ spec.add_development_dependency "coveralls"
28
+ end
@@ -0,0 +1,259 @@
1
+ require "spec_helper"
2
+
3
+ describe Article do
4
+
5
+ before(:each) do
6
+ @bob = User.create :name => "Bob"
7
+ @sally = User.create :name => "Sally"
8
+ @alice = User.create :name => "Alice"
9
+ @article = Article.create :name => "Article"
10
+ @article1 = Article.create :name => "Article 1"
11
+ @article2 = Article.create :name => "Article 2"
12
+ end
13
+
14
+ subject { @article }
15
+ it { should respond_to :overall }
16
+ it { should respond_to :overall! }
17
+ it { should respond_to :unoverall! }
18
+ it { should respond_to :did_overall? }
19
+ it { should respond_to :overall_average }
20
+ it { should respond_to :overall_data }
21
+ it { should respond_to :overall_by }
22
+
23
+ describe "#overall_values" do
24
+ it "should be an array with overall values" do
25
+ @article.overall_values.should be_an_instance_of Array
26
+ @article.overall! 1, @bob
27
+ @article.overall_values.should eq [1]
28
+ end
29
+ end
30
+
31
+ context "when overall" do
32
+ before (:each) do
33
+ @article.overall! 1, @bob
34
+ end
35
+
36
+ describe "#overall" do
37
+ it "should track #overall properly" do
38
+ @article.overall! 1, @sally
39
+ @article.overall_count.should eql 2
40
+ @article.overall.should eql 1.0
41
+ end
42
+
43
+ it "should not mark fields as dirty" do
44
+ @article.overall_count_changed?.should be_false
45
+ @article.overall_sum_changed?.should be_false
46
+ @article.overall_average_changed?.should be_false
47
+ end
48
+
49
+ it "should limit #overalls by user properly" do
50
+ @article.overall! 5, @bob
51
+ @article.overall.should eql 5.0
52
+ end
53
+
54
+ context "when overall_value in rating range" do
55
+ it { expect { @article.overall 1, @sally }.not_to raise_error }
56
+ end
57
+
58
+ context "when overall_value not in rating range" do
59
+ it { expect { @article.overall 17, @sally }.to raise_error() }
60
+ it { expect { @article.overall -17, @sally }.to raise_error() }
61
+ end
62
+
63
+ describe "when using positive values" do
64
+ let(:num) { rand(1..5) }
65
+ let(:exp) { ((num + 1) / 2.0) - 1 }
66
+ it { expect { @article.overall num, @sally }.to change { @article.overall }.by(exp) }
67
+ end
68
+
69
+ describe "when using negative values" do
70
+ let(:num) { -rand(1..5) }
71
+ let(:exp) { ((num + 1) / 2.0) - 1 }
72
+ it { expect { @article.overall num, @sally }.to change { @article.overall }.by(exp) }
73
+ end
74
+ end
75
+
76
+ describe "#overall_by?" do
77
+ describe "for Bob" do
78
+ specify { @article.overall_by?(@bob).should be_true }
79
+ end
80
+ describe "for Bob" do
81
+ specify { @article1.overall_by?(@bob).should be_false }
82
+ specify { @article.overall_by?(@alice).should be_false }
83
+ end
84
+
85
+ describe "when overall by someone else" do
86
+ before do
87
+ @article.overall 1, @alice
88
+ end
89
+
90
+ describe "for Alice" do
91
+ specify { @article.overall_by?(@alice).should be_true }
92
+ end
93
+ end
94
+
95
+ describe "when not overall by someone else" do
96
+ describe "for Sally" do
97
+ specify { @article.overall_by?(@sally).should be_false }
98
+ end
99
+ end
100
+ end
101
+
102
+ describe "#unoverall" do
103
+ before { @article.unoverall @bob }
104
+
105
+ it "should have null #overall_count" do
106
+ @article.overall_count.should eql 0
107
+ end
108
+
109
+ it "should have null #overall" do
110
+ @article.overall.should be_nil
111
+ end
112
+
113
+ it "#overall_by?(@bob) should be false after unoverall" do
114
+ @article.overall_by?(@bob).should be_false
115
+ end
116
+ end
117
+
118
+ describe "#overall_count" do
119
+ it "should know how many overalls have been cast" do
120
+ @article.overall 1, @sally
121
+ @article.overall_count.should eql 2
122
+ end
123
+ end
124
+
125
+ describe "#rating" do
126
+ it "should calculate the average overall" do
127
+ @article.overall 4, @sally
128
+ @article.overall.should eq 2.5
129
+ end
130
+
131
+ it "should calculate the average overall if the result is zero" do
132
+ @article.overall -1, @sally
133
+ @article.overall.should eq 0.0
134
+ end
135
+ end
136
+ end
137
+
138
+ context "when not overall" do
139
+ describe "#overalls" do
140
+ specify { @article.overall_count.should eql 0 }
141
+ end
142
+
143
+ describe "#rating" do
144
+ specify { @article.overall.should be_nil }
145
+ end
146
+
147
+ describe "#unoverall" do
148
+ before do
149
+ @article.unoverall @sally
150
+ end
151
+
152
+ it "should have null #overall_count" do
153
+ @article.overall_count.should eql 0
154
+ end
155
+
156
+ it "should have null #overalls" do
157
+ @article.overall.should be_nil
158
+ end
159
+ end
160
+ end
161
+
162
+ context "when saving the collection" do
163
+ before (:each) do
164
+ @article.overall 3, @bob
165
+ @article.overall -5, @sally
166
+ @article.save
167
+ @f_article = Article.where(:name => "Article").first
168
+ end
169
+
170
+ it "disallows incorrect rates" do
171
+ expect { @article.overall 8, @bob }.to raise_error
172
+ expect { @article.overall -10, @sally }.to raise_error
173
+ end
174
+
175
+ describe "#overall_by?" do
176
+ describe "for Bob" do
177
+ specify { @f_article.overall_by?(@bob).should be_true }
178
+ specify { @f_article.overall_by(@bob).should eq 3 }
179
+ end
180
+
181
+ describe "for Sally" do
182
+ specify { @f_article.overall_by?(@sally).should be_true }
183
+ specify { @f_article.overall_by(@sally).should eq -5 }
184
+ end
185
+
186
+ describe "for Alice" do
187
+ specify { @f_article.overall_by?(@alice).should be_false}
188
+ specify { @f_article.overall_by(@alice).should be_nil }
189
+ end
190
+ end
191
+
192
+ describe "#overall" do
193
+ specify { @f_article.overall.should eql -1.0 }
194
+ end
195
+
196
+ describe "#overall_count" do
197
+ specify { @f_article.overall_count.should eql 2 }
198
+ end
199
+ end
200
+
201
+ describe "#scopes" do
202
+ before (:each) do
203
+ @article.delete
204
+ @article1 = Article.create(:name => "Article 1")
205
+ @article2 = Article.create(:name => "Article 2")
206
+ @article3 = Article.create(:name => "Article 3")
207
+ @article4 = Article.create(:name => "Article 4")
208
+ @article5 = Article.create(:name => "Article 5")
209
+ @article1.overall 5, @sally
210
+ @article1.overall 3, @bob
211
+ @article4.overall 1, @sally
212
+ end
213
+
214
+ describe "#overall_by" do
215
+ it "should return proper count of articles overall by Bob" do
216
+ Article.overall_by(@bob).size.should eql 1
217
+ end
218
+
219
+ it "should return proper count of articles overall by Sally" do
220
+ Article.overall_by(@sally).size.should eql 2
221
+ end
222
+ end
223
+
224
+ describe "#overall_in" do
225
+ before (:each) do
226
+ @article1.overall 4, @alice
227
+ @article2.overall 2, @alice
228
+ @article3.overall 5, @alice
229
+ @article4.overall 2, @alice
230
+ end
231
+
232
+ it "should return proper count of articles with rating 4..5" do
233
+ Article.overall_in(4..5).to_a.length.should eql 2
234
+ end
235
+
236
+ it "should return proper count of articles with rating 0..2" do
237
+ Article.overall_in(0..2).to_a.length.should eql 2
238
+ end
239
+
240
+ it "should return proper count of articles with rating 0..5" do
241
+ Article.overall_in(0..5).to_a.length.should eql 4
242
+ end
243
+ end
244
+
245
+ describe "#highest_overall" do
246
+ it "should return proper count of articles" do
247
+ Article.highest_overall.limit(1).count(true).should eq 1
248
+ end
249
+
250
+ it "should return proper count of articles" do
251
+ Article.highest_overall.limit(10).count(true).should eq 2
252
+ end
253
+
254
+ it "should return proper document" do
255
+ Article.highest_overall.limit(1).first.name.should eql "Article 1"
256
+ end
257
+ end
258
+ end
259
+ end