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 +4 -4
- data/README.md +3 -3
- data/app/models/scorecard/point.rb +12 -1
- data/app/models/scorecard/progress.rb +28 -0
- data/app/models/scorecard/user_badge.rb +28 -0
- data/db/migrate/3_create_user_badges.rb +21 -0
- data/db/migrate/4_create_progresses.rb +15 -0
- data/lib/scorecard.rb +16 -0
- data/lib/scorecard/applied_badge.rb +18 -0
- data/lib/scorecard/badge.rb +14 -0
- data/lib/scorecard/badger.rb +78 -0
- data/lib/scorecard/badges.rb +25 -0
- data/lib/scorecard/card.rb +20 -0
- data/lib/scorecard/cleaner.rb +6 -2
- data/lib/scorecard/clear_worker.rb +1 -1
- data/lib/scorecard/engine.rb +1 -1
- data/lib/scorecard/parameters.rb +30 -0
- data/lib/scorecard/progression.rb +10 -0
- data/lib/scorecard/progressions.rb +31 -0
- data/lib/scorecard/refresh_worker.rb +7 -0
- data/lib/scorecard/rules.rb +2 -2
- data/lib/scorecard/score_worker.rb +2 -8
- data/lib/scorecard/scorer.rb +14 -8
- data/lib/scorecard/subscriber.rb +28 -1
- data/scorecard.gemspec +1 -1
- data/spec/acceptance/badges_spec.rb +102 -0
- data/spec/acceptance/clearing_points_spec.rb +1 -1
- data/spec/acceptance/progessions_spec.rb +78 -0
- data/spec/acceptance/read_points_spec.rb +2 -2
- data/spec/acceptance/score_points_spec.rb +7 -7
- data/spec/acceptance/unbadging_spec.rb +63 -0
- data/spec/spec_helper.rb +2 -0
- metadata +35 -17
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 52409db56a58a6ec606d0f7e967aefe8b45dff3e
|
4
|
+
data.tar.gz: 68ff1f82181c3dd3714258e0fab235773d4b16ea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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.
|
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.
|
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).
|
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
|
data/lib/scorecard.rb
CHANGED
@@ -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
|
data/lib/scorecard/card.rb
CHANGED
@@ -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
|
data/lib/scorecard/cleaner.rb
CHANGED
@@ -1,6 +1,10 @@
|
|
1
1
|
class Scorecard::Cleaner
|
2
|
-
def self.points(
|
3
|
-
|
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
|
data/lib/scorecard/engine.rb
CHANGED
@@ -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,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
|
data/lib/scorecard/rules.rb
CHANGED
@@ -5,11 +5,11 @@ class Scorecard::Rules
|
|
5
5
|
@point_rules = []
|
6
6
|
end
|
7
7
|
|
8
|
-
def
|
8
|
+
def add(context, amount, options = {})
|
9
9
|
point_rules << Scorecard::PointRule.new(context, amount, options)
|
10
10
|
end
|
11
11
|
|
12
|
-
def
|
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
|
-
|
6
|
-
|
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
|
data/lib/scorecard/scorer.rb
CHANGED
@@ -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
|
-
|
18
|
-
|
17
|
+
Scorecard::ScoreWorker.perform_async context,
|
18
|
+
Scorecard::Parameters.new(options).expand
|
19
|
+
end
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
options
|
23
|
-
|
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
|
-
|
28
|
+
def self.refresh_async(options)
|
29
|
+
Scorecard::RefreshWorker.perform_async(
|
30
|
+
Scorecard::Parameters.new(options).expand
|
31
|
+
)
|
26
32
|
end
|
27
33
|
end
|
data/lib/scorecard/subscriber.rb
CHANGED
@@ -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.
|
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
|
data/scorecard.gemspec
CHANGED
@@ -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
|
@@ -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.
|
7
|
-
config.rules.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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.
|
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
|
data/spec/spec_helper.rb
CHANGED
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
|
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:
|
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.
|
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
|