#364 Active Record Reputation System

If you need to calculate an average user's rating or sum up a number of votes, consider using the activerecord-reputation-system gem. Here I will cover the basics and also briefly present a from-scratch solution.

Below is a screenshot from an application called “You Haiku” which lets users write Haikus and add them to the site.

A number of Haikus have already been added to our application and we’d like users to be able to vote for them by either voting them up or down. Our application has no voting system in place yet so how can we add this feature? We could write this from scratch but instead we’ll use a gem called Active Record Reputation System. This allows us to easily calculate an average user rating, sum up the number of votes and more and here we’ll show you how it get it working in our application.

Getting Started

We’ll start by adding the gem to the gemfile and running bundle to install it. Note that we need to require the file separately as reputation_system.

/Gemfile

gem 'activerecord-reputation-system', require:'reputation_system'

We’ll need to run a generator to create migration files for the database tables that are needed. We’ll take a closer look at these files later but for now we’ll just migrate the database to add the necessary tables and fields.

terminal

$ rails g reputation_system
$ rake db:migrate

Next we’ll modify the model that we want users to be able to vote on and add a call to has_reputation. We pass this the name we want to give the reputation, in this case votes, and two more options: source, which is the name of the model that will be doing the voting, and aggregated_by, which can be set to sum, average or product, depending on how we want the calculation to happen. These options are documented in the README along with the other options we can pass in.

With this code in place we can begin to work on our voting system. We want two links next to each haiku so that a user can vote it up or down, but where should these links route to? We have a couple of options here: we could make a separate haiku_votes resource or we could make this a member action on the Haiku resource. We’ll take the latter option and add a votes action that takes POST requests.

Next we’ll add the vote action to the HaikusController. The value of a vote should be 1 or -1, depending on whether it’s a vote up or down. We’ll use a type parameter and count the vote as an upvote if it has the value “up”. Next we’ll fetch the haiku by its id and call a method called add_evaluation. This method takes three arguments: the name of the reputation, the value to add and the source object which in this case is the current user. Finally we redirect back to the referrer and display a flash notice.

We may have to restart the server for all these changes to be picked up but once we have we should see the voting links when we reload the page.

Updating Votes

If we vote a Haiku up and then change our mind and vote it down again we’ll see an ActiveRecord error. This is because the same user has voted on the same Haiku twice and the reputation system automatically prevents duplicate votes. We could rescue from this exception in the HaikusController but instead we’ll use a different method called add_or_update_evaluation to record the vote.

This will update an existing vote if one if found. Now if a user votes more than once their current vote will be changed. It would be nice if we could see the number of votes a Haiku has received so we’ll add this next. We can do this by calling reputation_value_for on the haiku and passing in the reputation that we want the value for. This returns a float value so we call to_i on it to round it down.

It would be good to sort the list of haikus based on the net number of upvotes. We’ll do this inside the HaikusController’s index action where we currently just fetch all the haikus. We can instead use find_with_reputation to fetch them in the correct order.

The second argument here is the scope we want to use. We haven’t discussed reputation scopes yet. These aren’t ActiveRecord named scopes but are specific to the Reputation System and we use the :all scope here to find everything. When we reload the page now the haikus are displayed in the correct order.

Next we want to display the total number of votes that a user has received for their haikus and the Reputation System allows to to define reputations indirectly so we can go about it like this.

Here we use has_reputation with a hash for the source. This instructs the Reputation System to delegate this to the reputation called votes in the Haiku model. We aggregate the result of this to give an overall score for the user. We can now use this where we display the user’s name in the application’s layout file.

When we reload the page now we’ll see the current user’s score next to the user’s name.

Showing The User The Haikus They Have Voted for

To make it easier for a user to see which haikus they have voted for we’ll show something next to those haikus and hide the links. The gem doesn’t seem to provide a way to do this but it’s still possible. If we look at one of the migration files that was generated earlier we’ll see that the gem creates a database table called rs_evaluations and a record is added here when a user votes.

This table keeps track of the reputation name, which in our case is votes, the source, which is the User model, the target (the Haiku model) and the value which will be 1 or -1 depending on whether the vote was up or down. Note that both source and target are polymorphic associations. There is an RSEvaluation model that goes along with this table and this means that we can associate a User record with that model like this:

/app/models/user.rb

has_many :evaluations, class_name:"RSEvaluation", as::source

The as: option is necessary here as this is a polymorphic association. With this in place we can determine whether a given user has voted for a specific haiku.

Here we fetch all the evaluations and determine whether one exists with correct type and id. There are more efficient ways to do this if we’re doing this a lot on a single page but this approach will work for us here. We can use this method now to hide the links if a user has already voted.

When we reload the page now the links have disappeared from the haikus that we’ve voted for.

There’s more that we could do with this application such as adding this restriction to the controller action and also preventing a user from voting for their own haikus but we won’t do that here.

Adding Voting From Scratch

That’s it for our quick tour of the ActiveRecord Reputation System. It’s a handy gem but it seems that it would be fairly easy to reproduce the same functionality from scratch. This is indeed the case and you can see the source code for this on Github. For the final part of this episode we’ll walk quickly through this code.

Here we have a HaikuVote model that belongs to both a Haiku and a User. What’s nice about this approach is that we can put custom validations here, including the vote values that we accept and whether or not the user that is voting is voting for one of their own haikus.

It takes some SQL code to get this functionality working, but it does work.

Another tricky area is determining the number of votes that a user has received, although ActiveRecord’s joins method means that we don’t need to use SQL code here.

/app/models/user.rb

deftotal_votesHaikuVote.joins(:haiku).where(haikus: {user_id:self.id}).sum('value')
end

So, is it best to write this functionality from scratch or use the gem? The gem is useful if we have a more complicated setup, especially if we have multiple models that we handling the reputation of. It’s use of polymorphic associations can really help here. If we have a simpler setup, like the example application we’ve shown here then starting from scratch is the better option.