From 79197b907f4bd23a8b8dfda17aae12b787f5a0ef Mon Sep 17 00:00:00 2001 From: PaoloCappelli Date: Tue, 5 Mar 2024 20:35:45 -0300 Subject: [PATCH 1/7] Create Review model --- app/models/review.rb | 7 +++++++ app/models/subject.rb | 1 + app/models/user.rb | 2 ++ db/migrate/20240305231624_create_reviews.rb | 13 +++++++++++++ db/schema.rb | 16 +++++++++++++++- 5 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 app/models/review.rb create mode 100644 db/migrate/20240305231624_create_reviews.rb diff --git a/app/models/review.rb b/app/models/review.rb new file mode 100644 index 00000000..2eeaac7a --- /dev/null +++ b/app/models/review.rb @@ -0,0 +1,7 @@ +class Review < ApplicationRecord + belongs_to :user + belongs_to :subject + + validates :user_id, uniqueness: { scope: :subject_id, message: "You can only review a subject once." } + validates :rating, presence: true, inclusion: { in: 1..5 } +end diff --git a/app/models/subject.rb b/app/models/subject.rb index f825715c..56b0e979 100644 --- a/app/models/subject.rb +++ b/app/models/subject.rb @@ -2,6 +2,7 @@ class Subject < ApplicationRecord has_one :course, -> { where is_exam: false }, class_name: 'Approvable', dependent: :destroy, inverse_of: :subject has_one :exam, -> { where is_exam: true }, class_name: 'Approvable', dependent: :destroy, inverse_of: :subject belongs_to :group, class_name: 'SubjectGroup', optional: true + has_many :reviews, dependent: :destroy validates :name, presence: true validates :credits, presence: true diff --git a/app/models/user.rb b/app/models/user.rb index c9f7ad73..d8eb2678 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -5,6 +5,8 @@ class User < ApplicationRecord serialize :approvals, type: Array, coder: YAML + has_many :reviews, dependent: :destroy + def self.from_omniauth(auth, cookie) # check that user with same email exists existing_user = User.find_by(email: auth.info.email) diff --git a/db/migrate/20240305231624_create_reviews.rb b/db/migrate/20240305231624_create_reviews.rb new file mode 100644 index 00000000..5f8d8566 --- /dev/null +++ b/db/migrate/20240305231624_create_reviews.rb @@ -0,0 +1,13 @@ +class CreateReviews < ActiveRecord::Migration[7.1] + def change + create_table :reviews do |t| + t.references :user, null: false, foreign_key: true + t.references :subject, null: false, foreign_key: true + t.integer :rating + + t.timestamps + end + + add_index :reviews, [:user_id, :subject_id], unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index c4d032ec..b83aeee9 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_02_22_002214) do +ActiveRecord::Schema[8.0].define(version: 2024_03_05_231624) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "unaccent" @@ -33,6 +33,17 @@ t.integer "amount_of_subjects_needed" end + create_table "reviews", force: :cascade do |t| + t.bigint "user_id", null: false + t.bigint "subject_id", null: false + t.integer "rating" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["subject_id"], name: "index_reviews_on_subject_id" + t.index ["user_id", "subject_id"], name: "index_reviews_on_user_id_and_subject_id", unique: true + t.index ["user_id"], name: "index_reviews_on_user_id" + end + create_table "subject_groups", force: :cascade do |t| t.string "name", null: false t.datetime "created_at", precision: nil, null: false @@ -72,4 +83,7 @@ t.index ["email"], name: "index_users_on_email", unique: true t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true end + + add_foreign_key "reviews", "subjects" + add_foreign_key "reviews", "users" end From 02a523eca217cdeda53b78c2b00e7d4cff238c70 Mon Sep 17 00:00:00 2001 From: PaoloCappelli Date: Sat, 16 Mar 2024 02:17:29 -0300 Subject: [PATCH 2/7] Test Review model --- spec/factories/reviews.rb | 7 +++++++ spec/models/review_spec.rb | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 spec/factories/reviews.rb create mode 100644 spec/models/review_spec.rb diff --git a/spec/factories/reviews.rb b/spec/factories/reviews.rb new file mode 100644 index 00000000..85b7f5f1 --- /dev/null +++ b/spec/factories/reviews.rb @@ -0,0 +1,7 @@ +FactoryBot.define do + factory :review do + user + subject + rating { 3 } + end +end diff --git a/spec/models/review_spec.rb b/spec/models/review_spec.rb new file mode 100644 index 00000000..f785351d --- /dev/null +++ b/spec/models/review_spec.rb @@ -0,0 +1,20 @@ +require 'rails_helper' + +RSpec.describe Review, type: :model do + describe 'associations' do + it { is_expected.to belong_to(:user) } + it { is_expected.to belong_to(:subject) } + end + + describe 'validations' do + subject { create :review } + + it { + is_expected.to validate_uniqueness_of(:user_id) + .scoped_to(:subject_id) + .with_message("You can only review a subject once.") + } + it { is_expected.to validate_presence_of(:rating) } + it { is_expected.to validate_inclusion_of(:rating).in_range(1..5) } + end +end From 7182369d6b3d538b085ff9b2ce3bfc59bbb6c1aa Mon Sep 17 00:00:00 2001 From: PaoloCappelli Date: Sat, 16 Mar 2024 02:36:21 -0300 Subject: [PATCH 3/7] Add average rating to Subject --- db/migrate/20240316053425_add_average_rating_to_subjects.rb | 5 +++++ db/schema.rb | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 db/migrate/20240316053425_add_average_rating_to_subjects.rb diff --git a/db/migrate/20240316053425_add_average_rating_to_subjects.rb b/db/migrate/20240316053425_add_average_rating_to_subjects.rb new file mode 100644 index 00000000..eb0c69fe --- /dev/null +++ b/db/migrate/20240316053425_add_average_rating_to_subjects.rb @@ -0,0 +1,5 @@ +class AddAverageRatingToSubjects < ActiveRecord::Migration[7.1] + def change + add_column :subjects, :average_rating, :float + end +end diff --git a/db/schema.rb b/db/schema.rb index b83aeee9..a247daac 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2024_03_05_231624) do +ActiveRecord::Schema[8.0].define(version: 2024_03_16_053425) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" enable_extension "unaccent" @@ -65,6 +65,7 @@ t.string "code" t.string "category", default: "optional" t.boolean "current_optional_subject", default: false + t.float "average_rating" t.index ["code"], name: "index_subjects_on_code", unique: true end From deaa99ebce50ddc0a5f6a31f67b8425178d11f22 Mon Sep 17 00:00:00 2001 From: PaoloCappelli Date: Sat, 16 Mar 2024 02:45:39 -0300 Subject: [PATCH 4/7] Update average rating after committing a review --- app/models/review.rb | 9 +++++++++ app/models/subject.rb | 4 ++++ 2 files changed, 13 insertions(+) diff --git a/app/models/review.rb b/app/models/review.rb index 2eeaac7a..1067014b 100644 --- a/app/models/review.rb +++ b/app/models/review.rb @@ -4,4 +4,13 @@ class Review < ApplicationRecord validates :user_id, uniqueness: { scope: :subject_id, message: "You can only review a subject once." } validates :rating, presence: true, inclusion: { in: 1..5 } + + after_commit :update_subject_rating, if: :saved_change_to_rating? + after_destroy :update_subject_rating + + private + + def update_subject_rating + subject.update_rating + end end diff --git a/app/models/subject.rb b/app/models/subject.rb index 56b0e979..4ce17120 100644 --- a/app/models/subject.rb +++ b/app/models/subject.rb @@ -48,5 +48,9 @@ def hidden_by_default? revalid? || inactive? || outside_montevideo? || extension_module? end + def update_rating + update(average_rating: reviews.average(:rating)) + end + delegate :available?, to: :course end From 10f1c231a641b49a79793242a1440e6600c7f4b5 Mon Sep 17 00:00:00 2001 From: PaoloCappelli Date: Sat, 16 Mar 2024 03:08:33 -0300 Subject: [PATCH 5/7] Test average rating update --- spec/models/review_spec.rb | 29 +++++++++++++++++++++++++++++ spec/models/subject_spec.rb | 21 +++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/spec/models/review_spec.rb b/spec/models/review_spec.rb index f785351d..fc456517 100644 --- a/spec/models/review_spec.rb +++ b/spec/models/review_spec.rb @@ -17,4 +17,33 @@ it { is_expected.to validate_presence_of(:rating) } it { is_expected.to validate_inclusion_of(:rating).in_range(1..5) } end + + describe 'callbacks' do + context 'calls update_rating on subject after commit' do + let(:review) { build :review } + + it 'calls update_rating on subject after create' do + expect(review.subject).to receive(:update_rating) + review.save! + end + + it 'calls update_rating on subject after update' do + review.save! + expect(review.subject).to receive(:update_rating) + review.update!(rating: 5) + end + + it 'calls update_rating on subject after destroy' do + review.save! + expect(review.reload.subject).to receive(:update_rating) + review.destroy! + end + + it 'does not call update_rating on subject if rating was not changed' do + review.save! + expect(review.subject).not_to receive(:update_rating) + review.update!(rating: review.rating) + end + end + end end diff --git a/spec/models/subject_spec.rb b/spec/models/subject_spec.rb index 36dc7afe..5347a2ec 100644 --- a/spec/models/subject_spec.rb +++ b/spec/models/subject_spec.rb @@ -119,4 +119,25 @@ expect(Subject.current_semester_optionals).to eq([s1]) end end + + describe '#update_rating' do + let(:subject) { create :subject } + + context 'when there are no reviews' do + it 'updates average rating' do + subject.update_rating + expect(subject.average_rating).to eq(nil) + end + end + + context 'when there are reviews' do + let!(:review) { create :review, subject: subject, rating: 5 } + let!(:another_review) { create :review, subject: subject, rating: 3 } + + it 'updates average rating' do + subject.update_rating + expect(subject.average_rating).to eq(4) + end + end + end end From 925652e47a84812945345d934b5fc5df54c06418 Mon Sep 17 00:00:00 2001 From: PaoloCappelli Date: Tue, 19 Nov 2024 23:10:59 -0300 Subject: [PATCH 6/7] Create Reviews controller --- app/controllers/reviews_controller.rb | 28 ++++++++++ config/routes.rb | 1 + spec/controllers/reviews_controller_spec.rb | 60 +++++++++++++++++++++ spec/rails_helper.rb | 2 + 4 files changed, 91 insertions(+) create mode 100644 app/controllers/reviews_controller.rb create mode 100644 spec/controllers/reviews_controller_spec.rb diff --git a/app/controllers/reviews_controller.rb b/app/controllers/reviews_controller.rb new file mode 100644 index 00000000..e1a25a9d --- /dev/null +++ b/app/controllers/reviews_controller.rb @@ -0,0 +1,28 @@ +class ReviewsController < ApplicationController + before_action :authenticate_user! + before_action :set_review, only: [:update, :destroy] + + def create + @review = current_user.reviews.create!(subject_id: params[:subject_id], rating: params[:rating]) + + redirect_to subject_path(@review.subject) + end + + def update + @review.update!(rating: params[:rating]) + + redirect_to subject_path(@review.subject) + end + + def destroy + @review.destroy! + + redirect_to subject_path(@review.subject) + end + + private + + def set_review + @review = current_user.reviews.find(params[:id]) + end +end diff --git a/config/routes.rb b/config/routes.rb index 669cd17c..ca9c080a 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -29,4 +29,5 @@ resource :user_onboardings, only: :update resources :current_optional_subjects, only: :index + resources :reviews, only: [:create, :update, :destroy] end diff --git a/spec/controllers/reviews_controller_spec.rb b/spec/controllers/reviews_controller_spec.rb new file mode 100644 index 00000000..98e92a2a --- /dev/null +++ b/spec/controllers/reviews_controller_spec.rb @@ -0,0 +1,60 @@ +require 'rails_helper' + +RSpec.describe ReviewsController, type: :request do + let(:user) { create(:user) } + let(:subject) { create(:subject) } + let(:review) { create(:review, user: user, subject: subject) } + + before do + # https://github.com/heartcombo/devise/issues/5705 + Rails.application.reload_routes_unless_loaded + end + + before do + sign_in user + end + + describe 'POST #create' do + it 'creates a new review' do + expect { + post reviews_path, params: { subject_id: subject.id, rating: 5 } + }.to change(Review, :count).by(1) + end + + it 'redirects to the subject page' do + post reviews_path, params: { subject_id: subject.id, rating: 5 } + + expect(response).to redirect_to(subject_path(subject)) + end + end + + describe 'PATCH #update' do + it 'updates the review' do + patch review_path(review), params: { rating: 4 } + + expect(review.reload.rating).to eq(4) + end + + it 'redirects to the subject page' do + patch review_path(review), params: { rating: 4 } + + expect(response).to redirect_to(subject_path(review.subject)) + end + end + + describe 'DELETE #destroy' do + it 'deletes the review' do + review + + expect { + delete review_path(review) + }.to change(Review, :count).by(-1) + end + + it 'redirects to the subject page' do + delete review_path(review) + + expect(response).to redirect_to(subject_path(review.subject)) + end + end +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index ac8cf556..02b0d807 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -60,6 +60,8 @@ config.filter_rails_from_backtrace! # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") + + config.include Devise::Test::IntegrationHelpers, type: :request end Shoulda::Matchers.configure do |config| From c3adc2074121941651db64194c86a4910bcbf791 Mon Sep 17 00:00:00 2001 From: PaoloCappelli Date: Tue, 19 Nov 2024 23:11:40 -0300 Subject: [PATCH 7/7] Create reviews UI --- app/assets/stylesheets/_rating.scss | 26 ++++++++++++++++++++ app/controllers/subjects_controller.rb | 2 ++ app/views/subjects/_rating.html.erb | 33 ++++++++++++++++++++++++++ app/views/subjects/show.html.erb | 2 ++ 4 files changed, 63 insertions(+) create mode 100644 app/assets/stylesheets/_rating.scss create mode 100644 app/views/subjects/_rating.html.erb diff --git a/app/assets/stylesheets/_rating.scss b/app/assets/stylesheets/_rating.scss new file mode 100644 index 00000000..63ff73c2 --- /dev/null +++ b/app/assets/stylesheets/_rating.scss @@ -0,0 +1,26 @@ +.rating-container { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + gap: 12px; +} + +.rating-item { + display: flex; + align-items: center; +} + +.interactive-star { + transition: transform 0.25s ease; + + &:hover { + transform: scale(1.3); + } +} + +.login-container { + display: flex; + gap: 4px; + align-items: center; +} diff --git a/app/controllers/subjects_controller.rb b/app/controllers/subjects_controller.rb index 2de04ccc..2511eda0 100644 --- a/app/controllers/subjects_controller.rb +++ b/app/controllers/subjects_controller.rb @@ -7,6 +7,8 @@ def index end def show + @user_review = current_user.reviews.find_by(subject:) if current_user + respond_to do |format| format.html { subject } end diff --git a/app/views/subjects/_rating.html.erb b/app/views/subjects/_rating.html.erb new file mode 100644 index 00000000..d6626b53 --- /dev/null +++ b/app/views/subjects/_rating.html.erb @@ -0,0 +1,33 @@ + diff --git a/app/views/subjects/show.html.erb b/app/views/subjects/show.html.erb index ea003271..3720dfae 100644 --- a/app/views/subjects/show.html.erb +++ b/app/views/subjects/show.html.erb @@ -19,6 +19,8 @@ Grupo: Desconocido <% end %> + + <%= render 'rating' %>