paths_of_glory 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG ADDED
@@ -0,0 +1,7 @@
1
+ Mon Feb 22 2010
2
+ --------------
3
+ - Added "options" type for list
4
+
5
+ Mon Feb 8 2010
6
+ --------------
7
+ - Added "integer" and "decimal" types
data/README.md ADDED
@@ -0,0 +1,73 @@
1
+ Paths of Glory
2
+ ==============
3
+
4
+ Paths of Glory sets you on the road to achievements!
5
+
6
+ In short, it's a re-usable model for creating an achievement system.
7
+
8
+ It's extracted from http://peertester.com and inspired by: http://stackoverflow.com/questions/885277/how-to-implement-an-achievement-system-in-ror
9
+
10
+
11
+
12
+ Installation
13
+ ============
14
+
15
+ Paths of Glory is easy to install.
16
+
17
+ As a plugin:
18
+
19
+ ./script/plugin install git://github.com/paulca/paths_of_glory.git
20
+
21
+ Or as a gem. Add this to your environment.rb:
22
+
23
+ gem install paths_of_glory
24
+
25
+ config.gem 'paths_of_glory'
26
+
27
+ Then generate the migration:
28
+
29
+ ./script/generate paths_of_glory
30
+
31
+ And run the migration:
32
+
33
+ rake db:migrate
34
+
35
+ This creates the tables.
36
+
37
+ Then, in your User model:
38
+
39
+ class User < ActiveRecord::Base
40
+ include Achievements
41
+ end
42
+
43
+ Basic Usage
44
+ ===========
45
+
46
+ Paths of Glory gives you access to a DSL to generate achievements. Achievements can have multiple levels or can just be standalone achievements.
47
+
48
+ The pattern is basically:
49
+
50
+ Create an achievement:
51
+
52
+ ./script/generate achievement Glory
53
+
54
+ This gives you `app/models/achievements/glory.rb` and `app/models/achievements/glory_observer.rb` with some bootstrapped code.
55
+
56
+ You're on your own after that. Have fun!
57
+
58
+
59
+ Running the tests
60
+ =================
61
+
62
+ You can run the tests by checking out the code into vendor/plugins of a Rails app and running:
63
+
64
+ rake
65
+
66
+ About me
67
+ ========
68
+
69
+ I'm Paul Campbell. I'm an avid Ruby on Rails web developer. Follow my ramblings at [http://www.pabcas.com](http://www.pabcas.com)
70
+
71
+ Follow me on Twitter [http://twitter.com/paulca](http://twitter.com/paulca)
72
+
73
+ Copyright (c) 2010 Paul Campbell, released under the MIT license
data/Rakefile ADDED
@@ -0,0 +1,43 @@
1
+ require 'rake'
2
+ require 'spec/rake/spectask'
3
+
4
+ desc 'Default: run specs.'
5
+ task :default => :spec
6
+
7
+ desc 'Run the specs'
8
+ Spec::Rake::SpecTask.new(:spec) do |t|
9
+ t.spec_opts = ['--colour --format progress --loadby mtime --reverse']
10
+ t.spec_files = FileList['spec/**/*_spec.rb']
11
+ end
12
+
13
+ PKG_FILES = FileList[
14
+ '[a-zA-Z]*',
15
+ 'app/**/*',
16
+ 'generators/**/*',
17
+ 'config/*',
18
+ 'lib/**/*',
19
+ 'rails/**/*',
20
+ 'spec/**/*',
21
+ 'features/**/*'
22
+ ]
23
+
24
+ begin
25
+ require 'jeweler'
26
+ Jeweler::Tasks.new do |s|
27
+ s.name = "paths_of_glory"
28
+ s.version = "0.1.0"
29
+ s.author = "Paul Campbell"
30
+ s.email = "paul@rslw.com"
31
+ s.homepage = "http://www.github.com/paulca/paths_of_glory"
32
+ s.platform = Gem::Platform::RUBY
33
+ s.summary = "Getting you started on the road to achievements."
34
+ s.files = PKG_FILES.to_a
35
+ s.require_path = "lib"
36
+ s.has_rdoc = false
37
+ s.extra_rdoc_files = ["README.md"]
38
+ end
39
+ rescue LoadError
40
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
41
+ end
42
+
43
+ Jeweler::GemcutterTasks.new
@@ -0,0 +1,65 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{achievement_system}
8
+ s.version = "0.1.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Paul Campbell"]
12
+ s.date = %q{2010-05-20}
13
+ s.email = %q{paul@rslw.com}
14
+ s.extra_rdoc_files = [
15
+ "README.md"
16
+ ]
17
+ s.files = [
18
+ "CHANGELOG",
19
+ "README.md",
20
+ "Rakefile",
21
+ "app/models/achievement.rb",
22
+ "app/models/achievements.rb",
23
+ "generators/achievement/USAGE",
24
+ "generators/achievement/achievement_generator.rb",
25
+ "generators/achievement/templates/achievement.rb",
26
+ "generators/achievement/templates/achievement_observer.rb",
27
+ "generators/paths_of_glory/USAGE",
28
+ "generators/paths_of_glory/paths_of_glory_generator.rb",
29
+ "generators/paths_of_glory/templates/20100311101933_create_achievements.rb",
30
+ "rails/init.rb",
31
+ "spec/achievement_generator_spec.rb",
32
+ "spec/achievement_spec.rb",
33
+ "spec/blueprints.rb",
34
+ "spec/database.yml",
35
+ "spec/debug.log",
36
+ "spec/paths_of_glory_generator_spec.rb",
37
+ "spec/schema.rb",
38
+ "spec/spec_helper.rb"
39
+ ]
40
+ s.has_rdoc = false
41
+ s.homepage = %q{http://www.github.com/paulca/achievement_system}
42
+ s.rdoc_options = ["--charset=UTF-8"]
43
+ s.require_paths = ["lib"]
44
+ s.rubygems_version = %q{1.3.6}
45
+ s.summary = %q{Getting you started on the road to achievements.}
46
+ s.test_files = [
47
+ "spec/achievement_generator_spec.rb",
48
+ "spec/achievement_spec.rb",
49
+ "spec/blueprints.rb",
50
+ "spec/paths_of_glory_generator_spec.rb",
51
+ "spec/schema.rb",
52
+ "spec/spec_helper.rb"
53
+ ]
54
+
55
+ if s.respond_to? :specification_version then
56
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
57
+ s.specification_version = 3
58
+
59
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
60
+ else
61
+ end
62
+ else
63
+ end
64
+ end
65
+
@@ -0,0 +1,73 @@
1
+ class Achievement < ActiveRecord::Base
2
+
3
+ belongs_to :user
4
+
5
+ named_scope :not_notified, :conditions => {:notified => false}
6
+ named_scope :recent, :order => "created_at desc"
7
+ named_scope :kind_of, lambda { |type| {:conditions => {:type => type.to_s}}} do
8
+ def current
9
+ order("level desc").limit(1).first
10
+ end
11
+ end
12
+
13
+ named_scope :order, lambda { |order| {:order => order} }
14
+ named_scope :limit, lambda { |limit| {:limit => limit} }
15
+
16
+ class << self
17
+ def levels
18
+ @levels ||= []
19
+ end
20
+
21
+ def level(level, options = {})
22
+ levels << {:level => level, :quota => options[:quota]}
23
+ end
24
+
25
+ def set_thing_to_check(&block)
26
+ @thing_to_check = block
27
+ end
28
+
29
+ def thing_to_check(object)
30
+ @thing_to_check.call(object)
31
+ end
32
+
33
+ def select_level(level)
34
+ levels.select { |l| l[:level] == level }.first
35
+ end
36
+
37
+ def quota_for(level)
38
+ select_level(level)[:quota] if select_level(level)
39
+ end
40
+
41
+ def has_level?(level)
42
+ select_level(level).present?
43
+ end
44
+
45
+ def current_level(user)
46
+ if current_achievement = user.achievements.kind_of(self).current
47
+ current_achievement.level
48
+ else
49
+ 0
50
+ end
51
+ end
52
+
53
+ def next_level(user)
54
+ current_level(user) + 1
55
+ end
56
+
57
+ def current_progress(user)
58
+ thing_to_check(user) - quota_for(current_level(user)).to_i
59
+ end
60
+
61
+ def next_level_quota(user)
62
+ quota_for(next_level(user)) - quota_for(current_level(user)).to_i
63
+ end
64
+
65
+ def progress_to_next_level(user)
66
+ if(has_level?(next_level(user)))
67
+ return [(current_progress(user) * 100) / next_level_quota(user), 95].min
68
+ else
69
+ return nil
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,36 @@
1
+ module Achievements
2
+
3
+ def self.included(base)
4
+ base.class_eval do
5
+ has_many :achievements do
6
+ def include?(achievement, level = nil)
7
+ all.select { |a| a.type.to_s == achievement.to_s and a.level == level }.any?
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ def award_achievement(achievement, level = nil)
14
+ achievement.create!(:user => self, :level => level)
15
+ end
16
+
17
+ def has_achievement?(achievement, level = nil)
18
+ conditions = {:type => achievement.to_s, :user_id => id}
19
+ conditions[:level] = level if level
20
+ achievements.first(:conditions => conditions).present?
21
+ end
22
+
23
+ def get_badges_in_progress(badges)
24
+ badges.collect do |achievement|
25
+ {
26
+ :type => achievement,
27
+ :level => achievement.next_level(self),
28
+ :progress => achievement.progress_to_next_level(self),
29
+ :next_level_quota => achievement.next_level_quota(self),
30
+ :current_progress => achievement.current_progress(self),
31
+ :next_level => achievement.next_level(self)
32
+ }
33
+ end.sort_by { |achievement| achievement[:progress] }.reverse[0,3]
34
+ end
35
+
36
+ end
File without changes
@@ -0,0 +1,32 @@
1
+ class AchievementGenerator < Rails::Generator::NamedBase
2
+ def manifest
3
+ record do |m|
4
+ m.class_collisions class_name, "#{class_name}"
5
+ m.class_collisions class_name, "#{class_name}Observer"
6
+
7
+ m.directory 'app/models/achievements'
8
+ m.template "achievement.rb", "app/models/achievements/#{file_name}.rb"
9
+ m.template "achievement_observer.rb", "app/models/achievements/#{file_name}_observer.rb"
10
+ end
11
+ end
12
+
13
+ def after_generate
14
+ puts "
15
+
16
+ Your new achievement, '#{class_name}' has been created.
17
+
18
+ Now you need to edit:
19
+
20
+ app/models/achievements/#{file_name}.rb
21
+
22
+ and
23
+
24
+ app/models/achievements/#{file_name}_observer.rb
25
+
26
+ Then you need to activate the observer, by adding ':#{file_name}_observer' to environment.rb, eg:
27
+
28
+ config.active_record.observers = :#{file_name}_observer
29
+
30
+ "
31
+ end
32
+ end
@@ -0,0 +1,19 @@
1
+ class <%= class_name %> < Achievement
2
+
3
+ # level 1, :quota => 5
4
+ # level 2, :quota => 10
5
+ # level 3, :quota => 30
6
+ # level 4, :quota => 100
7
+ # level 5, :quota => 500
8
+
9
+ # set_thing_to_check { |user| ... }
10
+
11
+ def self.award_achievements_for(user)
12
+ return unless user
13
+ levels.each do |level|
14
+ if user.not.has_achievement?(self, level[:level]) and thing_to_check(user) >= level[:quota]
15
+ user.award_achievement(self, level[:level])
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,7 @@
1
+ class <%= class_name %>Observer < ActiveRecord::Observer
2
+ observe :user
3
+
4
+ def after_save(user)
5
+ Dedicated.award_achievements_for(user) if ('your conditions here')
6
+ end
7
+ end
@@ -0,0 +1,5 @@
1
+ Description:
2
+ Creates a migration to create a table to store achievements called "achievements"
3
+
4
+ Examples:
5
+ `./script/generate paths_of_glory`
@@ -0,0 +1,30 @@
1
+ class PathsOfGloryGenerator < Rails::Generator::Base
2
+ def manifest
3
+ record do |m|
4
+ m.directory 'db/migrate'
5
+ m.file "20100311101933_create_achievements.rb", "db/migrate/20100311101933_create_achievements.rb"
6
+ end
7
+ end
8
+
9
+ def after_generate
10
+ puts "
11
+
12
+ You're on the Path to Glory
13
+
14
+ Now, add 'include Achievements' to your User model:
15
+
16
+ class User < ActiveRecord::Base
17
+ include Achievements
18
+ end
19
+
20
+ and run:
21
+
22
+ rake db:migrate,
23
+
24
+ To generate a new Achievement, run:
25
+
26
+ ./script/generate achievement Glory
27
+
28
+ where 'Glory' is the name of the achievement."
29
+ end
30
+ end
@@ -0,0 +1,16 @@
1
+ class CreateAchievements < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :achievements do |t|
4
+ t.string :type
5
+ t.integer :level
6
+ t.integer :user_id
7
+ t.notified :boolean, :default => false
8
+
9
+ t.timestamps
10
+ end
11
+ end
12
+
13
+ def self.down
14
+ drop_table :achievements
15
+ end
16
+ end
data/rails/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'behavior'
@@ -0,0 +1,50 @@
1
+ require 'spec_helper'
2
+ require 'rails_generator'
3
+ require 'rails_generator/scripts/generate'
4
+
5
+ describe AchievementGenerator do
6
+
7
+ before do
8
+ @dirs = {}
9
+ @dirs['achievements'] = fake_rails_root + '/app/models/achievements'
10
+ @dirs.each do |key, dir|
11
+ FileUtils.mkdir_p(dir)
12
+ end
13
+ @original_files = {}
14
+ @original_files['achievements'] = file_list(@dirs['achievements'])
15
+ Rails::Generator::Scripts::Generate.new.run(["achievement", 'glory'], :destination => fake_rails_root, :backtrace => true)
16
+
17
+ @files = (file_list(@dirs['achievements']) - @original_files['achievements'])
18
+ end
19
+
20
+ let(:achievement_file) { @files.first }
21
+ let(:observer_file) { @files.last }
22
+
23
+ it "should create the achievement file" do
24
+ File.basename(achievement_file).should == "glory.rb"
25
+ File.basename(observer_file).should == "glory_observer.rb"
26
+ end
27
+
28
+ it "should make them beautiful" do
29
+ File.read(achievement_file).should match(/Glory < Achievement/)
30
+ end
31
+
32
+ it "should make them beautiful" do
33
+ File.read(observer_file).should match(/GloryObserver < ActiveRecord::Observer/)
34
+ end
35
+
36
+
37
+ after do
38
+ FileUtils.rm_r(fake_rails_root)
39
+ end
40
+
41
+
42
+ def fake_rails_root
43
+ File.join(File.dirname(__FILE__), 'spec', 'rails_root')
44
+ end
45
+
46
+ def file_list(dir)
47
+ Dir.glob(File.join(dir, "*"))
48
+ end
49
+
50
+ end
@@ -0,0 +1,102 @@
1
+ require 'spec_helper'
2
+
3
+ describe Achievement do
4
+ class Thing < Achievement
5
+ level 1, :quota => 1
6
+ level 2, :quota => 2
7
+
8
+ set_thing_to_check {|u| u.sign_in_count }
9
+ end
10
+
11
+ class Dedicated < Achievement
12
+
13
+ level 1, :quota => 5
14
+ level 2, :quota => 10
15
+ level 3, :quota => 30
16
+ level 4, :quota => 100
17
+ level 5, :quota => 500
18
+
19
+ set_thing_to_check { |user| user.sign_in_count.to_i }
20
+
21
+ def self.award_achievements_for(user)
22
+ return unless user
23
+ levels.each do |level|
24
+ if !user.has_achievement?(self, level[:level]) and thing_to_check(user) >= level[:quota]
25
+ user.award_achievement(self, level[:level])
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ class User < ActiveRecord::Base
32
+ include Achievements
33
+
34
+ def after_save
35
+ Dedicated.award_achievements_for(self) if sign_in_count_changed?
36
+ end
37
+ end
38
+
39
+ User.blueprint do
40
+ email 'joe@king.com'
41
+ encrypted_password 'blah'
42
+ password_salt 'are you joking?'
43
+ end
44
+
45
+ it "should have 2 levels" do
46
+ Thing.levels.size.should == 2
47
+ end
48
+
49
+ it "should have a levels array" do
50
+ Thing.levels.should == [{:quota=> 1, :level =>1}, {:quota=>2, :level=>2}]
51
+ end
52
+
53
+ describe ".quota_for" do
54
+ it "should give me back the quota" do
55
+ Dedicated.quota_for(2).should == 10
56
+ end
57
+ end
58
+
59
+ describe ".has_level?" do
60
+ it "should tell me if it has a level" do
61
+ Dedicated.has_level?(1).should be_true
62
+ end
63
+
64
+ it "should tell me if it doesn't have a level" do
65
+ Dedicated.has_level?(6).should be_false
66
+ end
67
+ end
68
+
69
+ describe ".progress_to_next_level" do
70
+ before do
71
+ @user = User.make(:sign_in_count => 2)
72
+ end
73
+
74
+ it "should tell me the thing to check" do
75
+ Thing.thing_to_check(@user).should == 2
76
+ end
77
+
78
+ it "should show me that I'm 40% the way to dedicated" do
79
+ Dedicated.progress_to_next_level(@user).should == 40
80
+ end
81
+
82
+ it "should update the progress as things happen" do
83
+ @user.update_attribute(:sign_in_count, 4)
84
+ Dedicated.progress_to_next_level(@user).should == 80
85
+ end
86
+
87
+ it "should work for more than one level" do
88
+ @user.sign_in_count = 5
89
+ @user.save!
90
+ Dedicated.thing_to_check(@user).should == 5
91
+ Dedicated.progress_to_next_level(@user).should == 0
92
+ @user.update_attribute(:sign_in_count, 6)
93
+ Dedicated.progress_to_next_level(@user).should == 20
94
+ end
95
+
96
+ it "should return nil finally" do
97
+ @user.sign_in_count = 500
98
+ @user.save!
99
+ Dedicated.progress_to_next_level(@user).should be_nil
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,6 @@
1
+ require 'machinist/active_record'
2
+ require 'faker'
3
+ require 'sham'
4
+
5
+ Achievement.blueprint do
6
+ end
data/spec/database.yml ADDED
@@ -0,0 +1,21 @@
1
+ sqlite:
2
+ :adapter: sqlite
3
+ :database: spec/paths_of_glory.sqlite.db
4
+
5
+ sqlite3:
6
+ :adapter: sqlite3
7
+ :database: spec/paths_of_glory.sqlite3.db
8
+
9
+ postgresql:
10
+ :adapter: postgresql
11
+ :username: postgres
12
+ :password: postgres
13
+ :database: paths_of_glory_test
14
+ :min_messages: ERROR
15
+
16
+ mysql:
17
+ :adapter: mysql
18
+ :host: localhost
19
+ :username: root
20
+ :password: password
21
+ :database: paths_of_glory_test