merit 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ gem "rails", "3.0.10"
4
+ gem "sqlite3"
5
+ gem "haml"
6
+ gem "capybara"
@@ -0,0 +1,21 @@
1
+ == MIT License
2
+
3
+ Copyright (c) 2011 [Tute Costa - tutecosta@gmail.com]
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,127 @@
1
+ = Merit Rails Gem
2
+
3
+ Define reputation for users and data on your application.
4
+
5
+ http://i567.photobucket.com/albums/ss118/DeuceBigglebags/th_nspot26_300.jpg
6
+
7
+
8
+ = Installation
9
+
10
+ 1. Add 'merit' to your Gemfile
11
+ 2. Run +rails+ +g+ +merit+:+install+
12
+ 3. Run +rails+ +g+ +merit+ +MODEL_NAME+
13
+ 4. Run +rake+ +db+:+migrate+
14
+ 5. Configure reputation rules for your application
15
+
16
+
17
+ = Defining badge rules
18
+
19
+ You may give badges to any resource on your application if some condition
20
+ holds. Badges may have levels, and may be temporary.
21
+
22
+ Define rules on +app/models/merit_badge_rules.rb+:
23
+
24
+ +grant_on+ accepts:
25
+ * +controller+#+action+ string (similar to Rails routes)
26
+ * :+badge+ for badge name
27
+ * :+level+ for badge level
28
+ * :+to+: method name over target_object which obtains user to badge.
29
+ * :+temporary+ (boolean): if the condition doesn't hold and the receiver had
30
+ the badge, it gets removed. +false+ by default (badges are kept forever).
31
+ * &+block+
32
+ * empty (always grants)
33
+ * a block which evaluates to boolean (recieves target object as parameter)
34
+ * a block with a hash composed of methods to run on the target object with
35
+ expected values
36
+
37
+ == Examples
38
+
39
+ grant_on 'comments#vote', :badge => 'relevant-commenter', :to => :user do
40
+ { :votes => 5 }
41
+ end
42
+
43
+ grant_on ['users#create', 'users#update'], :badge => 'autobiographer', :temporary => true do |user|
44
+ user.name.present? && user.address.present?
45
+ end
46
+
47
+
48
+ = Defining point rules
49
+
50
+ Points are a simple integer value which are given to "meritable" resources.
51
+ They are given on actions-triggered, either to the action user or to the
52
+ method (or array of methods) defined in the +:to+ option.
53
+
54
+ Define rules on +app/models/merit_point_rules.rb+:
55
+
56
+ == Examples
57
+
58
+ score 10, :on => [
59
+ 'users#update'
60
+ ]
61
+
62
+ score 15, :on => 'reviews#create', :to => [:reviewer, :reviewed]
63
+
64
+ score 20, :on => [
65
+ 'comments#create',
66
+ 'photos#create'
67
+ ]
68
+
69
+
70
+ = Defining rank rules
71
+
72
+ Rankings are very similar to badges. They give "badges" which have a hierarchy
73
+ defined by +level+'s lexicografical order (greater is better). If a rank is
74
+ granted, lower level ranks are taken off. 5 stars is a common ranking use
75
+ case.
76
+
77
+ They are not given at specified actions like badges, you should define a cron
78
+ job to test if ranks are to be granted.
79
+
80
+ Define rules on +app/models/merit_rank_rules.rb+:
81
+
82
+ +set_rank+ accepts:
83
+ * +badge_name+ name of this ranking
84
+ * :+level+ ranking level (greater is better)
85
+ * :+to+ model or scope to check if new rankings apply
86
+
87
+ Check for rules on a rake task executed in background like:
88
+ task :cron => :environment do
89
+ MeritRankRules.new.check_rank_rules
90
+ end
91
+
92
+
93
+ == Examples
94
+
95
+ set_rank :stars, :level => 2, :to => Commiter.active do |commiter|
96
+ commiter.branches > 1 && commiter.followers >= 10
97
+ end
98
+
99
+ set_rank :stars, :level => 3, :to => Commiter.active do |commiter|
100
+ commiter.branches > 2 && commiter.followers >= 20
101
+ end
102
+
103
+
104
+ = Grant manually
105
+
106
+ You may also add badges/rank "by hand" from controller actions:
107
+ Badge.find(3).grant_to(current_user)
108
+
109
+
110
+ = Test application
111
+
112
+ To run the test application inside this gem follow:
113
+ 1. cd test/dummy
114
+ 2. rails g merit:install
115
+ 3. rails g merit user
116
+ 4. rake db:migrate ; rake db:seed
117
+ 5. rails s
118
+
119
+
120
+ = To-do list
121
+
122
+ * Test points granting with different options.
123
+ * Ranking should not be badges, so .badges doesn't return them (2-stars shouldn't be badge).
124
+ * grep -r 'FIXME\|TODO' .
125
+ * :value parameter (for star voting for example) should be configurable (depends
126
+ on params[:value] on the controller).
127
+ * Make fixtures for integration testing (now creating objects on test file!).
@@ -0,0 +1,29 @@
1
+ # encoding: UTF-8
2
+ require 'rubygems'
3
+ begin
4
+ require 'bundler/setup'
5
+ rescue LoadError
6
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
7
+ end
8
+
9
+ require 'rake'
10
+ require 'rake/rdoctask'
11
+
12
+ require 'rake/testtask'
13
+
14
+ Rake::TestTask.new(:test) do |t|
15
+ t.libs << 'lib'
16
+ t.libs << 'test'
17
+ t.pattern = 'test/**/*_test.rb'
18
+ t.verbose = false
19
+ end
20
+
21
+ task :default => :test
22
+
23
+ Rake::RDocTask.new(:rdoc) do |rdoc|
24
+ rdoc.rdoc_dir = 'rdoc'
25
+ rdoc.title = 'Merit'
26
+ rdoc.options << '--line-numbers' << '--inline-source'
27
+ rdoc.rdoc_files.include('README.rdoc')
28
+ rdoc.rdoc_files.include('lib/**/*.rb')
29
+ end
@@ -0,0 +1,39 @@
1
+ class Badge < ActiveRecord::Base
2
+ has_many :badges_sashes
3
+ has_many :sashes, :through => :badges_sashes
4
+
5
+ # Grant badge to sash
6
+ def grant_to(object_or_sash)
7
+ object_or_sash.create_sash_if_none unless object_or_sash.kind_of?(Sash)
8
+ sash = object_or_sash.respond_to?(:sash) ? object_or_sash.sash : object_or_sash
9
+ unless sash.badges.include? self
10
+ sash.badges << self
11
+ sash.save
12
+ end
13
+ end
14
+
15
+ # Take out badge from sash
16
+ def delete_from(object_or_sash)
17
+ object_or_sash.create_sash_if_none unless object_or_sash.kind_of?(Sash)
18
+ sash = object_or_sash.respond_to?(:sash) ? object_or_sash.sash : object_or_sash
19
+ if sash.badges.include? self
20
+ sash.badges -= [self]
21
+ sash.save
22
+ end
23
+ end
24
+
25
+ # Give rank to sash if it's greater. Delete lower ranks it may have.
26
+ def grant_rank_to(sash)
27
+ # Grant to sash if had lower rank. Do nothing if has same or greater rank.
28
+ if sash.has_lower_rank_than(self)
29
+ sash.badges -= Badge.where(:name => name) # Clean up old ranks
30
+ sash.badges << self
31
+ sash.save
32
+ end
33
+ end
34
+
35
+ def self.latest(limit = nil)
36
+ scope = order('created_at DESC')
37
+ limit.present? ? scope.limit(limit) : scope
38
+ end
39
+ end
@@ -0,0 +1,14 @@
1
+ class BadgesSash < ActiveRecord::Base
2
+ belongs_to :badge
3
+ belongs_to :sash
4
+
5
+ # TODO: Better way to do it? With composite keys ARel complained:
6
+ # NoMethodError: undefined method `eq' for nil:NilClass
7
+ # from ~/.rvm/gems/ruby-1.9.2-p0/gems/activesupport-3.0.9/lib/active_support/whiny_nil.rb:48:in `method_missing'
8
+ # from ~/.rvm/gems/ruby-1.9.2-p0/gems/activerecord-3.0.9/lib/active_record/persistence.rb:259:in `update'
9
+ def set_notified!(badge, sash)
10
+ ActiveRecord::Base.connection.execute("UPDATE badges_sashes
11
+ SET notified_user = TRUE
12
+ WHERE badge_id = #{badge.id} AND sash_id = #{sash.id}")
13
+ end
14
+ end
@@ -0,0 +1,48 @@
1
+ class MeritAction < ActiveRecord::Base
2
+ # Check rules defined for a merit_action
3
+ def check_badge_rules(defined_rules)
4
+ action_name = "#{target_model}\##{action_method}"
5
+
6
+ # Check Badge rules
7
+ if defined_rules[action_name].present?
8
+ defined_rules[action_name].each do |rule|
9
+ rule.grant_or_delete_badge(self)
10
+ end
11
+ end
12
+
13
+ # Check Point rules
14
+ actions_to_point = MeritPointRules.new.actions_to_point
15
+ if actions_to_point[action_name].present?
16
+ point_rule = actions_to_point[action_name]
17
+ point_rule[:to].each do |to|
18
+ if to == :action_user
19
+ if !(target = User.find_by_id(user_id))
20
+ Rails.logger.warn "[merit] no user found to grant points"
21
+ return
22
+ end
23
+ else
24
+ begin
25
+ target = target_object.send(to)
26
+ rescue
27
+ Rails.logger.warn "[merit] No target_object found on check_badge_rules."
28
+ return
29
+ end
30
+ end
31
+ target.update_attribute(:points, target.points + point_rule[:score])
32
+ end
33
+ end
34
+
35
+ processed!
36
+ end
37
+
38
+ # Action's target object
39
+ def target_object
40
+ klass = target_model.singularize.camelize.constantize
41
+ klass.find_by_id(target_id)
42
+ end
43
+
44
+ # Mark merit_action as processed
45
+ def processed!
46
+ self.update_attribute(:processed, true)
47
+ end
48
+ end
@@ -0,0 +1,19 @@
1
+ class Sash < ActiveRecord::Base
2
+ has_many :badges_sashes
3
+ has_many :badges, :through => :badges_sashes
4
+
5
+ # Latest badges granted by Merit
6
+ def self.latest_badges(limit = 10)
7
+ select('DISTINCT sashes.id, sashes.*').joins(:badges_sashes).order('badges_sashes.created_at DESC').limit(limit)
8
+ end
9
+
10
+ # Decides if sash has lower rank than a given badge
11
+ def has_lower_rank_than(badge)
12
+ levels(badge.name).all_lower_than badge.level
13
+ end
14
+
15
+ # Collect Sash levels given a badge name
16
+ def levels(badge_name)
17
+ badges.where(:name => badge_name).collect(&:level)
18
+ end
19
+ end
@@ -0,0 +1,27 @@
1
+ require 'rails/generators/active_record'
2
+
3
+ module ActiveRecord
4
+ module Generators
5
+ class MeritGenerator < ActiveRecord::Generators::Base
6
+ argument :attributes, :type => :array, :default => [], :banner => "field:type field:type"
7
+
8
+ source_root File.expand_path("../templates", __FILE__)
9
+
10
+ def model_exists?
11
+ File.exists?(File.join(destination_root, model_path))
12
+ end
13
+
14
+ def model_path
15
+ @model_path ||= File.join("app", "models", "#{file_path}.rb")
16
+ end
17
+
18
+ def copy_merit_migration
19
+ migration_template "add_fields_to_model.rb", "db/migrate/add_fields_to_#{table_name}"
20
+ end
21
+
22
+ def inject_merit_content
23
+ inject_into_class(model_path, class_name, " has_merit\n\n") if model_exists?
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,13 @@
1
+ class AddFieldsTo<%= table_name.camelize %> < ActiveRecord::Migration
2
+ def self.up
3
+ add_column :<%= table_name %>, :sash_id, :integer
4
+ add_column :<%= table_name %>, :points, :integer, :default => 0
5
+ <%- resource = table_name.singularize -%>
6
+ <%= resource.camelize %>.all.each{|<%= resource %>| <%= resource %>.update_attribute(:points, 0) } # Update existing entries
7
+ end
8
+
9
+ def self.down
10
+ remove_column :<%= table_name %>, :sash_id
11
+ remove_column :<%= table_name %>, :points
12
+ end
13
+ end
@@ -0,0 +1,31 @@
1
+ require 'rails/generators/migration'
2
+
3
+ module Merit
4
+ module Generators
5
+ class InstallGenerator < ::Rails::Generators::Base
6
+ include Rails::Generators::Migration
7
+ source_root File.expand_path('../templates', __FILE__)
8
+ desc "add the migrations"
9
+
10
+ def self.next_migration_number(path)
11
+ unless @prev_migration_nr
12
+ @prev_migration_nr = Time.now.utc.strftime("%Y%m%d%H%M%S").to_i
13
+ else
14
+ @prev_migration_nr += 1
15
+ end
16
+ @prev_migration_nr.to_s
17
+ end
18
+
19
+ def copy_migrations_and_model
20
+ migration_template 'create_merit_actions.rb', 'db/migrate/create_merit_actions.rb'
21
+ migration_template 'create_badges.rb', 'db/migrate/create_badges.rb'
22
+ migration_template 'create_sashes.rb', 'db/migrate/create_sashes.rb'
23
+ migration_template 'create_badges_sashes.rb', 'db/migrate/create_badges_sashes.rb'
24
+ template 'merit_badge_rules.rb', 'app/models/merit_badge_rules.rb'
25
+ template 'merit_point_rules.rb', 'app/models/merit_point_rules.rb'
26
+ template 'merit_rank_rules.rb', 'app/models/merit_rank_rules.rb'
27
+ template 'merit.rb', 'config/initializers/merit.rb'
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,8 @@
1
+ module Merit
2
+ module Generators
3
+ class MeritGenerator < Rails::Generators::NamedBase
4
+ source_root File.expand_path("../templates", __FILE__)
5
+ hook_for :orm
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,19 @@
1
+ class CreateBadges < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :badges do |t|
4
+ t.string :name
5
+ t.string :level
6
+ t.string :image
7
+ t.string :description
8
+ t.timestamps
9
+ end
10
+
11
+ # First badges:
12
+ # Badge.create( :name => 'just-registered' )
13
+ # Badge.create( :name => 'creator', :level => 'inspired', :image => 'http://upload.wikimedia.org/wikipedia/commons/9/94/Luca_prodan.jpg' )
14
+ end
15
+
16
+ def self.down
17
+ drop_table :badges
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ class CreateBadgesSashes < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :badges_sashes, :id => false 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
@@ -0,0 +1,17 @@
1
+ class CreateMeritActions < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :merit_actions do |t|
4
+ t.integer :user_id # source
5
+ t.string :action_method
6
+ t.integer :action_value
7
+ t.string :target_model
8
+ t.integer :target_id
9
+ t.boolean :processed, :default => false
10
+ t.timestamps
11
+ end
12
+ end
13
+
14
+ def self.down
15
+ drop_table :merit_actions
16
+ end
17
+ end
@@ -0,0 +1,11 @@
1
+ class CreateSashes < ActiveRecord::Migration
2
+ def self.up
3
+ create_table :sashes do |t|
4
+ t.timestamps
5
+ end
6
+ end
7
+
8
+ def self.down
9
+ drop_table :sashes
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ # Use this hook to configure merit parameters
2
+ Merit.setup do |config|
3
+ # Check rules on each request or in background
4
+ # config.checks_on_each_request = true
5
+ end
@@ -0,0 +1,40 @@
1
+ # +grant_on+ accepts:
2
+ # * Nothing (always grants)
3
+ # * A block which evaluates to boolean (recieves the object as parameter)
4
+ # * A block with a hash composed of methods to run on the target object with
5
+ # expected values (+:votes => 5+ for instance).
6
+ #
7
+ # +grant_on+ can have a +:to+ method name, which called over the target object
8
+ # should retrieve the object to badge (could be +:user+, +:self+, +:follower+,
9
+ # etc). If it's not defined merit will apply the badge to the user who
10
+ # triggered the action (:action_user by default). If it's :itself, it badges
11
+ # the created object (new user for instance).
12
+ #
13
+ # The :temporary option indicates that if the condition doesn't hold but the
14
+ # badge is granted, then it's removed. It's false by default (badges are kept
15
+ # forever).
16
+
17
+ class MeritBadgeRules
18
+ include Merit::BadgeRules
19
+
20
+ def initialize
21
+ # If it creates user, grant badge
22
+ # Should be "current_user" after registration for badge to be granted.
23
+ # grant_on 'users#create', :badge => 'just-registered', :to => :itself
24
+
25
+ # If it has 10 comments, grant commenter-10 badge
26
+ # grant_on 'comments#create', :badge => 'commenter', :level => 10 do
27
+ # { :user => { :comments => { :count => 10 } } }
28
+ # end
29
+
30
+ # If it has 5 votes, grant relevant-commenter badge
31
+ # grant_on 'comments#vote', :badge => 'relevant-commenter', :to => :user do
32
+ # { :votes => 5 }
33
+ # end
34
+
35
+ # Changes his name by one wider than 4 chars (arbitrary ruby code case)
36
+ # grant_on 'users#update', :badge => 'autobiographer', :temporary => true do |user|
37
+ # user.name.length > 4
38
+ # end
39
+ end
40
+ end
@@ -0,0 +1,21 @@
1
+ # Points are a simple integer value which are given to "meritable" resources
2
+ # according to rules in +app/models/merit_point_rules.rb+. They are given on
3
+ # actions-triggered, either to the action user or to the method (or array of
4
+ # methods) defined in the +:to+ option.
5
+
6
+ class MeritPointRules
7
+ include Merit::PointRules
8
+
9
+ def initialize
10
+ # score 10, :on => [
11
+ # 'users#update'
12
+ # ]
13
+ #
14
+ # score 15, :on => 'reviews#create', :to => [:reviewer, :reviewed]
15
+ #
16
+ # score 20, :on => [
17
+ # 'comments#create',
18
+ # 'photos#create'
19
+ # ]
20
+ end
21
+ end
@@ -0,0 +1,30 @@
1
+ # Rankings are very similar to badges. They give "badges" which have a hierarchy
2
+ # defined by +level+'s lexicografical order (greater is better). If a rank is
3
+ # granted, lower level ranks are taken off. 5 stars is a common ranking use
4
+ # case.
5
+ #
6
+ # They are not given at specified actions like badges, you should define a cron
7
+ # job to test if ranks are to be granted.
8
+ #
9
+ # +set_rank+ accepts:
10
+ # * +badge_name+ name of this ranking
11
+ # * :+level+ ranking level (greater is better)
12
+ # * :+to+ model or scope to check if new rankings apply
13
+
14
+ class MeritRankRules
15
+ include Merit::RankRules
16
+
17
+ def initialize
18
+ # set_rank :stars, :level => 1, :to => Commiter.active do |commiter|
19
+ # commiter.repositories.count > 1 && commiter.followers >= 10
20
+ # end
21
+ #
22
+ # set_rank :stars, :level => 2, :to => Commiter.active do |commiter|
23
+ # commiter.branches.count > 1 && commiter.followers >= 10
24
+ # end
25
+ #
26
+ # set_rank :stars, :level => 3, :to => Commiter.active do |commiter|
27
+ # commiter.branches.count > 2 && commiter.followers >= 20
28
+ # end
29
+ end
30
+ end
@@ -0,0 +1,26 @@
1
+ require 'merit/core_extensions'
2
+ require 'merit/rule'
3
+ require 'merit/rules_badge'
4
+ require 'merit/rules_points'
5
+ require 'merit/rules_rank'
6
+ require 'merit/controller_extensions'
7
+ require 'merit/model_additions'
8
+
9
+ module Merit
10
+ # Check rules on each request
11
+ mattr_accessor :checks_on_each_request
12
+ @@checks_on_each_request = true
13
+
14
+ # Load configuration from initializer
15
+ def self.setup
16
+ yield self
17
+ end
18
+
19
+ class Engine < Rails::Engine
20
+ initializer 'merit.controller' do |app|
21
+ ActiveSupport.on_load(:action_controller) do
22
+ include Merit::ControllerExtensions
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,36 @@
1
+ module Merit
2
+ # This module sets up an after_filter to update merit_actions table.
3
+ # It executes on every action, and checks rules only if
4
+ # 'controller_name#action_name' has defined badge or point rules
5
+ module ControllerExtensions
6
+ def self.included(base)
7
+ base.after_filter do |controller|
8
+ action = "#{controller_name}\##{action_name}"
9
+ badge_rules = ::MeritBadgeRules.new
10
+ point_rules = ::MeritPointRules.new
11
+ if badge_rules.defined_rules[action].present? || point_rules.actions_to_point[action].present?
12
+ target_id = params[:id]
13
+ # TODO: target_object should be configurable (now it's singularized controller name)
14
+ unless target_id =~ /^[0-9]+$/ # id nil, or string (friendly_id)?
15
+ target_id = instance_variable_get(:"@#{controller_name.singularize}").try(:id)
16
+ end
17
+
18
+ # TODO: value relies on params[:value] on the controller, should be configurable
19
+ value = params[:value]
20
+ MeritAction.create(
21
+ :user_id => current_user.try(:id),
22
+ :action_method => action_name,
23
+ :action_value => value,
24
+ :target_model => controller_name,
25
+ :target_id => target_id
26
+ )
27
+
28
+ # Check rules in after_filter?
29
+ if Merit.checks_on_each_request
30
+ ::MeritBadgeRules.new.check_new_actions
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,28 @@
1
+ # Hash core extensions:
2
+ # * conditions_apply?(obj)
3
+ class Hash
4
+ # Methods over object (applied recursively) respond what's expected?
5
+ # Example (evaluates to true):
6
+ # { :first => { :odd? => true }, :count => 2 }.conditions_apply? [1,3]
7
+ def conditions_apply?(obj)
8
+ applies = true
9
+ self.each do |method, value|
10
+ called_obj = obj.send(method)
11
+ if value.kind_of?(Hash)
12
+ applies = applies && value.conditions_apply?(called_obj)
13
+ else
14
+ applies = applies && called_obj == value
15
+ end
16
+ end
17
+ applies
18
+ end
19
+ end
20
+
21
+ # Array core extensions:
22
+ # * all_lower_than(value)
23
+ class Array
24
+ # All array values are lower than parameter
25
+ def all_lower_than(value)
26
+ self.select{|elem| elem >= value }.empty?
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ module Merit
2
+ def self.included(base)
3
+ base.send :extend, ClassMethods
4
+ end
5
+
6
+ module ClassMethods
7
+ def has_merit(options = {})
8
+ belongs_to :sash
9
+ send :include, InstanceMethods
10
+ end
11
+ end
12
+
13
+ module InstanceMethods
14
+ # Return it's sash badges
15
+ def badges
16
+ create_sash_if_none
17
+ sash.badges
18
+ end
19
+
20
+ # Create sash if doesn't have
21
+ def create_sash_if_none
22
+ if sash.nil?
23
+ self.sash = Sash.new
24
+ self.save(:validate => false)
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ ActiveRecord::Base.send :include, Merit
@@ -0,0 +1,7 @@
1
+ require 'merit'
2
+ require 'rails'
3
+
4
+ module Merit
5
+ class Engine < Rails::Engine
6
+ end
7
+ end
@@ -0,0 +1,82 @@
1
+ module Merit
2
+ # Rules has a badge name and level, a target to badge, a conditions block
3
+ # and a temporary option.
4
+ class Rule
5
+ attr_accessor :badge_name, :level, :to, :temporary, :block
6
+
7
+ # Does this rule's condition block apply?
8
+ def applies?(target_obj = nil)
9
+ # no block given: always true
10
+ no_block_or_true = true
11
+ unless block.nil?
12
+ case block.parameters.count
13
+ when 1
14
+ # block expects parameter: pass target_object
15
+ if target_obj.nil?
16
+ no_block_or_true = false
17
+ Rails.logger.warn "[merit] no target_obj found on Rule#applies?"
18
+ else
19
+ no_block_or_true = block.call(target_obj)
20
+ end
21
+
22
+ when 0
23
+ # block evaluates to true, or is a hash of methods and expected value
24
+ called = block.call
25
+ if called.kind_of?(Hash)
26
+ no_block_or_true = called.conditions_apply? target_obj
27
+ end
28
+
29
+ end
30
+ end
31
+ no_block_or_true
32
+ end
33
+
34
+ # Is this rule's badge temporary?
35
+ def temporary?; self.temporary; end
36
+
37
+ # Grant badge if rule applies. If it doesn't, and the badge is temporary,
38
+ # then remove it.
39
+ def grant_or_delete_badge(action)
40
+ if sash = sash_to_badge(action)
41
+ if applies? action.target_object
42
+ badge.grant_to sash
43
+ elsif temporary?
44
+ badge.delete_from sash
45
+ end
46
+ else
47
+ Rails.logger.warn "[merit] no sash found on Rule#grant_or_delete_badge"
48
+ end
49
+ end
50
+
51
+ # Subject to badge: source_user or target.user?
52
+ def sash_to_badge(action)
53
+ target = case to
54
+ when :action_user
55
+ User.find_by_id(action.user_id) # _by_id doens't raise ActiveRecord::RecordNotFound
56
+ when :itself
57
+ action.target_object
58
+ else
59
+ begin
60
+ action.target_object.send(to)
61
+ rescue
62
+ Rails.logger.warn "[merit] #{action.target_model.singularize}.find(#{action.target_id}) not found, no badges giving today"
63
+ return
64
+ end
65
+ end
66
+ if target
67
+ target.create_sash_if_none
68
+ target.sash
69
+ end
70
+ end
71
+
72
+ # Get rule's related Badge.
73
+ def badge
74
+ if @badge.nil?
75
+ badges = Badge.where(:name => badge_name)
76
+ badges = badges.where(:level => level.to_s) unless level.nil?
77
+ @badge = badges.first
78
+ end
79
+ @badge
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,64 @@
1
+ module Merit
2
+ # La configuración para especificar cuándo aplicar cada badge va en
3
+ # app/models/merit_rules.rb, con la siguiente sintaxis:
4
+ #
5
+ # grant_on 'users#create', :badge => 'just', :level => 'registered' do
6
+ # # Nothing, or code block which evaluates to true
7
+ # # or with a { methods -> expected_values } hash.
8
+ # end
9
+ #
10
+ # También se puede asignar medallas desde métodos en controladores:
11
+ #
12
+ # Badge.find(3).grant_to(current_user)
13
+ #
14
+ # Merit crea una tabla Badges similar a:
15
+ # ___________________________________________________
16
+ # id | name | level | image
17
+ # 1 | creador | inspirado | creador-inspirado.png
18
+ # 2 | creador | blogger | creador-blogger.png
19
+ # 2 | creador | best-seller | creador-bestseller.png
20
+ # ___________________________________________________
21
+ #
22
+ # Y llena una tabla de acciones, del estilo de:
23
+ # ______________________________________________________________
24
+ # source (user_id) | action (method, value) | target (model, id) | processed
25
+ # 1 | comment nil | List 8 | true
26
+ # 1 | vote 3 | List 12 | true
27
+ # 3 | follow nil | User 1 | false
28
+ # X | create nil | User #{generated_id} | false
29
+ # ______________________________________________________________
30
+ #
31
+ # Luego chequea las condiciones sincronizadamente, o mediante un proceso en
32
+ # background, por ejemplo cada 5 minutos (Merit::BadgeRules#check_new_actions).
33
+ module BadgeRules
34
+ # Define rule for granting badges
35
+ def grant_on(action, *args, &block)
36
+ options = args.extract_options!
37
+
38
+ rule = Rule.new
39
+ rule.badge_name = options[:badge]
40
+ rule.level = options[:level]
41
+ rule.to = options[:to] || :action_user
42
+ rule.temporary = options[:temporary] || false
43
+ rule.block = block
44
+
45
+ actions = action.kind_of?(String) ? [action] : action
46
+ actions.each do |action|
47
+ defined_rules[action] ||= []
48
+ defined_rules[action] << rule
49
+ end
50
+ end
51
+
52
+ # Check non processed actions and grant badges if applies
53
+ def check_new_actions
54
+ MeritAction.where(:processed => false).each do |merit_action|
55
+ merit_action.check_badge_rules(defined_rules)
56
+ end
57
+ end
58
+
59
+ # Currently defined rules
60
+ def defined_rules
61
+ @defined_rules ||= {}
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,23 @@
1
+ module Merit
2
+ # Points are a simple integer value which are given to "meritable" resources
3
+ # according to rules in +app/models/merit_point_rules.rb+. They are given on
4
+ # actions-triggered.
5
+ module PointRules
6
+ # Define rules on certaing actions for giving points
7
+ def score(points, *args, &block)
8
+ options = args.extract_options!
9
+
10
+ actions = options[:on].kind_of?(Array) ? options[:on] : [options[:on]]
11
+ options[:to] ||= [:action_user]
12
+ targets = options[:to].kind_of?(Array) ? options[:to] : [options[:to]]
13
+ actions.each do |action|
14
+ actions_to_point[action] = { to: targets, score: points }
15
+ end
16
+ end
17
+
18
+ # Currently defined rules
19
+ def actions_to_point
20
+ @actions_to_point ||= {}
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,54 @@
1
+ module Merit
2
+ # Rankings are very similar to badges. They give "badges" which have a hierarchy
3
+ # defined by +level+'s lexicografical order (greater is better). If a rank is
4
+ # granted, lower level ranks are taken off. 5 stars is a common ranking use
5
+ # case.
6
+ #
7
+ # They are not given at specified actions like badges, you should define a cron
8
+ # job to test if ranks are to be granted.
9
+ #
10
+ # +set_rank+ accepts:
11
+ # * +badge_name+ name of this ranking
12
+ # * :+level+ ranking level (greater is better)
13
+ # * :+to+ model or scope to check if new rankings apply
14
+ module RankRules
15
+ # Populates +defined_rules+ hash with following hierarchy:
16
+ # defined_rules[ModelToRank][rankings] = [level, conditions_block]
17
+ def set_rank(ranking, *args, &block)
18
+ options = args.extract_options!
19
+
20
+ rule = Rule.new
21
+ rule.badge_name = ranking
22
+ rule.level = options[:level]
23
+ rule.block = block
24
+
25
+ defined_rules[options[:to]] ||= {}
26
+ defined_rules[options[:to]][ranking] ||= []
27
+ defined_rules[options[:to]][ranking] << rule
28
+ end
29
+
30
+ # Check rules defined for a merit_action
31
+ def check_rank_rules
32
+ defined_rules.each do |scoped_model, rankings| # For each model
33
+ rankings.each do |ranking, rules| # For each model's ranking (stars, etc)
34
+ rules.each do |rule| # For each ranking's rule (level)
35
+ scoped_model.all.each {|obj| grant_rank(rule, obj) }
36
+ end
37
+ end
38
+ end
39
+ end
40
+
41
+ # Grant rank if rule applies
42
+ # Badge checks if it's rank is greater than sash's current one.
43
+ def grant_rank(rule, target_object)
44
+ if rule.applies? target_object
45
+ rule.badge.grant_rank_to target_object.sash
46
+ end
47
+ end
48
+
49
+ # Currently defined rules
50
+ def defined_rules
51
+ @defined_rules ||= {}
52
+ end
53
+ end
54
+ end
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: merit
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.3
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Tute Costa
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-11-25 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: General reputation Rails engine.
15
+ email: tutecosta@gmail.com
16
+ executables: []
17
+ extensions: []
18
+ extra_rdoc_files: []
19
+ files:
20
+ - app/models/badge.rb
21
+ - app/models/merit_action.rb
22
+ - app/models/badges_sash.rb
23
+ - app/models/sash.rb
24
+ - lib/merit/rules_points.rb
25
+ - lib/merit/rule.rb
26
+ - lib/merit/railtie.rb
27
+ - lib/merit/core_extensions.rb
28
+ - lib/merit/model_additions.rb
29
+ - lib/merit/rules_rank.rb
30
+ - lib/merit/controller_extensions.rb
31
+ - lib/merit/rules_badge.rb
32
+ - lib/merit.rb
33
+ - lib/generators/merit/templates/merit_rank_rules.rb
34
+ - lib/generators/merit/templates/create_merit_actions.rb
35
+ - lib/generators/merit/templates/merit_point_rules.rb
36
+ - lib/generators/merit/templates/create_sashes.rb
37
+ - lib/generators/merit/templates/merit.rb
38
+ - lib/generators/merit/templates/create_badges.rb
39
+ - lib/generators/merit/templates/create_badges_sashes.rb
40
+ - lib/generators/merit/templates/merit_badge_rules.rb
41
+ - lib/generators/merit/merit_generator.rb
42
+ - lib/generators/merit/install_generator.rb
43
+ - lib/generators/active_record/templates/add_fields_to_model.rb
44
+ - lib/generators/active_record/merit_generator.rb
45
+ - MIT-LICENSE
46
+ - Rakefile
47
+ - Gemfile
48
+ - README.rdoc
49
+ homepage:
50
+ licenses: []
51
+ post_install_message:
52
+ rdoc_options: []
53
+ require_paths:
54
+ - lib
55
+ required_ruby_version: !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ! '>='
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ required_rubygems_version: !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ! '>='
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubyforge_project:
69
+ rubygems_version: 1.8.5
70
+ signing_key:
71
+ specification_version: 3
72
+ summary: General reputation Rails engine.
73
+ test_files: []