scorecard 0.0.3 → 0.1.0

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