merit 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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: []
|