# Rating

[![Build Status](https://travis-ci.org/wbotelhos/rating.svg)](https://travis-ci.org/wbotelhos/rating)
[![Gem Version](https://badge.fury.io/rb/rating.svg)](https://badge.fury.io/rb/rating)
[![Maintainability](https://api.codeclimate.com/v1/badges/cc5efe8b06bc1d5e9e8a/maintainability)](https://codeclimate.com/github/wbotelhos/rating/maintainability)
[![Patreon](https://img.shields.io/badge/donate-%3C3-brightgreen.svg)](https://www.patreon.com/wbotelhos)

A true Bayesian rating system with scope and cache enabled.

## JS Rating?

This is **Raty**: https://github.com/wbotelhos/raty :star2:

## Description

Rating uses the know as "True Bayesian Estimate" inspired on [IMDb rating](http://www.imdb.com/help/show_leaf?votestopfaq) with the following formula:

```
(WR) = (v ÷ (v + m)) × R + (m ÷ (v + m)) × C
```

**IMDb Implementation:**

`WR`: weighted rating

`R`:  average for the movie (mean) = (Rating)

`v`:  number of votes for the movie = (votes)

`m`:  minimum votes required to be listed in the Top 250

`C`:  the mean vote across the whole report

**Rating Implementation:**

`WR`: weighted rating

`R`:  average for the resource

`v`:  number of votes for the resource

`m`:  average of the number of votes

`C`:  the average rating based on all resources

## Install

Add the following code on your `Gemfile` and run `bundle install`:

```ruby
gem 'rating'
```

Run the following task to create a Rating migration:

```bash
rails g rating:install
```

Then execute the migrations to create the to create tables `rating_rates` and `rating_ratings`:

```bash
rake db:migrate
```

## Usage

Just add the callback `rating` to your model:

```ruby
class Author < ApplicationRecord
  rating
end
```

Now this model can vote or receive votes.

### rate

You can vote on some resource:

```ruby
author   = Author.last
resource = Article.last

author.rate resource, 3
```

### rating

A voted resource exposes a cached data about it state:

```ruby
resource = Article.last

resource.rating
```

It will return a `Rating` object that keeps:

`average`: the normal mean of votes;

`estimate`: the true Bayesian estimate mean value (you should use this over average);

`sum`: the sum of votes for this resource;

`total`: the total of votes for this resource.

### rate_for

You can retrieve the rate of some author gave to some resource:

```ruby
author   = Author.last
resource = Article.last

author.rate_for resource
```

It will return a `Rate` object that keeps:

`author`: the author of vote;

`resource`: the resource that received the vote;

`value`: the value of the vote.

### rated?

Maybe you want just to know if some author already rated some resource and receive `true` or `false`:

```ruby
author   = Author.last
resource = Article.last

author.rated? resource
```

### rates

You can retrieve all rates received by some resource:

```ruby
resource = Article.last

resource.rates
```

It will return a collection of `Rate` object.

### rated

In the same way you can retrieve all rates that some author received:

```ruby
author = Author.last

author.rated
```

It will return a collection of `Rate` object.

### order_by_rating

You can list resource ordered by rating data:

```ruby
Article.order_by_rating
```

It will return a collection of resource ordered by `estimate desc` as default.
The order column and direction can be changed:

```ruby
Article.order_by_rating :average, :asc
```

It will return a collection of resource ordered by `Rating` table data.

### Scope

All methods support scope query, since you may want to vote on items of a resource instead the resource itself.
Let's say an article belongs to one or more categories and you want to vote on some categories of this article.

```ruby
category_1 = Category.first
category_2 = Category.second
author     = Author.last
resource   = Article.last
```

In this situation you should scope the vote of article with some category:

**rate**

```ruby
author.rate resource, 3, scope: category_1
author.rate resource, 5, scope: category_2
```

Now `resource` has a rating for `category_1` and another one for `category_2`.

**rating**

Recovering the rating values for resource, we have:

```ruby
resource.rating
# nil
```

But using the scope to make the right query:

```ruby
resource.rating scope: category_1
# { average: 3, estimate: 3, sum: 3, total: 1 }

resource.rating scope: category_2
# { average: 5, estimate: 5, sum: 5, total: 1 }
```

**rated**

On the same way you can find your rates with a scoped query:

```ruby
author.rated scope: category_1
# { value: 3, scopeable: category_1 }
```

**rates**

The resource still have the power to consult its rates:

```ruby
article.rates scope: category_1
# { value: 3, scopeable: category_1 }

article.rates scope: category_2
# { value: 3, scopeable: category_2 }
```

**order_by_rating**

To order the rating you do the same thing:

```ruby
Article.order_by_rating scope: category_1
```

### Records

Maybe you want to recover all records with or without scope, so you can add the suffix `_records` on relations:

```ruby
category_1 = Category.first
category_2 = Category.second
author     = Author.last
resource   = Article.last

author.rate resource, 1
author.rate resource, 3, scope: category_1
author.rate resource, 5, scope: category_2

author.rating_records
# { average: 1, estimate: 1, scopeable: nil       , sum: 1, total: 1 },
# { average: 3, estimate: 3, scopeable: category_1, sum: 3, total: 1 },
# { average: 5, estimate: 5, scopeable: category_2, sum: 5, total: 1 }

author.rated_records
# { value: 1 }, { value: 3, scopeable: category_1 }, { value: 5, scopeable: category_2 }

article.rates_records
# { value: 1 }, { value: 3, scopeable: category_1 }, { value: 5, scopeable: category_2 }
```

### As

If you have a model that will only be able to rate but not to receive a rate, configure it as `author`.
An author model still can be rated, but won't genarate a Rating record with all values as zero to warm up the cache.

```ruby
rating as: :author
```

### Metadata

Maybe you want include a `comment` together your rating or even a `fingerprint` field to make your rating more secure.
So, first you will need to add more fields to the `Rating::Rate` table:

```ruby
class AddCommentAndFingerprintOnRatingRates < ActiveRecord::Migration
  def change
    add_column :rating_rates, :comment, :text

    add_reference :rating_rates, :fingerprint, foreign_key: true, index: true, null: false
  end
end
```

As you can seed, we can add any kind of field we want. Now we just provide this values when we make the rate:

```ruby
author      = Author.last
resource    = Article.last
comment     = 'This is a very nice rating. s2'
fingerprint = Fingerprint.new(ip: '127.0.0.1')

author.rate resource, 3, metadata: { comment: comment, fingerprint: fingerprint }
```

Now you can have this data into your model normally:


```ruby
author = Author.last
rate   = author.rates.last

rate.comment     # 'This is a very nice rating. s2'
rate.fingerprint # <Fingerprint id:...>
rate.value       # 3
```

### Scoping

If you need to warm up a record with scope, you need to setup the `scoping` relation.

```ruby
class Resource < ApplicationRecord
  voting scoping: :categories
end
```

Now, when a resource is created, the cache will be generated for each related `category` as `scopeable`.

### Table Name

You can choose the table where Rating will write the data via YAML config.
You should just to provide a `config/rating.yml` file with the following content:

```yml
rating:
  rate_table: 'reviews'
  rating_table: 'review_ratings'
```

Now the rates will be written on `reviews` table over `rating_rates` and calculation will be on `review_ratings` over `rating_ratings`.
You can change one table o both of them.

## Love it!

Via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=X8HEP2878NDEG&item_name=rating) or [Patreon](https://www.patreon.com/wbotelhos). Thanks! (: