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