merit 0.1.3
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +6 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +127 -0
- data/Rakefile +29 -0
- data/app/models/badge.rb +39 -0
- data/app/models/badges_sash.rb +14 -0
- data/app/models/merit_action.rb +48 -0
- data/app/models/sash.rb +19 -0
- data/lib/generators/active_record/merit_generator.rb +27 -0
- data/lib/generators/active_record/templates/add_fields_to_model.rb +13 -0
- data/lib/generators/merit/install_generator.rb +31 -0
- data/lib/generators/merit/merit_generator.rb +8 -0
- data/lib/generators/merit/templates/create_badges.rb +19 -0
- data/lib/generators/merit/templates/create_badges_sashes.rb +16 -0
- data/lib/generators/merit/templates/create_merit_actions.rb +17 -0
- data/lib/generators/merit/templates/create_sashes.rb +11 -0
- data/lib/generators/merit/templates/merit.rb +5 -0
- data/lib/generators/merit/templates/merit_badge_rules.rb +40 -0
- data/lib/generators/merit/templates/merit_point_rules.rb +21 -0
- data/lib/generators/merit/templates/merit_rank_rules.rb +30 -0
- data/lib/merit.rb +26 -0
- data/lib/merit/controller_extensions.rb +36 -0
- data/lib/merit/core_extensions.rb +28 -0
- data/lib/merit/model_additions.rb +30 -0
- data/lib/merit/railtie.rb +7 -0
- data/lib/merit/rule.rb +82 -0
- data/lib/merit/rules_badge.rb +64 -0
- data/lib/merit/rules_points.rb +23 -0
- data/lib/merit/rules_rank.rb +54 -0
- metadata +73 -0
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.rdoc
ADDED
@@ -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!).
|
data/Rakefile
ADDED
@@ -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
|
data/app/models/badge.rb
ADDED
@@ -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
|
data/app/models/sash.rb
ADDED
@@ -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,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,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
|
data/lib/merit.rb
ADDED
@@ -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
|
data/lib/merit/rule.rb
ADDED
@@ -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: []
|