mongoid_rating 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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