Only allow users to create one review per movie

2019-06-01 08:31发布

问题:

I have my app setup where users can write reviews for a movie. What I'd like to do is limit the user to create only one review per movie. I've managed to accomplish this in my reviews controller as so:

class ReviewsController < ApplicationController
  before_action :has_reviewed, only [:new]
  ....
  def has_reviewed?
    if Review.where(user_id: current_user.id, movie_id: @movie.id).any?
      redirect_to movie_reviews_path

      flash[:notice] = "You've already written a review for this movie."
    end
  end
end

Where I'm now having trouble is translating this same logic into my index view template with the helper methods of Devise and CanCanCan at my disposal.

<% if user_signed_in? && ... %> # current_user has already created a review for this movie
  <%= link_to "Edit Review", edit_movie_review_path(@movie, review) %>
<% else %>
  <%= link_to "Write a Review", new_movie_review_path %>
<% end %>

Also: Is there any way to improve the lookup in my has_reviewed? method? I feel like there's a better way to write it but can't determine the most appropriate fix.

回答1:

Why not use a validation:

#app/models/review.rb
class Review < ActiveRecord::Base
    validates :movie_id, uniqueness: { scope: :user_id, message: "You've reviewed this movie!" }
end

This is considering your review model belongs_to :movie


You could also use an ActiveRecord callback:

#app/models/review.rb
class Review < ActiveRecord::Base
   before_create :has_review?
   belongs_to :user, inverse_of: :reviews
   belongs_to :movie

   def has_review?
      return if Review.exists?(user: user, movie_id: movie_id)
   end
end

#app/models/user.rb
class User < ActiveRecord::Base
   has_many :reviews, inverse_of: :user
end

Is there any way to improve the lookup in my has_reviewed? method?

  def has_reviewed?
      redirect_to album_reviews_path, notice: "You've already written a review for this album." if current_user.reviews.exists?(movie: @movie)
  end


回答2:

Why not make a has_reviewed? method on your User class?

e.g.

def has_reviewed?(reviewable)
   # query in here
end

Then you should be able use that just fine in your controller and your views.



回答3:

You will want to do this for both new and create. Otherwise a savvy user would be able to run a post that would get past your new action.

I would put the link_to in either a helper or a presenter object. It would generally look like this.

def create_or_edit_review_path(movie, current_user)
  return '' if current_user.blank?

  if current_user.review.present?
    #Generate review edit link
  else
    #generate new link
  end 
end

After that in all of your views it would just be

<%= create_or_edit_review_path(@movie, current_user) %>

Then in your controller for both new and create you could do either a before action or just redirect on each.

before_action :enforce_single_review, only: [:create, :new]

def enforce_single_review
  if current_user.review.present?
    redirect_to review_path(current_user.review)
  end
end


回答4:

Here's what I came up with:

I created an instance method to retrieve a user's movie review using the find_by method on the Review model:

class User < ActiveRecord::Base
  ....
  def movie_review(album)
    Review.find_by(user_id: self, album_id: album)
  end
end

This method also comes in handy when setting up my callback:

class ReviewsController < ApplicationController
  before_action :limit_review, only: [:new, :create]
  ....
  private
    def limit_review
      user_review = current_user.movie_review(@movie)

      if user_review.present?
        redirect_to edit_movie_review_path(@movie, user_review)
      end 
    end
end

Created a helper method for showing the appropriate link to edit or create a review. Big thanks to Austio and his suggestion:

module ReviewsHelper
  def create_or_edit_review_path(movie)
    user_review = current_user.movie_review(movie) if user_signed_in?

    if user_signed_in? && user_review.present?
      link_to "Edit review", edit_movie_review_path(movie, user_review)
    else
      link_to "Write a review", new_movie_review_path
    end
  end
end

And at last this is how I call the helper in my view template(s):

....
<%= create_or_edit_review_path(@album) %>