Commit c608ace206b3032dbe4b1dcad829bf34ceb4751b
0 parents
Exists in
master
and in
1 other branch
gamification: setup merit gem and example of point and badge rules
Showing
21 changed files
with
447 additions
and
0 deletions
Show diff stats
1 | +++ a/db/migrate/20150320192316_create_merit_actions.rb | ||
@@ -0,0 +1,15 @@ | @@ -0,0 +1,15 @@ | ||
1 | +class CreateMeritActions < ActiveRecord::Migration | ||
2 | + def change | ||
3 | + create_table :merit_actions do |t| | ||
4 | + t.integer :user_id | ||
5 | + t.string :action_method | ||
6 | + t.integer :action_value | ||
7 | + t.boolean :had_errors, default: false | ||
8 | + t.string :target_model | ||
9 | + t.integer :target_id | ||
10 | + t.text :target_data | ||
11 | + t.boolean :processed, default: false | ||
12 | + t.timestamps | ||
13 | + end | ||
14 | + end | ||
15 | +end |
1 | +++ a/db/migrate/20150320192317_create_merit_activity_logs.rb | ||
@@ -0,0 +1,11 @@ | @@ -0,0 +1,11 @@ | ||
1 | +class CreateMeritActivityLogs < ActiveRecord::Migration | ||
2 | + def change | ||
3 | + create_table :merit_activity_logs do |t| | ||
4 | + t.integer :action_id | ||
5 | + t.string :related_change_type | ||
6 | + t.integer :related_change_id | ||
7 | + t.string :description | ||
8 | + t.datetime :created_at | ||
9 | + end | ||
10 | + end | ||
11 | +end |
1 | +++ a/db/migrate/20150320192319_create_badges_sashes.rb | ||
@@ -0,0 +1,16 @@ | @@ -0,0 +1,16 @@ | ||
1 | +class CreateBadgesSashes < ActiveRecord::Migration | ||
2 | + def self.up | ||
3 | + create_table :badges_sashes do |t| | ||
4 | + t.integer :badge_id, :sash_id | ||
5 | + t.boolean :notified_user, default: false | ||
6 | + t.datetime :created_at | ||
7 | + end | ||
8 | + add_index :badges_sashes, [:badge_id, :sash_id] | ||
9 | + add_index :badges_sashes, :badge_id | ||
10 | + add_index :badges_sashes, :sash_id | ||
11 | + end | ||
12 | + | ||
13 | + def self.down | ||
14 | + drop_table :badges_sashes | ||
15 | + end | ||
16 | +end |
1 | +++ a/db/migrate/20150320192320_create_scores_and_points.rb | ||
@@ -0,0 +1,15 @@ | @@ -0,0 +1,15 @@ | ||
1 | +class CreateScoresAndPoints < ActiveRecord::Migration | ||
2 | + def change | ||
3 | + create_table :merit_scores do |t| | ||
4 | + t.references :sash | ||
5 | + t.string :category, default: 'default' | ||
6 | + end | ||
7 | + | ||
8 | + create_table :merit_score_points do |t| | ||
9 | + t.references :score | ||
10 | + t.integer :num_points, default: 0 | ||
11 | + t.string :log | ||
12 | + t.datetime :created_at | ||
13 | + end | ||
14 | + end | ||
15 | +end |
1 | +++ a/lib/gamification_plugin.rb | ||
@@ -0,0 +1,42 @@ | @@ -0,0 +1,42 @@ | ||
1 | +class GamificationPlugin < Noosfero::Plugin | ||
2 | + | ||
3 | + def self.plugin_name | ||
4 | + "Gamification Plugin" | ||
5 | + end | ||
6 | + | ||
7 | + def self.plugin_description | ||
8 | + _("Gamification Plugin") | ||
9 | + end | ||
10 | + | ||
11 | + Merit.setup do |config| | ||
12 | + config.checks_on_each_request = false | ||
13 | + config.user_model_name = 'Profile' | ||
14 | + config.current_user_method = 'current_person' | ||
15 | + end | ||
16 | + | ||
17 | + require 'merit_ext' | ||
18 | + | ||
19 | + Merit::Badge.create!( | ||
20 | + id: 1, | ||
21 | + name: "commenter", | ||
22 | + description: "Commenter" | ||
23 | + ) | ||
24 | + Merit::Badge.create!( | ||
25 | + id: 2, | ||
26 | + name: "relevant-commenter", | ||
27 | + description: "Relevant Commenter" | ||
28 | + ) | ||
29 | + Merit::Badge.create!( | ||
30 | + id: 3, | ||
31 | + name: "article-creator", | ||
32 | + description: "Article Creator", | ||
33 | + level: 1 | ||
34 | + ) | ||
35 | + Merit::Badge.create!( | ||
36 | + id: 4, | ||
37 | + name: "article-creator", | ||
38 | + description: "Article Creator", | ||
39 | + level: 2 | ||
40 | + ) | ||
41 | + | ||
42 | +end |
1 | +++ a/lib/merit/badge_rules.rb | ||
@@ -0,0 +1,62 @@ | @@ -0,0 +1,62 @@ | ||
1 | +# Be sure to restart your server when you modify this file. | ||
2 | +# | ||
3 | +# +grant_on+ accepts: | ||
4 | +# * Nothing (always grants) | ||
5 | +# * A block which evaluates to boolean (recieves the object as parameter) | ||
6 | +# * A block with a hash composed of methods to run on the target object with | ||
7 | +# expected values (+votes: 5+ for instance). | ||
8 | +# | ||
9 | +# +grant_on+ can have a +:to+ method name, which called over the target object | ||
10 | +# should retrieve the object to badge (could be +:user+, +:self+, +:follower+, | ||
11 | +# etc). If it's not defined merit will apply the badge to the user who | ||
12 | +# triggered the action (:action_user by default). If it's :itself, it badges | ||
13 | +# the created object (new user for instance). | ||
14 | +# | ||
15 | +# The :temporary option indicates that if the condition doesn't hold but the | ||
16 | +# badge is granted, then it's removed. It's false by default (badges are kept | ||
17 | +# forever). | ||
18 | + | ||
19 | +module Merit | ||
20 | + class BadgeRules | ||
21 | + include Merit::BadgeRulesMethods | ||
22 | + | ||
23 | + def initialize | ||
24 | + | ||
25 | + grant_on 'comment#create', badge: 'commenter' do |comment| | ||
26 | + comment.author.present? && comment.author.comments.count == 5 | ||
27 | + end | ||
28 | + | ||
29 | + grant_on 'article#create', badge: 'article-creator', level: 1 do |article| | ||
30 | + article.author.present? && article.author.articles.count == 5 | ||
31 | + end | ||
32 | + | ||
33 | + grant_on 'article#create', badge: 'article-creator', level: 2 do |article| | ||
34 | + article.author.present? && article.author.articles.count == 10 | ||
35 | + end | ||
36 | + | ||
37 | + grant_on 'vote_plugin_profile#vote', badge: 'relevant-commenter', model_name: 'comment', to: 'author' do |voteable| | ||
38 | + return false if voteable.nil? || !voteable.kind_of?(Comment) | ||
39 | + voteable.votes.count == 2 | ||
40 | + end | ||
41 | + | ||
42 | + # If it has 10 comments, grant commenter-10 badge | ||
43 | + # grant_on 'comments#create', badge: 'commenter', level: 10 do |comment| | ||
44 | + # comment.user.comments.count == 10 | ||
45 | + # end | ||
46 | + | ||
47 | + # If it has 5 votes, grant relevant-commenter badge | ||
48 | + # grant_on 'comments#vote', badge: 'relevant-commenter', | ||
49 | + # to: :user do |comment| | ||
50 | + # | ||
51 | + # comment.votes.count == 5 | ||
52 | + # end | ||
53 | + | ||
54 | + # Changes his name by one wider than 4 chars (arbitrary ruby code case) | ||
55 | + # grant_on 'registrations#update', badge: 'autobiographer', | ||
56 | + # temporary: true, model_name: 'User' do |user| | ||
57 | + # | ||
58 | + # user.name.length > 4 | ||
59 | + # end | ||
60 | + end | ||
61 | + end | ||
62 | +end |
1 | +++ a/lib/merit/point_rules.rb | ||
@@ -0,0 +1,27 @@ | @@ -0,0 +1,27 @@ | ||
1 | +# Be sure to restart your server when you modify this file. | ||
2 | +# | ||
3 | +# Points are a simple integer value which are given to "meritable" resources | ||
4 | +# according to rules in +app/models/merit/point_rules.rb+. They are given on | ||
5 | +# actions-triggered, either to the action user or to the method (or array of | ||
6 | +# methods) defined in the +:to+ option. | ||
7 | +# | ||
8 | +# 'score' method may accept a block which evaluates to boolean | ||
9 | +# (recieves the object as parameter) | ||
10 | + | ||
11 | +module Merit | ||
12 | + class PointRules | ||
13 | + include Merit::PointRulesMethods | ||
14 | + | ||
15 | + def initialize | ||
16 | + score 10, :on => 'comment#create' | ||
17 | + score -10, :on => 'comment#destroy' | ||
18 | + | ||
19 | + score 50, :on => 'article#create' | ||
20 | + score -50, :on => 'article#destroy' | ||
21 | + | ||
22 | + score lambda {|vote| 5 * vote.vote}, :on => 'vote#create', :to => lambda {|vote| vote.voteable.author} | ||
23 | + score lambda {|vote| 5 * vote.vote}, :on => 'vote#create', :to => lambda {|vote| vote.voteable} | ||
24 | + score lambda {|vote| -5 * vote.vote}, :on => 'vote#destroy', :to => lambda {|vote| vote.voteable.author} | ||
25 | + end | ||
26 | + end | ||
27 | +end |
1 | +++ a/lib/merit/rank_rules.rb | ||
@@ -0,0 +1,31 @@ | @@ -0,0 +1,31 @@ | ||
1 | +# Be sure to restart your server when you modify this file. | ||
2 | +# | ||
3 | +# 5 stars is a common ranking use case. They are not given at specified | ||
4 | +# actions like badges, you should define a cron job to test if ranks are to be | ||
5 | +# granted. | ||
6 | +# | ||
7 | +# +set_rank+ accepts: | ||
8 | +# * :+level+ ranking level (greater is better) | ||
9 | +# * :+to+ model or scope to check if new rankings apply | ||
10 | +# * :+level_name+ attribute name (default is empty and results in 'level' | ||
11 | +# attribute, if set it's appended like 'level_#{level_name}') | ||
12 | + | ||
13 | +module Merit | ||
14 | + class RankRules | ||
15 | + include Merit::RankRulesMethods | ||
16 | + | ||
17 | + def initialize | ||
18 | + # set_rank :level => 1, :to => Commiter.active do |commiter| | ||
19 | + # commiter.repositories.count > 1 && commiter.followers >= 10 | ||
20 | + # end | ||
21 | + # | ||
22 | + # set_rank :level => 2, :to => Commiter.active do |commiter| | ||
23 | + # commiter.branches.count > 1 && commiter.followers >= 10 | ||
24 | + # end | ||
25 | + # | ||
26 | + # set_rank :level => 3, :to => Commiter.active do |commiter| | ||
27 | + # commiter.branches.count > 2 && commiter.followers >= 20 | ||
28 | + # end | ||
29 | + end | ||
30 | + end | ||
31 | +end |
1 | +++ a/lib/merit_ext.rb | ||
@@ -0,0 +1,54 @@ | @@ -0,0 +1,54 @@ | ||
1 | +module Merit | ||
2 | + module ControllerExtensions | ||
3 | + | ||
4 | + private | ||
5 | + | ||
6 | + def log_merit_action | ||
7 | + logger.warn('[merit_ext] log_merit_action from controller filter disabled') | ||
8 | + end | ||
9 | + | ||
10 | + end | ||
11 | + | ||
12 | + class TargetFinder | ||
13 | + # Accept proc in rule.to | ||
14 | + def other_target | ||
15 | + rule.to.respond_to?(:call) ? rule.to.call(base_target) : base_target.send(rule.to) | ||
16 | + rescue NoMethodError | ||
17 | + str = "[merit] NoMethodError on `#{base_target.class.name}##{rule.to}`" \ | ||
18 | + ' (called from Merit::TargetFinder#other_target)' | ||
19 | + Rails.logger.warn str | ||
20 | + end | ||
21 | + end | ||
22 | + | ||
23 | + module ClassMethods | ||
24 | + | ||
25 | + def has_merit_actions(options = {}) | ||
26 | + after_create { |obj| obj.new_merit_action(:create, options) } | ||
27 | + before_destroy { |obj| obj.new_merit_action(:destroy, options) } | ||
28 | + end | ||
29 | + | ||
30 | + # change to update_atribute to fix validation | ||
31 | + def _merit_sash_initializer | ||
32 | + define_method(:_sash) do | ||
33 | + sash || reload.sash || update_attribute(:sash, Sash.create) | ||
34 | + sash | ||
35 | + end | ||
36 | + end | ||
37 | + end | ||
38 | + | ||
39 | + def new_merit_action(action, options={}) | ||
40 | + user_method = options[:user_method] | ||
41 | + user = user_method.nil? ? nil : user_method.respond_to?(:call) ? user_method.call(self) : self.send(user_method) | ||
42 | + | ||
43 | + action = Merit::Action.create!({ | ||
44 | + :user_id => user ? user.id : nil, | ||
45 | + :action_method => action, | ||
46 | + :had_errors => self.errors.present?, | ||
47 | + :target_model => self.class.base_class.name.downcase, | ||
48 | + :target_id => self.id, | ||
49 | + :target_data => self.to_yaml | ||
50 | + }) | ||
51 | + action.check_all_rules | ||
52 | + end | ||
53 | + | ||
54 | +end |
1 | +++ a/test/unit/article_test.rb | ||
@@ -0,0 +1,53 @@ | @@ -0,0 +1,53 @@ | ||
1 | +require_relative "../test_helper" | ||
2 | + | ||
3 | +class ArticleTest < ActiveSupport::TestCase | ||
4 | + | ||
5 | + def setup | ||
6 | + @person = create_user('testuser').person | ||
7 | + end | ||
8 | + | ||
9 | + attr_accessor :person | ||
10 | + | ||
11 | + should 'add merit points to author when create a new article' do | ||
12 | + create(Article, :profile_id => person.id, :author => person) | ||
13 | + assert_equal 1, person.score_points.count | ||
14 | + end | ||
15 | + | ||
16 | + should 'subtract merit points to author when destroy an article' do | ||
17 | + article = create(Article, :profile_id => person.id, :author => person) | ||
18 | + assert_equal 1, person.score_points.count | ||
19 | + article.destroy | ||
20 | + assert_equal 2, person.score_points.count | ||
21 | + assert_equal 0, person.points | ||
22 | + end | ||
23 | + | ||
24 | + should 'add merit badge to author when create 5 new articles' do | ||
25 | + 5.times { create(Article, :profile_id => person.id, :author => person) } | ||
26 | + assert_equal 'article-creator', person.badges.first.name | ||
27 | + assert_equal 1, person.badges.first.level | ||
28 | + end | ||
29 | + | ||
30 | + should 'add merit badge level 2 to author when create 10 new articles' do | ||
31 | + 10.times { create(Article, :profile_id => person.id, :author => person) } | ||
32 | + assert_equal ['article-creator'], person.badges.map(&:name).uniq | ||
33 | + assert_equal [1, 2], person.badges.map(&:level) | ||
34 | + end | ||
35 | + | ||
36 | + should 'add merit points to article owner when an user like it' do | ||
37 | + article = create(Article, :name => 'Test', :profile => person, :author => person) | ||
38 | + | ||
39 | + assert_difference 'article.author.points', 5 do | ||
40 | + Vote.create!(:voter => person, :voteable => article, :vote => 1) | ||
41 | + end | ||
42 | + end | ||
43 | + | ||
44 | + should 'add merit points to article when an user like it' do | ||
45 | + article = create(Article, :name => 'Test', :profile => person, :author => person) | ||
46 | + article = article.reload | ||
47 | + | ||
48 | + assert_difference 'article.points', 5 do | ||
49 | + Vote.create!(:voter => person, :voteable => article, :vote => 1) | ||
50 | + end | ||
51 | + end | ||
52 | + | ||
53 | +end |
1 | +++ a/test/unit/comment_test.rb | ||
@@ -0,0 +1,63 @@ | @@ -0,0 +1,63 @@ | ||
1 | +require_relative "../test_helper" | ||
2 | + | ||
3 | +class CommentTest < ActiveSupport::TestCase | ||
4 | + | ||
5 | + def setup | ||
6 | + @person = create_user('testuser').person | ||
7 | + @article = create(TextileArticle, :profile_id => person.id) | ||
8 | + end | ||
9 | + attr_accessor :person, :article | ||
10 | + | ||
11 | + should 'add merit points to author when create a new comment' do | ||
12 | + create(Comment, :source => article, :author_id => person.id) | ||
13 | + assert_equal 1, person.score_points.count | ||
14 | + end | ||
15 | + | ||
16 | + should 'subtract merit points from author when destroy a comment' do | ||
17 | + comment = create(Comment, :source => article, :author_id => person.id) | ||
18 | + assert_equal 1, person.score_points.count | ||
19 | + comment.destroy | ||
20 | + assert_equal 2, person.score_points.count | ||
21 | + assert_equal 0, person.points | ||
22 | + end | ||
23 | + | ||
24 | + should 'add merit badge to author when create 5 new comments' do | ||
25 | + 5.times { create(Comment, :source => article, :author_id => person.id) } | ||
26 | + assert_equal 'commenter', person.badges.first.name | ||
27 | + end | ||
28 | + | ||
29 | + should 'add merit points to comment owner when an user like his comment' do | ||
30 | + comment = create(Comment, :source => article, :author_id => person.id) | ||
31 | + | ||
32 | + assert_difference 'comment.author.points', 5 do | ||
33 | + Vote.create!(:voter => person, :voteable => comment, :vote => 1) | ||
34 | + end | ||
35 | + end | ||
36 | + | ||
37 | + should 'subtract merit points to comment owner when an user unlike his comment' do | ||
38 | + comment = create(Comment, :source => article, :author_id => person.id) | ||
39 | + Vote.create!(:voter => person, :voteable => comment, :vote => 1) | ||
40 | + | ||
41 | + assert_difference 'comment.author.points', -5 do | ||
42 | + Vote.where(:voteable_id => comment.id).destroy_all | ||
43 | + end | ||
44 | + end | ||
45 | + | ||
46 | + should 'subtract merit points from comment owner when an user dislike his comment' do | ||
47 | + comment = create(Comment, :source => article, :author_id => person.id) | ||
48 | + | ||
49 | + assert_difference 'comment.author.points', -5 do | ||
50 | + Vote.create!(:voter => person, :voteable => comment, :vote => -1) | ||
51 | + end | ||
52 | + end | ||
53 | + | ||
54 | + should 'add merit points from comment owner when an user remove a dislike in his comment' do | ||
55 | + comment = create(Comment, :source => article, :author_id => person.id) | ||
56 | + Vote.create!(:voter => person, :voteable => comment, :vote => -1) | ||
57 | + | ||
58 | + assert_difference 'comment.author.points', 5 do | ||
59 | + Vote.where(:voteable_id => comment.id).destroy_all | ||
60 | + end | ||
61 | + end | ||
62 | + | ||
63 | +end |