Merit adds reputation behavior to Rails apps in the form of Badges, Points, and Rankings.
- Add
gem 'merit'
to yourGemfile
- Run
rails g merit:install
- Run
rails g merit MODEL_NAME
(e.g.user
) - Run
rake db:migrate
- Define badges in
config/initializers/merit.rb
. You can also define ORM::active_record
(default) or:mongoid
. - Configure reputation rules for your application in
app/models/merit/*
Create badges in config/initializers/merit.rb
Merit::Badge.create!
takes a hash describing the badge:
:id
integer (reqired):name
this is how you reference the badge (required):level
(optional):description
(optional):custom_fields
hash of anything else you want associated with the badge (optional)
Merit::Badge.create!(
id: 1,
name: "Yearling",
description: "Active member for a year",
custom_fields: { difficulty: :silver }
)
Badges can be automatically given to any resource in your application based on rules and conditions you create. Badges can also have levels, and be permanent or temporary (A temporary badge is revoked when the conditions of the badge are no longer met).
Badge rules / conditions are defined in app/models/merit/badge_rules.rb
initialize
block by calling grant_on
with the following parameters:
'controller#action'
a string similar to Rails routes:badge
corresponds to the:name
of the badge:level
corresponds to the:level
of the badge:to
the object's field to give the badge to- If you are putting badges on the related user then this field is probably
:user
. - Needs a variable named
@model
in the associated controller action, like@post
forposts_controller.rb
or@comment
forcomments_controller.rb
. Implementation note: Merit finds the object with following snippet:instance_variable_get(:"@#{controller_name.singularize}")
.
- If you are putting badges on the related user then this field is probably
:model_name
define the controller's name if it's different from the model's (e.g.RegistrationsController
for theUser
model).:multiple
whether or not the badge may be granted multiple times.false
by default.:temporary
whether or not the badge should be revoked if the condition no longer holds.false
-badges are kept for ever- by default.&block
can be one of the following:- empty / not included: always grant the badge
- a block which evaluates to boolean. It recieves the target object as
parameter (e.g.
@post
if you're working with a PostsController action). - a block with a hash composed of methods to run on the target object and expected method return values
# app/models/merit/badge_rules.rb
grant_on 'comments#vote', badge: 'relevant-commenter', to: :user do |comment|
comment.votes.count == 5
end
grant_on ['users#create', 'users#update'], badge: 'autobiographer', temporary: true do |user|
user.name? && user.email?
end
# Check granted badges
current_user.badges # Returns an array of badges
# Grant or remove manually
current_user.add_badge(badge.id)
current_user.rm_badge(badge.id)
# Get related entries of a given badge
Badge.find(1).users
Meritable models have a badges
method which returns an array of associated
badges:
<ul>
<% current_user.badges.each do |badge| %>
<li><%= badge.name %></li>
<% end %>
</ul>
Points are given to "meritable" resources on actions-triggered, either to the
action user or to the method(s) defined in the :to
option. Define rules on
app/models/merit/point_rules.rb
:
score
accepts:
score
Integer
Proc
, or any object that acceptscall
which takes one argument, where the target_object is passed in and the return value is used as the score.
:on
action as string or array of strings (similar to Rails routes):to
method(s) to send to the target_object (who should be scored?):model_name
(optional) to specify the model name if it cannot be guessed from the controller. (e.g.model_name: 'User'
forRegistrationsController
, ormodel_name: 'Comment'
forApi::CommentsController
):category
(optional) to categorize earned points.default
is used by default.&block
- empty (always scores)
- a block which evaluates to boolean (recieves target object as parameter)
# app/models/merit/point_rules.rb
score 10, to: :post_creator, on: 'comments#create', category: 'comment_activity' do |comment|
comment.title.present?
end
score 20, on: [
'comments#create',
'photos#create'
]
score 15, on: 'reviews#create', to: [:reviewer, :reviewed]
proc = lambda { |photo| PhotoPointsCalculator.calculate_score_for(photo) }
score proc, on: 'photos#create'
# Score manually
current_user.add_points(20, category: 'Optional category')
current_user.subtract_points(10, category: 'Optional category')
# Query awarded points since a given date
score_points = current_user.score_points(category: 'Optional category')
score_points.where("created_at > '#{1.month.ago}'").sum(:num_points)
Meritable models have a points
method which returns an integer:
<%= current_user.points(category: 'Optional category') %>
If category
left empty, it will return the sum of points for every category.
<%= current_user.points %>
A common ranking use case is 5 stars. They are not given at specified actions like badges, a cron job should be defined to test if ranks are to be granted.
Define rules on app/models/merit/rank_rules.rb
:
set_rank
accepts:
:level
ranking level (greater is better, Lexicographical order):to
model or scope to check if new rankings apply:level_name
attribute name (default is empty and results in 'level
' attribute, if set it's appended like 'level_#{level_name}
')
Check for rules on a rake task executed in background like:
task cron: :environment do
Merit::RankRules.new.check_rank_rules
end
set_rank level: 2, to: Committer.active do |committer|
committer.branches > 1 && committer.followers >= 10
end
set_rank level: 3, to: Committer.active do |committer|
committer.branches > 2 && committer.followers >= 20
end
<%= current_user.level %>
You can get observers notified any time merit changes reputation in your application.
To do so, add your observer (to app/models
or app/observers
, for example):
# reputation_change_observer.rb
class ReputationChangeObserver
def update(changed_data)
# description will be something like:
# granted 5 points
# granted just-registered badge
# removed autobiographer badge
description = changed_data[:description]
# If user is your meritable model, you can grab it like:
if changed_data[:merit_object]
sash_id = changed_data[:merit_object].sash_id
user = User.where(sash_id: sash_id).first
end
# To know where and when it happened:
merit_action = Merit::Action.find changed_data[:merit_action_id]
controller = merit_action.target_model
action = merit_action.action_method
when = merit_action.created_at
# From here on, you can create a new Notification assuming that's an
# ActiveRecord Model in your app, send an email, etc. For example:
Notification.create(
user: user,
what: description,
where: "#{controller}##{action}",
when: when)
end
end
# In `config/initializers/merit.rb`
config.add_observer 'ReputationChangeObserver'
TODO: Improve API sending in changed_data
concrete data instead of merit
objects.
- Run
rails d merit:install
- Run
rails d merit MODEL_NAME
(e.g.user
) - Run
rails g merit:remove MODEL_NAME
(e.g.user
) - Run
rake db:migrate
- Remove
merit
from your Gemfile