Commit c608ace206b3032dbe4b1dcad829bf34ceb4751b

Authored by Victor Costa
0 parents

gamification: setup merit gem and example of point and badge rules

Gemfile 0 → 100644
  1 +++ a/Gemfile
... ... @@ -0,0 +1 @@
  1 +gem 'merit', '~> 2.3.1'
... ...
db/migrate/20150320192316_create_merit_actions.rb 0 → 100644
  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
... ...
db/migrate/20150320192317_create_merit_activity_logs.rb 0 → 100644
  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
... ...
db/migrate/20150320192318_create_sashes.rb 0 → 100644
  1 +++ a/db/migrate/20150320192318_create_sashes.rb
... ... @@ -0,0 +1,7 @@
  1 +class CreateSashes < ActiveRecord::Migration
  2 + def change
  3 + create_table :sashes do |t|
  4 + t.timestamps
  5 + end
  6 + end
  7 +end
... ...
db/migrate/20150320192319_create_badges_sashes.rb 0 → 100644
  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
... ...
db/migrate/20150320192320_create_scores_and_points.rb 0 → 100644
  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
... ...
db/migrate/20150320192325_add_fields_to_profiles.rb 0 → 100644
  1 +++ a/db/migrate/20150320192325_add_fields_to_profiles.rb
... ... @@ -0,0 +1,6 @@
  1 +class AddFieldsToProfiles < ActiveRecord::Migration
  2 + def change
  3 + add_column :profiles, :sash_id, :integer
  4 + add_column :profiles, :level, :integer, :default => 0
  5 + end
  6 +end
... ...
db/migrate/20150324153300_add_fields_to_articles.rb 0 → 100644
  1 +++ a/db/migrate/20150324153300_add_fields_to_articles.rb
... ... @@ -0,0 +1,6 @@
  1 +class AddFieldsToArticles < ActiveRecord::Migration
  2 + def change
  3 + add_column :articles, :sash_id, :integer
  4 + add_column :articles, :level, :integer, :default => 0
  5 + end
  6 +end
... ...
lib/ext/article.rb 0 → 100644
  1 +++ a/lib/ext/article.rb
... ... @@ -0,0 +1,8 @@
  1 +require_dependency 'article'
  2 +
  3 +class Article
  4 +
  5 + has_merit
  6 + has_merit_actions :user_method => :author
  7 +
  8 +end
... ...
lib/ext/comment.rb 0 → 100644
  1 +++ a/lib/ext/comment.rb
... ... @@ -0,0 +1,7 @@
  1 +require_dependency 'comment'
  2 +
  3 +class Comment
  4 +
  5 + has_merit_actions :user_method => :author
  6 +
  7 +end
... ...
lib/ext/person.rb 0 → 100644
  1 +++ a/lib/ext/person.rb
... ... @@ -0,0 +1,8 @@
  1 +require_dependency 'person'
  2 +
  3 +class Person
  4 +
  5 + # TODO why this relationship doesn't exists in core?
  6 + has_many :comments, :foreign_key => 'author_id'
  7 +
  8 +end
... ...
lib/ext/profile.rb 0 → 100644
  1 +++ a/lib/ext/profile.rb
... ... @@ -0,0 +1,7 @@
  1 +require_dependency 'profile'
  2 +
  3 +class Profile
  4 +
  5 + has_merit
  6 +
  7 +end
... ...
lib/ext/vote.rb 0 → 100644
  1 +++ a/lib/ext/vote.rb
... ... @@ -0,0 +1,7 @@
  1 +require_dependency 'models/vote'
  2 +
  3 +class Vote
  4 +
  5 + has_merit_actions
  6 +
  7 +end
... ...
lib/gamification_plugin.rb 0 → 100644
  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
... ...
lib/merit/badge_rules.rb 0 → 100644
  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
... ...
lib/merit/point_rules.rb 0 → 100644
  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
... ...
lib/merit/rank_rules.rb 0 → 100644
  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
... ...
lib/merit_ext.rb 0 → 100644
  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
... ...
test/test_helper.rb 0 → 100644
  1 +++ a/test/test_helper.rb
... ... @@ -0,0 +1 @@
  1 +require_relative "../../../test/test_helper"
... ...
test/unit/article_test.rb 0 → 100644
  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
... ...
test/unit/comment_test.rb 0 → 100644
  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
... ...