diff --git a/app/assets/javascripts/rate/rating_file.js b/app/assets/javascripts/rate/rating_file.js new file mode 100644 index 00000000..43fa766b --- /dev/null +++ b/app/assets/javascripts/rate/rating_file.js @@ -0,0 +1 @@ +!function(t){var e={};function r(a){if(e[a])return e[a].exports;var s=e[a]={i:a,l:!1,exports:{}};return t[a].call(s.exports,s,s.exports,r),s.l=!0,s.exports}r.m=t,r.c=e,r.d=function(t,e,a){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:a})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var a=Object.create(null);if(r.r(a),Object.defineProperty(a,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var s in t)r.d(a,s,function(e){return t[e]}.bind(null,s));return a},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="",r(r.s=0)}([function(t,e){const r={value:Number($("#rating_rate").val()),stars:5,half:!1,emptyStar:"far fa-star",halfStar:"fas fa-star-half-alt",filledStar:"fas fa-star",color:"#fcd703",readonly:!1,click:function(t){console.error("No click callback provided!")}};jQuery.fn.extend({rating:function(t={}){return this.each((function(){$(this).attr("rating")&&$(this).empty(),this.stars=t.value?t.value:r.value,this.readonly=t.readonly?t.readonly:r.readonly,this.getStars=function(){return $(this).find($("i"))},$(this).css({color:t.color?t.color:r.color}).attr("rating",!0),this.readonly||($(this).off("mousemove").on("mousemove",(function(e){let a=t.half?t.half:r.half;if(this.getStars().index(e.target)>=0)if(a){$(this).find("i").attr("class",t.emptyStar?t.emptyStar:r.emptyStar);let a=.5;$(this).find("i").css({width:$(this).find("i").outerWidth()}),e.offsetX>$(e.target).outerWidth()/2&&(a=1);let s=this.getStars().index(e.target)+a;for(let e=0;e$(e.target).outerWidth()/2&&(t=1),this.stars=this.getStars().index(e.target)+t}else this.stars=this.getStars().index(e.target)+1;(t.click?t.click:r.click)({stars:this.stars,event:e})})));const e=t.stars?t.stars:r.stars;for(let a=0;a").addClass(t.emptyStar?t.emptyStar:r.emptyStar).appendTo($(this));if(this.readonly||e.css({cursor:"pointer"}),a>1e3)return}if(this.printStars=function(){if(t.half?t.half:r.half){$(this).find("i").attr("class",t.emptyStar?t.emptyStar:r.emptyStar);for(let e=0;e0){this.printStars();(t.click?t.click:r.click)({stars:this.stars})}}))}}),$((function(){$("[data-rating-stars]").each((function(){let t={},e=/^data-rating\-(.+)$/;$.each($(this).get(0).attributes,(function(r,a){if(e.test(a.nodeName)){let r=a.nodeName.match(e)[1];t[r]=a.nodeValue}})),null!=t.input&&(t.click=function(e){$(t.input).val(e.stars)}),$(this).rating(t)}))}))}]); \ No newline at end of file diff --git a/app/assets/javascripts/rate/rating_init.js b/app/assets/javascripts/rate/rating_init.js new file mode 100644 index 00000000..e08dfb32 --- /dev/null +++ b/app/assets/javascripts/rate/rating_init.js @@ -0,0 +1,16 @@ +if ($("#review").length > 0) { + + $("#review").rating({ + "click":function (e) { + if (e.stars > 0 && e.stars <= 5) { + if ($("#new_rating").length > 0) { + $("#rating_rate").val(e.stars); + $("#new_rating").submit(); + } else { + $("#rating_rate").val(e.stars); + $(".edit_rating").submit(); + } + } + } + }); +} diff --git a/app/controllers/articles_controller.rb b/app/controllers/articles_controller.rb index 4e471726..afa2337e 100644 --- a/app/controllers/articles_controller.rb +++ b/app/controllers/articles_controller.rb @@ -2,6 +2,7 @@ class ArticlesController < ApplicationController expose_decorated :article, -> { set_article } expose :comment, -> { set_comment } expose_decorated :company, -> { article.company } + expose :rating, -> { set_rating } layout "company" @@ -9,13 +10,17 @@ def show authorize article end + private + def set_article - Article.includes(:comments).find_by(id: params["id"]) + Article.includes(:comments, :ratings).find_by(id: params["id"]) end - private - def set_comment article.comments.new end + + def set_rating + Rating.find_or_initialize_by(article: article, user: current_user) + end end diff --git a/app/controllers/ratings_controller.rb b/app/controllers/ratings_controller.rb new file mode 100644 index 00000000..07b6df5d --- /dev/null +++ b/app/controllers/ratings_controller.rb @@ -0,0 +1,24 @@ +class RatingsController < ApplicationController + expose :rating + before_action :authenticate_user! + expose_decorated :article + expose :rating, build: ->(rating_params) { Rating.new(rating_params) } + + def create + redirect_to article_path(article) unless rating.save + end + + def update + redirect_to article_path(article) unless rating.update rating_params + end + + private + + def rating_params + { + user: current_user, + article_id: params[:article_id], + rate: params[:rating][:rate] + } + end +end diff --git a/app/decorators/article_decorator.rb b/app/decorators/article_decorator.rb index 3eeb8cce..08de3e79 100644 --- a/app/decorators/article_decorator.rb +++ b/app/decorators/article_decorator.rb @@ -13,4 +13,10 @@ def author def sorted_comments object.comments.sorted_by_created_at end + + def rating + return "Nobody has rated on this article yet" unless object.ratings.any? + + "#{object.ratings.average(:rate).round(2)}/5" + end end diff --git a/app/models/article.rb b/app/models/article.rb index b291c7da..f3924920 100644 --- a/app/models/article.rb +++ b/app/models/article.rb @@ -8,6 +8,7 @@ class Article < ApplicationRecord belongs_to :user belongs_to :company has_many :comments, dependent: :destroy + has_many :ratings, dependent: :destroy paginates_per 5 diff --git a/app/models/rating.rb b/app/models/rating.rb new file mode 100644 index 00000000..84342f9d --- /dev/null +++ b/app/models/rating.rb @@ -0,0 +1,6 @@ +class Rating < ApplicationRecord + validates :rate, inclusion: { in: [1, 2, 3, 4, 5] } + + belongs_to :user, required: true + belongs_to :article, required: true +end diff --git a/app/views/articles/_form.html.slim b/app/views/articles/_form.html.slim new file mode 100644 index 00000000..2fe219a1 --- /dev/null +++ b/app/views/articles/_form.html.slim @@ -0,0 +1,5 @@ +.row(id="rating_data") + .medium-6.columns + = "Rating: #{article.rating}" + = simple_form_for [article, rating], remote: true do |f| + = f.input :rate, as: :hidden diff --git a/app/views/articles/show.html.slim b/app/views/articles/show.html.slim index c5648094..ff584ce0 100644 --- a/app/views/articles/show.html.slim +++ b/app/views/articles/show.html.slim @@ -12,6 +12,9 @@ article.showed = "Published #{distance_of_time_in_words_to_now(article.updated_at)} ago" div = "#{article.author}" + .medium-2.columns + div(id="review") + = render "form" = render article.sorted_comments diff --git a/app/views/layouts/company.html.slim b/app/views/layouts/company.html.slim index 5ed0efb9..ab4669ae 100644 --- a/app/views/layouts/company.html.slim +++ b/app/views/layouts/company.html.slim @@ -11,6 +11,7 @@ html class="no-js" lang="en" = csrf_meta_tags = stylesheet_link_tag :application + = stylesheet_link_tag "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.11.2/css/all.min.css" = javascript_tag "window.App = {}" diff --git a/app/views/ratings/create.js.erb b/app/views/ratings/create.js.erb new file mode 100644 index 00000000..1af9eb12 --- /dev/null +++ b/app/views/ratings/create.js.erb @@ -0,0 +1,3 @@ +$("#rating_data").html(""); + +$("#rating_data").append("<%= escape_javascript(render :partial => 'articles/form') %>"); diff --git a/app/views/ratings/update.js.erb b/app/views/ratings/update.js.erb new file mode 100644 index 00000000..1af9eb12 --- /dev/null +++ b/app/views/ratings/update.js.erb @@ -0,0 +1,3 @@ +$("#rating_data").html(""); + +$("#rating_data").append("<%= escape_javascript(render :partial => 'articles/form') %>"); diff --git a/config/routes.rb b/config/routes.rb index d09ada2f..fe8955e5 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -24,6 +24,7 @@ resources :articles, only: :show do resources :comments, except: %i[index new] + resources :ratings, only: %i[create update] end namespace :admin_scope do diff --git a/db/migrate/20201129151451_create_ratings.rb b/db/migrate/20201129151451_create_ratings.rb new file mode 100644 index 00000000..f439137d --- /dev/null +++ b/db/migrate/20201129151451_create_ratings.rb @@ -0,0 +1,11 @@ +class CreateRatings < ActiveRecord::Migration[5.2] + def change + create_table :ratings do |t| + t.references :article, foreign_key: true, null: false + t.references :user, foreign_key: true, null: false + t.integer :rate, default: 0, null: false + end + + add_index :ratings, [:article_id, :user_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 1f152fd4..22f55e92 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -61,6 +61,15 @@ t.index ["company_id"], name: "index_companies_users_on_company_id" end + create_table "ratings", force: :cascade do |t| + t.bigint "article_id", null: false + t.bigint "user_id", null: false + t.integer "rate", default: 0, null: false + t.index ["article_id", "user_id"], name: "index_ratings_on_article_id_and_user_id", unique: true + t.index ["article_id"], name: "index_ratings_on_article_id" + t.index ["user_id"], name: "index_ratings_on_user_id" + end + create_table "static_pages", force: :cascade do |t| t.string "title" t.text "content" @@ -101,4 +110,6 @@ t.index ["unlock_token"], name: "index_users_on_unlock_token", unique: true end + add_foreign_key "ratings", "articles" + add_foreign_key "ratings", "users" end diff --git a/spec/factories/ratings.rb b/spec/factories/ratings.rb new file mode 100644 index 00000000..2b7e706f --- /dev/null +++ b/spec/factories/ratings.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :rating do + user + article + rate { 5 } + end +end diff --git a/spec/features/user/ratings/create_spec.rb b/spec/features/user/ratings/create_spec.rb new file mode 100644 index 00000000..56f73714 --- /dev/null +++ b/spec/features/user/ratings/create_spec.rb @@ -0,0 +1,20 @@ +require "rails_helper" + +feature "Create Rating" do + include_context "current user signed in" + let(:company) { create :company, owner: current_user } + let(:article) { create :article, :company, user: current_user, company: company } + let(:another_user) { create :user } + + background do + create :rating, user: another_user, article: article, rate: 5 + visit article_path(article) + end + + scenario "User choose 1 point", js: true do + expect(page).to have_content("5.0/5") + star = find(:xpath, "//i[1]") + star.click + expect(page).to have_content("3.0/5") + end +end diff --git a/spec/features/user/ratings/update_spec.rb b/spec/features/user/ratings/update_spec.rb new file mode 100644 index 00000000..1bbc5f7f --- /dev/null +++ b/spec/features/user/ratings/update_spec.rb @@ -0,0 +1,18 @@ +require "rails_helper" + +feature "Update Rating" do + include_context "current user signed in" + let(:company) { create :company, owner: current_user } + let(:article) { create :article, :company, user: current_user, company: company } + + background do + create :rating, user: current_user, article: article, rate: 4 + visit article_path(article) + end + + scenario "User choose 3 points", js: true do + expect(page).to have_content("4.0/5") + find(:xpath, "//i[3]").click + expect(page).to have_content("3.0/5") + end +end