honor 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,20 @@
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/*.log
15
+ spec/config/database.yml
16
+ spec/reports
17
+ test/tmp
18
+ test/version_tmp
19
+ tmp
20
+ .DS_Store
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --backtrace
@@ -0,0 +1,5 @@
1
+ ## v1.0.1
2
+
3
+ * Initial Release
4
+ * Points
5
+ * Scorecards
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in honor.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Jeremy Ward
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,155 @@
1
+ # Honor
2
+
3
+ Honor adds support for common gamification features such as points, leaderboards, and achievements.
4
+
5
+ ## Overview
6
+
7
+ Honor allows you to easily integrate points, rankings, and leaderboards into your Rails application.
8
+
9
+ ## Installation
10
+
11
+ To use Honor, first add it to you Gemfile:
12
+
13
+ ```ruby
14
+ gem 'honor'
15
+ ```
16
+
17
+ And then intall it with bundler by executing:
18
+
19
+ ```shell
20
+ bundle install
21
+ ```
22
+
23
+ Install and run the migrations:
24
+
25
+ ```shell
26
+ rails g honor:install
27
+ rails db:migrate
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ Honor is meant to be used with a **single model** (eg. User, Employee, Person, Company, etc). At this time, using Honor with multiple models is NOT supported.
33
+
34
+ ```ruby
35
+ class User < ActiveRecord::Base
36
+ include Honor
37
+ end
38
+ ```
39
+
40
+ Honor adds two new relationships to the receiving model.
41
+
42
+ 1. `has_many :points`
43
+ 2. `has_one :scorecard`
44
+
45
+ ### Points
46
+
47
+ Points are generally awarded for various User interactions throughout an application. Honor gives you the ability to provide a message as well as a classification to each point block awarded.
48
+
49
+ ```ruby
50
+ class User < ActiveRecord::Base
51
+ include Honor
52
+ end
53
+
54
+ @user = User.new
55
+
56
+ # add points to a user
57
+ @user.add_points(25)
58
+
59
+ # add points to a user with a bit more description
60
+ @user.add_points(25, "Awarded for some awesome action", "Social")
61
+
62
+ # subtract points to a user
63
+ @user.subtract_points(25)
64
+
65
+ # subtract points to a user with a bit more description
66
+ @user.subtract_points(25, "Awarded for some awesome action", "Social")
67
+
68
+ # Quick Accessors for commone point totals
69
+ @user.points_total
70
+ @user.points_today
71
+ @user.points_this_week
72
+ @user.points_this_month
73
+ @user.points_this_year
74
+
75
+ # If you're fastidious about keeping business logic categories semantic (same case, same spelling, etc), then you can easily total points for a given point category by passing a string representing the category into any of the accessors above:
76
+ @user.points_total("Social") # => returns total for points with a category matcing "Social"
77
+
78
+ ```
79
+
80
+ There's bond to be situations where you don't have or don't want to instantiate a new "honorable" object (User in the above example), but have access to the User's id....Honorable has you covered. In these cases, Honorable provides class methods similar to the above:
81
+
82
+ ```ruby
83
+ # example user id, could come from an array or whatever
84
+ user_id = 1
85
+
86
+ # add points to a user
87
+ Honor::Point.give_to(user_id, 25)
88
+
89
+ # add points to a user with a bit more description
90
+ Honor::Point.give_to(user_id, 25, "Awarded for some awesome action", "Social")
91
+
92
+ # subtract points to a user
93
+ Honor::Point.take_from(user_id, 25)
94
+
95
+ # subtract points to a user with a bit more description
96
+ Honor::Point.take_from(user_id, 25, "Awarded for some awesome action", "Social")
97
+
98
+
99
+ # Quick Accessors for commone point totals (does NOT accept a category at this time)
100
+ Honor::Point.points_total
101
+ Honor::Point.points_today
102
+ Honor::Point.points_this_week
103
+ Honor::Point.points_this_month
104
+ Honor::Point.points_this_year
105
+
106
+ ```
107
+
108
+ ### Scorecards
109
+
110
+ Every "honorable" object (User in the above examples) has one scorecard that is continually updated whenever a point record is saved. Scorecards keep a running tally of points for:
111
+
112
+ 1. daily
113
+ 2. weekly
114
+ 3. monthly
115
+ 4. yearly
116
+ 5. lifetime
117
+
118
+ From these subsets you can quickly and easily make Ranked Leaderboards that complies with the Standard Competition Ranking system. That is, it takes tied positions into account and adjusts ranks accordingly.
119
+
120
+ ```ruby
121
+ user_ids = current_user.friend_ids_and_me # => an array of user_ids (eg. [1, 5, 2, 23])
122
+ @daily_leaderboard = Honor::Scorecard.leaderboard(user_ids, rank_by: 'daily', sort_direction: 'desc')
123
+ @weekly_leaderboard = Honor::Scorecard.leaderboard(user_ids, rank_by: 'weekly', sort_direction: 'desc')
124
+ @monthly_leaderboard = Honor::Scorecard.leaderboard(user_ids, rank_by: 'monthly', sort_direction: 'desc')
125
+ @yearly_leaderboard = Honor::Scorecard.leaderboard(user_ids, rank_by: 'yearly', sort_direction: 'desc')
126
+ ```
127
+
128
+ Honor provides the Scorecards class with some utility methods useful for setting cron jobs to automate cleaning-up the scorecards.
129
+
130
+ ```ruby
131
+ Honor::Scorecard.reset_daily_scores
132
+ Honor::Scorecard.reset_weekly_scores
133
+ Honor::Scorecard.reset_monthly_scores
134
+ Honor::Scorecard.reset_yearly_scores
135
+ ```
136
+
137
+ ## Testing
138
+
139
+ Honor uses RSpec for its test coverage. Inside the gem directory, you can run the specs with:
140
+
141
+ ```shell
142
+ rake
143
+ ```
144
+
145
+ ## TODO
146
+
147
+ - Add Badges / Achievement functionality
148
+
149
+ ## Contributing
150
+
151
+ 1. Fork it
152
+ 2. Create your feature branch `git checkout -b my-new-feature`
153
+ 3. Commit your changes `git commit -am 'Add some feature'`
154
+ 4. Push to the branch `git push origin my-new-feature`
155
+ 5. Create new Pull Request
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,26 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'honor/version'
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "honor"
8
+ gem.version = Honor::VERSION
9
+ gem.authors = ["Jeremy Ward"]
10
+ gem.email = ["jrmy.ward@gmail.com"]
11
+ gem.description = %q{Adds support for common gamification features such as points, leaderboards, and achievements.}
12
+ gem.summary = %q{General gamification-centric reputation system for Rails Applications.}
13
+ gem.homepage = "https://github.com/jrmyward/honor"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_runtime_dependency 'rails', '~> 3.0'
21
+ gem.add_development_dependency "rspec"
22
+ gem.add_development_dependency 'ammeter'
23
+ gem.add_development_dependency "supermodel"
24
+ gem.add_development_dependency "database_cleaner"
25
+ gem.add_development_dependency "timecop"
26
+ end
Binary file
@@ -0,0 +1,40 @@
1
+ require 'rails/generators'
2
+ require 'rails/generators/active_record'
3
+ require 'rails/generators/migration'
4
+
5
+ module Honor
6
+ class InstallGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+
9
+ desc "Generates migration for Points and Scorecard"
10
+
11
+ def self.orm
12
+ Rails::Generators.options[:rails][:orm]
13
+ end
14
+
15
+ def self.source_root
16
+ File.join(File.dirname(__FILE__), 'templates', (orm.to_s unless orm.class.eql?(String)) )
17
+ end
18
+
19
+ def self.orm_has_migration?
20
+ [:active_record].include? orm
21
+ end
22
+
23
+ def self.next_migration_number(dirname)
24
+ if ActiveRecord::Base.timestamped_migrations
25
+ migration_number = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
26
+ migration_number += 1
27
+ migration_number.to_s
28
+ else
29
+ "%.3d" % (current_migration_number(dirname) + 1)
30
+ end
31
+ end
32
+
33
+ def create_migration_file
34
+ if self.class.orm_has_migration?
35
+ migration_template 'create_points.rb', 'db/migrate/create_honor_points.rb'
36
+ migration_template 'create_scorecards.rb', 'db/migrate/create_honor_scorecards.rb'
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,13 @@
1
+ class CreateHonorPointsMigration < ActiveRecord::Migration
2
+ def change
3
+ create_table :points do |t|
4
+ t.integer :user_id
5
+ t.integer :value
6
+ t.string :category
7
+ t.text :message
8
+
9
+ t.timestamps
10
+ end
11
+ add_index :points, :user_id
12
+ end
13
+ end
@@ -0,0 +1,15 @@
1
+ class CreateHonorScorecardsMigration < ActiveRecord::Migration
2
+ def change
3
+ create_table :scorecards do |t|
4
+ t.integer :user_id
5
+ t.integer :daily
6
+ t.integer :weekly
7
+ t.integer :monthly
8
+ t.integer :yearly
9
+ t.integer :lifetime
10
+
11
+ t.timestamps
12
+ end
13
+ add_index :scorecards, :user_id
14
+ end
15
+ end
@@ -0,0 +1,25 @@
1
+ require "active_record"
2
+ require "active_record/version"
3
+ require "honor/version"
4
+ require "honor/model_additions"
5
+ require "honor/point"
6
+ require "honor/scorecard"
7
+
8
+ module Honor
9
+
10
+ def self.included(receiver)
11
+ receiver.class_eval %Q{
12
+ has_many :points, :class_name => "Honor::Point", :foreign_key => "user_id", :dependent => :destroy
13
+ has_one :scorecard, :class_name => "Honor::Scorecard", :foreign_key => "user_id", :dependent => :destroy
14
+ }
15
+
16
+ Honor::Point.class_eval %Q{
17
+ belongs_to :#{receiver.to_s.underscore}, :class_name => "#{receiver.to_s}", :foreign_key => "user_id"
18
+ }
19
+
20
+ Honor::Scorecard.class_eval %Q{
21
+ belongs_to :#{receiver.to_s.underscore}, :class_name => "#{receiver.to_s}", :foreign_key => "user_id"
22
+ }
23
+ end
24
+
25
+ end
@@ -0,0 +1,51 @@
1
+ module Honor
2
+ extend ActiveSupport::Concern
3
+
4
+ def add_points(number_of_points, message = "Manually granted through 'add_points'", category = 'default' )
5
+ points.create!({value: number_of_points, message: message, category: category})
6
+ end
7
+
8
+ def subtract_points(number_of_points, message = "Manually granted through 'add_points'", category = 'default' )
9
+ add_points -number_of_points, message, category
10
+ end
11
+
12
+ def points_total(category = nil)
13
+ if category.nil?
14
+ points.sum(:value)
15
+ else
16
+ points.where("points.category = ?", category).sum(:value)
17
+ end
18
+ end
19
+
20
+ def points_today(category = nil)
21
+ if category.nil?
22
+ points.where(:created_at => Time.zone.now.beginning_of_day..Time.zone.now.end_of_day).sum(:value)
23
+ else
24
+ points.where(:created_at => Time.zone.now.beginning_of_day..Time.zone.now.end_of_day).where("points.category = ?", category).sum(:value)
25
+ end
26
+ end
27
+
28
+ def points_this_week(category = nil)
29
+ if category.nil?
30
+ points.where(:created_at => Time.zone.now.beginning_of_week..Time.zone.now.end_of_week).sum(:value)
31
+ else
32
+ points.where(:created_at => Time.zone.now.beginning_of_week..Time.zone.now.end_of_week).where("points.category = ?", category).sum(:value)
33
+ end
34
+ end
35
+
36
+ def points_this_month(category = nil)
37
+ if category.nil?
38
+ points.where(:created_at => Time.zone.now.beginning_of_month..Time.zone.now.end_of_month).sum(:value)
39
+ else
40
+ points.where(:created_at => Time.zone.now.beginning_of_month..Time.zone.now.end_of_month).where("points.category = ?", category).sum(:value)
41
+ end
42
+ end
43
+
44
+ def points_this_year(category = nil)
45
+ if category.nil?
46
+ points.where(:created_at => Time.zone.now.beginning_of_year..Time.zone.now.end_of_year).sum(:value)
47
+ else
48
+ points.where(:created_at => Time.zone.now.beginning_of_year..Time.zone.now.end_of_year).where("points.category = ?", category).sum(:value)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,51 @@
1
+ module Honor
2
+ class Point < ActiveRecord::Base
3
+ attr_accessible :category, :message, :user_id, :value
4
+
5
+ after_save :update_scorecard
6
+
7
+ class << self
8
+ def give_to(user_id, number_of_points, message = "Manually granted through 'add_points'", category = 'default')
9
+ create!({user_id: user_id, value: number_of_points, message: message, category: category})
10
+ end
11
+
12
+ def take_from(user_id, number_of_points, message = "Manually granted through 'add_points'", category = 'default')
13
+ give_to user_id, -number_of_points, message, category
14
+ end
15
+
16
+ def user_points_total(user_id)
17
+ where(:user_id => user_id).sum(:value)
18
+ end
19
+
20
+ def user_points_today(user_id)
21
+ where(:user_id => user_id).where(:created_at => Time.zone.now.beginning_of_day..Time.zone.now.end_of_day).sum(:value)
22
+ end
23
+
24
+ def user_points_this_week(user_id)
25
+ where(:user_id => user_id).where(:created_at => Time.zone.now.beginning_of_week..Time.zone.now.end_of_week).sum(:value)
26
+ end
27
+
28
+ def user_points_this_month(user_id)
29
+ where(:user_id => user_id).where(:created_at => Time.zone.now.beginning_of_month..Time.zone.now.end_of_month).sum(:value)
30
+ end
31
+
32
+ def user_points_this_year(user_id)
33
+ where(:user_id => user_id).where(:created_at => Time.zone.now.beginning_of_year..Time.zone.now.end_of_year).sum(:value)
34
+ end
35
+
36
+ end
37
+
38
+ private
39
+
40
+ def update_scorecard
41
+ scorecard = Honor::Scorecard.find_or_initialize_by_user_id(user_id)
42
+ scorecard.daily = Honor::Point.user_points_today(user_id)
43
+ scorecard.weekly = Honor::Point.user_points_this_week(user_id)
44
+ scorecard.monthly = Honor::Point.user_points_this_month(user_id)
45
+ scorecard.yearly = Honor::Point.user_points_this_year(user_id)
46
+ scorecard.lifetime = Honor::Point.user_points_total(user_id)
47
+ scorecard.save!
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,51 @@
1
+ module Honor
2
+
3
+ class Scorecard < ActiveRecord::Base
4
+ attr_accessible :daily, :lifetime, :monthly, :user_id, :weekly, :yearly, :position
5
+ attr_accessor :position
6
+
7
+ class << self
8
+ def leaderboard(scorecard_user_ids, opt={})
9
+ opt.reverse_merge! :rank_by => 'daily',
10
+ :sort_direction => 'desc'
11
+ scorecards = team_scorecards(scorecard_user_ids, rank_by: opt[:rank_by], sort_direction: opt[:sort_direction])
12
+ rankings = []
13
+ scorecards.each_with_index do |scorecard, i|
14
+ if i == 0
15
+ scorecard.position = 1
16
+ elsif scorecard[opt[:rank_by]] == scorecards[i-1][opt[:rank_by]]
17
+ scorecard.position = rankings[i-1]
18
+ else
19
+ scorecard.position = i + 1
20
+ end
21
+ rankings[i] = scorecard.position
22
+ end
23
+ return scorecards
24
+ end
25
+
26
+ def reset_daily_scores
27
+ where("updated_at < ?", Time.zone.now.beginning_of_day).update_all(:daily => 0)
28
+ end
29
+
30
+ def reset_weekly_scores
31
+ where("updated_at < ?", Time.zone.now.beginning_of_week).update_all(:weekly => 0)
32
+ end
33
+
34
+ def reset_monthly_scores
35
+ where("updated_at < ?", Time.zone.now.beginning_of_month).update_all(:monthly => 0)
36
+ end
37
+
38
+ def reset_yearly_scores
39
+ where("updated_at < ?", Time.zone.now.beginning_of_year).update_all(:yearly => 0)
40
+ end
41
+
42
+ def team_scorecards(scorecard_user_ids, opt={})
43
+ opt.reverse_merge! :rank_by => 'daily',
44
+ :sort_direction => 'desc'
45
+ includes(:user).where('user_id IN (?)', scorecard_user_ids).order("#{opt[:rank_by]} #{opt[:sort_direction]}")
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+ end
@@ -0,0 +1,3 @@
1
+ module Honor
2
+ VERSION = "1.0.0"
3
+ end
@@ -0,0 +1 @@
1
+ require 'honor'
@@ -0,0 +1,19 @@
1
+ sqlite3:
2
+ adapter: sqlite3
3
+ database: honor.sqlite3
4
+
5
+ mysql:
6
+ adapter: mysql2
7
+ hostname: localhost
8
+ username: root
9
+ password:
10
+ database: honor
11
+ charset: utf8
12
+
13
+ postgresql:
14
+ adapter: postgresql
15
+ hostname: localhost
16
+ username: postgres
17
+ password:
18
+ database: honor
19
+ encoding: utf8
@@ -0,0 +1,7 @@
1
+ class Employee < ActiveRecord::Base
2
+ include Honor
3
+ end
4
+
5
+ class User < ActiveRecord::Base
6
+ include Honor
7
+ end
@@ -0,0 +1,34 @@
1
+ ActiveRecord::Schema.define :version => 0 do
2
+ create_table "employees", :force => true do |t|
3
+ t.string "first_name"
4
+ t.string "last_name"
5
+ end
6
+
7
+ create_table "points", :force => true do |t|
8
+ t.integer "user_id"
9
+ t.integer "value"
10
+ t.string "category"
11
+ t.text "message"
12
+ t.datetime "created_at", :null => false
13
+ t.datetime "updated_at", :null => false
14
+ end
15
+ add_index "points", ["user_id"], :name => "index_points_on_user_id"
16
+
17
+ create_table "scorecards", :force => true do |t|
18
+ t.integer "user_id"
19
+ t.integer "daily"
20
+ t.integer "weekly"
21
+ t.integer "monthly"
22
+ t.integer "yearly"
23
+ t.integer "lifetime"
24
+ t.datetime "created_at", :null => false
25
+ t.datetime "updated_at", :null => false
26
+ end
27
+ add_index "scorecards", ["user_id"], :name => "index_scorecards_on_user_id"
28
+
29
+ create_table "users", :force => true do |t|
30
+ t.string "first_name"
31
+ t.string "last_name"
32
+ end
33
+
34
+ end
@@ -0,0 +1,27 @@
1
+ require 'spec_helper'
2
+
3
+ # Generators are not automatically loaded by Rails
4
+ require 'generators/install_generator'
5
+
6
+ describe Honor::InstallGenerator do
7
+ # Tell the generator where to put its output (what it thinks of as Rails.root)
8
+ destination File.expand_path("../../../../../tmp", __FILE__)
9
+
10
+ before do
11
+ prepare_destination
12
+ Rails::Generators.options[:rails][:orm] = :active_record
13
+ end
14
+ describe 'no arguments' do
15
+ before { run_generator }
16
+
17
+ describe 'db/migrate/create_honor_points.rb' do
18
+ subject { file('db/migrate/create_honor_points.rb') }
19
+ it { should be_a_migration }
20
+ end
21
+
22
+ describe 'db/migrate/create_honor_scorecards.rb' do
23
+ subject { file('db/migrate/create_honor_scorecards.rb') }
24
+ it { should be_a_migration }
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,115 @@
1
+ require 'spec_helper'
2
+
3
+ describe Honor do
4
+ let(:user) { User.create!({ first_name: 'James', last_name: 'Bond' }) }
5
+
6
+ before(:each) do
7
+ Time.zone = "Central Time (US & Canada)"
8
+ end
9
+
10
+ it "should create an instance attribute for points" do
11
+ user.should respond_to(:points)
12
+ end
13
+
14
+ it "should create an instance attribute for scorecard" do
15
+ user.should respond_to(:scorecard)
16
+ end
17
+
18
+ describe "Method" do
19
+ describe "add_points()" do
20
+ it "should add points through the current user" do
21
+ expect {
22
+ user.add_points(25, 'Manual Add', 'Test')
23
+ }.to change(Honor::Point, :count).by(1)
24
+ end
25
+ end
26
+ describe "subtract_points()" do
27
+ it "should subtract points through the current user" do
28
+ expect {
29
+ user.subtract_points(25, 'Manual Add', 'Test')
30
+ }.to change(Honor::Point, :count).by(1)
31
+ end
32
+ it "should set the value to a negative number" do
33
+ p = user.subtract_points(25, 'Manual Add', 'Test')
34
+ p.value.should == -25
35
+ end
36
+ end
37
+ end
38
+
39
+ describe "Scopes" do
40
+ before(:each) do
41
+ seed_points(user.id)
42
+ Timecop.freeze(2012, 11, 5, 16, 00)
43
+ end
44
+
45
+ after(:each) do
46
+ Timecop.return
47
+ end
48
+
49
+ context "User" do
50
+ describe "points_total" do
51
+ it "should return a sum of all of the user's points" do
52
+ user.points_total.should == 150
53
+ end
54
+ it "should return a sum of user points for a given category" do
55
+ user.points_total('Test 2').should == 75
56
+ end
57
+ end
58
+
59
+ describe "points_today" do
60
+ it "should return a sum of all of the user's points" do
61
+ user.points_today.should == 50
62
+ end
63
+ it "should return a sum of user points for a given category" do
64
+ user.points_today('Test 2').should == 25
65
+ end
66
+ end
67
+
68
+ describe "points_this_week" do
69
+ it "should return a sum of all of the user's points" do
70
+ user.points_this_week.should == 50
71
+ end
72
+ it "should return a sum of user points for a given category" do
73
+ user.points_this_week('Test 2').should == 25
74
+ end
75
+ end
76
+
77
+ describe "points_this_month" do
78
+ it "should return a sum of all of the user's points" do
79
+ user.points_this_month.should == 150
80
+ end
81
+ it "should return a sum of user points for a given category" do
82
+ user.points_this_month('Test 2').should == 75
83
+ end
84
+ end
85
+
86
+ describe "points_this_year" do
87
+ it "should return a sum of all of the user's points" do
88
+ user.points_this_year.should == 150
89
+ end
90
+ it "should return a sum of user points for a given category" do
91
+ user.points_this_year('Test 2').should == 75
92
+ end
93
+ end
94
+ end
95
+ end
96
+
97
+ end
98
+
99
+ def seed_points(user_id)
100
+ Timecop.freeze(2012, 1, 15, 12, 05)
101
+ Honor::Point.create!({user_id: user_id, value: -25, category: 'Test', message: ''})
102
+ Honor::Point.create!({user_id: user_id, value: -25, category: 'Test 2', message: ''})
103
+ Timecop.freeze(2012, 11, 5, 12, 05)
104
+ Honor::Point.create!({user_id: user_id, value: 25, category: 'Test', message: ''})
105
+ Honor::Point.create!({user_id: user_id, value: 25, category: 'Test 2', message: ''})
106
+ Timecop.freeze(2012, 11, 12, 12, 05)
107
+ Honor::Point.create!({user_id: user_id, value: 25, category: 'Test', message: ''})
108
+ Honor::Point.create!({user_id: user_id, value: 25, category: 'Test 2', message: ''})
109
+ Timecop.freeze(2012, 11, 19, 12, 05)
110
+ Honor::Point.create!({user_id: user_id, value: 25, category: 'Test', message: ''})
111
+ Honor::Point.create!({user_id: user_id, value: 25, category: 'Test 2', message: ''})
112
+ Timecop.freeze(2012, 12, 4, 12, 05)
113
+ Honor::Point.create!({user_id: user_id, value: 25, category: 'Test', message: ''})
114
+ Honor::Point.create!({user_id: user_id, value: 25, category: 'Test 2', message: ''})
115
+ end
@@ -0,0 +1,117 @@
1
+ require 'spec_helper'
2
+
3
+ describe Honor::Point do
4
+ let(:user) { User.create!({ first_name: 'James', last_name: 'Bond' }) }
5
+
6
+ before(:each) do
7
+ Time.zone = "Central Time (US & Canada)"
8
+ end
9
+
10
+ it "should respond to the reciever" do
11
+ Honor::Point.new.should respond_to(:user)
12
+ end
13
+
14
+ describe "Method" do
15
+ describe "#give_to()" do
16
+ it "should add points to a given user" do
17
+ expect {
18
+ Honor::Point.give_to(user.id, 25, 'Manual give to', 'Test')
19
+ }.to change(Honor::Point, :count).by(1)
20
+ end
21
+ end
22
+ describe "#take_from()" do
23
+ it "should add points to a given user"do
24
+ expect {
25
+ Honor::Point.take_from(user.id, 25, 'Manual take away', 'Test')
26
+ }.to change(Honor::Point, :count).by(1)
27
+ end
28
+ it "should set the value to a negative number" do
29
+ p = Honor::Point.take_from(user.id, 25, 'Manual take away', 'Test')
30
+ p.value.should == -25
31
+ end
32
+ end
33
+ end
34
+
35
+ describe "Scorecard" do
36
+ before(:each) do
37
+ seed_points(user.id)
38
+ Timecop.freeze(2012, 12, 4, 16, 00)
39
+ end
40
+
41
+ after(:each) do
42
+ Timecop.return
43
+ end
44
+
45
+ it "should update after Points are saved" do
46
+ sc = Honor::Scorecard.find_by_user_id(user.id)
47
+ sc.should_not be_nil
48
+ sc.daily.should == 50
49
+ sc.weekly.should == 50
50
+ sc.monthly.should == 50
51
+ sc.yearly.should == 150
52
+ sc.lifetime.should == 150
53
+ end
54
+ end
55
+
56
+ describe "Scopes" do
57
+ before(:each) do
58
+ seed_points(user.id)
59
+ Timecop.freeze(2012, 11, 5, 16, 00)
60
+ end
61
+
62
+ after(:each) do
63
+ Timecop.return
64
+ end
65
+
66
+ context "Self" do
67
+ describe "user_points_total" do
68
+ it "should return a sum of all of the user's points" do
69
+ Honor::Point.user_points_total(user.id).should == 150
70
+ end
71
+ end
72
+
73
+ describe "user_points_today" do
74
+ it "should return a sum of all of the user's points" do
75
+ Honor::Point.user_points_today(user.id).should == 50
76
+ end
77
+ end
78
+
79
+ describe "user_points_this_week" do
80
+ it "should return a sum of all of the user's points" do
81
+ Honor::Point.user_points_this_week(user.id).should == 50
82
+ end
83
+ end
84
+
85
+ describe "user_points_this_month" do
86
+ it "should return a sum of all of the user's points" do
87
+ Honor::Point.user_points_this_month(user.id).should == 150
88
+ end
89
+ end
90
+
91
+ describe "user_points_this_year" do
92
+ it "should return a sum of all of the user's points" do
93
+ Honor::Point.user_points_this_year(user.id).should == 150
94
+ end
95
+ end
96
+ end
97
+
98
+ end
99
+ end
100
+
101
+ def seed_points(user_id)
102
+ Timecop.freeze(2012, 1, 15, 12, 05)
103
+ Honor::Point.create!({user_id: user_id, value: -25, category: 'Test', message: ''})
104
+ Honor::Point.create!({user_id: user_id, value: -25, category: 'Test 2', message: ''})
105
+ Timecop.freeze(2012, 11, 5, 12, 05)
106
+ Honor::Point.create!({user_id: user_id, value: 25, category: 'Test', message: ''})
107
+ Honor::Point.create!({user_id: user_id, value: 25, category: 'Test 2', message: ''})
108
+ Timecop.freeze(2012, 11, 12, 12, 05)
109
+ Honor::Point.create!({user_id: user_id, value: 25, category: 'Test', message: ''})
110
+ Honor::Point.create!({user_id: user_id, value: 25, category: 'Test 2', message: ''})
111
+ Timecop.freeze(2012, 11, 19, 12, 05)
112
+ Honor::Point.create!({user_id: user_id, value: 25, category: 'Test', message: ''})
113
+ Honor::Point.create!({user_id: user_id, value: 25, category: 'Test 2', message: ''})
114
+ Timecop.freeze(2012, 12, 4, 12, 05)
115
+ Honor::Point.create!({user_id: user_id, value: 25, category: 'Test', message: ''})
116
+ Honor::Point.create!({user_id: user_id, value: 25, category: 'Test 2', message: ''})
117
+ end
@@ -0,0 +1,119 @@
1
+ require 'spec_helper'
2
+
3
+ describe Honor::Scorecard do
4
+ let(:valid_user) { FactoryGirl.attributes_for(:user) }
5
+ let (:valid_friend) { FactoryGirl.attributes_for(:user, username: 'goldfinger', email: Faker::Internet.email) }
6
+
7
+ before(:each) do
8
+ Time.zone = "Central Time (US & Canada)"
9
+ Timecop.freeze(2011, 12, 4, 16, 00)
10
+ @user_1 = User.create!({ first_name: 'Bruce', last_name: 'Wayne' })
11
+ @user_2 = User.create!({ first_name: 'Clark', last_name: 'Kent' })
12
+ Honor::Scorecard.create!({user_id: @user_1.id, daily: 25, weekly: 100, monthly: 350, yearly: 1250, lifetime: 1375})
13
+ Honor::Scorecard.create!({user_id: @user_2.id, daily: 50, weekly: 100, monthly: 350, yearly: 1250, lifetime: 1375})
14
+ Timecop.return
15
+ end
16
+
17
+ it "should respond to the reciever" do
18
+ Honor::Scorecard.new.should respond_to(:user)
19
+ end
20
+
21
+ describe "Ranking" do
22
+ it "should produce rankings with correctly calculated tied positions" do
23
+ user_ids = [@user_2.id, @user_1.id]
24
+ daily_rankings = Honor::Scorecard.leaderboard(user_ids, rank_by: 'daily', sort_direction: 'desc')
25
+ daily_rankings[0].position.should == 1
26
+ daily_rankings[1].position.should == 2
27
+ weekly_rankings = Honor::Scorecard.leaderboard(user_ids, rank_by: 'weekly', sort_direction: 'desc')
28
+ weekly_rankings[0].position.should == 1
29
+ weekly_rankings[1].position.should == 1
30
+ end
31
+ end
32
+
33
+ describe "Methods" do
34
+ describe "#reset_daily_scores" do
35
+ it "should set the daily attribute to 0 if no updates have been made today" do
36
+ Honor::Scorecard.reset_daily_scores
37
+ sc = Honor::Scorecard.find_by_user_id(@user_1.id)
38
+ sc.daily.should == 0
39
+ sc.weekly.should == 100
40
+ sc.monthly.should == 350
41
+ sc.yearly.should == 1250
42
+ end
43
+
44
+ it "should NOT change the daily attribute if an update was made today" do
45
+ sc_before = Honor::Scorecard.find_by_user_id(@user_1.id)
46
+ sc_before.update_attribute(:daily, 30)
47
+ Honor::Scorecard.reset_daily_scores
48
+ sc_after = Honor::Scorecard.find_by_user_id(@user_1.id)
49
+ sc_after.daily.should == 30
50
+ sc_after.weekly.should == 100
51
+ sc_after.monthly.should == 350
52
+ sc_after.yearly.should == 1250
53
+ end
54
+ end
55
+ describe "#reset_weekly_scores" do
56
+ it "should set the weekly attribute to 0 if no updates have been made this week" do
57
+ Honor::Scorecard.reset_weekly_scores
58
+ sc = Honor::Scorecard.find_by_user_id(@user_1.id)
59
+ sc.daily.should == 25
60
+ sc.weekly.should == 0
61
+ sc.monthly.should == 350
62
+ sc.yearly.should == 1250
63
+ end
64
+
65
+ it "should NOT change the weekly attribute if an update was made this week" do
66
+ sc_before = Honor::Scorecard.find_by_user_id(@user_1.id)
67
+ sc_before.update_attribute(:weekly, 125)
68
+ Honor::Scorecard.reset_weekly_scores
69
+ sc_after = Honor::Scorecard.find_by_user_id(@user_1.id)
70
+ sc_after.daily.should == 25
71
+ sc_after.weekly.should == 125
72
+ sc_after.monthly.should == 350
73
+ sc_after.yearly.should == 1250
74
+ end
75
+ end
76
+ describe "#reset_monthly_scores" do
77
+ it "should set the monthly attribute to 0 if no updates have been made this month" do
78
+ Honor::Scorecard.reset_monthly_scores
79
+ sc = Honor::Scorecard.find_by_user_id(@user_1.id)
80
+ sc.daily.should == 25
81
+ sc.weekly.should == 100
82
+ sc.monthly.should == 0
83
+ sc.yearly.should == 1250
84
+ end
85
+
86
+ it "should NOT change the monthly attribute if an update was made this month" do
87
+ sc_before = Honor::Scorecard.find_by_user_id(@user_1.id)
88
+ sc_before.update_attribute(:monthly, 400)
89
+ Honor::Scorecard.reset_monthly_scores
90
+ sc_after = Honor::Scorecard.find_by_user_id(@user_1.id)
91
+ sc_after.daily.should == 25
92
+ sc_after.weekly.should == 100
93
+ sc_after.monthly.should == 400
94
+ sc_after.yearly.should == 1250
95
+ end
96
+ end
97
+ describe "#reset_yearly_scores" do
98
+ it "should set the yearly attribute to 0 if no updates have been made this year" do
99
+ Honor::Scorecard.reset_yearly_scores
100
+ sc = Honor::Scorecard.find_by_user_id(@user_1.id)
101
+ sc.daily.should == 25
102
+ sc.weekly.should == 100
103
+ sc.monthly.should == 350
104
+ sc.yearly.should == 0
105
+ end
106
+
107
+ it "should NOT change the yearly attribute if an update was made this year" do
108
+ sc_before = Honor::Scorecard.find_by_user_id(@user_1.id)
109
+ sc_before.update_attribute(:yearly, 1300)
110
+ Honor::Scorecard.reset_yearly_scores
111
+ sc_after = Honor::Scorecard.find_by_user_id(@user_1.id)
112
+ sc_after.daily.should == 25
113
+ sc_after.weekly.should == 100
114
+ sc_after.monthly.should == 350
115
+ sc_after.yearly.should == 1300
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,63 @@
1
+ require 'rubygems'
2
+ require 'honor'
3
+ require 'ammeter/init'
4
+ require 'timecop'
5
+ require 'database_cleaner'
6
+
7
+ # set adapter to use, default is sqlite3
8
+ # to use an alternative adapter run => rake spec DB='postgresql'
9
+ db_name = ENV['DB'] || 'sqlite3'
10
+ database_yml = File.expand_path('../config/database.yml', __FILE__)
11
+
12
+ if File.exists?(database_yml)
13
+ active_record_configuration = YAML.load_file(database_yml)
14
+
15
+ ActiveRecord::Base.configurations = active_record_configuration
16
+ config = ActiveRecord::Base.configurations[db_name]
17
+
18
+ begin
19
+ ActiveRecord::Base.establish_connection(db_name)
20
+ ActiveRecord::Base.connection
21
+ rescue
22
+ case db_name
23
+ when /mysql/
24
+ ActiveRecord::Base.establish_connection(config.merge('database' => nil))
25
+ ActiveRecord::Base.connection.create_database(config['database'], {:charset => 'utf8', :collation => 'utf8_unicode_ci'})
26
+ when 'postgresql'
27
+ ActiveRecord::Base.establish_connection(config.merge('database' => 'postgres', 'schema_search_path' => 'public'))
28
+ ActiveRecord::Base.connection.create_database(config['database'], config.merge('encoding' => 'utf8'))
29
+ end
30
+
31
+ ActiveRecord::Base.establish_connection(config)
32
+ end
33
+
34
+ ActiveRecord::Base.logger = Logger.new(File.join(File.dirname(__FILE__), "debug.log"))
35
+ ActiveRecord::Base.default_timezone = :utc
36
+
37
+ ActiveRecord::Base.silence do
38
+ ActiveRecord::Migration.verbose = false
39
+
40
+ load(File.expand_path('../config/schema.rb', __FILE__))
41
+ load(File.expand_path('../config/models.rb', __FILE__))
42
+ end
43
+
44
+ else
45
+ raise "Please create #{database_yml} first to configure your database. Take a look at: spec/config/#{database_yml}.sample"
46
+ end
47
+
48
+ RSpec.configure do |config|
49
+ config.mock_with :rspec
50
+
51
+ config.before(:suite) do
52
+ DatabaseCleaner.strategy = :transaction
53
+ DatabaseCleaner.clean_with(:truncation)
54
+ end
55
+
56
+ config.before(:each) do
57
+ DatabaseCleaner.start
58
+ end
59
+
60
+ config.after(:each) do
61
+ DatabaseCleaner.clean
62
+ end
63
+ end
metadata ADDED
@@ -0,0 +1,177 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: honor
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jeremy Ward
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-03-03 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rails
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '3.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '3.0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rspec
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: ammeter
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: supermodel
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: database_cleaner
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: timecop
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ description: Adds support for common gamification features such as points, leaderboards,
111
+ and achievements.
112
+ email:
113
+ - jrmy.ward@gmail.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - .gitignore
119
+ - .rspec
120
+ - CHANGELOG.md
121
+ - Gemfile
122
+ - LICENSE.txt
123
+ - README.md
124
+ - Rakefile
125
+ - honor.gemspec
126
+ - honor.sqlite3
127
+ - lib/generators/install_generator.rb
128
+ - lib/generators/templates/active_record/create_points.rb
129
+ - lib/generators/templates/active_record/create_scorecards.rb
130
+ - lib/honor.rb
131
+ - lib/honor/model_additions.rb
132
+ - lib/honor/point.rb
133
+ - lib/honor/scorecard.rb
134
+ - lib/honor/version.rb
135
+ - rails/init.rb
136
+ - spec/config/database.yml.sample
137
+ - spec/config/models.rb
138
+ - spec/config/schema.rb
139
+ - spec/generators/install_generator_spec.rb
140
+ - spec/honor/model_additions_spec.rb
141
+ - spec/honor/point_spec.rb
142
+ - spec/honor/scorecard_spec.rb
143
+ - spec/spec_helper.rb
144
+ homepage: https://github.com/jrmyward/honor
145
+ licenses: []
146
+ post_install_message:
147
+ rdoc_options: []
148
+ require_paths:
149
+ - lib
150
+ required_ruby_version: !ruby/object:Gem::Requirement
151
+ none: false
152
+ requirements:
153
+ - - ! '>='
154
+ - !ruby/object:Gem::Version
155
+ version: '0'
156
+ required_rubygems_version: !ruby/object:Gem::Requirement
157
+ none: false
158
+ requirements:
159
+ - - ! '>='
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
162
+ requirements: []
163
+ rubyforge_project:
164
+ rubygems_version: 1.8.23
165
+ signing_key:
166
+ specification_version: 3
167
+ summary: General gamification-centric reputation system for Rails Applications.
168
+ test_files:
169
+ - spec/config/database.yml.sample
170
+ - spec/config/models.rb
171
+ - spec/config/schema.rb
172
+ - spec/generators/install_generator_spec.rb
173
+ - spec/honor/model_additions_spec.rb
174
+ - spec/honor/point_spec.rb
175
+ - spec/honor/scorecard_spec.rb
176
+ - spec/spec_helper.rb
177
+ has_rdoc: