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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 | ... | ... |