first commit

main
Washington Botelho 2017-10-29 23:44:23 -02:00
commit 598c58af7e
No known key found for this signature in database
GPG Key ID: BECE10A8106CC7A0
51 changed files with 1452 additions and 0 deletions

2
.gitignore vendored 100644
View File

@ -0,0 +1,2 @@
.byebug_history
*.gem

63
.rubocop.yml 100644
View File

@ -0,0 +1,63 @@
require: rubocop-rspec
inherit_from: .rubocop_todo.yml
AllCops:
Exclude:
- generators/rating/templates/**/*
Rails:
Enabled: true
Lint/Debugger:
Enabled: false
Metrics/AbcSize:
Enabled: false
Metrics/ClassLength:
Enabled: false
Metrics/CyclomaticComplexity:
Enabled: false
Metrics/LineLength:
Enabled: false
Metrics/MethodLength:
Enabled: false
Metrics/PerceivedComplexity:
Enabled: false
Rails/HasAndBelongsToMany:
Enabled: false
Rails/HttpPositionalArguments:
Enabled: false
Style/IfUnlessModifier:
MaxLineLength: 0
Style/PercentLiteralDelimiters:
PreferredDelimiters:
'%i': '[]'
'%I': '[]'
'%r': ()
'%w': '[]'
'%W': '[]'
Style/AlignParameters:
EnforcedStyle: with_fixed_indentation
Style/Documentation:
Enabled: false
Style/Encoding:
Enabled: false
Style/SpaceBeforeComma:
Enabled: false
Style/SpaceBeforeFirstArg:
Enabled: false

View File

1
.ruby-gemset 100644
View File

@ -0,0 +1 @@
rating

1
.ruby-version 100644
View File

@ -0,0 +1 @@
2.4.2

1
.travis.yml 100644
View File

@ -0,0 +1 @@
language: ruby

3
CHANGELOG.md 100644
View File

@ -0,0 +1,3 @@
## v0.1.0
- First release.

5
Gemfile 100644
View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
source 'https://rubygems.org'
gemspec

187
Gemfile.lock 100644
View File

@ -0,0 +1,187 @@
PATH
remote: .
specs:
rating (0.1.0)
rails (>= 4.2, < 6)
GEM
remote: https://rubygems.org/
specs:
actioncable (5.1.4)
actionpack (= 5.1.4)
nio4r (~> 2.0)
websocket-driver (~> 0.6.1)
actionmailer (5.1.4)
actionpack (= 5.1.4)
actionview (= 5.1.4)
activejob (= 5.1.4)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
actionpack (5.1.4)
actionview (= 5.1.4)
activesupport (= 5.1.4)
rack (~> 2.0)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.2)
actionview (5.1.4)
activesupport (= 5.1.4)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.0.3)
activejob (5.1.4)
activesupport (= 5.1.4)
globalid (>= 0.3.6)
activemodel (5.1.4)
activesupport (= 5.1.4)
activerecord (5.1.4)
activemodel (= 5.1.4)
activesupport (= 5.1.4)
arel (~> 8.0)
activesupport (5.1.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (~> 0.7)
minitest (~> 5.1)
tzinfo (~> 1.1)
arel (8.0.0)
ast (2.3.0)
builder (3.2.3)
byebug (9.1.0)
coderay (1.1.2)
concurrent-ruby (1.0.5)
crass (1.0.2)
database_cleaner (1.6.2)
diff-lcs (1.3)
erubi (1.7.0)
factory_bot (4.8.2)
activesupport (>= 3.0.0)
factory_bot_rails (4.8.2)
factory_bot (~> 4.8.2)
railties (>= 3.0.0)
globalid (0.4.1)
activesupport (>= 4.2.0)
i18n (0.9.0)
concurrent-ruby (~> 1.0)
loofah (2.1.1)
crass (~> 1.0.2)
nokogiri (>= 1.5.9)
mail (2.6.6)
mime-types (>= 1.16, < 4)
method_source (0.9.0)
mime-types (3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2016.0521)
mini_portile2 (2.3.0)
minitest (5.10.3)
nio4r (2.1.0)
nokogiri (1.8.1)
mini_portile2 (~> 2.3.0)
parallel (1.12.0)
parser (2.4.0.0)
ast (~> 2.2)
powerpack (0.1.1)
pry (0.11.2)
coderay (~> 1.1.0)
method_source (~> 0.9.0)
pry-byebug (3.5.0)
byebug (~> 9.1)
pry (~> 0.10)
rack (2.0.3)
rack-test (0.7.0)
rack (>= 1.0, < 3)
rails (5.1.4)
actioncable (= 5.1.4)
actionmailer (= 5.1.4)
actionpack (= 5.1.4)
actionview (= 5.1.4)
activejob (= 5.1.4)
activemodel (= 5.1.4)
activerecord (= 5.1.4)
activesupport (= 5.1.4)
bundler (>= 1.3.0)
railties (= 5.1.4)
sprockets-rails (>= 2.0.0)
rails-dom-testing (2.0.3)
activesupport (>= 4.2.0)
nokogiri (>= 1.6)
rails-html-sanitizer (1.0.3)
loofah (~> 2.0)
railties (5.1.4)
actionpack (= 5.1.4)
activesupport (= 5.1.4)
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.2.2)
rake
rake (12.2.1)
rspec (3.7.0)
rspec-core (~> 3.7.0)
rspec-expectations (~> 3.7.0)
rspec-mocks (~> 3.7.0)
rspec-core (3.7.0)
rspec-support (~> 3.7.0)
rspec-expectations (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0)
rspec-html-matchers (0.9.1)
nokogiri (~> 1)
rspec (>= 3.0.0.a, < 4)
rspec-mocks (3.7.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.7.0)
rspec-rails (3.7.1)
actionpack (>= 3.0)
activesupport (>= 3.0)
railties (>= 3.0)
rspec-core (~> 3.7.0)
rspec-expectations (~> 3.7.0)
rspec-mocks (~> 3.7.0)
rspec-support (~> 3.7.0)
rspec-support (3.7.0)
rubocop (0.51.0)
parallel (~> 1.10)
parser (>= 2.3.3.1, < 3.0)
powerpack (~> 0.1)
rainbow (>= 2.2.2, < 3.0)
ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-rspec (1.19.0)
rubocop (>= 0.51.0)
ruby-progressbar (1.9.0)
shoulda-matchers (3.1.2)
activesupport (>= 4.0.0)
sprockets (3.7.1)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
sprockets-rails (3.2.1)
actionpack (>= 4.0)
activesupport (>= 4.0)
sprockets (>= 3.0.0)
sqlite3 (1.3.13)
thor (0.20.0)
thread_safe (0.3.6)
tzinfo (1.2.4)
thread_safe (~> 0.1)
unicode-display_width (1.3.0)
websocket-driver (0.6.5)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2)
PLATFORMS
ruby
DEPENDENCIES
database_cleaner
factory_bot_rails
pry-byebug
rating!
rspec-html-matchers
rspec-rails
rubocop-rspec
shoulda-matchers
sqlite3
BUNDLED WITH
1.15.4

21
LICENSE 100644
View File

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2017 Washington Botelho
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

180
README.md 100644
View File

@ -0,0 +1,180 @@
# 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)
A true Bayesian rating system with 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 (mean) = (Rating)
`v`: number of votes for the movie = (votes)
`m`: average of the number of votes
`C`: the mean vote across the whole report
## 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 User < ApplicationRecord
rating
end
```
Now this model can vote or be voted.
### rate
You can vote on some resource:
```ruby
author = User.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 = User.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 = User.last
resource = Article.last
author.rated? resource
```
### rates
You can retrieve all rates made by some author:
```ruby
author = User.last
author.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 = User.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.
## Love it!
Via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=X8HEP2878NDEG&item_name=rating) or [Gratipay](https://gratipay.com/rating). Thanks! (:

7
Rakefile 100644
View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new
task default: :spec

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module Rating
class InstallGenerator < Rails::Generators::Base
source_root File.expand_path('../templates', __FILE__)
desc 'configure Rating'
def create_migration
version = Time.current.strftime('%Y%m%d%H%M%S')
template 'db/migrate/create_rating_tables.rb', "db/migrate/#{version}_create_rating_tables.rb"
end
end
end

View File

@ -0,0 +1,31 @@
# frozen_string_literal: true
class CreateRatingTables < ActiveRecord::Migration[5.0]
def change
create_table :rating_rates do |t|
t.decimal :value, default: 0, precision: 17, scale: 14
t.references :author , index: true, null: false, polymorphic: true
t.references :resource, index: true, null: false, polymorphic: true
t.timestamps null: false
end
add_index :rating_rates, %i[author_id author_type resource_id resource_type],
name: :index_rating_rates_on_author_and_resource,
unique: true
create_table :rating_ratings do |t|
t.decimal :average , default: 0, mull: false, precision: 17, scale: 14
t.decimal :estimate, default: 0, mull: false, precision: 17, scale: 14
t.integer :sum , default: 0, mull: false
t.integer :total , default: 0, mull: false
t.references :resource, index: true, null: false, polymorphic: true
t.timestamps null: false
end
add_index :rating_ratings, %i[resource_id resource_type], unique: true
end
end

10
lib/rating.rb 100644
View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
module Rating
end
require 'rating/models/rating/extension'
require 'rating/models/rating/rate'
require 'rating/models/rating/rating'
ActiveRecord::Base.include Rating::Extension

View File

@ -0,0 +1,46 @@
# frozen_string_literal: true
module Rating
module Extension
extend ActiveSupport::Concern
included do
def rate(resource, value, author: self)
Rate.create author: author, resource: resource, value: value
end
def rate_for(resource)
Rate.rate_for author: self, resource: resource
end
def rated?(resource)
!rate_for(resource).nil?
end
end
module ClassMethods
def rating
after_create { Rating.find_or_create_by resource: self }
has_one :rating,
as: :resource,
class_name: '::Rating::Rating',
dependent: :destroy
has_many :rates,
as: :resource,
class_name: '::Rating::Rate',
dependent: :destroy
has_many :rated,
as: :author,
class_name: '::Rating::Rate',
dependent: :destroy
scope :order_by_rating, ->(column = :estimate, direction = :desc) {
includes(:rating).order("#{Rating.table_name}.#{column} #{direction}")
}
end
end
end
end

View File

@ -0,0 +1,41 @@
# frozen_string_literal: true
module Rating
class Rate < ActiveRecord::Base
self.table_name = 'rating_rates'
after_save :update_rating
belongs_to :author , polymorphic: true
belongs_to :resource, polymorphic: true
validates :author, :resource, :value, presence: true
validates :value, numericality: { greater_than_or_equal_to: 1, less_than_or_equal_to: 100 }
validates :author_id, uniqueness: {
case_sensitive: false,
scope: %i[author_type resource_id resource_type]
}
def self.create(author:, resource:, value:)
record = find_or_initialize_by(author: author, resource: resource)
return record if record.persisted? && value == record.value
record.value = value
record.save
record
end
def self.rate_for(author:, resource:)
find_by author: author, resource: resource
end
private
def update_rating
::Rating::Rating.update_rating resource
end
end
end

View File

@ -0,0 +1,96 @@
# frozen_string_literal: true
module Rating
class Rating < ActiveRecord::Base
self.table_name = 'rating_ratings'
belongs_to :resource, polymorphic: true
validates :average, :estimate, :resource, :sum, :total, presence: true
validates :average, :estimate, :sum, :total, numericality: true
class << self
def averager_data(resource)
total_count = how_many_resource_received_votes_sql?
distinct_count = how_many_resource_received_votes_sql?(distinct: true)
sql = %(
SELECT
(#{total_count} / CAST(#{distinct_count} AS float)) count_avg,
COALESCE(AVG(value), 0) rating_avg
FROM #{rate_table_name}
WHERE resource_type = :resource_type
).squish
execute_sql [sql, resource_type: resource.class.base_class.name]
end
def data(resource)
averager = averager_data(resource)
values = values_data(resource)
{
average: values.rating_avg,
estimate: estimate(averager, values),
sum: values.rating_sum,
total: values.rating_count
}
end
def values_data(resource)
sql = %(
SELECT
COALESCE(AVG(value), 0) rating_avg,
COALESCE(SUM(value), 0) rating_sum,
COUNT(1) rating_count
FROM #{rate_table_name}
WHERE resource_type = ? and resource_id = ?
).squish
execute_sql [sql, resource.class.base_class.name, resource.id]
end
def update_rating(resource)
record = find_or_initialize_by(resource: resource)
result = data(resource)
record.average = result[:average]
record.sum = result[:sum]
record.total = result[:total]
record.estimate = result[:estimate]
record.save
end
private
def estimate(averager, values)
resource_type_rating_avg = averager.rating_avg
count_avg = averager.count_avg
resource_rating_avg = values.rating_avg
resource_rating_count = values.rating_count.to_f
(resource_rating_count / (resource_rating_count + count_avg)) * resource_rating_avg +
(count_avg / (resource_rating_count + count_avg)) * resource_type_rating_avg
end
def execute_sql(sql)
Rate.find_by_sql(sql).first
end
def how_many_resource_received_votes_sql?(distinct: false)
count = distinct ? 'COUNT(DISTINCT resource_id)' : 'COUNT(1)'
%((
SELECT #{count}
FROM #{rate_table_name}
WHERE resource_type = :resource_type
))
end
def rate_table_name
@rate_table_name ||= Rate.table_name
end
end
end
end

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
module Rating
VERSION = '0.1.0'
end

32
rating.gemspec 100644
View File

@ -0,0 +1,32 @@
# frozen_string_literal: true
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'rating/version'
Gem::Specification.new do |spec|
spec.author = 'Washington Botelho'
spec.description = 'A true Bayesian rating system with cache enabled.'
spec.email = 'wbotelhos@gmail.com'
spec.files = Dir['lib/**/*'] + %w[CHANGELOG.md LICENSE README.md]
spec.homepage = 'https://github.com/wbotelhos/rating'
spec.license = 'MIT'
spec.name = 'rating'
spec.platform = Gem::Platform::RUBY
spec.summary = 'A true Bayesian rating system with cache enabled.'
spec.test_files = Dir['spec/**/*']
spec.version = Rating::VERSION
spec.add_dependency 'rails', '>= 4.2', '< 6'
spec.add_development_dependency 'database_cleaner'
spec.add_development_dependency 'factory_bot_rails'
spec.add_development_dependency 'pry-byebug'
spec.add_development_dependency 'rspec-html-matchers'
spec.add_development_dependency 'rspec-rails'
spec.add_development_dependency 'rubocop-rspec'
spec.add_development_dependency 'shoulda-matchers'
spec.add_development_dependency 'sqlite3'
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
factory :article do
sequence(:name) { |i| "Name #{i}" }
end
end

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
FactoryBot.define do
factory :rating_rate, class: Rating::Rate do
value 100
author { create :user }
resource { create :article }
end
end

View File

@ -0,0 +1,12 @@
# frozen_string_literal: true
FactoryBot.define do
factory :rating_rating, class: Rating::Rating do
average 100
estimate 100
sum 100
total 1
resource { create :article }
end
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
FactoryBot.define do
factory :user do
sequence(:name) { |i| "Name #{i}" }
end
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Rating::Extension, ':after_create' do
context 'when creates object' do
let!(:user) { create :user }
it 'creates a record with zero values just to be easy to make the count' do
rating = Rating::Rating.find_by(resource: user)
expect(rating.average).to eq 0
expect(rating.estimate).to eq 0
expect(rating.resource).to eq user
expect(rating.sum).to eq 0
expect(rating.total).to eq 0
end
end
end

View File

@ -0,0 +1,121 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Rating::Extension, ':order_by_rating' do
let!(:user_1) { create :user }
let!(:user_2) { create :user }
let!(:article_1) { create :article }
let!(:article_2) { create :article }
let!(:article_3) { create :article }
before do
create :rating_rate, author: user_1, resource: article_1, value: 100
create :rating_rate, author: user_1, resource: article_2, value: 11
create :rating_rate, author: user_1, resource: article_3, value: 10
create :rating_rate, author: user_2, resource: article_1, value: 1
end
context 'with default filters' do
it 'sorts by :estimate :desc' do
expect(Article.order_by_rating).to eq [
article_1,
article_2,
article_3
]
end
end
context 'filtering by :average' do
context 'as asc' do
it 'works' do
expect(Article.order_by_rating(:average, :asc)).to eq [
article_3,
article_2,
article_1
]
end
end
context 'as desc' do
it 'works' do
expect(Article.order_by_rating(:average, :desc)).to eq [
article_1,
article_2,
article_3
]
end
end
end
context 'filtering by :estimate' do
context 'as asc' do
it 'works' do
expect(Article.order_by_rating(:estimate, :asc)).to eq [
article_3,
article_2,
article_1
]
end
end
context 'as desc' do
it 'works' do
expect(Article.order_by_rating(:estimate, :desc)).to eq [
article_1,
article_2,
article_3
]
end
end
end
context 'filtering by :sum' do
context 'as asc' do
it 'works' do
expect(Article.order_by_rating(:sum, :asc)).to eq [
article_3,
article_2,
article_1
]
end
end
context 'as desc' do
it 'works' do
expect(Article.order_by_rating(:sum, :desc)).to eq [
article_1,
article_2,
article_3
]
end
end
context 'filtering by :total' do
context 'as asc' do
it 'works' do
result = Article.order_by_rating(:total, :asc)
expect(result[0..1]).to match_array [article_2, article_3]
expect(result.last).to eq article_1
end
end
context 'as desc' do
it 'works' do
result = Article.order_by_rating(:total, :desc)
expect(result.first).to eq article_1
expect(result[1..2]).to match_array [article_2, article_3]
end
end
end
end
context 'with other resource' do
it 'works' do
expect(User.order_by_rating(:total, :desc)).to match_array [user_1, user_2]
end
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Rating::Extension, ':rate_for' do
let!(:user) { create :user }
let!(:article) { create :article }
it 'delegates to rate object' do
expect(Rating::Rate).to receive(:rate_for).with author: user, resource: article
user.rate_for article
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Rating::Extension, ':rate' do
let!(:user) { create :user }
let!(:article) { create :article }
it 'delegates to rate object' do
expect(Rating::Rate).to receive(:create).with author: user, resource: article, value: 3
user.rate article, 3
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Rating::Extension, ':rated?' do
let!(:user) { create :user }
let!(:article) { create :article }
context 'when has no rate for the given resource' do
before { allow(user).to receive(:rate_for).with(article) { nil } }
specify { expect(user.rated?(article)).to eq false }
end
context 'when has rate for the given resource' do
before { allow(user).to receive(:rate_for).with(article) { double } }
specify { expect(user.rated?(article)).to eq true }
end
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Rating::Extension, ':rated' do
let!(:user) { create :user }
let!(:article) { create :article }
before { user.rate article, 3 }
it 'returns rates made by the caller' do
expect(user.rated).to eq [Rating::Rate.find_by(resource: article)]
end
context 'when destroy author' do
before do
expect(Rating::Rate.where(author: user).count).to eq 1
user.destroy!
end
it 'destroys rates of this author' do
expect(Rating::Rate.where(author: user).count).to eq 0
end
end
context 'when destroy resource rated by author' do
before do
expect(Rating::Rate.where(resource: article).count).to eq 1
article.destroy!
end
it 'destroys rates for that resource' do
expect(Rating::Rate.where(resource: article).count).to eq 0
end
end
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Rating::Extension, ':rates' do
let!(:user) { create :user }
let!(:article) { create :article }
before { user.rate article, 3 }
it 'returns rates record' do
expect(article.rates).to eq [Rating::Rate.last]
end
context 'when destroy author' do
before do
expect(Rating::Rate.where(resource: article).count).to eq 1
user.destroy!
end
it 'destroys rates of that resource' do
expect(Rating::Rate.where(resource: article).count).to eq 0
end
end
context 'when destroy resource' do
before do
expect(Rating::Rate.where(resource: article).count).to eq 1
article.destroy!
end
it 'destroys rates of that resource' do
expect(Rating::Rate.where(resource: article).count).to eq 0
end
end
end

View File

@ -0,0 +1,38 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Rating::Extension, ':rating' do
let!(:user_1) { create :user }
let!(:article) { create :article }
before { user_1.rate article, 1 }
it 'returns rating record' do
expect(article.rating).to eq Rating::Rating.last
end
context 'when destroy author' do
let!(:user_2) { create :user }
before { user_2.rate article, 2 }
it 'does not destroy resource rating' do
expect(Rating::Rating.where(resource: article).count).to eq 1
user_1.destroy!
expect(Rating::Rating.where(resource: article).count).to eq 1
end
end
context 'when destroy resource' do
it 'destroys resource rating too' do
expect(Rating::Rating.where(resource: article).count).to eq 1
article.destroy!
expect(Rating::Rating.where(resource: article).count).to eq 0
end
end
end

View File

@ -0,0 +1,64 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Rating::Rate, ':create' do
let!(:user) { create :user }
let!(:article) { create :article }
before { create :rating_rate, author: user, resource: article, value: 3 }
context 'when rate does not exist yet' do
it 'creates a rate entry' do
rate = described_class.last
expect(rate.author).to eq user
expect(rate.resource).to eq article
expect(rate.value).to eq 3
end
it 'creates a rating entry' do
rating = Rating::Rating.last
expect(rating.average).to eq 3
expect(rating.estimate).to eq 3
expect(rating.resource).to eq article
expect(rating.sum).to eq 3
expect(rating.total).to eq 1
end
end
context 'when rate already exists' do
let!(:user_2) { create :user }
before { create :rating_rate, author: user_2, resource: article, value: 4 }
it 'creates one more rate entry' do
rates = described_class.where(author: [user, user_2]).order('created_at asc')
expect(rates.size).to eq 2
rate = rates[0]
expect(rate.author).to eq user
expect(rate.resource).to eq article
expect(rate.value).to eq 3
rate = rates[1]
expect(rate.author).to eq user_2
expect(rate.resource).to eq article
expect(rate.value).to eq 4
end
it 'updates the unique rating entry' do
rating = Rating::Rating.find_by(resource: article)
expect(rating.average).to eq 3.5
expect(rating.estimate).to eq 3.5
expect(rating.resource).to eq article
expect(rating.sum).to eq 7
expect(rating.total).to eq 2
end
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Rating::Rate, ':rate_for' do
let!(:user) { create :user }
let!(:article) { create :article }
context 'when rate does not exist' do
specify { expect(described_class.rate_for(author: user, resource: article)).to eq nil }
end
context 'when rate does not exist' do
before { described_class.create author: user, resource: article, value: 3 }
it 'returns the record' do
expect(described_class.rate_for(author: user, resource: article)).to eq described_class.last
end
end
end

View File

@ -0,0 +1,26 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Rating::Rate do
let!(:object) { build :rating_rate }
it { expect(object).to be_valid }
it { is_expected.to belong_to :author }
it { is_expected.to belong_to :resource }
it { is_expected.to validate_presence_of :author }
it { is_expected.to validate_presence_of :resource }
it { is_expected.to validate_presence_of :value }
specify do
is_expected.to validate_numericality_of(:value).is_less_than_or_equal_to(100).is_less_than_or_equal_to 100
end
specify do
expect(object).to validate_uniqueness_of(:author_id)
.scoped_to(%i[author_type resource_id resource_type])
.case_insensitive
end
end

View File

@ -0,0 +1,29 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Rating::Rating, ':averager_data' do
subject { described_class.averager_data article_1 }
let!(:user_1) { create :user }
let!(:user_2) { create :user }
let!(:article_1) { create :article }
let!(:article_2) { create :article }
let!(:article_3) { create :article }
before do
create :rating_rate, author: user_1, resource: article_1, value: 100
create :rating_rate, author: user_1, resource: article_2, value: 11
create :rating_rate, author: user_1, resource: article_3, value: 10
create :rating_rate, author: user_2, resource: article_1, value: 1
end
it 'returns the values average of given resource type' do
expect(subject.as_json['rating_avg']).to eq 30.5
end
it 'returns the average of number of records for the given resource type' do
expect(subject.as_json['count_avg']).to eq 1.3333333333333333
end
end

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Rating::Rating, ':data' do
subject { described_class.data article_1 }
let!(:user_1) { create :user }
let!(:user_2) { create :user }
let!(:article_1) { create :article }
let!(:article_2) { create :article }
let!(:article_3) { create :article }
before do
create :rating_rate, author: user_1, resource: article_1, value: 100
create :rating_rate, author: user_1, resource: article_2, value: 11
create :rating_rate, author: user_1, resource: article_3, value: 10
create :rating_rate, author: user_2, resource: article_1, value: 1
end
it 'returns the average of value for a resource' do
expect(subject[:average]).to eq 50.5
end
it 'returns the sum of values for a resource' do
expect(subject[:sum]).to eq 101
end
it 'returns the count of votes for a resource' do
expect(subject[:total]).to eq 2
end
it 'returns the estimate for a resource' do
expect(subject[:estimate]).to eq 42.50000000000001
end
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Rating::Rating, ':update_rating' do
let!(:user_1) { create :user }
let!(:user_2) { create :user }
let!(:article_1) { create :article }
let!(:article_2) { create :article }
let!(:article_3) { create :article }
before do
create :rating_rate, author: user_1, resource: article_1, value: 100
create :rating_rate, author: user_1, resource: article_2, value: 11
create :rating_rate, author: user_1, resource: article_3, value: 10
create :rating_rate, author: user_2, resource: article_1, value: 1
end
it 'updates the rating data of the given resource' do
record = described_class.find_by(resource: article_1)
expect(record.average).to eq 50.50000000000001
expect(record.estimate).to eq 42.50000000000001
expect(record.sum).to eq 101
expect(record.total).to eq 2
end
end

View File

@ -0,0 +1,33 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Rating::Rating, ':values_data' do
subject { described_class.values_data article_1 }
let!(:user_1) { create :user }
let!(:user_2) { create :user }
let!(:article_1) { create :article }
let!(:article_2) { create :article }
let!(:article_3) { create :article }
before do
create :rating_rate, author: user_1, resource: article_1, value: 100
create :rating_rate, author: user_1, resource: article_2, value: 11
create :rating_rate, author: user_1, resource: article_3, value: 10
create :rating_rate, author: user_2, resource: article_1, value: 4
end
it 'returns the average of value for a resource' do
expect(subject.as_json['rating_avg']).to eq 52.0
end
it 'returns the sum of values for a resource' do
expect(subject.as_json['rating_sum']).to eq 104
end
it 'returns the count of votes for a resource' do
expect(subject.as_json['rating_count']).to eq 2
end
end

View File

@ -0,0 +1,17 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Rating::Rating do
let!(:object) { build :rating_rating }
it { expect(object).to be_valid }
it { is_expected.to belong_to :resource }
it { is_expected.to validate_presence_of :average }
it { is_expected.to validate_presence_of :estimate }
it { is_expected.to validate_presence_of :resource }
it { is_expected.to validate_presence_of :sum }
it { is_expected.to validate_presence_of :total }
end

View File

@ -0,0 +1,11 @@
# frozen_string_literal: true
ENV['RAILS_ENV'] ||= 'test'
require 'active_record/railtie'
require 'pry-byebug'
require 'rating'
ActiveRecord::Base.establish_connection adapter: :sqlite3, database: ':memory:'
Dir[File.expand_path('support/**/*.rb', __dir__)].each { |file| require file }

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'rspec/rails'
RSpec.configure do |config|
config.filter_run_when_matching :focus
config.disable_monkey_patching!
config.infer_spec_type_from_file_location!
config.expect_with :rspec do |expectations|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.mock_with :rspec do |mocks|
mocks.verify_partial_doubles = true
end
config.infer_base_class_for_anonymous_controllers = false
config.order = :random
config.profile_examples = true
end

View File

@ -0,0 +1,19 @@
# frozen_string_literal: true
require 'database_cleaner'
RSpec.configure do |config|
config.before :suite do
DatabaseCleaner.strategy = :transaction
DatabaseCleaner.clean_with :truncation
end
config.before { DatabaseCleaner.start }
config.around do |spec|
DatabaseCleaner.cleaning do
spec.run
end
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class CreateArticlesTable < ActiveRecord::Migration[5.0]
def change
create_table :articles do |t|
t.string :name, null: false
end
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
class CreateUsersTable < ActiveRecord::Migration[5.0]
def change
create_table :users do |t|
t.string :name, null: false
end
end
end

View File

@ -0,0 +1,9 @@
# frozen_string_literal: true
require 'factory_bot'
Dir[File.expand_path('../factories/**/*.rb', __dir__)].each { |file| require file }
RSpec.configure do |config|
config.include FactoryBot::Syntax::Methods
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
require 'rspec-html-matchers'
RSpec.configure do |config|
config.include RSpecHtmlMatchers
end

View File

@ -0,0 +1,7 @@
# frozen_string_literal: true
require File.expand_path('../../lib/generators/rating/templates/db/migrate/create_rating_tables.rb', __dir__)
CreateRatingTables.new.change
CreateUsersTable.new.change
CreateArticlesTable.new.change

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
class Article < ::ActiveRecord::Base
rating
end

View File

@ -0,0 +1,5 @@
# frozen_string_literal: true
class User < ::ActiveRecord::Base
rating
end

View File

@ -0,0 +1,10 @@
# frozen_string_literal: true
require 'shoulda-matchers'
Shoulda::Matchers.configure do |config|
config.integrate do |with|
with.library :rails
with.test_framework :rspec
end
end