scorecard 0.0.3 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1232899ca7b26af8e4a8d8af2baaae024343a688
4
- data.tar.gz: c3c252f4debd3424989324fa5a0b239a13f90185
3
+ metadata.gz: 52409db56a58a6ec606d0f7e967aefe8b45dff3e
4
+ data.tar.gz: 68ff1f82181c3dd3714258e0fab235773d4b16ea
5
5
  SHA512:
6
- metadata.gz: 2dee14b5121ee947bf95bd12d17ed95a08b36cbb5e6aa469871d7582129c0b48b3b3517f3332790adf72a75df7e1a134008a5f20f63b4c65f87ce9260a8eaa2e
7
- data.tar.gz: 88d9897dd851e560050b03bfeb8b88258264ff0c580dcba90b7f7b1efda7eda10109e50b11aab721de74e39523c202fff8b1fd8718e031bf4cf36a2c17a27950
6
+ metadata.gz: 21eea64081d80a56970635315e654b8ee119e735b95d435bbf3735658f8c4e48f8dbf66f18baa7741d9fd9693e9a102028d2a988c9338ddb0c1904aa27824d4e
7
+ data.tar.gz: 2da28683c83900f432e8c8524839f0a785ed60172586ac4b8cf351ca3d6ea82169a97ff52a7d8797882e0109389345badb31e3b9bb29293ee903abea771f002b
data/README.md CHANGED
@@ -8,7 +8,7 @@ For anyone who comes across this, please note this is not yet feature complete,
8
8
 
9
9
  Add this line to your application's Gemfile:
10
10
 
11
- gem 'scorecard', '0.0.3'
11
+ gem 'scorecard', '0.1.0'
12
12
 
13
13
  Don't forget to bundle:
14
14
 
@@ -24,7 +24,7 @@ In an initializer, define the events that points are tied to - with a unique con
24
24
 
25
25
  ```ruby
26
26
  Scorecard.configure do |config|
27
- config.rules.add_rule :new_post, 50
27
+ config.rules.add :new_post, 50
28
28
  end
29
29
  ```
30
30
 
@@ -32,7 +32,7 @@ You can also provide a block with logic for whether to award the points, the max
32
32
 
33
33
  ```ruby
34
34
  Scorecard.configure do |config|
35
- config.rules.add_rule :new_post, 50, limit: 100, timeframe: :day,
35
+ config.rules.add :new_post, 50, limit: 100, timeframe: :day,
36
36
  if: lambda { |payload| payload[:user].posts.count <= 1 }
37
37
  end
38
38
  ```
@@ -15,6 +15,9 @@ class Scorecard::Point < ActiveRecord::Base
15
15
  validates :amount, presence: true
16
16
  validates :user, presence: true
17
17
 
18
+ scope :chronological, -> { order('created_at ASC') }
19
+ scope :reverse, -> { order('created_at DESC') }
20
+
18
21
  def self.for_context(context)
19
22
  where context: context
20
23
  end
@@ -27,7 +30,15 @@ class Scorecard::Point < ActiveRecord::Base
27
30
  where gameable_id: gameable.id, gameable_type: gameable.class.name
28
31
  end
29
32
 
33
+ def self.for_raw_gameable(gameable_type, gameable_id)
34
+ where gameable_id: gameable_id, gameable_type: gameable_type
35
+ end
36
+
37
+ def self.for_timeframe(timeframe)
38
+ where created_at: timeframe
39
+ end
40
+
30
41
  def self.for_user_in_timeframe(context, user, timeframe)
31
- for_context(context).for_user(user).where(created_at: timeframe)
42
+ for_context(context).for_user(user).for_timeframe(timeframe)
32
43
  end
33
44
  end
@@ -0,0 +1,28 @@
1
+ class Scorecard::Progress < ActiveRecord::Base
2
+ self.table_name = 'scorecard_progresses'
3
+
4
+ belongs_to :user, polymorphic: true
5
+
6
+ if Rails.version.to_s < '4.0.0'
7
+ attr_accessible :identifier, :user
8
+ end
9
+
10
+ validates :identifier, presence: true, uniqueness: {
11
+ scope: [:user_type, :user_id]
12
+ }
13
+ validates :user, presence: true
14
+
15
+ delegate :amount, to: :progression
16
+
17
+ def self.for_user(user)
18
+ where user_id: user.id, user_type: user.class.name
19
+ end
20
+
21
+ def self.for_identifier(identifier)
22
+ where identifier: identifier.to_s
23
+ end
24
+
25
+ def progression
26
+ Scorecard.progressions.find identifier.to_sym
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ class Scorecard::UserBadge < ActiveRecord::Base
2
+ self.table_name = 'scorecard_user_badges'
3
+
4
+ belongs_to :user, polymorphic: true
5
+ belongs_to :gameable, polymorphic: true
6
+
7
+ if Rails.version.to_s < '4.0.0'
8
+ attr_accessible :badge, :identifier, :gameable, :user
9
+ end
10
+
11
+ validates :badge, presence: true
12
+ validates :identifier, presence: true, uniqueness: {
13
+ scope: [:badge, :user_type, :user_id]
14
+ }
15
+ validates :user, presence: true
16
+
17
+ def self.for(badge, user)
18
+ for_user(user).where badge: badge
19
+ end
20
+
21
+ def self.for_user(user)
22
+ where user_id: user.id, user_type: user.class.name
23
+ end
24
+
25
+ def self.for_gameable(gameable)
26
+ where gameable_id: gameable.id, gameable_type: gameable.class.name
27
+ end
28
+ end
@@ -0,0 +1,21 @@
1
+ class CreateUserBadges < ActiveRecord::Migration
2
+ def change
3
+ create_table :scorecard_user_badges do |table|
4
+ table.string :badge, null: false
5
+ table.string :identifier, null: false
6
+ table.string :user_type, null: false
7
+ table.integer :user_id, null: false
8
+ table.string :gameable_type
9
+ table.integer :gameable_id
10
+ table.timestamps
11
+ end
12
+
13
+ add_index :scorecard_user_badges, :badge
14
+ add_index :scorecard_user_badges, :identifier
15
+ add_index :scorecard_user_badges, [:user_type, :user_id]
16
+ add_index :scorecard_user_badges, [:gameable_type, :gameable_id]
17
+ add_index :scorecard_user_badges,
18
+ [:badge, :identifier, :user_type, :user_id],
19
+ unique: true, name: :unique_badges
20
+ end
21
+ end
@@ -0,0 +1,15 @@
1
+ class CreateProgresses < ActiveRecord::Migration
2
+ def change
3
+ create_table :scorecard_progresses do |table|
4
+ table.string :identifier, null: false
5
+ table.string :user_type, null: false
6
+ table.integer :user_id, null: false
7
+ table.timestamps
8
+ end
9
+
10
+ add_index :scorecard_progresses, :identifier
11
+ add_index :scorecard_progresses, [:user_type, :user_id]
12
+ add_index :scorecard_progresses, [:identifier, :user_type, :user_id],
13
+ unique: true, name: :unique_progresses
14
+ end
15
+ end
@@ -5,15 +5,31 @@ module Scorecard
5
5
  yield self
6
6
  end
7
7
 
8
+ def self.badges
9
+ @badges ||= Scorecard::Badges.new
10
+ end
11
+
12
+ def self.progressions
13
+ @progressions ||= Scorecard::Progressions.new
14
+ end
15
+
8
16
  def self.rules
9
17
  @rules ||= Scorecard::Rules.new
10
18
  end
11
19
  end
12
20
 
21
+ require 'scorecard/applied_badge'
22
+ require 'scorecard/badge'
23
+ require 'scorecard/badger'
24
+ require 'scorecard/badges'
13
25
  require 'scorecard/card'
14
26
  require 'scorecard/cleaner'
15
27
  require 'scorecard/engine'
28
+ require 'scorecard/parameters'
16
29
  require 'scorecard/point_rule'
30
+ require 'scorecard/progression'
31
+ require 'scorecard/progressions'
32
+ require 'scorecard/refresh_worker'
17
33
  require 'scorecard/rules'
18
34
  require 'scorecard/scorer'
19
35
  require 'scorecard/subscriber'
@@ -0,0 +1,18 @@
1
+ class Scorecard::AppliedBadge
2
+ attr_accessor :badge, :user
3
+
4
+ delegate :name, :locked, :unlocked, :repeatable?, :identifier, to: :badge
5
+
6
+ def initialize(identifier, user)
7
+ @user = user
8
+ @badge = Scorecard.badges.find identifier
9
+ end
10
+
11
+ def count
12
+ Scorecard::UserBadge.for(badge.identifier, user).count
13
+ end
14
+
15
+ def gameables
16
+ Scorecard::UserBadge.for(badge.identifier, user).collect &:gameable
17
+ end
18
+ end
@@ -0,0 +1,14 @@
1
+ class Scorecard::Badge
2
+ attr_reader :identifier
3
+ attr_accessor :name, :locked, :unlocked, :check, :gameables
4
+
5
+ def initialize(identifier, &block)
6
+ @identifier = identifier
7
+
8
+ block.call self
9
+ end
10
+
11
+ def repeatable?
12
+ @gameables.present?
13
+ end
14
+ end
@@ -0,0 +1,78 @@
1
+ class Scorecard::Badger
2
+ def self.update(user)
3
+ Scorecard.badges.each do |badge|
4
+ new(user, badge).update
5
+ end
6
+ end
7
+
8
+ def initialize(user, badge)
9
+ @user, @badge = user, badge
10
+ end
11
+
12
+ def update
13
+ if badge.check.call user
14
+ return unless existing.empty? || badge.repeatable?
15
+
16
+ remove_old
17
+ add_new
18
+ else
19
+ remove_all
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :user, :badge
26
+
27
+ def add_new
28
+ new_gameables.each do |gameable|
29
+ user_badge = Scorecard::UserBadge.create(
30
+ badge: badge.identifier,
31
+ gameable: gameable,
32
+ user: user,
33
+ identifier: gameable.id
34
+ )
35
+
36
+ ActiveSupport::Notifications.instrument(
37
+ 'badge.scorecard', user: user,
38
+ badge: Scorecard::AppliedBadge.new(badge.identifier, user),
39
+ gameable: gameable
40
+ ) if user_badge.persisted?
41
+ end
42
+ end
43
+
44
+ def existing
45
+ @existing ||= Scorecard::UserBadge.for badge.identifier, user
46
+ end
47
+
48
+ def gameables
49
+ @gameables ||= badge.repeatable? ? badge.gameables.call(user) : [user]
50
+ end
51
+
52
+ def new_gameables
53
+ gameables.reject { |gameable|
54
+ existing.any? { |existing| existing.gameable == gameable }
55
+ }
56
+ end
57
+
58
+ def old_user_badges
59
+ existing.reject { |user_badge|
60
+ gameables.any? { |gameable| user_badge.gameable == gameable }
61
+ }
62
+ end
63
+
64
+ def remove(user_badge)
65
+ user_badge.destroy
66
+
67
+ ActiveSupport::Notifications.instrument 'unbadge.scorecard',
68
+ user: user, badge: badge.identifier, gameable: user_badge.gameable
69
+ end
70
+
71
+ def remove_all
72
+ existing.each { |user_badge| remove user_badge }
73
+ end
74
+
75
+ def remove_old
76
+ old_user_badges.each { |user_badge| remove user_badge }
77
+ end
78
+ end
@@ -0,0 +1,25 @@
1
+ class Scorecard::Badges
2
+ include Enumerable
3
+
4
+ attr_reader :badges
5
+
6
+ def initialize
7
+ @badges = []
8
+ end
9
+
10
+ def add(identifier, &block)
11
+ badges << Scorecard::Badge.new(identifier, &block)
12
+ end
13
+
14
+ def each(&block)
15
+ badges.each &block
16
+ end
17
+
18
+ def find(identifier)
19
+ badges.detect { |badge| badge.identifier == identifier }
20
+ end
21
+
22
+ def clear
23
+ badges.clear
24
+ end
25
+ end
@@ -5,6 +5,15 @@ class Scorecard::Card
5
5
  @user = user
6
6
  end
7
7
 
8
+ def badges
9
+ @badges ||= begin
10
+ identifiers = Scorecard::UserBadge.for_user(user).pluck(:badge).uniq
11
+ identifiers.collect { |identifier|
12
+ Scorecard::AppliedBadge.new identifier.to_sym, user
13
+ }
14
+ end
15
+ end
16
+
8
17
  def level
9
18
  @level ||= begin
10
19
  record = Scorecard::Level.for_user(user)
@@ -15,4 +24,15 @@ class Scorecard::Card
15
24
  def points
16
25
  @points ||= Scorecard::Point.for_user(user).sum(:amount)
17
26
  end
27
+
28
+ def progress
29
+ @progress ||= Scorecard::Progress.for_user(user).collect(&:amount).sum
30
+ end
31
+
32
+ def remaining_progressions
33
+ @remaining_progressions ||= Scorecard.progressions.without(
34
+ Scorecard::Progress.for_user(user).collect(&:identifier).
35
+ collect(&:to_sym)
36
+ )
37
+ end
18
38
  end
@@ -1,6 +1,10 @@
1
1
  class Scorecard::Cleaner
2
- def self.points(gameable)
3
- Scorecard::Point.for_gameable(gameable).each do |point|
2
+ def self.points(gameable_or_type, gameable_id = nil)
3
+ points = gameable_id.nil? ?
4
+ Scorecard::Point.for_gameable(gameable_or_type) :
5
+ Scorecard::Point.for_raw_gameable(gameable_or_type, gameable_id)
6
+
7
+ points.each do |point|
4
8
  point.destroy
5
9
  ActiveSupport::Notifications.instrument 'scorecard', user: point.user
6
10
  end
@@ -2,6 +2,6 @@ class Scorecard::ClearWorker
2
2
  include Sidekiq::Worker
3
3
 
4
4
  def perform(class_name, id)
5
- Scorecard::Cleaner.points class_name.constantize.find(id)
5
+ Scorecard::Cleaner.points class_name, id
6
6
  end
7
7
  end
@@ -2,6 +2,6 @@ class Scorecard::Engine < Rails::Engine
2
2
  engine_name :scorecard
3
3
 
4
4
  initializer :scorecard do |app|
5
- Scorecard::Subscriber.attach_to :scorecard
5
+ Scorecard::Subscriber.attach_to 'internal.scorecard'
6
6
  end
7
7
  end
@@ -0,0 +1,30 @@
1
+ class Scorecard::Parameters
2
+ attr_reader :options, :prefixes
3
+
4
+ def initialize(options, prefixes = [:gameable, :user])
5
+ @options, @prefixes = options.clone, prefixes
6
+ end
7
+
8
+ def expand
9
+ prefixes.collect(&:to_sym).each do |prefix|
10
+ next unless options[prefix]
11
+
12
+ options["#{prefix}_id"] = options[prefix].id
13
+ options["#{prefix}_type"] = options[prefix].class.name
14
+ options.delete prefix
15
+ end
16
+
17
+ options.stringify_keys
18
+ end
19
+
20
+ def contract
21
+ prefixes.collect(&:to_s).each do |prefix|
22
+ next unless options["#{prefix}_type"]
23
+
24
+ klass = options.delete("#{prefix}_type").constantize
25
+ options[prefix] = klass.find options.delete("#{prefix}_id")
26
+ end
27
+
28
+ options.symbolize_keys
29
+ end
30
+ end
@@ -0,0 +1,10 @@
1
+ class Scorecard::Progression
2
+ attr_reader :identifier, :amount
3
+ attr_accessor :link_text, :link_url, :check
4
+
5
+ def initialize(identifier, amount, &block)
6
+ @identifier, @amount = identifier, amount
7
+
8
+ block.call self
9
+ end
10
+ end
@@ -0,0 +1,31 @@
1
+ class Scorecard::Progressions
2
+ include Enumerable
3
+
4
+ attr_reader :progressions
5
+
6
+ def initialize
7
+ @progressions = []
8
+ end
9
+
10
+ def add(identifier, amount, &block)
11
+ progressions << Scorecard::Progression.new(identifier, amount, &block)
12
+ end
13
+
14
+ def each(&block)
15
+ progressions.each &block
16
+ end
17
+
18
+ def find(identifier)
19
+ progressions.detect { |progression| progression.identifier == identifier }
20
+ end
21
+
22
+ def without(identifiers)
23
+ progressions.reject { |progression|
24
+ identifiers.include?(progression.identifier)
25
+ }
26
+ end
27
+
28
+ def clear
29
+ progressions.clear
30
+ end
31
+ end
@@ -0,0 +1,7 @@
1
+ class Scorecard::RefreshWorker
2
+ include Sidekiq::Worker
3
+
4
+ def perform(options)
5
+ Scorecard::Scorer.refresh Scorecard::Parameters.new(options).contract
6
+ end
7
+ end
@@ -5,11 +5,11 @@ class Scorecard::Rules
5
5
  @point_rules = []
6
6
  end
7
7
 
8
- def add_rule(context, amount, options = {})
8
+ def add(context, amount, options = {})
9
9
  point_rules << Scorecard::PointRule.new(context, amount, options)
10
10
  end
11
11
 
12
- def find_rule(context)
12
+ def find(context)
13
13
  point_rules.detect { |rule| rule.context == context }
14
14
  end
15
15
 
@@ -2,13 +2,7 @@ class Scorecard::ScoreWorker
2
2
  include Sidekiq::Worker
3
3
 
4
4
  def perform(context, options)
5
- %w(gameable user).each do |prefix|
6
- next unless options["#{prefix}_type"]
7
-
8
- klass = options.delete("#{prefix}_type").constantize
9
- options[prefix] = klass.find options.delete("#{prefix}_id")
10
- end
11
-
12
- Scorecard::Scorer.points context.to_sym, options.symbolize_keys
5
+ Scorecard::Scorer.points context.to_sym,
6
+ Scorecard::Parameters.new(options).contract
13
7
  end
14
8
  end
@@ -9,19 +9,25 @@ class Scorecard::Scorer
9
9
  end
10
10
 
11
11
  def self.points(context, options)
12
- ActiveSupport::Notifications.instrument 'points.scorecard',
12
+ ActiveSupport::Notifications.instrument 'points.internal.scorecard',
13
13
  options.merge(context: context)
14
14
  end
15
15
 
16
16
  def self.points_async(context, options)
17
- [:gameable, :user].each do |prefix|
18
- next unless options[prefix]
17
+ Scorecard::ScoreWorker.perform_async context,
18
+ Scorecard::Parameters.new(options).expand
19
+ end
19
20
 
20
- options["#{prefix}_id"] = options[prefix].id
21
- options["#{prefix}_type"] = options[prefix].class.name
22
- options.delete prefix
23
- end
21
+ def self.refresh(options)
22
+ ActiveSupport::Notifications.instrument 'progress.internal.scorecard',
23
+ options
24
+ ActiveSupport::Notifications.instrument 'badge.internal.scorecard',
25
+ options
26
+ end
24
27
 
25
- Scorecard::ScoreWorker.perform_async context, options.stringify_keys
28
+ def self.refresh_async(options)
29
+ Scorecard::RefreshWorker.perform_async(
30
+ Scorecard::Parameters.new(options).expand
31
+ )
26
32
  end
27
33
  end
@@ -13,8 +13,13 @@ class Scorecard::Subscriber
13
13
  send method, ActiveSupport::Notifications::Event.new(message, *args)
14
14
  end
15
15
 
16
+ def badge(event)
17
+ Scorecard::Badger.update event.payload[:user]
18
+ end
19
+
16
20
  def points(event)
17
- rule = Scorecard.rules.find_rule event.payload[:context]
21
+ rule = Scorecard.rules.find event.payload[:context]
22
+ return if rule.nil?
18
23
 
19
24
  event.payload[:amount] ||= rule.amount
20
25
  event.payload[:identifier] ||= event.payload[:gameable].id
@@ -29,4 +34,26 @@ class Scorecard::Subscriber
29
34
  'scorecard', user: event.payload[:user]
30
35
  ) if point.persisted?
31
36
  end
37
+
38
+ def progress(event)
39
+ Scorecard.progressions.each do |progression|
40
+ if progression.check.call event.payload[:user]
41
+ progress = Scorecard::Progress.create(
42
+ user: event.payload[:user], identifier: progression.identifier
43
+ )
44
+
45
+ ActiveSupport::Notifications.instrument(
46
+ 'progress.scorecard', user: event.payload[:user]
47
+ ) if progress.persisted?
48
+ else
49
+ progresses = Scorecard::Progress.for_user(event.payload[:user]).
50
+ for_identifier(progression.identifier)
51
+ progresses.each &:destroy
52
+
53
+ ActiveSupport::Notifications.instrument(
54
+ 'progress.scorecard', user: event.payload[:user]
55
+ ) if progresses.any?
56
+ end
57
+ end
58
+ end
32
59
  end
@@ -1,7 +1,7 @@
1
1
  # coding: utf-8
2
2
  Gem::Specification.new do |spec|
3
3
  spec.name = 'scorecard'
4
- spec.version = '0.0.3'
4
+ spec.version = '0.1.0'
5
5
  spec.authors = ['Pat Allan']
6
6
  spec.email = ['pat@freelancing-gods.com']
7
7
  spec.summary = 'Rails Engine for common scorecard patterns'
@@ -0,0 +1,102 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Badges' do
4
+ let(:user) { User.create! }
5
+ let(:post) { Post.create! user: user }
6
+ let(:card) { Scorecard::Card.new user }
7
+
8
+ before :each do
9
+ Scorecard.configure do |config|
10
+ config.badges.add :new_post do |badge|
11
+ badge.name = 'Beginner'
12
+ badge.locked = 'Write a post'
13
+ badge.unlocked = 'You wrote a post!'
14
+ badge.check = lambda { |user| Post.where(user_id: user.id).any? }
15
+ end
16
+ end
17
+
18
+ post
19
+ end
20
+
21
+ it 'assigns badges to users' do
22
+ Scorecard::Scorer.refresh user: user
23
+
24
+ expect(card.badges.collect(&:name)).to eq(['Beginner'])
25
+ end
26
+
27
+ it "fires a badge notification when the badge is awarded" do
28
+ fired = false
29
+
30
+ subscriber = ActiveSupport::Notifications.subscribe 'badge.scorecard' do |*args|
31
+ payload = ActiveSupport::Notifications::Event.new(*args).payload
32
+ fired = (payload[:user] == user) && (payload[:badge].identifier == :new_post)
33
+ end
34
+
35
+ Scorecard::Scorer.refresh user: user
36
+
37
+ ActiveSupport::Notifications.unsubscribe(subscriber)
38
+
39
+ expect(fired).to be_true
40
+ end
41
+
42
+ it "doesn't repeat badges with different identifiers" do
43
+ Scorecard::Scorer.refresh user: user
44
+ Scorecard::Scorer.refresh user: user
45
+
46
+ expect(card.badges.collect(&:name)).to eq(['Beginner'])
47
+ end
48
+
49
+ it "fires a badge notification only when the badge is awarded" do
50
+ count = 0
51
+
52
+ subscriber = ActiveSupport::Notifications.subscribe 'badge.scorecard' do |*args|
53
+ count += 1
54
+ end
55
+
56
+ Scorecard::Scorer.refresh user: user
57
+ Scorecard::Scorer.refresh user: user
58
+
59
+ ActiveSupport::Notifications.unsubscribe(subscriber)
60
+
61
+ expect(count).to eq(1)
62
+ end
63
+
64
+ it "repeats repeatable badges with different identifiers" do
65
+ Scorecard.badges.find(:new_post).gameables = lambda { |user|
66
+ Post.where(user_id: user.id)
67
+ }
68
+ second = Post.create! user: user
69
+
70
+ Scorecard::Scorer.refresh user: user
71
+
72
+ badge = card.badges.first
73
+ expect(badge.name).to eq('Beginner')
74
+ expect(badge.count).to eq(2)
75
+ expect(badge.gameables).to eq([post, second])
76
+ end
77
+
78
+ it "fires a badge notification each time it is awarded" do
79
+ Scorecard.badges.find(:new_post).gameables = lambda { |user|
80
+ Post.where(user_id: user.id)
81
+ }
82
+ Post.create! user: user
83
+
84
+ count = 0
85
+
86
+ subscriber = ActiveSupport::Notifications.subscribe 'badge.scorecard' do |*args|
87
+ count += 1
88
+ end
89
+
90
+ Scorecard::Scorer.refresh user: user
91
+
92
+ ActiveSupport::Notifications.unsubscribe(subscriber)
93
+
94
+ expect(count).to eq(2)
95
+ end
96
+
97
+ it 'assigns badges to users via Sidekiq' do
98
+ Scorecard::Scorer.refresh_async user: user
99
+
100
+ expect(card.badges.collect(&:name)).to eq(['Beginner'])
101
+ end
102
+ end
@@ -6,7 +6,7 @@ describe 'Clearing Points' do
6
6
 
7
7
  before :each do
8
8
  Scorecard.configure do |config|
9
- config.rules.add_rule :new_post, 50
9
+ config.rules.add :new_post, 50
10
10
  end
11
11
 
12
12
  post
@@ -0,0 +1,78 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Progressions' do
4
+ let(:user) { User.create! }
5
+ let(:card) { Scorecard::Card.new user }
6
+
7
+ before :each do
8
+ Scorecard.configure do |config|
9
+ config.progressions.add :add_info, 10 do |progression|
10
+ progression.link_text = 'Add some information'
11
+ progression.link_url = '/foo'
12
+ progression.check = lambda { |user| true }
13
+ end
14
+ end
15
+ end
16
+
17
+ it 'marks progress for users' do
18
+ Scorecard::Scorer.refresh user: user
19
+
20
+ expect(card.progress).to eq(10)
21
+ end
22
+
23
+ it "fires a notification when progress is made" do
24
+ fired = false
25
+
26
+ subscriber = ActiveSupport::Notifications.subscribe 'progress.scorecard' do |*args|
27
+ payload = ActiveSupport::Notifications::Event.new(*args).payload
28
+ fired = (payload[:user] == user)
29
+ end
30
+
31
+ Scorecard::Scorer.refresh user: user
32
+
33
+ ActiveSupport::Notifications.unsubscribe(subscriber)
34
+
35
+ expect(fired).to be_true
36
+ end
37
+
38
+ it "doesn't duplicate progress" do
39
+ Scorecard::Scorer.refresh user: user
40
+ Scorecard::Scorer.refresh user: user
41
+
42
+ expect(card.progress).to eq(10)
43
+ end
44
+
45
+ it "fires only a single notification when progress is made" do
46
+ count = 0
47
+
48
+ subscriber = ActiveSupport::Notifications.subscribe 'progress.scorecard' do |*args|
49
+ count += 1
50
+ end
51
+
52
+ Scorecard::Scorer.refresh user: user
53
+ Scorecard::Scorer.refresh user: user
54
+
55
+ ActiveSupport::Notifications.unsubscribe(subscriber)
56
+
57
+ expect(count).to eq(1)
58
+ end
59
+
60
+ it 'marks progress for users via Sidekiq' do
61
+ Scorecard::Scorer.refresh_async user: user
62
+
63
+ expect(card.progress).to eq(10)
64
+ end
65
+
66
+ it "removes percentage if progression no longer applies" do
67
+ Scorecard::Scorer.refresh user: user
68
+
69
+ expect(card.progress).to eq(10)
70
+
71
+ Scorecard.progressions.find(:add_info).check = lambda { |user| false }
72
+
73
+ Scorecard::Scorer.refresh_async user: user
74
+
75
+ card = Scorecard::Card.new user
76
+ expect(card.progress).to eq(0)
77
+ end
78
+ end
@@ -3,8 +3,8 @@ require 'spec_helper'
3
3
  describe 'Reading points' do
4
4
  it "returns the total points for a user" do
5
5
  Scorecard.configure do |config|
6
- config.rules.add_rule :new_user, 20
7
- config.rules.add_rule :new_post, 30
6
+ config.rules.add :new_user, 20
7
+ config.rules.add :new_post, 30
8
8
  end
9
9
 
10
10
  user = User.create!
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  describe 'Scoring points' do
4
4
  it "stores points for configured behaviour" do
5
5
  Scorecard.configure do |config|
6
- config.rules.add_rule :new_post, 50
6
+ config.rules.add :new_post, 50
7
7
  end
8
8
 
9
9
  user = User.create!
@@ -23,7 +23,7 @@ describe 'Scoring points' do
23
23
 
24
24
  it "only stores points when provided logic passes" do
25
25
  Scorecard.configure do |config|
26
- config.rules.add_rule :new_post, 50,
26
+ config.rules.add :new_post, 50,
27
27
  if: lambda { |payload| Post.count <= 1 }
28
28
  end
29
29
 
@@ -40,7 +40,7 @@ describe 'Scoring points' do
40
40
 
41
41
  it "does not double-up on points for the same event" do
42
42
  Scorecard.configure do |config|
43
- config.rules.add_rule :new_post, 50
43
+ config.rules.add :new_post, 50
44
44
  end
45
45
 
46
46
  user = User.create!
@@ -61,7 +61,7 @@ describe 'Scoring points' do
61
61
 
62
62
  it "respects limit options" do
63
63
  Scorecard.configure do |config|
64
- config.rules.add_rule :new_post, 50, limit: 100
64
+ config.rules.add :new_post, 50, limit: 100
65
65
  end
66
66
 
67
67
  user = User.create!
@@ -75,7 +75,7 @@ describe 'Scoring points' do
75
75
 
76
76
  it "respects timeframe options" do
77
77
  Scorecard.configure do |config|
78
- config.rules.add_rule :new_post, 50, timeframe: :day
78
+ config.rules.add :new_post, 50, timeframe: :day
79
79
  end
80
80
 
81
81
  user = User.create!
@@ -88,7 +88,7 @@ describe 'Scoring points' do
88
88
 
89
89
  it "allows for processing via Sidekiq" do
90
90
  Scorecard.configure do |config|
91
- config.rules.add_rule :new_user, 20
91
+ config.rules.add :new_user, 20
92
92
  end
93
93
 
94
94
  user = User.create!
@@ -108,7 +108,7 @@ describe 'Scoring points' do
108
108
 
109
109
  it "fires a generic notification" do
110
110
  Scorecard.configure do |config|
111
- config.rules.add_rule :new_post, 50
111
+ config.rules.add :new_post, 50
112
112
  end
113
113
 
114
114
  fired = false
@@ -0,0 +1,63 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'Unbadging' do
4
+ let(:user) { User.create! }
5
+ let(:post) { Post.create! user: user }
6
+ let(:card) { Scorecard::Card.new user }
7
+
8
+ before :each do
9
+ Scorecard.configure do |config|
10
+ config.badges.add :new_post do |badge|
11
+ badge.name = 'Beginner'
12
+ badge.locked = 'Write a post'
13
+ badge.unlocked = 'You wrote a post!'
14
+ badge.check = lambda { |user| Post.where(user_id: user.id).any? }
15
+ end
16
+ end
17
+
18
+ post
19
+ Scorecard::Scorer.refresh user: user
20
+ post.destroy
21
+ end
22
+
23
+ it 'removes badges from users' do
24
+ Scorecard::Scorer.refresh user: user
25
+
26
+ expect(card.badges.collect(&:name)).to be_empty
27
+ end
28
+
29
+ it "fires a badge notification when the badge is removed" do
30
+ fired = false
31
+
32
+ subscriber = ActiveSupport::Notifications.subscribe 'unbadge.scorecard' do |*args|
33
+ payload = ActiveSupport::Notifications::Event.new(*args).payload
34
+ fired = (payload[:user] == user) && (payload[:badge] == :new_post)
35
+ end
36
+
37
+ Scorecard::Scorer.refresh user: user
38
+
39
+ ActiveSupport::Notifications.unsubscribe(subscriber)
40
+
41
+ expect(fired).to be_true
42
+ end
43
+
44
+ it 'removes badges when there were many and some are no longer valid' do
45
+ Scorecard.badges.find(:new_post).gameables = lambda { |user|
46
+ Post.where(user_id: user.id)
47
+ }
48
+ second = Post.create! user: user
49
+
50
+ Scorecard::Scorer.refresh user: user
51
+
52
+ badge = card.badges.first
53
+ expect(badge.name).to eq('Beginner')
54
+ expect(badge.count).to eq(1)
55
+ expect(badge.gameables).to eq([second])
56
+ end
57
+
58
+ it 'removes badges to users via Sidekiq' do
59
+ Scorecard::Scorer.refresh_async user: user
60
+
61
+ expect(card.badges.collect(&:name)).to be_empty
62
+ end
63
+ end
@@ -17,5 +17,7 @@ RSpec.configure do |config|
17
17
  config.use_transactional_fixtures = true
18
18
  config.before :each do
19
19
  Scorecard.rules.clear
20
+ Scorecard.badges.clear
21
+ Scorecard.progressions.clear
20
22
  end
21
23
  end
metadata CHANGED
@@ -1,83 +1,83 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: scorecard
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pat Allan
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2013-10-29 00:00:00.000000000 Z
11
+ date: 2014-04-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '>='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: '3.2'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - '>='
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '3.2'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: combustion
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - ~>
31
+ - - "~>"
32
32
  - !ruby/object:Gem::Version
33
33
  version: 0.5.1
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - ~>
38
+ - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: 0.5.1
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec-rails
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - ~>
45
+ - - "~>"
46
46
  - !ruby/object:Gem::Version
47
47
  version: 2.14.0
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - ~>
52
+ - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: 2.14.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: sidekiq
57
57
  requirement: !ruby/object:Gem::Requirement
58
58
  requirements:
59
- - - ~>
59
+ - - "~>"
60
60
  - !ruby/object:Gem::Version
61
61
  version: '2.15'
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
- - - ~>
66
+ - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '2.15'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: sqlite3
71
71
  requirement: !ruby/object:Gem::Requirement
72
72
  requirements:
73
- - - ~>
73
+ - - "~>"
74
74
  - !ruby/object:Gem::Version
75
75
  version: 1.3.8
76
76
  type: :development
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
- - - ~>
80
+ - - "~>"
81
81
  - !ruby/object:Gem::Version
82
82
  version: 1.3.8
83
83
  description: Use an engine to track points, badges and levels in your Rails app.
@@ -87,32 +87,47 @@ executables: []
87
87
  extensions: []
88
88
  extra_rdoc_files: []
89
89
  files:
90
- - .gitignore
91
- - .travis.yml
90
+ - ".gitignore"
91
+ - ".travis.yml"
92
92
  - Gemfile
93
93
  - LICENSE.txt
94
94
  - README.md
95
95
  - Rakefile
96
96
  - app/models/scorecard/level.rb
97
97
  - app/models/scorecard/point.rb
98
+ - app/models/scorecard/progress.rb
99
+ - app/models/scorecard/user_badge.rb
98
100
  - config.ru
99
101
  - db/migrate/1_create_points.rb
100
102
  - db/migrate/2_create_levels.rb
103
+ - db/migrate/3_create_user_badges.rb
104
+ - db/migrate/4_create_progresses.rb
101
105
  - lib/scorecard.rb
106
+ - lib/scorecard/applied_badge.rb
107
+ - lib/scorecard/badge.rb
108
+ - lib/scorecard/badger.rb
109
+ - lib/scorecard/badges.rb
102
110
  - lib/scorecard/card.rb
103
111
  - lib/scorecard/cleaner.rb
104
112
  - lib/scorecard/clear_worker.rb
105
113
  - lib/scorecard/engine.rb
114
+ - lib/scorecard/parameters.rb
106
115
  - lib/scorecard/point_rule.rb
116
+ - lib/scorecard/progression.rb
117
+ - lib/scorecard/progressions.rb
118
+ - lib/scorecard/refresh_worker.rb
107
119
  - lib/scorecard/rules.rb
108
120
  - lib/scorecard/score_worker.rb
109
121
  - lib/scorecard/scorer.rb
110
122
  - lib/scorecard/subscriber.rb
111
123
  - scorecard.gemspec
124
+ - spec/acceptance/badges_spec.rb
112
125
  - spec/acceptance/clearing_points_spec.rb
113
126
  - spec/acceptance/levels_spec.rb
127
+ - spec/acceptance/progessions_spec.rb
114
128
  - spec/acceptance/read_points_spec.rb
115
129
  - spec/acceptance/score_points_spec.rb
130
+ - spec/acceptance/unbadging_spec.rb
116
131
  - spec/internal/app/models/post.rb
117
132
  - spec/internal/app/models/user.rb
118
133
  - spec/internal/config/database.yml
@@ -131,25 +146,28 @@ require_paths:
131
146
  - lib
132
147
  required_ruby_version: !ruby/object:Gem::Requirement
133
148
  requirements:
134
- - - '>='
149
+ - - ">="
135
150
  - !ruby/object:Gem::Version
136
151
  version: '0'
137
152
  required_rubygems_version: !ruby/object:Gem::Requirement
138
153
  requirements:
139
- - - '>='
154
+ - - ">="
140
155
  - !ruby/object:Gem::Version
141
156
  version: '0'
142
157
  requirements: []
143
158
  rubyforge_project:
144
- rubygems_version: 2.1.2
159
+ rubygems_version: 2.2.2
145
160
  signing_key:
146
161
  specification_version: 4
147
162
  summary: Rails Engine for common scorecard patterns
148
163
  test_files:
164
+ - spec/acceptance/badges_spec.rb
149
165
  - spec/acceptance/clearing_points_spec.rb
150
166
  - spec/acceptance/levels_spec.rb
167
+ - spec/acceptance/progessions_spec.rb
151
168
  - spec/acceptance/read_points_spec.rb
152
169
  - spec/acceptance/score_points_spec.rb
170
+ - spec/acceptance/unbadging_spec.rb
153
171
  - spec/internal/app/models/post.rb
154
172
  - spec/internal/app/models/user.rb
155
173
  - spec/internal/config/database.yml