From 598c58af7e43ab66053a067600ca0b3d176664de Mon Sep 17 00:00:00 2001 From: Washington Botelho Date: Sun, 29 Oct 2017 23:44:23 -0200 Subject: [PATCH] first commit --- .gitignore | 2 + .rubocop.yml | 63 ++++++ .rubocop_todo.yml | 0 .ruby-gemset | 1 + .ruby-version | 1 + .travis.yml | 1 + CHANGELOG.md | 3 + Gemfile | 5 + Gemfile.lock | 187 ++++++++++++++++++ LICENSE | 21 ++ README.md | 180 +++++++++++++++++ Rakefile | 7 + lib/generators/rating/install_generator.rb | 15 ++ .../db/migrate/create_rating_tables.rb | 31 +++ lib/rating.rb | 10 + lib/rating/models/rating/extension.rb | 46 +++++ lib/rating/models/rating/rate.rb | 41 ++++ lib/rating/models/rating/rating.rb | 96 +++++++++ lib/rating/version.rb | 5 + rating.gemspec | 32 +++ spec/factories/article.rb | 7 + spec/factories/rating/rate.rb | 10 + spec/factories/rating/rating.rb | 12 ++ spec/factories/user.rb | 7 + spec/models/extension/after_create_spec.rb | 19 ++ spec/models/extension/order_by_rating_spec.rb | 121 ++++++++++++ spec/models/extension/rate_for_spec.rb | 14 ++ spec/models/extension/rate_spec.rb | 14 ++ spec/models/extension/rated_question_spec.rb | 20 ++ spec/models/extension/rated_spec.rb | 38 ++++ spec/models/extension/rates_spec.rb | 38 ++++ spec/models/extension/rating_spec.rb | 38 ++++ spec/models/rate/create_spec.rb | 64 ++++++ spec/models/rate/rate_for_spec.rb | 20 ++ spec/models/rate_spec.rb | 26 +++ spec/models/rating/averager_data_spec.rb | 29 +++ spec/models/rating/data_spec.rb | 37 ++++ spec/models/rating/update_rating_spec.rb | 28 +++ spec/models/rating/values_data_spec.rb | 33 ++++ spec/models/rating_spec.rb | 17 ++ spec/rails_helper.rb | 11 ++ spec/support/common.rb | 22 +++ spec/support/database_cleaner.rb | 19 ++ .../db/migrate/create_articles_table.rb | 9 + spec/support/db/migrate/create_users_table.rb | 9 + spec/support/factory_bot.rb | 9 + spec/support/html_matchers.rb | 7 + spec/support/migrate.rb | 7 + spec/support/models/article.rb | 5 + spec/support/models/user.rb | 5 + spec/support/shoulda.rb | 10 + 51 files changed, 1452 insertions(+) create mode 100644 .gitignore create mode 100644 .rubocop.yml create mode 100644 .rubocop_todo.yml create mode 100644 .ruby-gemset create mode 100644 .ruby-version create mode 100644 .travis.yml create mode 100644 CHANGELOG.md create mode 100644 Gemfile create mode 100644 Gemfile.lock create mode 100644 LICENSE create mode 100644 README.md create mode 100644 Rakefile create mode 100644 lib/generators/rating/install_generator.rb create mode 100644 lib/generators/rating/templates/db/migrate/create_rating_tables.rb create mode 100644 lib/rating.rb create mode 100644 lib/rating/models/rating/extension.rb create mode 100644 lib/rating/models/rating/rate.rb create mode 100644 lib/rating/models/rating/rating.rb create mode 100644 lib/rating/version.rb create mode 100644 rating.gemspec create mode 100644 spec/factories/article.rb create mode 100644 spec/factories/rating/rate.rb create mode 100644 spec/factories/rating/rating.rb create mode 100644 spec/factories/user.rb create mode 100644 spec/models/extension/after_create_spec.rb create mode 100644 spec/models/extension/order_by_rating_spec.rb create mode 100644 spec/models/extension/rate_for_spec.rb create mode 100644 spec/models/extension/rate_spec.rb create mode 100644 spec/models/extension/rated_question_spec.rb create mode 100644 spec/models/extension/rated_spec.rb create mode 100644 spec/models/extension/rates_spec.rb create mode 100644 spec/models/extension/rating_spec.rb create mode 100644 spec/models/rate/create_spec.rb create mode 100644 spec/models/rate/rate_for_spec.rb create mode 100644 spec/models/rate_spec.rb create mode 100644 spec/models/rating/averager_data_spec.rb create mode 100644 spec/models/rating/data_spec.rb create mode 100644 spec/models/rating/update_rating_spec.rb create mode 100644 spec/models/rating/values_data_spec.rb create mode 100644 spec/models/rating_spec.rb create mode 100644 spec/rails_helper.rb create mode 100644 spec/support/common.rb create mode 100644 spec/support/database_cleaner.rb create mode 100644 spec/support/db/migrate/create_articles_table.rb create mode 100644 spec/support/db/migrate/create_users_table.rb create mode 100644 spec/support/factory_bot.rb create mode 100644 spec/support/html_matchers.rb create mode 100644 spec/support/migrate.rb create mode 100644 spec/support/models/article.rb create mode 100644 spec/support/models/user.rb create mode 100644 spec/support/shoulda.rb diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e1052c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.byebug_history +*.gem diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000..aa4331e --- /dev/null +++ b/.rubocop.yml @@ -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 diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml new file mode 100644 index 0000000..e69de29 diff --git a/.ruby-gemset b/.ruby-gemset new file mode 100644 index 0000000..2202a51 --- /dev/null +++ b/.ruby-gemset @@ -0,0 +1 @@ +rating diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..8e8299d --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +2.4.2 diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f819a51 --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: ruby diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ae045c3 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## v0.1.0 + +- First release. diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..7f4f5e9 --- /dev/null +++ b/Gemfile @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +source 'https://rubygems.org' + +gemspec diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..075d65b --- /dev/null +++ b/Gemfile.lock @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..694ff7c --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a78fcf7 --- /dev/null +++ b/README.md @@ -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! (: diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..dc8c23d --- /dev/null +++ b/Rakefile @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'rspec/core/rake_task' + +RSpec::Core::RakeTask.new + +task default: :spec diff --git a/lib/generators/rating/install_generator.rb b/lib/generators/rating/install_generator.rb new file mode 100644 index 0000000..5332239 --- /dev/null +++ b/lib/generators/rating/install_generator.rb @@ -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 diff --git a/lib/generators/rating/templates/db/migrate/create_rating_tables.rb b/lib/generators/rating/templates/db/migrate/create_rating_tables.rb new file mode 100644 index 0000000..48b3dec --- /dev/null +++ b/lib/generators/rating/templates/db/migrate/create_rating_tables.rb @@ -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 diff --git a/lib/rating.rb b/lib/rating.rb new file mode 100644 index 0000000..68a56da --- /dev/null +++ b/lib/rating.rb @@ -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 diff --git a/lib/rating/models/rating/extension.rb b/lib/rating/models/rating/extension.rb new file mode 100644 index 0000000..07a2e85 --- /dev/null +++ b/lib/rating/models/rating/extension.rb @@ -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 diff --git a/lib/rating/models/rating/rate.rb b/lib/rating/models/rating/rate.rb new file mode 100644 index 0000000..7ca79ab --- /dev/null +++ b/lib/rating/models/rating/rate.rb @@ -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 diff --git a/lib/rating/models/rating/rating.rb b/lib/rating/models/rating/rating.rb new file mode 100644 index 0000000..b07a1dc --- /dev/null +++ b/lib/rating/models/rating/rating.rb @@ -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 diff --git a/lib/rating/version.rb b/lib/rating/version.rb new file mode 100644 index 0000000..8b9cb0c --- /dev/null +++ b/lib/rating/version.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +module Rating + VERSION = '0.1.0' +end diff --git a/rating.gemspec b/rating.gemspec new file mode 100644 index 0000000..eb2a622 --- /dev/null +++ b/rating.gemspec @@ -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 diff --git a/spec/factories/article.rb b/spec/factories/article.rb new file mode 100644 index 0000000..7c18c71 --- /dev/null +++ b/spec/factories/article.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :article do + sequence(:name) { |i| "Name #{i}" } + end +end diff --git a/spec/factories/rating/rate.rb b/spec/factories/rating/rate.rb new file mode 100644 index 0000000..5b85595 --- /dev/null +++ b/spec/factories/rating/rate.rb @@ -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 diff --git a/spec/factories/rating/rating.rb b/spec/factories/rating/rating.rb new file mode 100644 index 0000000..920540d --- /dev/null +++ b/spec/factories/rating/rating.rb @@ -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 diff --git a/spec/factories/user.rb b/spec/factories/user.rb new file mode 100644 index 0000000..d6e337f --- /dev/null +++ b/spec/factories/user.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +FactoryBot.define do + factory :user do + sequence(:name) { |i| "Name #{i}" } + end +end diff --git a/spec/models/extension/after_create_spec.rb b/spec/models/extension/after_create_spec.rb new file mode 100644 index 0000000..6c68ce0 --- /dev/null +++ b/spec/models/extension/after_create_spec.rb @@ -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 diff --git a/spec/models/extension/order_by_rating_spec.rb b/spec/models/extension/order_by_rating_spec.rb new file mode 100644 index 0000000..7f6b450 --- /dev/null +++ b/spec/models/extension/order_by_rating_spec.rb @@ -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 diff --git a/spec/models/extension/rate_for_spec.rb b/spec/models/extension/rate_for_spec.rb new file mode 100644 index 0000000..daa4bd0 --- /dev/null +++ b/spec/models/extension/rate_for_spec.rb @@ -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 diff --git a/spec/models/extension/rate_spec.rb b/spec/models/extension/rate_spec.rb new file mode 100644 index 0000000..ea4b9a8 --- /dev/null +++ b/spec/models/extension/rate_spec.rb @@ -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 diff --git a/spec/models/extension/rated_question_spec.rb b/spec/models/extension/rated_question_spec.rb new file mode 100644 index 0000000..29f84f2 --- /dev/null +++ b/spec/models/extension/rated_question_spec.rb @@ -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 diff --git a/spec/models/extension/rated_spec.rb b/spec/models/extension/rated_spec.rb new file mode 100644 index 0000000..b20e91b --- /dev/null +++ b/spec/models/extension/rated_spec.rb @@ -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 diff --git a/spec/models/extension/rates_spec.rb b/spec/models/extension/rates_spec.rb new file mode 100644 index 0000000..1f3167a --- /dev/null +++ b/spec/models/extension/rates_spec.rb @@ -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 diff --git a/spec/models/extension/rating_spec.rb b/spec/models/extension/rating_spec.rb new file mode 100644 index 0000000..574a257 --- /dev/null +++ b/spec/models/extension/rating_spec.rb @@ -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 diff --git a/spec/models/rate/create_spec.rb b/spec/models/rate/create_spec.rb new file mode 100644 index 0000000..ae1f7fc --- /dev/null +++ b/spec/models/rate/create_spec.rb @@ -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 diff --git a/spec/models/rate/rate_for_spec.rb b/spec/models/rate/rate_for_spec.rb new file mode 100644 index 0000000..1527421 --- /dev/null +++ b/spec/models/rate/rate_for_spec.rb @@ -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 diff --git a/spec/models/rate_spec.rb b/spec/models/rate_spec.rb new file mode 100644 index 0000000..c343615 --- /dev/null +++ b/spec/models/rate_spec.rb @@ -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 diff --git a/spec/models/rating/averager_data_spec.rb b/spec/models/rating/averager_data_spec.rb new file mode 100644 index 0000000..7257beb --- /dev/null +++ b/spec/models/rating/averager_data_spec.rb @@ -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 diff --git a/spec/models/rating/data_spec.rb b/spec/models/rating/data_spec.rb new file mode 100644 index 0000000..cbbedad --- /dev/null +++ b/spec/models/rating/data_spec.rb @@ -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 diff --git a/spec/models/rating/update_rating_spec.rb b/spec/models/rating/update_rating_spec.rb new file mode 100644 index 0000000..416ec35 --- /dev/null +++ b/spec/models/rating/update_rating_spec.rb @@ -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 diff --git a/spec/models/rating/values_data_spec.rb b/spec/models/rating/values_data_spec.rb new file mode 100644 index 0000000..ff42692 --- /dev/null +++ b/spec/models/rating/values_data_spec.rb @@ -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 diff --git a/spec/models/rating_spec.rb b/spec/models/rating_spec.rb new file mode 100644 index 0000000..5fd3bf9 --- /dev/null +++ b/spec/models/rating_spec.rb @@ -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 diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb new file mode 100644 index 0000000..c3e5af4 --- /dev/null +++ b/spec/rails_helper.rb @@ -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 } diff --git a/spec/support/common.rb b/spec/support/common.rb new file mode 100644 index 0000000..d601c1b --- /dev/null +++ b/spec/support/common.rb @@ -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 diff --git a/spec/support/database_cleaner.rb b/spec/support/database_cleaner.rb new file mode 100644 index 0000000..43fbd7c --- /dev/null +++ b/spec/support/database_cleaner.rb @@ -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 diff --git a/spec/support/db/migrate/create_articles_table.rb b/spec/support/db/migrate/create_articles_table.rb new file mode 100644 index 0000000..96fa9ae --- /dev/null +++ b/spec/support/db/migrate/create_articles_table.rb @@ -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 diff --git a/spec/support/db/migrate/create_users_table.rb b/spec/support/db/migrate/create_users_table.rb new file mode 100644 index 0000000..f3e4835 --- /dev/null +++ b/spec/support/db/migrate/create_users_table.rb @@ -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 diff --git a/spec/support/factory_bot.rb b/spec/support/factory_bot.rb new file mode 100644 index 0000000..9a650f3 --- /dev/null +++ b/spec/support/factory_bot.rb @@ -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 diff --git a/spec/support/html_matchers.rb b/spec/support/html_matchers.rb new file mode 100644 index 0000000..dfab9c8 --- /dev/null +++ b/spec/support/html_matchers.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +require 'rspec-html-matchers' + +RSpec.configure do |config| + config.include RSpecHtmlMatchers +end diff --git a/spec/support/migrate.rb b/spec/support/migrate.rb new file mode 100644 index 0000000..f768526 --- /dev/null +++ b/spec/support/migrate.rb @@ -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 diff --git a/spec/support/models/article.rb b/spec/support/models/article.rb new file mode 100644 index 0000000..85e8d8c --- /dev/null +++ b/spec/support/models/article.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class Article < ::ActiveRecord::Base + rating +end diff --git a/spec/support/models/user.rb b/spec/support/models/user.rb new file mode 100644 index 0000000..a92ffd2 --- /dev/null +++ b/spec/support/models/user.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class User < ::ActiveRecord::Base + rating +end diff --git a/spec/support/shoulda.rb b/spec/support/shoulda.rb new file mode 100644 index 0000000..18b550e --- /dev/null +++ b/spec/support/shoulda.rb @@ -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