From c608ace206b3032dbe4b1dcad829bf34ceb4751b Mon Sep 17 00:00:00 2001 From: Victor Costa Date: Tue, 24 Mar 2015 13:15:46 -0300 Subject: [PATCH] gamification: setup merit gem and example of point and badge rules --- Gemfile | 1 + db/migrate/20150320192316_create_merit_actions.rb | 15 +++++++++++++++ db/migrate/20150320192317_create_merit_activity_logs.rb | 11 +++++++++++ db/migrate/20150320192318_create_sashes.rb | 7 +++++++ db/migrate/20150320192319_create_badges_sashes.rb | 16 ++++++++++++++++ db/migrate/20150320192320_create_scores_and_points.rb | 15 +++++++++++++++ db/migrate/20150320192325_add_fields_to_profiles.rb | 6 ++++++ db/migrate/20150324153300_add_fields_to_articles.rb | 6 ++++++ lib/ext/article.rb | 8 ++++++++ lib/ext/comment.rb | 7 +++++++ lib/ext/person.rb | 8 ++++++++ lib/ext/profile.rb | 7 +++++++ lib/ext/vote.rb | 7 +++++++ lib/gamification_plugin.rb | 42 ++++++++++++++++++++++++++++++++++++++++++ lib/merit/badge_rules.rb | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ lib/merit/point_rules.rb | 27 +++++++++++++++++++++++++++ lib/merit/rank_rules.rb | 31 +++++++++++++++++++++++++++++++ lib/merit_ext.rb | 54 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ test/test_helper.rb | 1 + test/unit/article_test.rb | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ test/unit/comment_test.rb | 63 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 21 files changed, 447 insertions(+), 0 deletions(-) create mode 100644 Gemfile create mode 100644 db/migrate/20150320192316_create_merit_actions.rb create mode 100644 db/migrate/20150320192317_create_merit_activity_logs.rb create mode 100644 db/migrate/20150320192318_create_sashes.rb create mode 100644 db/migrate/20150320192319_create_badges_sashes.rb create mode 100644 db/migrate/20150320192320_create_scores_and_points.rb create mode 100644 db/migrate/20150320192325_add_fields_to_profiles.rb create mode 100644 db/migrate/20150324153300_add_fields_to_articles.rb create mode 100644 lib/ext/article.rb create mode 100644 lib/ext/comment.rb create mode 100644 lib/ext/person.rb create mode 100644 lib/ext/profile.rb create mode 100644 lib/ext/vote.rb create mode 100644 lib/gamification_plugin.rb create mode 100644 lib/merit/badge_rules.rb create mode 100644 lib/merit/point_rules.rb create mode 100644 lib/merit/rank_rules.rb create mode 100644 lib/merit_ext.rb create mode 100644 test/test_helper.rb create mode 100644 test/unit/article_test.rb create mode 100644 test/unit/comment_test.rb diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..706056c --- /dev/null +++ b/Gemfile @@ -0,0 +1 @@ +gem 'merit', '~> 2.3.1' diff --git a/db/migrate/20150320192316_create_merit_actions.rb b/db/migrate/20150320192316_create_merit_actions.rb new file mode 100644 index 0000000..f4677bc --- /dev/null +++ b/db/migrate/20150320192316_create_merit_actions.rb @@ -0,0 +1,15 @@ +class CreateMeritActions < ActiveRecord::Migration + def change + create_table :merit_actions do |t| + t.integer :user_id + t.string :action_method + t.integer :action_value + t.boolean :had_errors, default: false + t.string :target_model + t.integer :target_id + t.text :target_data + t.boolean :processed, default: false + t.timestamps + end + end +end diff --git a/db/migrate/20150320192317_create_merit_activity_logs.rb b/db/migrate/20150320192317_create_merit_activity_logs.rb new file mode 100644 index 0000000..6ee0a0e --- /dev/null +++ b/db/migrate/20150320192317_create_merit_activity_logs.rb @@ -0,0 +1,11 @@ +class CreateMeritActivityLogs < ActiveRecord::Migration + def change + create_table :merit_activity_logs do |t| + t.integer :action_id + t.string :related_change_type + t.integer :related_change_id + t.string :description + t.datetime :created_at + end + end +end diff --git a/db/migrate/20150320192318_create_sashes.rb b/db/migrate/20150320192318_create_sashes.rb new file mode 100644 index 0000000..6d02028 --- /dev/null +++ b/db/migrate/20150320192318_create_sashes.rb @@ -0,0 +1,7 @@ +class CreateSashes < ActiveRecord::Migration + def change + create_table :sashes do |t| + t.timestamps + end + end +end diff --git a/db/migrate/20150320192319_create_badges_sashes.rb b/db/migrate/20150320192319_create_badges_sashes.rb new file mode 100644 index 0000000..db99a73 --- /dev/null +++ b/db/migrate/20150320192319_create_badges_sashes.rb @@ -0,0 +1,16 @@ +class CreateBadgesSashes < ActiveRecord::Migration + def self.up + create_table :badges_sashes do |t| + t.integer :badge_id, :sash_id + t.boolean :notified_user, default: false + t.datetime :created_at + end + add_index :badges_sashes, [:badge_id, :sash_id] + add_index :badges_sashes, :badge_id + add_index :badges_sashes, :sash_id + end + + def self.down + drop_table :badges_sashes + end +end diff --git a/db/migrate/20150320192320_create_scores_and_points.rb b/db/migrate/20150320192320_create_scores_and_points.rb new file mode 100644 index 0000000..901ee31 --- /dev/null +++ b/db/migrate/20150320192320_create_scores_and_points.rb @@ -0,0 +1,15 @@ +class CreateScoresAndPoints < ActiveRecord::Migration + def change + create_table :merit_scores do |t| + t.references :sash + t.string :category, default: 'default' + end + + create_table :merit_score_points do |t| + t.references :score + t.integer :num_points, default: 0 + t.string :log + t.datetime :created_at + end + end +end diff --git a/db/migrate/20150320192325_add_fields_to_profiles.rb b/db/migrate/20150320192325_add_fields_to_profiles.rb new file mode 100644 index 0000000..14792a7 --- /dev/null +++ b/db/migrate/20150320192325_add_fields_to_profiles.rb @@ -0,0 +1,6 @@ +class AddFieldsToProfiles < ActiveRecord::Migration + def change + add_column :profiles, :sash_id, :integer + add_column :profiles, :level, :integer, :default => 0 + end +end diff --git a/db/migrate/20150324153300_add_fields_to_articles.rb b/db/migrate/20150324153300_add_fields_to_articles.rb new file mode 100644 index 0000000..020b5c5 --- /dev/null +++ b/db/migrate/20150324153300_add_fields_to_articles.rb @@ -0,0 +1,6 @@ +class AddFieldsToArticles < ActiveRecord::Migration + def change + add_column :articles, :sash_id, :integer + add_column :articles, :level, :integer, :default => 0 + end +end diff --git a/lib/ext/article.rb b/lib/ext/article.rb new file mode 100644 index 0000000..f7f8d2c --- /dev/null +++ b/lib/ext/article.rb @@ -0,0 +1,8 @@ +require_dependency 'article' + +class Article + + has_merit + has_merit_actions :user_method => :author + +end diff --git a/lib/ext/comment.rb b/lib/ext/comment.rb new file mode 100644 index 0000000..7e59005 --- /dev/null +++ b/lib/ext/comment.rb @@ -0,0 +1,7 @@ +require_dependency 'comment' + +class Comment + + has_merit_actions :user_method => :author + +end diff --git a/lib/ext/person.rb b/lib/ext/person.rb new file mode 100644 index 0000000..dca6fe1 --- /dev/null +++ b/lib/ext/person.rb @@ -0,0 +1,8 @@ +require_dependency 'person' + +class Person + + # TODO why this relationship doesn't exists in core? + has_many :comments, :foreign_key => 'author_id' + +end diff --git a/lib/ext/profile.rb b/lib/ext/profile.rb new file mode 100644 index 0000000..51a43b2 --- /dev/null +++ b/lib/ext/profile.rb @@ -0,0 +1,7 @@ +require_dependency 'profile' + +class Profile + + has_merit + +end diff --git a/lib/ext/vote.rb b/lib/ext/vote.rb new file mode 100644 index 0000000..2711ccc --- /dev/null +++ b/lib/ext/vote.rb @@ -0,0 +1,7 @@ +require_dependency 'models/vote' + +class Vote + + has_merit_actions + +end diff --git a/lib/gamification_plugin.rb b/lib/gamification_plugin.rb new file mode 100644 index 0000000..2e7d3fd --- /dev/null +++ b/lib/gamification_plugin.rb @@ -0,0 +1,42 @@ +class GamificationPlugin < Noosfero::Plugin + + def self.plugin_name + "Gamification Plugin" + end + + def self.plugin_description + _("Gamification Plugin") + end + + Merit.setup do |config| + config.checks_on_each_request = false + config.user_model_name = 'Profile' + config.current_user_method = 'current_person' + end + + require 'merit_ext' + + Merit::Badge.create!( + id: 1, + name: "commenter", + description: "Commenter" + ) + Merit::Badge.create!( + id: 2, + name: "relevant-commenter", + description: "Relevant Commenter" + ) + Merit::Badge.create!( + id: 3, + name: "article-creator", + description: "Article Creator", + level: 1 + ) + Merit::Badge.create!( + id: 4, + name: "article-creator", + description: "Article Creator", + level: 2 + ) + +end diff --git a/lib/merit/badge_rules.rb b/lib/merit/badge_rules.rb new file mode 100644 index 0000000..caf47bc --- /dev/null +++ b/lib/merit/badge_rules.rb @@ -0,0 +1,62 @@ +# Be sure to restart your server when you modify this file. +# +# +grant_on+ accepts: +# * Nothing (always grants) +# * A block which evaluates to boolean (recieves the object as parameter) +# * A block with a hash composed of methods to run on the target object with +# expected values (+votes: 5+ for instance). +# +# +grant_on+ can have a +:to+ method name, which called over the target object +# should retrieve the object to badge (could be +:user+, +:self+, +:follower+, +# etc). If it's not defined merit will apply the badge to the user who +# triggered the action (:action_user by default). If it's :itself, it badges +# the created object (new user for instance). +# +# The :temporary option indicates that if the condition doesn't hold but the +# badge is granted, then it's removed. It's false by default (badges are kept +# forever). + +module Merit + class BadgeRules + include Merit::BadgeRulesMethods + + def initialize + + grant_on 'comment#create', badge: 'commenter' do |comment| + comment.author.present? && comment.author.comments.count == 5 + end + + grant_on 'article#create', badge: 'article-creator', level: 1 do |article| + article.author.present? && article.author.articles.count == 5 + end + + grant_on 'article#create', badge: 'article-creator', level: 2 do |article| + article.author.present? && article.author.articles.count == 10 + end + + grant_on 'vote_plugin_profile#vote', badge: 'relevant-commenter', model_name: 'comment', to: 'author' do |voteable| + return false if voteable.nil? || !voteable.kind_of?(Comment) + voteable.votes.count == 2 + end + + # If it has 10 comments, grant commenter-10 badge + # grant_on 'comments#create', badge: 'commenter', level: 10 do |comment| + # comment.user.comments.count == 10 + # end + + # If it has 5 votes, grant relevant-commenter badge + # grant_on 'comments#vote', badge: 'relevant-commenter', + # to: :user do |comment| + # + # comment.votes.count == 5 + # end + + # Changes his name by one wider than 4 chars (arbitrary ruby code case) + # grant_on 'registrations#update', badge: 'autobiographer', + # temporary: true, model_name: 'User' do |user| + # + # user.name.length > 4 + # end + end + end +end diff --git a/lib/merit/point_rules.rb b/lib/merit/point_rules.rb new file mode 100644 index 0000000..f6503c0 --- /dev/null +++ b/lib/merit/point_rules.rb @@ -0,0 +1,27 @@ +# Be sure to restart your server when you modify this file. +# +# Points are a simple integer value which are given to "meritable" resources +# according to rules in +app/models/merit/point_rules.rb+. They are given on +# actions-triggered, either to the action user or to the method (or array of +# methods) defined in the +:to+ option. +# +# 'score' method may accept a block which evaluates to boolean +# (recieves the object as parameter) + +module Merit + class PointRules + include Merit::PointRulesMethods + + def initialize + score 10, :on => 'comment#create' + score -10, :on => 'comment#destroy' + + score 50, :on => 'article#create' + score -50, :on => 'article#destroy' + + score lambda {|vote| 5 * vote.vote}, :on => 'vote#create', :to => lambda {|vote| vote.voteable.author} + score lambda {|vote| 5 * vote.vote}, :on => 'vote#create', :to => lambda {|vote| vote.voteable} + score lambda {|vote| -5 * vote.vote}, :on => 'vote#destroy', :to => lambda {|vote| vote.voteable.author} + end + end +end diff --git a/lib/merit/rank_rules.rb b/lib/merit/rank_rules.rb new file mode 100644 index 0000000..344237b --- /dev/null +++ b/lib/merit/rank_rules.rb @@ -0,0 +1,31 @@ +# Be sure to restart your server when you modify this file. +# +# 5 stars is a common ranking use case. They are not given at specified +# actions like badges, you should define a cron job to test if ranks are to be +# granted. +# +# +set_rank+ accepts: +# * :+level+ ranking level (greater is better) +# * :+to+ model or scope to check if new rankings apply +# * :+level_name+ attribute name (default is empty and results in 'level' +# attribute, if set it's appended like 'level_#{level_name}') + +module Merit + class RankRules + include Merit::RankRulesMethods + + def initialize + # set_rank :level => 1, :to => Commiter.active do |commiter| + # commiter.repositories.count > 1 && commiter.followers >= 10 + # end + # + # set_rank :level => 2, :to => Commiter.active do |commiter| + # commiter.branches.count > 1 && commiter.followers >= 10 + # end + # + # set_rank :level => 3, :to => Commiter.active do |commiter| + # commiter.branches.count > 2 && commiter.followers >= 20 + # end + end + end +end diff --git a/lib/merit_ext.rb b/lib/merit_ext.rb new file mode 100644 index 0000000..0a58fd7 --- /dev/null +++ b/lib/merit_ext.rb @@ -0,0 +1,54 @@ +module Merit + module ControllerExtensions + + private + + def log_merit_action + logger.warn('[merit_ext] log_merit_action from controller filter disabled') + end + + end + + class TargetFinder + # Accept proc in rule.to + def other_target + rule.to.respond_to?(:call) ? rule.to.call(base_target) : base_target.send(rule.to) + rescue NoMethodError + str = "[merit] NoMethodError on `#{base_target.class.name}##{rule.to}`" \ + ' (called from Merit::TargetFinder#other_target)' + Rails.logger.warn str + end + end + + module ClassMethods + + def has_merit_actions(options = {}) + after_create { |obj| obj.new_merit_action(:create, options) } + before_destroy { |obj| obj.new_merit_action(:destroy, options) } + end + + # change to update_atribute to fix validation + def _merit_sash_initializer + define_method(:_sash) do + sash || reload.sash || update_attribute(:sash, Sash.create) + sash + end + end + end + + def new_merit_action(action, options={}) + user_method = options[:user_method] + user = user_method.nil? ? nil : user_method.respond_to?(:call) ? user_method.call(self) : self.send(user_method) + + action = Merit::Action.create!({ + :user_id => user ? user.id : nil, + :action_method => action, + :had_errors => self.errors.present?, + :target_model => self.class.base_class.name.downcase, + :target_id => self.id, + :target_data => self.to_yaml + }) + action.check_all_rules + end + +end diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..6affe9f --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1 @@ +require_relative "../../../test/test_helper" diff --git a/test/unit/article_test.rb b/test/unit/article_test.rb new file mode 100644 index 0000000..0731a86 --- /dev/null +++ b/test/unit/article_test.rb @@ -0,0 +1,53 @@ +require_relative "../test_helper" + +class ArticleTest < ActiveSupport::TestCase + + def setup + @person = create_user('testuser').person + end + + attr_accessor :person + + should 'add merit points to author when create a new article' do + create(Article, :profile_id => person.id, :author => person) + assert_equal 1, person.score_points.count + end + + should 'subtract merit points to author when destroy an article' do + article = create(Article, :profile_id => person.id, :author => person) + assert_equal 1, person.score_points.count + article.destroy + assert_equal 2, person.score_points.count + assert_equal 0, person.points + end + + should 'add merit badge to author when create 5 new articles' do + 5.times { create(Article, :profile_id => person.id, :author => person) } + assert_equal 'article-creator', person.badges.first.name + assert_equal 1, person.badges.first.level + end + + should 'add merit badge level 2 to author when create 10 new articles' do + 10.times { create(Article, :profile_id => person.id, :author => person) } + assert_equal ['article-creator'], person.badges.map(&:name).uniq + assert_equal [1, 2], person.badges.map(&:level) + end + + should 'add merit points to article owner when an user like it' do + article = create(Article, :name => 'Test', :profile => person, :author => person) + + assert_difference 'article.author.points', 5 do + Vote.create!(:voter => person, :voteable => article, :vote => 1) + end + end + + should 'add merit points to article when an user like it' do + article = create(Article, :name => 'Test', :profile => person, :author => person) + article = article.reload + + assert_difference 'article.points', 5 do + Vote.create!(:voter => person, :voteable => article, :vote => 1) + end + end + +end diff --git a/test/unit/comment_test.rb b/test/unit/comment_test.rb new file mode 100644 index 0000000..9f8af02 --- /dev/null +++ b/test/unit/comment_test.rb @@ -0,0 +1,63 @@ +require_relative "../test_helper" + +class CommentTest < ActiveSupport::TestCase + + def setup + @person = create_user('testuser').person + @article = create(TextileArticle, :profile_id => person.id) + end + attr_accessor :person, :article + + should 'add merit points to author when create a new comment' do + create(Comment, :source => article, :author_id => person.id) + assert_equal 1, person.score_points.count + end + + should 'subtract merit points from author when destroy a comment' do + comment = create(Comment, :source => article, :author_id => person.id) + assert_equal 1, person.score_points.count + comment.destroy + assert_equal 2, person.score_points.count + assert_equal 0, person.points + end + + should 'add merit badge to author when create 5 new comments' do + 5.times { create(Comment, :source => article, :author_id => person.id) } + assert_equal 'commenter', person.badges.first.name + end + + should 'add merit points to comment owner when an user like his comment' do + comment = create(Comment, :source => article, :author_id => person.id) + + assert_difference 'comment.author.points', 5 do + Vote.create!(:voter => person, :voteable => comment, :vote => 1) + end + end + + should 'subtract merit points to comment owner when an user unlike his comment' do + comment = create(Comment, :source => article, :author_id => person.id) + Vote.create!(:voter => person, :voteable => comment, :vote => 1) + + assert_difference 'comment.author.points', -5 do + Vote.where(:voteable_id => comment.id).destroy_all + end + end + + should 'subtract merit points from comment owner when an user dislike his comment' do + comment = create(Comment, :source => article, :author_id => person.id) + + assert_difference 'comment.author.points', -5 do + Vote.create!(:voter => person, :voteable => comment, :vote => -1) + end + end + + should 'add merit points from comment owner when an user remove a dislike in his comment' do + comment = create(Comment, :source => article, :author_id => person.id) + Vote.create!(:voter => person, :voteable => comment, :vote => -1) + + assert_difference 'comment.author.points', 5 do + Vote.where(:voteable_id => comment.id).destroy_all + end + end + +end -- libgit2 0.21.2