"Users should be able to upvote or downvote reviews"
- Three states: upvoted, downvoted, not voted
- Can change vote (upvote → downvote)
- Net score calculation (upvotes - downvotes)
- More complex UI (two buttons, current state indication)
- New
review_votestable withvaluecolumn ReviewVotemodel- Vote/change vote/remove vote functionality
- Display score and current vote state
# db/migrate/xxx_create_review_votes.rb
class CreateReviewVotes < ActiveRecord::Migration[7.1]
def change
create_table :review_votes do |t|
t.references :user, null: false, foreign_key: true
t.references :review, null: false, foreign_key: true
t.integer :value, null: false
t.timestamps
end
add_index :review_votes, [:user_id, :review_id], unique: true
end
endLook for: value column with null: false, unique composite index.
# app/models/review_vote.rb
class ReviewVote < ApplicationRecord
belongs_to :user
belongs_to :review
validates :review_id, uniqueness: { scope: :user_id }
validates :value, inclusion: { in: [1, -1] }
end# app/models/review.rb (add)
has_many :review_votes, dependent: :destroy
def score
review_votes.sum(:value)
end
def vote_from(user)
return nil unless user
review_votes.find_by(user: user)
end# app/models/user.rb (add)
has_many :review_votes, dependent: :destroyLook for: Value validation with inclusion, score method using sum, helper method for finding user's vote.
# config/routes.rb (add)
resources :reviews, only: [] do
resource :vote, only: [:create, :destroy], controller: 'review_votes'
endGenerates:
POST /reviews/:review_id/voteDELETE /reviews/:review_id/vote
# app/controllers/review_votes_controller.rb
class ReviewVotesController < ApplicationController
before_action :require_authenticated_user
before_action :set_review
def create
value = params[:value].to_i
unless [1, -1].include?(value)
return redirect_back fallback_location: @review.book, alert: "Invalid vote"
end
vote = current_user.review_votes.find_or_initialize_by(review: @review)
vote.value = value
if vote.save
redirect_back fallback_location: @review.book
else
redirect_back fallback_location: @review.book, alert: "Could not save vote"
end
end
def destroy
vote = current_user.review_votes.find_by(review: @review)
vote&.destroy
redirect_back fallback_location: @review.book
end
private
def set_review
@review = Review.find(params[:review_id])
end
endLook for:
- Value validation in controller (allowlist)
find_or_initialize_bypattern for upsert- Scoped to
current_user
<%# app/views/reviews/_vote_controls.html.erb %>
<%# locals: (review:) %>
<div class="vote-controls">
<span class="score">Score: <%= review.score %></span>
<% if current_user %>
<% current_vote = review.vote_from(current_user) %>
<%= button_to review_vote_path(review),
params: { value: 1 },
method: :post,
class: current_vote&.value == 1 ? "btn-voted" : "btn-vote" do %>
Upvote
<% end %>
<%= button_to review_vote_path(review),
params: { value: -1 },
method: :post,
class: current_vote&.value == -1 ? "btn-voted" : "btn-vote" do %>
Downvote
<% end %>
<% if current_vote %>
<%= button_to "Clear", review_vote_path(review), method: :delete, class: "btn-clear" %>
<% end %>
<% end %>
</div>Look for:
- Passing value as param
- Visual indication of current vote state
- Clear vote option when voted
- Login check
# Separate actions (more RESTful, more code)
def upvote
vote_with_value(1)
end
def downvote
vote_with_value(-1)
end
# vs single action with param (shown above)
def create
value = params[:value].to_i
# ...
end# Integer (shown above)
validates :value, inclusion: { in: [1, -1] }
# Enum (also acceptable)
enum vote_type: { upvote: 1, downvote: -1 }| Question | What You're Looking For |
|---|---|
"50 reviews on page, each calling .score - problem?" |
N+1 awareness, could preload or add counter cache |
| "What if we want to sort reviews by score?" | Add scope: scope :by_score, -> { ... } with join/subquery |
| "Should users vote on their own reviews?" | Business logic awareness, could add validation |
| "Integer value vs enum - tradeoffs?" | Enum more readable, integer more flexible for future values |
- Not validating
valuein controller (security issue - user could pass any integer) - Creating new record instead of updating existing vote
- N+1 when displaying vote state for many reviews
- Forgetting to handle the "change vote" case
- Not scoping queries to
current_user
- Add
scope :by_scoreto Review model - Mention counter cache for performance
- Add validation preventing self-votes
- Extract vote checking into a helper method
- Consider what happens if review is deleted (dependent destroy)
- Target: 30-40 minutes
- Fast candidate: 25 minutes
- Struggling: 45+ minutes (offer hints on
find_or_initialize_by)
| If stuck on... | Hint |
|---|---|
| Changing existing vote | "What ActiveRecord method finds OR creates a record?" |
| Passing value to controller | "Can button_to accept additional params?" |
| Showing current vote state | "Could the Review model have a helper method?" |
| Vote validation | "What values should be allowed? How do you validate that?" |