achievements 0.0.2

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.
File without changes
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2010 Michael R. Bernstein
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,103 @@
1
+ # Achievements
2
+
3
+ Achievements is a drop in Achievements/Badging engine designed to work
4
+ with Ruby, ORM backed web applications.
5
+
6
+ Achievements uses Redis for persistence and tries to be as abstract as
7
+ possible, essentially acting as a counter server with assignable
8
+ thresholds.
9
+
10
+ The application Achievements was developed for uses a User class to
11
+ instantiate the Engine and an Achievement class to persist the details
12
+ of the achievements on the application side.
13
+
14
+ ## Achievements, Briefly
15
+
16
+ Here's how you get achievements into your application:
17
+
18
+ require 'achievements'
19
+
20
+ class Engine
21
+ # Include the AchievementsEngine module from the Achievements
22
+ # gem to make your Engine class an AchievementsEngine class.
23
+ include Achievements::AchievementsEngine
24
+
25
+ # Now you can define achievements in this class. One at a time
26
+ # using the achievement method:
27
+
28
+ # One achievement, one level
29
+ achievement :context1, :one_time, 1
30
+
31
+ # Two achievements, one level
32
+ achievement :context2, :one_time, 1
33
+ achievement :context2, :three_times, 3
34
+
35
+ # One achievement, multiple levels
36
+ achievement :context3, :multiple_levels, [1, 5, 10]
37
+
38
+ # Or all at once using the achievements (plural) method. Right
39
+ # now this relies on the existence of a class which responds to
40
+ # id, name, and achievement methods:
41
+ achievements Achievement.all
42
+
43
+ end
44
+
45
+ Here's how you could interact with it in a session with the above
46
+ class loaded:
47
+
48
+
49
+
50
+
51
+ For the most up to date look at what this library is supposed to do,
52
+ please refer to the test directory.
53
+
54
+ ## Details
55
+
56
+ ### Contexts
57
+
58
+ Contexts are categories for achievements. Every time an achievement
59
+ is triggered, it's "parent" or context counter is also triggered and
60
+ incremented. This makes it easier to gauge overall participation in
61
+ the system and makes "score" based calculations less expensive.
62
+
63
+ ### Achievements
64
+
65
+ Achievements are named, contextualized counters with one or more thresholds.
66
+
67
+ ### Output
68
+
69
+ When a threshold isn't crossed, and nothing changes, the engine will
70
+ return nothing when triggered.
71
+
72
+ When a threshold is crossed, the engine will return an array of
73
+ symbols which correspond to the names of the achievements which have
74
+ been reached, along with the threshold being crossed. Your application can consume this output as you see
75
+ fit.
76
+
77
+ ### Achievement API Compliance
78
+
79
+ Your Achievement class must have a name, context, and threshold method
80
+ in order to adapt to the Engine, in order to be consumed by the Agent
81
+ class directly.
82
+
83
+ ## Project Metainfo
84
+
85
+ ### TODO
86
+
87
+ * Better Redis interface methods for making/closing/keeping conneciton
88
+
89
+ ### Contributing
90
+
91
+ 0. Do some work on the library, commit
92
+ 1. [Fork][1] Achievements
93
+ 2. Create a topic branch - `git checkout -b my_branch`
94
+ 3. Push to your branch - `git push origin my_branch`
95
+ 4. Create an [Issue][2] with a link to your branch
96
+ 5. That's it!
97
+
98
+ [1]: http://help.github.com/forking/
99
+ [2]: http://github.com/mrb/achievements/issues
100
+
101
+ ### Author
102
+
103
+ Michael R. Bernstein - *michaelrbernstein@gmail.com* - twitter.com/mrb_bk
@@ -0,0 +1,17 @@
1
+ $LOAD_PATH.unshift 'lib'
2
+ #
3
+ # Publishing
4
+ #
5
+
6
+ desc "Push a new version to Gemcutter"
7
+ task :publish do
8
+ require 'achievements/version'
9
+
10
+ sh "gem build achievements.gemspec"
11
+ sh "gem push achievements-#{Achievements::Version}.gem"
12
+ sh "git tag v#{Achievements::Version}"
13
+ sh "git push origin v#{Achievements::Version}"
14
+ sh "git push origin master"
15
+ sh "git clean -fd"
16
+
17
+ end
@@ -0,0 +1,21 @@
1
+ # An Abstract, Redis-Backed Achievements Engine
2
+ require 'redis'
3
+
4
+ require 'achievements/achievement'
5
+ require 'achievements/achievements_achievement'
6
+ require 'achievements/achievements_agent'
7
+ require 'achievements/achievements_engine'
8
+ require 'achievements/agent'
9
+ require 'achievements/counter'
10
+ require 'achievements/engine'
11
+ require 'achievements/version'
12
+
13
+ module Achievements
14
+ def self.redis
15
+ @redis || @redis = Redis.connect
16
+ end
17
+
18
+ def self.engine
19
+ AchievementsEngine.engine
20
+ end
21
+ end
@@ -0,0 +1,23 @@
1
+ module Achievements
2
+
3
+ # Achievement, basis of counters
4
+ class Achievement
5
+ attr_accessor :name
6
+ attr_accessor :threshold
7
+ attr_accessor :context
8
+
9
+ # A method needs a context, name, and threshold, in that order.
10
+ #
11
+ def initialize(context, name, threshold)
12
+ @context = context
13
+ @name = name
14
+ @threshold = threshold
15
+ end
16
+
17
+ # Convenience to_hash method
18
+ def to_hash
19
+ {:name => @name, :threshold => @threshold, :context => @context}
20
+ end
21
+
22
+ end
23
+ end
@@ -0,0 +1,16 @@
1
+ module Achievements
2
+ module AchievementsAchievement
3
+ # Convenience methods
4
+ def self.included(base)
5
+ base.extend IncludeClassMethods
6
+ end
7
+
8
+ module IncludeClassMethods
9
+ # Update the threshold for the AchievementEngine achievement
10
+ # that matches context, achievement, and sets threshold to new_threshold
11
+ def update_threshold(context, achievement, new_threshold)
12
+ # find achievement and update threshold
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ module Achievements
2
+ module AchievementsAgent
3
+ # Convenience methods for instantiating an engine and adding achievements
4
+ def self.included(base)
5
+ base.extend IncludeClassMethods
6
+ end
7
+
8
+ # Convenience Class Methods for ActiveRecord::Base like User Classes
9
+ module IncludeClassMethods
10
+ def engine_class(engine_class)
11
+ @engine_class = engine_class
12
+ end
13
+
14
+ def engine
15
+ @engine_class
16
+ end
17
+ end
18
+
19
+ # Agent instance methods
20
+
21
+ # Agent instance level achievement trigger. Automatically sends
22
+ # agent id along with context and name to the AchievementEngine
23
+ def achieve(context,name)
24
+ self.class.engine.achieve context, @id, name
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,77 @@
1
+ module Achievements
2
+ module AchievementsEngine
3
+ def self.engine
4
+ @engine
5
+ end
6
+
7
+ def self.included(base)
8
+ base.extend IncludeClassMethods
9
+ end
10
+
11
+ module IncludeClassMethods
12
+ # Instantiates the AchievementEngine and sets
13
+ # the contexts, which instantiate context specific counters. Use
14
+ # only once.
15
+ #
16
+ # make_engine [:context1,:context2]
17
+ #
18
+ def make_engine(contexts)
19
+ @redis = Achievements.redis
20
+ @engine = Engine.new(@redis)
21
+ end
22
+
23
+ # Convenience method for accessing the current engine instance directly
24
+ def engine
25
+ @engine
26
+ end
27
+
28
+ # Convenienve method for accessing redis instance
29
+ def redis
30
+ @redis
31
+ end
32
+
33
+ # Binds an achievement with a specific counter threshold. Use as
34
+ # many as you'd like.
35
+ #
36
+ # bind :context, :name, threshold
37
+ #
38
+ def achievement(context, name, threshold)
39
+ make_engine(context) if !@engine
40
+ @engine.achievement(context,name,threshold)
41
+ end
42
+
43
+ # Alternately, bind an entire array of achievement objects. To
44
+ # use this, achievements must respond to the context, name, and
45
+ # threshold methods.
46
+ #
47
+ # For example, when using with rails:
48
+ #
49
+ # bind_all Achievement.all
50
+ #
51
+ def achievements(object_array)
52
+ object_array.each do |object|
53
+ make_engine(context) if !@engine
54
+ @engine.achievement object.context, object.name, object.threshold
55
+ end
56
+ end
57
+
58
+ # Trigger a bound achievement method. Since this is a class
59
+ # level method, you must include the agent id along with the
60
+ # method call
61
+ #
62
+ # trigger agent_id, context, name
63
+ #
64
+ def achieve(context, agent_id, name)
65
+ @engine.achieve context, agent_id, name
66
+ end
67
+
68
+ def achieves(context_name_array)
69
+ context_name_array.each do |cna|
70
+ Achievements.engine.achieve cna[0], @id, cna[1]
71
+ end
72
+ end
73
+
74
+
75
+ end
76
+ end
77
+ end
File without changes
@@ -0,0 +1,8 @@
1
+ module Achievements
2
+ # Counter class is a Redis key factory.
3
+ class Counter
4
+ def self.make(context, agent_id, name)
5
+ @key = "#{context}:agent:#{agent_id}:#{name}"
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,69 @@
1
+ module Achievements
2
+ # Triggering multiple at once
3
+
4
+ class Engine
5
+ attr_accessor :achievements
6
+ attr_accessor :contexts
7
+ attr_accessor :redis
8
+
9
+ def initialize(redis)
10
+ @contexts = []
11
+ @redis = redis
12
+ @achievements = {}
13
+ end
14
+
15
+ def achievement(context,name,threshold)
16
+ @contexts << context if !@contexts.include?(context)
17
+ if achievement = Achievement.new(context,name,threshold)
18
+ [threshold].flatten.each do |thresh|
19
+ @redis.sadd "#{context}:#{name}:threshold", thresh.to_s
20
+ end
21
+ end
22
+ end
23
+
24
+ # The trigger method accepts:
25
+ # context, agent_id, name
26
+ #
27
+ # And returns:
28
+ # context, name, threshold
29
+ def achieve(context, agent_id, name)
30
+ achieved = []
31
+
32
+ # Increment parent counter
33
+ counter = Counter.make(context,agent_id,"parent")
34
+ incr counter
35
+
36
+ # Increment child counter
37
+ counter = Counter.make(context,agent_id,name)
38
+ result = incr counter
39
+
40
+ # Check Threshold
41
+
42
+ if @redis.sismember("#{context}:#{name}:threshold", result) == true
43
+ achieved << [context,name, result.to_s]
44
+ return achieved
45
+ else
46
+ return []
47
+ end
48
+
49
+ end
50
+
51
+ # incr key
52
+ def incr(counter)
53
+ @redis.incr counter
54
+ end
55
+
56
+ # decr key
57
+ def decr(counter)
58
+ @redis.decr counter
59
+ end
60
+
61
+ def deactiveate(counter)
62
+ @redis.set counter, "ACHIEVED"
63
+ end
64
+
65
+ ## Class Methods
66
+
67
+
68
+ end
69
+ end
@@ -0,0 +1,4 @@
1
+
2
+ module Achievements
3
+ Version = VERSION = '0.0.2'
4
+ end
@@ -0,0 +1,95 @@
1
+ require File.dirname(__FILE__) + '/test_helper'
2
+
3
+ context "Achievement Restructure Test" do
4
+
5
+ setup do
6
+ # Flush redis before each test
7
+ Achievements.redis.flushall
8
+
9
+ # A sample engine class that demonstrates how to use the
10
+ # AchievementsEngine include to instantiate the engine, make
11
+ # achievements, etc.
12
+ class Engine
13
+ include Achievements::AchievementsEngine
14
+
15
+ # One achievement, one level
16
+ achievement :context1, :one_time, 1
17
+
18
+ # Two achievements, one level
19
+ achievement :context2, :one_time, 1
20
+ achievement :context2, :three_times, 3
21
+
22
+ # One achievement, multiple levels
23
+ achievement :context3, :multiple_levels, [1, 5, 10]
24
+ end
25
+
26
+ # A sample agent class that demonstrates how to use the
27
+ # AchievementsAgent include to achieve directly from a user
28
+ # class. User class must have an id attribute to comply with the API.
29
+ class User
30
+ attr_accessor :id
31
+ include Achievements::AchievementsAgent
32
+ engine_class Engine
33
+
34
+ def initialize(id)
35
+ @id = id
36
+ end
37
+ end
38
+
39
+ # Make a new user with an ID different from the one used in most tests
40
+ @user = User.new(10)
41
+
42
+ # For testing purposes, access the engine and redis directly
43
+ @engine = Engine.engine
44
+ @redis = Engine.redis
45
+ end
46
+
47
+ test "Engine gets assigned appropriate contexts" do
48
+ assert_equal @engine.contexts, [:context1,:context2,:context3]
49
+ end
50
+
51
+ test "Contexts get threshold sets" do
52
+ assert_equal @redis.smembers("context1:one_time:threshold"), ["1"]
53
+ assert_equal @redis.smembers("context2:one_time:threshold"), ["1"]
54
+ assert_equal @redis.smembers("context2:three_times:threshold"), ["3"]
55
+ assert_equal @redis.smembers("context3:multiple_levels:threshold").sort, ["1","10","5"]
56
+ end
57
+
58
+ test "Trigger should increment parent (context) and child counter" do
59
+ @engine.achieve(:context1,1,:one_time)
60
+ assert_equal @redis.get("context1:agent:1:parent"), "1"
61
+ assert_equal @redis.get("context1:agent:1:one_time"), "1"
62
+ end
63
+
64
+ test "Trigger should return crossed threshold" do
65
+ response = @engine.achieve(:context1, 1,:one_time)
66
+ assert_equal [[:context1,:one_time, "1"]], response
67
+ end
68
+
69
+ test "Trigger with threshold higher than 1" do
70
+ @engine.achieve(:context2, 1, :three_times)
71
+ @engine.achieve(:context2, 1, :three_times)
72
+ result = @engine.achieve(:context2, 1, :three_times)
73
+ assert_equal [[:context2,:three_times, "3"]], result
74
+ end
75
+
76
+ test "Multiple thresholds" do
77
+ results = []
78
+ 10.times do
79
+ results << @engine.achieve(:context3,1,:multiple_levels)
80
+ end
81
+ assert_equal results, [[[:context3, :multiple_levels, "1"]], [], [], [], [[:context3, :multiple_levels, "5"]], [], [], [], [], [[:context3, :multiple_levels, "10"]]]
82
+ end
83
+
84
+ test "Achieving from the agent class" do
85
+ response = @user.achieve(:context1,:one_time)
86
+ assert_equal response, [[:context1,:one_time,"1"]]
87
+ end
88
+
89
+ test "Achieving after threshold returns empty from agent and engine class" do
90
+ @user.achieve(:context1,:one_time)
91
+ assert_equal @user.achieve(:context1,:one_time), []
92
+ @engine.achieve(:context1,1,:one_time)
93
+ assert_equal @engine.achieve(:context1,1,:one_time),[]
94
+ end
95
+ end
@@ -0,0 +1,64 @@
1
+ dir = File.dirname(File.expand_path(__FILE__))
2
+ $LOAD_PATH.unshift dir + '/../lib'
3
+ $TESTING = true
4
+ require 'rubygems'
5
+ require 'test/unit'
6
+ require 'achievements'
7
+
8
+ begin
9
+ require 'leftright'
10
+ rescue LoadError
11
+ end
12
+
13
+ # Thanks, Resque!
14
+ # http://github.com/defunkt/resque
15
+ if !system("which redis-server")
16
+ puts '', "** can't find `redis-server` in your path"
17
+ abort ''
18
+ end
19
+
20
+ #
21
+ # start our own redis when the tests start,
22
+ # kill it when they end
23
+ #
24
+
25
+ at_exit do
26
+ next if $!
27
+
28
+ if defined?(MiniTest)
29
+ exit_code = MiniTest::Unit.new.run(ARGV)
30
+ else
31
+ exit_code = Test::Unit::AutoRunner.run
32
+ end
33
+
34
+ pid = `ps -A -o pid,command | grep [r]edis-test`.split(" ")[0]
35
+ puts "Killing test redis server..."
36
+ `rm -f #{dir}/dump.rdb`
37
+ Process.kill("KILL", pid.to_i)
38
+ exit exit_code
39
+ end
40
+
41
+ puts "Starting redis for testing at localhost:9736..."
42
+ `redis-server #{dir}/redis-test.conf`
43
+ # AchievementEngine.redis = 'localhost:9736'
44
+
45
+ ##
46
+ # test/spec/mini 3
47
+ # http://gist.github.com/25455
48
+ # chris@ozmm.org
49
+ #
50
+ def context(*args, &block)
51
+ return super unless (name = args.first) && block
52
+ require 'test/unit'
53
+ klass = Class.new(defined?(ActiveSupport::TestCase) ? ActiveSupport::TestCase : Test::Unit::TestCase) do
54
+ def self.test(name, &block)
55
+ define_method("test_#{name.gsub(/\W/,'_')}", &block) if block
56
+ end
57
+ def self.xtest(*args) end
58
+ def self.setup(&block) define_method(:setup, &block) end
59
+ def self.teardown(&block) define_method(:teardown, &block) end
60
+ end
61
+ (class << klass; self end).send(:define_method, :name) { name.gsub(/\W/,'_') }
62
+ klass.class_eval &block
63
+ end
64
+
metadata ADDED
@@ -0,0 +1,97 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: achievements
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 2
10
+ version: 0.0.2
11
+ platform: ruby
12
+ authors:
13
+ - Michael R. Bernstein
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-11-02 00:00:00 -04:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: redis
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 25
30
+ segments:
31
+ - 2
32
+ - 0
33
+ - 11
34
+ version: 2.0.11
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ description: " Achievements is an abstract, Redis-backed, counter based achievements engine\n designed to be included in Model classes in web applications.\n\n Achievements lets you track and persist user actions with a simple bind and\n trigger design pattern.\n"
38
+ email: michaelrbernstein@gmail.com
39
+ executables: []
40
+
41
+ extensions: []
42
+
43
+ extra_rdoc_files:
44
+ - LICENSE
45
+ - README.markdown
46
+ files:
47
+ - README.markdown
48
+ - Rakefile
49
+ - LICENSE
50
+ - HISTORY.markdown
51
+ - lib/achievements/counter.rb
52
+ - lib/achievements/achievements_engine.rb
53
+ - lib/achievements/achievements_achievement.rb
54
+ - lib/achievements/achievements_agent.rb
55
+ - lib/achievements/version.rb
56
+ - lib/achievements/agent.rb
57
+ - lib/achievements/engine.rb
58
+ - lib/achievements/achievement.rb
59
+ - lib/achievements.rb
60
+ - test/achievements_test.rb
61
+ - test/test_helper.rb
62
+ has_rdoc: true
63
+ homepage: http://github.com/mrb/achievements
64
+ licenses: []
65
+
66
+ post_install_message:
67
+ rdoc_options:
68
+ - --charset=UTF-8
69
+ require_paths:
70
+ - lib
71
+ required_ruby_version: !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ">="
75
+ - !ruby/object:Gem::Version
76
+ hash: 3
77
+ segments:
78
+ - 0
79
+ version: "0"
80
+ required_rubygems_version: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ">="
84
+ - !ruby/object:Gem::Version
85
+ hash: 3
86
+ segments:
87
+ - 0
88
+ version: "0"
89
+ requirements: []
90
+
91
+ rubyforge_project:
92
+ rubygems_version: 1.3.7
93
+ signing_key:
94
+ specification_version: 3
95
+ summary: Achievements adds Redis-backed achievments
96
+ test_files: []
97
+