cheer 1.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3fbc7d10a59452417daeb9b6556e483a239e5cb4
4
+ data.tar.gz: 625f081e1de184703ec2b8b22f60b0ccce6c6e59
5
+ SHA512:
6
+ metadata.gz: be88f6e3d1d420ee199a771d06000fcf8c5ff190fd9aef2c3020f880a524caaf4ddbafcee90ba61162ae3153a86e7e8d956e2b3c5f624d4933e44445ee7a950e
7
+ data.tar.gz: b17a8987079615013178485f2cf4ac75af61149e3bbca52c23a34a0feeea5613e50a9f543489347601305f6f6702291059624736b810d16c12698e7c21191c83
@@ -0,0 +1,18 @@
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
18
+ vendor
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format documentation
@@ -0,0 +1 @@
1
+ cheer
@@ -0,0 +1 @@
1
+ ruby-2.0.0-p353
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in user-ranking.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 TODO: Write your name
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,158 @@
1
+ # Cheer
2
+
3
+ A ruby gem to quickly add leaderboard functionality to any existing model in a rails application. This gem makes it easy to add leaderboards not only in games, where they are usually used but also into any other application which contains rankable models.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ ```ruby
10
+ gem 'cheer'
11
+ ```
12
+
13
+ And then execute:
14
+
15
+ ```
16
+ $ bundle
17
+ ```
18
+
19
+ Or install it yourself as:
20
+
21
+ ```
22
+ $ gem install cheer
23
+ ```
24
+
25
+ ## Basic Usage
26
+
27
+ Consider a `Movie` model which has a `views` column that stores the number of times users have viewed the movie and a `profit` column that contains the total profit made by that movie.
28
+
29
+ ```ruby
30
+ class Movie < ActiveRecord::Base
31
+ extend Cheer::ModelAdditions
32
+
33
+ leaderboard :viewership_leaderboard, column_name: :views
34
+ end
35
+ ```
36
+
37
+ For example consider the following movies in the database:
38
+
39
+ <table>
40
+ <tr>
41
+ <th>Name</th>
42
+ <th>Views</th>
43
+ <th>Profit</th>
44
+ </tr>
45
+ <tr>
46
+ <td>Pulp Fiction</td>
47
+ <td>50</td>
48
+ <td>$500</td>
49
+ </tr>
50
+ <tr>
51
+ <td>Reservoir Dogs</td>
52
+ <td>40</td>
53
+ <td>$600</td>
54
+ </tr>
55
+ <tr>
56
+ <td>Kill Bill</td>
57
+ <td>30</td>
58
+ <td>$200</td>
59
+ </tr>
60
+ <tr>
61
+ <td>Death Proof</td>
62
+ <td>20</td>
63
+ <td>$100</td>
64
+ </tr>
65
+ <tr>
66
+ <td>Jackie Brown</td>
67
+ <td>10</td>
68
+ <td>$400</td>
69
+ </tr>
70
+ </table>
71
+
72
+ ## leaderboard class method
73
+
74
+ The gem will automatically add the `leaderboard` class method to the Movie class which will allow
75
+ you to configure custom leaderboards.
76
+ This method takes these arguments: `name`, `column_name`, `sort_order`, `around_limit`.
77
+
78
+ ```ruby
79
+ class Movie < ActiveRecord::Base
80
+ extend Cheer::ModelAdditions
81
+
82
+ leaderboard :profitability_leaderboard, column_name: :profit,
83
+ sort_order: %w(name),
84
+ around_limit: 1
85
+
86
+ # This will add the :profitability_leaderboard instance method.
87
+ # It also takes an optional argument(integer) to limit the number of records returned from `top_movies` method.
88
+ end
89
+
90
+ pulp_fiction = Movie.find_by_name("Pulp Fiction")
91
+ reservoir_dogs = Movie.find_by_name("Reservoir Dogs")
92
+ kill_bill = Movie.find_by_name("Kill Bill")
93
+ death_proof = Movie.find_by_name("Death Proof")
94
+ jackie_brown = Movie.find_by_name("Jackie Brown")
95
+
96
+ pulp_fiction.profitability_leaderboard # => #<Cheer::Leaderboard:0xb121a14>
97
+ pulp_fiction.profitability_leaderboard.current_movie_rank # => 2
98
+ pulp_fiction.profitability_leaderboard.movies_around # => [reservoir_dogs, pulp_fiction, jackie_brown]
99
+ pulp_fiction.profitability_leaderboard.top_movies # => [pulp_fiction, reservoir_dogs, kill_bill]
100
+ pulp_fiction.profitability_leaderboard.top_movies(2) # => [pulp_fiction, reservoir_dogs]
101
+
102
+ pulp_fiction.profitability_leaderboard.to_hash # => {current_movie_rank: 2, movies_around: [reservoir_dogs, pulp_fiction, jackie_brown], top_movies: [pulp_fiction, reservoir_dogs, kill_bill]}
103
+
104
+ death_proof.profitability_leaderboard.to_hash(2) # => {current_movie_rank: 6, movies_around: [kill_bill, death_proof], top_movies: [pulp_fiction, reservoir_dogs]}
105
+ ```
106
+
107
+
108
+ ## Additional Options
109
+
110
+ ### :sort_order
111
+
112
+ ```ruby
113
+ class Movie < ActiveRecord::Base
114
+ extend Cheer::ModelAdditions
115
+
116
+ leaderboard :viewership_leaderboard, column_name: :views,
117
+ sort_order: ["released_on asc", "number_of_awards desc"]
118
+ end
119
+ ```
120
+
121
+ The gem also allows you to specify additional sort orders to resolve conflicts when there are a bunch of movies with the same number of views. The additional sort order can be specified as shown above. In this case, if two movies have the same number of views, the one released earlier will have a higher ranking. In case the number of views and the release date is the same, the one with more awards will have a higher ranking.
122
+
123
+ If the `:sort_order` is not specified, the conflicts will be resolved using the `id asc` sort order.
124
+
125
+ ### :around_limit
126
+
127
+ The default number of objects returned by `movies_around` can be overwritten using the option `around_limit`. In the example below, we set the option to 1 forcing the gem to return at most 1 movie before and after the current movie:
128
+
129
+ ```ruby
130
+ class Movie < ActiveRecord::Base
131
+ extend Cheer::ModelAdditions
132
+
133
+ leaderboard :viewership_leaderboard, column_name: :views,
134
+ around_limit: 1
135
+ end
136
+
137
+ kill_bill = Movie.find_by_name("Kill Bill")
138
+ kill_bill.viewership_leaderboard.movies_around # => [reservoir_dogs, kill_bill, death_proof]
139
+ ```
140
+
141
+
142
+ ## Roadmap
143
+ * Scopes - Calculate leaderboards and ranking for a specific scope. For example, this will help us generate leaderboards for all movies released in 2012 or for all movies produced by DreamWorks, etc.
144
+
145
+
146
+ ## Authors
147
+ * Karthik C: https://github.com/karthikc
148
+ * Nitin Misra: https://github.com/nitinstp23
149
+ * Rakesh Verma: https://github.com/rakesh87
150
+
151
+
152
+ ## Contributing
153
+
154
+ 1. Fork it
155
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
156
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
157
+ 4. Push to the branch (`git push origin my-new-feature`)
158
+ 5. Create new Pull Request
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rspec/core/rake_task'
4
+ RSpec::Core::RakeTask.new
5
+ task :default => :spec
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/cheer/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = 'cheer'
6
+ gem.version = Cheer::VERSION
7
+ gem.authors = ['Eos Software Systems Pvt Ltd']
8
+ gem.email = ['info@eossys.com']
9
+ gem.description = %q{A ruby gem to quickly add rankings & leaderboards to existing models in a rails application}
10
+ gem.summary = gem.description
11
+ gem.homepage = 'https://github.com/eossys/cheer'
12
+ gem.license = 'MIT'
13
+
14
+ gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
15
+ gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) }
16
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
17
+ gem.require_paths = ['lib']
18
+
19
+ # Runtime Dependencies
20
+ gem.add_runtime_dependency 'activerecord', '>= 3.0'
21
+
22
+ # Development dependencies
23
+ gem.add_development_dependency 'rspec', '~> 2.14'
24
+ gem.add_development_dependency 'sqlite3', '~> 1.3'
25
+ end
@@ -0,0 +1,7 @@
1
+ require 'cheer/version' # created by Bundler
2
+
3
+ require 'cheer/error'
4
+ require 'cheer/argument'
5
+ require 'cheer/leaderboard'
6
+ require 'cheer/rank_evaluator'
7
+ require 'cheer/model_additions'
@@ -0,0 +1,56 @@
1
+ module Cheer
2
+ class Argument
3
+
4
+ attr_reader :column_name, :sort_order, :around_limit, :model_klass
5
+
6
+ def initialize(args = {})
7
+ @model_klass = args[:model_klass]
8
+ @column_name = parse_column_name(args[:column_name])
9
+ @sort_order = parse_sort_order(args[:sort_order])
10
+ @around_limit = parse_around_limit(args[:around_limit])
11
+
12
+ validate_column_name
13
+ validate_sort_order
14
+ validate_around_limit
15
+
16
+ merge_sort_order
17
+ end
18
+
19
+ def parse_column_name(column_name)
20
+ [String, Symbol].include?(column_name.class) ? column_name.to_s : ''
21
+ end
22
+
23
+ def parse_sort_order(sort_order)
24
+ sort_order.present? ? sort_order : ['id']
25
+ end
26
+
27
+ def parse_around_limit(around_limit)
28
+ around_limit ? around_limit : 2
29
+ end
30
+
31
+ def validate_column_name
32
+ return if column_name.present? && column_exists_in_db?
33
+ raise Error::InvalidColumnName
34
+ end
35
+
36
+ def validate_around_limit
37
+ return if around_limit.to_i > 0
38
+ raise Error::InvalidAroundLimit
39
+ end
40
+
41
+ def validate_sort_order
42
+ return if sort_order.is_a?(Array)
43
+ raise Error::InvalidSortOrder
44
+ end
45
+
46
+ def column_exists_in_db?
47
+ model_klass.column_names.include?(column_name)
48
+ end
49
+
50
+ def merge_sort_order
51
+ # Ensure 'id' is always present in Sort Order.
52
+ @sort_order | ['id']
53
+ @sort_order = @sort_order.join(',')
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,23 @@
1
+ module Cheer
2
+ module Error
3
+
4
+ class InvalidColumnName < ArgumentError
5
+ def message
6
+ ':column_name option is not a symbol or string'
7
+ end
8
+ end
9
+
10
+ class InvalidSortOrder < ArgumentError
11
+ def message
12
+ ':sort_order option is not an array of symbols or strings'
13
+ end
14
+ end
15
+
16
+ class InvalidAroundLimit < ArgumentError
17
+ def message
18
+ ':around_limit option is not an integer'
19
+ end
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,48 @@
1
+ module Cheer
2
+ class Leaderboard
3
+
4
+ def initialize(attributes = {})
5
+ @model_klass = attributes[:model_klass]
6
+ @rank_evaluator = attributes[:rank_evaluator]
7
+
8
+ define_public_methods
9
+ end
10
+
11
+ def to_hash(user_limit = 3)
12
+ {
13
+ standing_methods[:current_rank] => self.public_send(standing_methods[:current_rank]),
14
+ standing_methods[:rank_around] => self.public_send(standing_methods[:rank_around]),
15
+ standing_methods[:top_rankers] => self.public_send(standing_methods[:top_rankers], user_limit)
16
+ }
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :model_klass, :rank_evaluator
22
+
23
+ def standing_methods
24
+ @standing_methods ||= model_klass.standing_methods
25
+ end
26
+
27
+ def define_public_methods
28
+ current_rank_method = standing_methods[:current_rank]
29
+ rank_around_method = standing_methods[:rank_around]
30
+ top_rankers_method = standing_methods[:top_rankers]
31
+
32
+ self.class_eval do
33
+ define_method current_rank_method do
34
+ rank_evaluator.current_rank
35
+ end
36
+
37
+ define_method rank_around_method do
38
+ rank_evaluator.rank_around
39
+ end
40
+
41
+ define_method top_rankers_method do |user_limit = 3|
42
+ rank_evaluator.top_rankers(user_limit)
43
+ end
44
+ end
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,34 @@
1
+ module Cheer
2
+ module ModelAdditions
3
+
4
+ # Hookup event for ModelAdditions Module
5
+ def self.extended(base)
6
+ base.class_eval do
7
+ model_name = self.name.underscore
8
+
9
+ define_singleton_method :standing_methods do
10
+ {
11
+ current_rank: "current_#{model_name}_rank".to_sym,
12
+ rank_around: "#{model_name.pluralize}_around".to_sym,
13
+ top_rankers: "top_#{model_name.pluralize}".to_sym
14
+ }
15
+ end
16
+ end
17
+ end
18
+
19
+ def leaderboard(name, args = {})
20
+ define_method name.to_sym do |user_limit = 3|
21
+ rank_evaluator = RankEvaluator.new(
22
+ model_object: self,
23
+ config: Argument.new(args.merge(model_klass: self.class))
24
+ )
25
+
26
+ Leaderboard.new(
27
+ model_klass: self.class,
28
+ rank_evaluator: rank_evaluator
29
+ )
30
+ end
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,67 @@
1
+ module Cheer
2
+ class RankEvaluator
3
+ extend Forwardable
4
+
5
+ def_delegator :@config, :column_name, :column_name
6
+ def_delegator :@config, :sort_order, :sort_order
7
+ def_delegator :@config, :around_limit, :around_limit
8
+ def_delegator :@config, :model_klass, :model_klass
9
+
10
+ def initialize(attributes = {})
11
+ @model_object = attributes[:model_object]
12
+ @config = attributes[:config]
13
+ end
14
+
15
+ def current_rank
16
+ return high_rankers unless equal_rankers?
17
+ high_rankers + position_amongst_equal_rankers
18
+ end
19
+
20
+ def rank_around
21
+ offset = current_rank - (around_limit + 1)
22
+ limit_setting = around_limit * 2 + 1
23
+
24
+ if offset < 0
25
+ limit_setting += offset
26
+ offset = 0
27
+ end
28
+
29
+ top_rankers(limit_setting).offset(offset)
30
+ end
31
+
32
+ def top_rankers(user_limit)
33
+ model_klass.order("#{column_name} DESC, #{sort_order}")
34
+ .limit(user_limit)
35
+ end
36
+
37
+ private
38
+
39
+ attr_reader :model_object
40
+
41
+ def column_value
42
+ @column_value ||= model_object.public_send(column_name)
43
+ end
44
+
45
+ def high_rankers
46
+ rankers = model_klass.where("#{column_name} > ?", column_value)
47
+ .order("#{column_name} DESC")
48
+
49
+ @high_rankers ||= rankers.count + 1
50
+ end
51
+
52
+ def equal_rankers?
53
+ equal_rankers && equal_rankers.count > 1
54
+ end
55
+
56
+ def equal_rankers
57
+ @equal_rankers ||= model_klass.where("#{column_name} = ?", column_value)
58
+ .order(sort_order)
59
+ .select(:id)
60
+ end
61
+
62
+ def position_amongst_equal_rankers
63
+ equal_rankers.index(model_object).to_i
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,4 @@
1
+ module Cheer
2
+ # cheer version
3
+ VERSION = '1.0.4'
4
+ end
@@ -0,0 +1,362 @@
1
+ require 'spec_helper'
2
+
3
+ describe Cheer do
4
+
5
+ context "Students With Equal Score" do
6
+ before do
7
+ Student.delete_all
8
+ @subrat = Student.create!(name: 'Subrat Behera', score: 23)
9
+ @rakesh = Student.create!(name: 'Rakesh Verma', score: 22)
10
+ @girish = Student.create!(name: 'Girish Kumar', score: 21)
11
+ @nitin = Student.create!(name: 'Nitin Misra', score: 22)
12
+ @bhanu = Student.create!(name: 'Bhanu Chander', score: 19)
13
+ @prateeth = Student.create!(name: 'Prateeth S', score: 20)
14
+ end
15
+
16
+ it "returns leaderboard objects" do
17
+ @subrat.topers.should be_an_instance_of(Cheer::Leaderboard)
18
+ @rakesh.topers.should be_an_instance_of(Cheer::Leaderboard)
19
+ @girish.topers.should be_an_instance_of(Cheer::Leaderboard)
20
+ @nitin.topers.should be_an_instance_of(Cheer::Leaderboard)
21
+ @bhanu.topers.should be_an_instance_of(Cheer::Leaderboard)
22
+ @prateeth.topers.should be_an_instance_of(Cheer::Leaderboard)
23
+ end
24
+
25
+ it "returns the current student rank if students with equal score exists" do
26
+ # For students with equal scores, sorting should be done on the basis of id,
27
+ # since the Student model doesn't define sort_order array.
28
+ @bhanu.topers.current_student_rank.should == 6
29
+ @nitin.topers.current_student_rank.should == 3
30
+ @subrat.topers.current_student_rank.should == 1
31
+ @rakesh.topers.current_student_rank.should == 2
32
+ @girish.topers.current_student_rank.should == 4
33
+ @prateeth.topers.current_student_rank.should == 5
34
+ end
35
+ end
36
+
37
+ context "GameUsers" do
38
+ before do
39
+ GameUser.delete_all
40
+ @dark_lord = GameUser.create!(name: 'Dark Lord', score: 11)
41
+ @tom_riddle = GameUser.create!(name: 'Tom Riddle', score: 25)
42
+ @ron_weasely = GameUser.create!(name: 'Ron Weasely', score: 23)
43
+ @harry_potter = GameUser.create!(name: 'Harry Potter', score: 14)
44
+ @jack_sparrow = GameUser.create!(name: 'Jack Sparrow', score: 12)
45
+ @albus_dumbelldore = GameUser.create!(name: 'Albus Dumbelldore', score: 2)
46
+ end
47
+
48
+ it "returns leaderboard objects" do
49
+ @dark_lord.high_scorers.should be_an_instance_of(Cheer::Leaderboard)
50
+ @tom_riddle.high_scorers.should be_an_instance_of(Cheer::Leaderboard)
51
+ @ron_weasely.high_scorers.should be_an_instance_of(Cheer::Leaderboard)
52
+ @harry_potter.high_scorers.should be_an_instance_of(Cheer::Leaderboard)
53
+ @jack_sparrow.high_scorers.should be_an_instance_of(Cheer::Leaderboard)
54
+ @albus_dumbelldore.high_scorers.should be_an_instance_of(Cheer::Leaderboard)
55
+ end
56
+
57
+ it "returns the current user rank" do
58
+ @dark_lord.high_scorers.current_game_user_rank.should == 5
59
+ @tom_riddle.high_scorers.current_game_user_rank.should == 1
60
+ @ron_weasely.high_scorers.current_game_user_rank.should == 2
61
+ @jack_sparrow.high_scorers.current_game_user_rank.should == 4
62
+ @harry_potter.high_scorers.current_game_user_rank.should == 3
63
+ @albus_dumbelldore.high_scorers.current_game_user_rank.should == 6
64
+ end
65
+
66
+ it "returns the top rank users" do
67
+ @dark_lord.high_scorers.top_game_users.should == [@tom_riddle, @ron_weasely, @harry_potter]
68
+ @tom_riddle.high_scorers.top_game_users(2).should == [@tom_riddle, @ron_weasely]
69
+ @ron_weasely.high_scorers.top_game_users(5).should == [@tom_riddle, @ron_weasely, @harry_potter, @jack_sparrow, @dark_lord]
70
+ @jack_sparrow.high_scorers.top_game_users(4).should == [@tom_riddle, @ron_weasely, @harry_potter, @jack_sparrow]
71
+ @harry_potter.high_scorers.top_game_users(1).should == [@tom_riddle]
72
+ end
73
+
74
+ it "returns user around to current user according score descending" do
75
+ @tom_riddle.high_scorers.game_users_around.should == [@tom_riddle, @ron_weasely, @harry_potter]
76
+ @harry_potter.high_scorers.game_users_around.should == [@tom_riddle, @ron_weasely, @harry_potter, @jack_sparrow, @dark_lord]
77
+ @albus_dumbelldore.high_scorers.game_users_around.should == [@jack_sparrow, @dark_lord, @albus_dumbelldore]
78
+ end
79
+ end
80
+
81
+ context "GameUsers With Equal Score" do
82
+ before do
83
+ GameUser.delete_all
84
+ @dark_lord = GameUser.create!(name: 'Dark Lord', score: 11)
85
+ @tom_riddle = GameUser.create!(name: 'Tom Riddle', score: 25)
86
+ @ivan_potter = GameUser.create!(name: 'Ivan Potter', score: 14)
87
+ @ron_weasely = GameUser.create!(name: 'Ron Weasely', score: 23)
88
+ @harry_potter = GameUser.create!(name: 'Harry Potter', score: 14)
89
+ @jerry_potter = GameUser.create!(name: 'Jerry Potter', score: 14)
90
+ @jack_sparrow_1 = GameUser.create!(name: 'Jack Sparrow', score: 12, age: 22)
91
+ @jack_sparrow_2 = GameUser.create!(name: 'Jack Sparrow', score: 12, age: 23)
92
+ @albus_dumbelldore = GameUser.create!(name: 'Albus Dumbelldore', score: 2)
93
+ @duglous_dumbelldore = GameUser.create!(name: 'Duglous Dumbelldore', score: 2)
94
+ end
95
+
96
+ it "returns the current user rank if users with equal score exists" do
97
+ @dark_lord.high_scorers.current_game_user_rank.should == 8
98
+ @tom_riddle.high_scorers.current_game_user_rank.should == 1
99
+ @ivan_potter.high_scorers.current_game_user_rank.should == 4
100
+ @ron_weasely.high_scorers.current_game_user_rank.should == 2
101
+ @harry_potter.high_scorers.current_game_user_rank.should == 3
102
+ @jerry_potter.high_scorers.current_game_user_rank.should == 5
103
+ @jack_sparrow_1.high_scorers.current_game_user_rank.should == 7
104
+ @jack_sparrow_2.high_scorers.current_game_user_rank.should == 6
105
+ @albus_dumbelldore.high_scorers.current_game_user_rank.should == 9
106
+ @duglous_dumbelldore.high_scorers.current_game_user_rank.should == 10
107
+ end
108
+
109
+ it "returns the top rank users if equal score users exists" do
110
+ @dark_lord.high_scorers.top_game_users.should == [@tom_riddle, @ron_weasely, @harry_potter]
111
+ @tom_riddle.high_scorers.top_game_users(1).should == [@tom_riddle]
112
+ @ivan_potter.high_scorers.top_game_users(2).should == [@tom_riddle, @ron_weasely]
113
+ @ron_weasely.high_scorers.top_game_users(4).should == [@tom_riddle, @ron_weasely, @harry_potter, @ivan_potter]
114
+ @harry_potter.high_scorers.top_game_users(5).should == [@tom_riddle, @ron_weasely, @harry_potter, @ivan_potter, @jerry_potter]
115
+ end
116
+
117
+ it "returns user around current user if users with equal score exists" do
118
+ @dark_lord.high_scorers.game_users_around.should == [@jack_sparrow_2, @jack_sparrow_1, @dark_lord, @albus_dumbelldore, @duglous_dumbelldore]
119
+ @tom_riddle.high_scorers.game_users_around.should == [@tom_riddle, @ron_weasely, @harry_potter]
120
+ @ivan_potter.high_scorers.game_users_around.should == [@ron_weasely, @harry_potter, @ivan_potter, @jerry_potter, @jack_sparrow_2]
121
+ @ron_weasely.high_scorers.game_users_around.should == [@tom_riddle, @ron_weasely, @harry_potter, @ivan_potter]
122
+ @harry_potter.high_scorers.game_users_around.should == [@tom_riddle, @ron_weasely, @harry_potter, @ivan_potter, @jerry_potter]
123
+ @jerry_potter.high_scorers.game_users_around.should == [@harry_potter, @ivan_potter, @jerry_potter, @jack_sparrow_2, @jack_sparrow_1]
124
+ @jack_sparrow_1.high_scorers.game_users_around.should == [@jerry_potter, @jack_sparrow_2, @jack_sparrow_1, @dark_lord, @albus_dumbelldore]
125
+ @jack_sparrow_2.high_scorers.game_users_around.should == [@ivan_potter, @jerry_potter, @jack_sparrow_2, @jack_sparrow_1, @dark_lord]
126
+ @albus_dumbelldore.high_scorers.game_users_around.should == [@jack_sparrow_1, @dark_lord, @albus_dumbelldore, @duglous_dumbelldore]
127
+ @duglous_dumbelldore.high_scorers.game_users_around.should == [@dark_lord, @albus_dumbelldore, @duglous_dumbelldore]
128
+ end
129
+ end
130
+
131
+ context "Products" do
132
+ before do
133
+ Product.delete_all
134
+ @cell = Product.create!(name: 'cell', price: 122)
135
+ @ring = Product.create!(name: 'ring', price: 14)
136
+ @shoe = Product.create!(name: 'shoe', price: 235)
137
+ @belt = Product.create!(name: 'belt', price: 21)
138
+ @watch = Product.create!(name: 'watch', price: 267)
139
+ @food = Product.create!(name: 'food', price: 9)
140
+ end
141
+
142
+ it "returns leaderboard objects" do
143
+ @cell.costliest.should be_an_instance_of(Cheer::Leaderboard)
144
+ @ring.costliest.should be_an_instance_of(Cheer::Leaderboard)
145
+ @shoe.costliest.should be_an_instance_of(Cheer::Leaderboard)
146
+ @belt.costliest.should be_an_instance_of(Cheer::Leaderboard)
147
+ @watch.costliest.should be_an_instance_of(Cheer::Leaderboard)
148
+ @food.costliest.should be_an_instance_of(Cheer::Leaderboard)
149
+ end
150
+
151
+ it "returns the current product rank" do
152
+ @cell.costliest.current_product_rank.should == 3
153
+ @ring.costliest.current_product_rank.should == 5
154
+ @shoe.costliest.current_product_rank.should == 2
155
+ @belt.costliest.current_product_rank.should == 4
156
+ @watch.costliest.current_product_rank.should == 1
157
+ @food.costliest.current_product_rank.should == 6
158
+ end
159
+
160
+ it "returns the Top products" do
161
+ @cell.costliest.top_products(2).should == [@watch, @shoe]
162
+ @cell.costliest.top_products(3).should == [@watch, @shoe, @cell]
163
+ @cell.costliest.top_products(4).should == [@watch, @shoe, @cell, @belt]
164
+ @cell.costliest.top_products(5).should == [@watch, @shoe, @cell, @belt, @ring]
165
+ @cell.costliest.top_products(6).should == [@watch, @shoe, @cell, @belt, @ring, @food]
166
+ @cell.costliest.top_products(7).should == [@watch, @shoe, @cell, @belt, @ring, @food]
167
+ end
168
+
169
+ it "returns product around the current product" do
170
+ @cell.costliest.products_around.should == [@watch, @shoe, @cell, @belt, @ring, @food]
171
+ @ring.costliest.products_around.should == [@shoe, @cell, @belt, @ring, @food]
172
+ @shoe.costliest.products_around.should == [@watch, @shoe, @cell, @belt, @ring]
173
+ @belt.costliest.products_around.should == [@watch, @shoe, @cell, @belt, @ring, @food]
174
+ @watch.costliest.products_around.should == [@watch, @shoe, @cell, @belt]
175
+ @food.costliest.products_around.should == [@cell, @belt, @ring, @food]
176
+ end
177
+
178
+ it "returns leaderboard for the current product" do
179
+ @cell.costliest.to_hash.should == {
180
+ current_product_rank: 3,
181
+ products_around: [@watch, @shoe, @cell, @belt, @ring, @food],
182
+ top_products: [@watch, @shoe, @cell]
183
+ }
184
+
185
+ @ring.costliest.to_hash(5).should == {
186
+ current_product_rank: 5,
187
+ products_around: [@shoe, @cell, @belt, @ring, @food],
188
+ top_products: [@watch, @shoe, @cell, @belt, @ring]
189
+ }
190
+
191
+ @shoe.costliest.to_hash(2).should == {
192
+ current_product_rank: 2,
193
+ products_around: [@watch, @shoe, @cell, @belt, @ring],
194
+ top_products: [@watch, @shoe]
195
+ }
196
+
197
+ @belt.costliest.to_hash.should == {
198
+ current_product_rank: 4,
199
+ products_around: [@watch, @shoe, @cell, @belt, @ring, @food],
200
+ top_products: [@watch, @shoe, @cell]
201
+ }
202
+
203
+ @watch.costliest.to_hash(4).should == {
204
+ current_product_rank: 1,
205
+ products_around: [@watch, @shoe, @cell, @belt],
206
+ top_products: [@watch, @shoe, @cell, @belt]
207
+ }
208
+
209
+ @food.costliest.to_hash(3).should == {
210
+ current_product_rank: 6,
211
+ products_around: [@cell, @belt, @ring, @food],
212
+ top_products: [@watch, @shoe, @cell]
213
+ }
214
+ end
215
+ end
216
+
217
+ # Bad Configurations
218
+ context "Bad Configuration Options" do
219
+ before do
220
+ @klass = Class.new do
221
+ include ActiveModel::Model
222
+
223
+ def self.name; "anonymus"; end
224
+
225
+ attr_accessor :score
226
+
227
+ extend Cheer::ModelAdditions
228
+
229
+ def self.column_names; ["score"]; end
230
+ end
231
+ end
232
+
233
+ it "fails if column_name is blank" do
234
+ @klass.class_eval do
235
+ # Without primary column name
236
+ leaderboard :bad_config, column_name: '',
237
+ sort_order: ["name", "age DESC"],
238
+ around_limit: 2
239
+ end
240
+
241
+ lambda {
242
+ @klass.new(score: 5).bad_config
243
+ }.should raise_error(Cheer::Error::InvalidColumnName)
244
+ end
245
+
246
+ it "fails if column_name is not a database column" do
247
+ @klass.class_eval do
248
+ leaderboard :bad_config, column_name: :im_not_in_db,
249
+ sort_order: ["name", "age DESC"],
250
+ around_limit: 2
251
+ end
252
+
253
+ lambda {
254
+ @klass.new(score: 5).bad_config
255
+ }.should raise_error(Cheer::Error::InvalidColumnName)
256
+ end
257
+
258
+ it "fails if sort_order is not an array" do
259
+ @klass.class_eval do
260
+ # Invalid sort order
261
+ leaderboard :bad_config, column_name: :score,
262
+ sort_order: "name",
263
+ around_limit: 2
264
+ end
265
+
266
+ lambda {
267
+ @klass.new(score: 5).bad_config
268
+ }.should raise_error(Cheer::Error::InvalidSortOrder)
269
+ end
270
+
271
+ it "fails if around_limit is zero or less" do
272
+ @klass.class_eval do
273
+ # Invalid around limit
274
+ leaderboard :bad_config, column_name: :score,
275
+ sort_order: ["name", "age DESC"],
276
+ around_limit: [0, -1, -2].sample
277
+ end
278
+
279
+ lambda {
280
+ @klass.new(score: 5).bad_config
281
+ }.should raise_error(Cheer::Error::InvalidAroundLimit)
282
+ end
283
+ end
284
+
285
+ describe ".leaderboard" do
286
+ before do
287
+ Developer.delete_all
288
+ @aaron = Developer.create!(name: 'Aaron Patterson', ruby_gems_created: 100, total_experience: 15)
289
+ @corey = Developer.create!(name: 'Corey Haines', ruby_gems_created: 90, total_experience: 20)
290
+ @jim = Developer.create!(name: 'Jim Weirich', ruby_gems_created: 50, total_experience: 30)
291
+ end
292
+
293
+ it "returns leaderboard objects" do
294
+ @aaron.ruby_heroes.should be_an_instance_of(Cheer::Leaderboard)
295
+ @corey.ruby_heroes.should be_an_instance_of(Cheer::Leaderboard)
296
+ @jim.ruby_heroes.should be_an_instance_of(Cheer::Leaderboard)
297
+ end
298
+
299
+ context "#current_developer_rank" do
300
+ it "returns current rank" do
301
+ @aaron.ruby_heroes.current_developer_rank == 1
302
+ @corey.ruby_heroes.current_developer_rank == 2
303
+ @jim.ruby_heroes.current_developer_rank == 3
304
+ end
305
+ end
306
+
307
+ context "#developers_around" do
308
+ it "returns developers around" do
309
+ @aaron.ruby_heroes.developers_around == [ @aaron, @corey, @jim ]
310
+ @corey.ruby_heroes.developers_around == [ @aaron, @corey, @jim ]
311
+ @jim.ruby_heroes.developers_around == [ @aaron, @corey, @jim ]
312
+ end
313
+ end
314
+
315
+ context "#top_developers" do
316
+ it "returns top developers" do
317
+ @aaron.ruby_heroes.top_developers == [ @aaron, @corey, @jim ]
318
+ @corey.ruby_heroes.top_developers(2) == [ @aaron, @corey ]
319
+ @jim.ruby_heroes.top_developers(1) == [ @aaron ]
320
+ end
321
+ end
322
+
323
+ context "#to_hash method" do
324
+ it "returns leaderboard hash" do
325
+ @aaron.ruby_heroes.to_hash.should == {
326
+ current_developer_rank: 1,
327
+ developers_around: [ @aaron, @corey, @jim ],
328
+ top_developers: [ @aaron, @corey, @jim ]
329
+ }
330
+ @corey.ruby_heroes.to_hash(1).should == {
331
+ current_developer_rank: 2,
332
+ developers_around: [ @aaron, @corey, @jim ],
333
+ top_developers: [ @aaron ]
334
+ }
335
+ @jim.ruby_heroes.to_hash(2).should == {
336
+ current_developer_rank: 3,
337
+ developers_around: [ @aaron, @corey, @jim ],
338
+ top_developers: [ @aaron, @corey ]
339
+ }
340
+ end
341
+
342
+ it "returns leaderboard hash" do
343
+ @aaron.veterans.to_hash(2).should == {
344
+ current_developer_rank: 3,
345
+ developers_around: [ @corey, @aaron ],
346
+ top_developers: [ @jim, @corey ]
347
+ }
348
+ @corey.veterans.to_hash.should == {
349
+ current_developer_rank: 2,
350
+ developers_around: [ @jim, @corey, @aaron ],
351
+ top_developers: [ @jim, @corey, @aaron ]
352
+ }
353
+ @jim.veterans.to_hash(1).should == {
354
+ current_developer_rank: 1,
355
+ developers_around: [ @jim, @corey ],
356
+ top_developers: [ @jim ]
357
+ }
358
+ end
359
+ end
360
+ end
361
+
362
+ end
@@ -0,0 +1,96 @@
1
+ # This file was generated by the `rspec --init` command. Conventionally, all
2
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
3
+ # Require this file using `require "spec_helper"` to ensure that it is only
4
+ # loaded once.
5
+ #
6
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
7
+ $:.unshift File.dirname(__FILE__) + '/../lib'
8
+
9
+ require 'active_record'
10
+ require 'sqlite3'
11
+ require 'cheer'
12
+
13
+ ActiveRecord::Base.establish_connection(
14
+ adapter: "sqlite3",
15
+ database: ":memory:"
16
+ )
17
+
18
+ RSpec.configure do |config|
19
+ config.treat_symbols_as_metadata_keys_with_true_values = true
20
+ config.order = 'random'
21
+ config.filter_run :focus
22
+ config.run_all_when_everything_filtered = true
23
+ end
24
+
25
+ ActiveRecord::Migration.verbose = false
26
+
27
+ ActiveRecord::Schema.define do
28
+ create_table :game_users do |t|
29
+ t.string :name
30
+ t.float :score, default: 0.0
31
+ t.integer :age, default: 20
32
+ t.timestamps
33
+ end
34
+
35
+ create_table :products do |t|
36
+ t.string :name
37
+ t.float :price, default: 0.0
38
+ t.integer :review, default: 1
39
+ t.timestamps
40
+ end
41
+
42
+ create_table :students do |t|
43
+ t.string :name
44
+ t.float :score, default: 0.0
45
+ t.integer :age, default: 5
46
+ t.timestamps
47
+ end
48
+
49
+ create_table :developers do |t|
50
+ t.string :name
51
+ t.float :total_experience, default: 0.0
52
+ t.integer :ruby_gems_created, default: 0
53
+ t.timestamps
54
+ end
55
+ end
56
+
57
+ class GameUser < ActiveRecord::Base
58
+ extend Cheer::ModelAdditions
59
+
60
+ # leaderboard leaderboard_name, {:column_name, :sort_order, :around_limit}
61
+ # Default Sort Order will be ID for game_users with equal score.
62
+ leaderboard :high_scorers, column_name: :score,
63
+ sort_order: ["name", "age DESC"],
64
+ around_limit: 2
65
+ end
66
+
67
+ class Product < ActiveRecord::Base
68
+ extend Cheer::ModelAdditions
69
+
70
+ # leaderboard leaderboard_name, {:column_name, :sort_order, :around_limit}
71
+ # Default Sort Order will be ID for products with equal price.
72
+ leaderboard :costliest, column_name: :price,
73
+ sort_order: %w(name),
74
+ around_limit: 3
75
+ end
76
+
77
+ class Student < ActiveRecord::Base
78
+ extend Cheer::ModelAdditions
79
+
80
+ # leaderboard leaderboard_name, {:column_name, :sort_order, :around_limit}
81
+ # Default Sort Order will be ID for students with equal score.
82
+ leaderboard :topers, column_name: :score
83
+ end
84
+
85
+ class Developer < ActiveRecord::Base
86
+ extend Cheer::ModelAdditions
87
+
88
+ # leaderboard leaderboard_name, {:column_name, :sort_order, :around_limit}
89
+ leaderboard :ruby_heroes, column_name: :ruby_gems_created,
90
+ sort_order: %w(name),
91
+ around_limit: 3
92
+
93
+ leaderboard :veterans, column_name: 'total_experience',
94
+ sort_order: %w(name),
95
+ around_limit: 1
96
+ end
metadata ADDED
@@ -0,0 +1,108 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cheer
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.4
5
+ platform: ruby
6
+ authors:
7
+ - Eos Software Systems Pvt Ltd
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-02-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '3.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '3.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ~>
32
+ - !ruby/object:Gem::Version
33
+ version: '2.14'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ~>
39
+ - !ruby/object:Gem::Version
40
+ version: '2.14'
41
+ - !ruby/object:Gem::Dependency
42
+ name: sqlite3
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ~>
46
+ - !ruby/object:Gem::Version
47
+ version: '1.3'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ~>
53
+ - !ruby/object:Gem::Version
54
+ version: '1.3'
55
+ description: A ruby gem to quickly add rankings & leaderboards to existing models
56
+ in a rails application
57
+ email:
58
+ - info@eossys.com
59
+ executables: []
60
+ extensions: []
61
+ extra_rdoc_files: []
62
+ files:
63
+ - .gitignore
64
+ - .rspec
65
+ - .ruby-gemset
66
+ - .ruby-version
67
+ - Gemfile
68
+ - LICENSE
69
+ - README.md
70
+ - Rakefile
71
+ - cheer.gemspec
72
+ - lib/cheer.rb
73
+ - lib/cheer/argument.rb
74
+ - lib/cheer/error.rb
75
+ - lib/cheer/leaderboard.rb
76
+ - lib/cheer/model_additions.rb
77
+ - lib/cheer/rank_evaluator.rb
78
+ - lib/cheer/version.rb
79
+ - spec/cheer_spec.rb
80
+ - spec/spec_helper.rb
81
+ homepage: https://github.com/eossys/cheer
82
+ licenses:
83
+ - MIT
84
+ metadata: {}
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubyforge_project:
101
+ rubygems_version: 2.2.1
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: A ruby gem to quickly add rankings & leaderboards to existing models in a
105
+ rails application
106
+ test_files:
107
+ - spec/cheer_spec.rb
108
+ - spec/spec_helper.rb