"Users should be able to upvote helpful reviews"
- New
review_upvotestable ReviewUpvotemodel- Toggle upvote on/off
- Display upvote count on reviews
# db/migrate/xxx_create_review_upvotes.rb
class CreateReviewUpvotes < ActiveRecord::Migration[7.1]
def change
create_table :review_upvotes do |t|
t.references :user, null: false, foreign_key: true
t.references :review, null: false, foreign_key: true
t.timestamps
end
add_index :review_upvotes, [:user_id, :review_id], unique: true
end
endLook for: Foreign keys, unique composite index.
# app/models/review_upvote.rb
class ReviewUpvote < ApplicationRecord
belongs_to :user
belongs_to :review
validates :review_id, uniqueness: { scope: :user_id }
end# app/models/review.rb (add)
has_many :review_upvotes, dependent: :destroy# app/models/user.rb (add)
has_many :review_upvotes, dependent: :destroyLook for: Both associations, uniqueness validation, dependent destroy.
# config/routes.rb (add)
resources :reviews, only: [] do
resource :upvote, only: [:create, :destroy], controller: 'review_upvotes'
endGenerates:
POST /reviews/:review_id/upvoteDELETE /reviews/:review_id/upvote
Look for: Nested route, singular resource (not resources).
# app/controllers/review_upvotes_controller.rb
class ReviewUpvotesController < ApplicationController
before_action :require_authenticated_user
def create
@review = Review.find(params[:review_id])
current_user.review_upvotes.create(review: @review)
redirect_back fallback_location: @review.book
end
def destroy
@review = Review.find(params[:review_id])
upvote = current_user.review_upvotes.find_by(review: @review)
upvote&.destroy
redirect_back fallback_location: @review.book
end
endLook for: Auth check, scoped to current_user, safe destroy with &..
Add to review partial or show page: (n+1 in the review.review_upvotes.count)
<div class="upvote-section">
<span><%= review.review_upvotes.count %> upvotes</span>
<% if current_user %>
<% if current_user.review_upvotes.exists?(review: review) %>
<%= button_to "Remove Upvote", review_upvote_path(review), method: :delete %>
<% else %>
<%= button_to "Upvote", review_upvote_path(review), method: :post %>
<% end %>
<% end %>
</div>Look for: Toggle state check, button_to with correct method, login check.
| Question | What You're Looking For |
|---|---|
| "Can a user upvote their own review?" | Awareness of edge case, could add validation |
| "This page has 50 reviews - any concerns?" | N+1 awareness (.count in loop) |
| "What if we add downvotes later?" | Forward thinking, might mention adding a value column |
- Forgetting
dependent: :destroyon associations - Using
resources(plural) instead ofresource(singular) - Not checking if user is logged in before showing button
- Forgetting the unique index on migration