Skip to content

Instantly share code, notes, and snippets.

@prfraser
Created January 8, 2026 02:14
Show Gist options
  • Select an option

  • Save prfraser/480bab2fc3d29f180cda9fa2d667ac36 to your computer and use it in GitHub Desktop.

Select an option

Save prfraser/480bab2fc3d29f180cda9fa2d667ac36 to your computer and use it in GitHub Desktop.

Review Upvotes

The Task

"Users should be able to upvote helpful reviews"

What They Need to Build

  • New review_upvotes table
  • ReviewUpvote model
  • Toggle upvote on/off
  • Display upvote count on reviews

Good Solution

1. Migration

# 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
end

Look for: Foreign keys, unique composite index.


2. Model

# 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: :destroy

Look for: Both associations, uniqueness validation, dependent destroy.


3. Routes

# config/routes.rb (add)
resources :reviews, only: [] do
  resource :upvote, only: [:create, :destroy], controller: 'review_upvotes'
end

Generates:

  • POST /reviews/:review_id/upvote
  • DELETE /reviews/:review_id/upvote

Look for: Nested route, singular resource (not resources).


4. Controller

# 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
end

Look for: Auth check, scoped to current_user, safe destroy with &..


5. View

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.


Discussion Points

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

Common Mistakes

  • Forgetting dependent: :destroy on associations
  • Using resources (plural) instead of resource (singular)
  • Not checking if user is logged in before showing button
  • Forgetting the unique index on migration
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment