achievements 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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
+