0

How can we limit a user to 1 like per comment?

comments_controller.rb

  def like
    @comment = Comment.find(params[:id])
    @comment.increment!(:likes)
    @comment.create_activity :like
    flash[:success] = 'Thanks for liking!'
    redirect_to(:back)
  end

_comments.html.erb

 <% @comments.each do |comment| %>
    <%= User.find(comment.user_id).name %>
    <%= simple_format comment.content %>
    <%= pluralize(comment.likes, 'like') %>
    <%= link_to content_tag(:span, '', class: 'glyphicon glyphicon-thumbs-up') + 
    ' Like it', like_comment_path(:id => comment.id), method: :post %>
 <% end %>

valuation.rb

class Valuation < ActiveRecord::Base
  belongs_to :user
  has_many :comments, as: :commentable
  include PublicActivity::Model
  tracked owner: ->(controller, model) { controller && controller.current_user }
end

routes.rb

resources :valuations do
  resources :comments
  member do
    post :like
  end
end

I followed along this tutorial: http://www.sitepoint.com/activity-feeds-rails/ to implement the public_activity gem and the like button.

Thank you for your time and expertise!

UPDATE

class CommentsController < ApplicationController
    before_action :load_commentable
  before_action :set_comment, only: [:show, :edit, :update, :destroy, :like]
  before_action :logged_in_user, only: [:create, :destroy]

    def index
        @comments = @commentable.comments
    end

    def new
        @comment = @commentable.comments.new
    end

    def create
        @comment = @commentable.comments.new(comment_params)
        if @comment.save
            @comment.create_activity :create, owner: current_user 
            redirect_to @commentable, notice: "comment created."
        else
            render :new
        end
    end

    def edit
        @comment = current_user.comments.find(params[:id])
    end

    def update
        @comment = current_user.comments.find(params[:id])
        if @comment.update_attributes(comment_params)
            redirect_to @commentable, notice: "Comment was updated."
        else
            render :edit
        end
    end

    def destroy
        @comment = current_user.comments.find(params[:id])
        @comment.destroy
        @comment.create_activity :destroy, owner: current_user
        redirect_to @commentable, notice: "comment destroyed."
    end

  def like
    @comment = Comment.find(params[:id])
    @comment.increment!(:likes)
    @comment.create_activity :like
    flash[:success] = 'Thanks for liking!'
    redirect_to(:back)
  end

private
  def set_comment
    @comment = Comment.find(params[:id])
  end

    def load_commentable
        resource, id = request.path.split('/')[1, 2]
        @commentable = resource.singularize.classify.constantize.find(id)
    end

    def comment_params
        params[:comment][:user_id] = current_user.id
        params.require(:comment).permit(:content, :commentable, :user_id)
    end
end

comment.rb

class Comment < ActiveRecord::Base
    include PublicActivity::Common
    # tracked except: :update,  owner: ->(controller, model) { controller && controller.current_user }
    belongs_to :commentable, polymorphic: true
    belongs_to :user
end
Donald Duck
  • 8,409
  • 22
  • 75
  • 99
AnthonyGalli.com
  • 2,796
  • 5
  • 31
  • 80
  • 2
    This line is inexcusable: <%= User.find(comment.user_id).name %>. You could just do <%= comment.user.name %>. Where are you checking the user in your comments controller? – NM Pennypacker Apr 14 '15 at 20:44
  • @NickM I was just going to say that. Although I would recommend taking it further and making sure `users` are eager_loaded with `comments` to avoid the n + 1 issue here. As for limiting to 1 like though what you have provided is not all that useful but just adding a simple validation or a method to `Comment`s shouldn't be too difficult if we can understand the relationship. – engineersmnky Apr 14 '15 at 20:53
  • @engineersmnky do you need the whole comments controller? – AnthonyGalli.com Apr 14 '15 at 20:58
  • Thanks @NickM I'll have to look into that. It took 6 hours just trying to figure out how to write that one line :/ – AnthonyGalli.com Apr 14 '15 at 20:59
  • Models would actually be preferable but slimmed down to just the context of this question. While you will have to change your controller to not just automatically increment likes understanding the relationship between `Comments` and their likers is an integral part of this question. – engineersmnky Apr 14 '15 at 21:02
  • There is no like model. There is a comment model which basically has ` belongs_to :commentable, polymorphic: true` and I added the valuation model and the full comments controller @engineersmnky – AnthonyGalli.com Apr 14 '15 at 21:16
  • @engineersmnky and I added part of routes. Maybe that will show the relationship best. – AnthonyGalli.com Apr 14 '15 at 21:19
  • nothing here shows the `Comment` class. I am assuming that right now the user that "liked" a comment is untracked since you are just incrementing likes. I think to make this limitation you will need a class to handle what comments a user liked possibly `UserLike` class that just stores user_id and comment_id. Then when a user clicks like you can check to see if they already liked a comment or you could check before hand and disable liking that way. – engineersmnky Apr 15 '15 at 13:00
  • Thanks @engineersmnky! I added comments model above. It is polymorphic but not much else in there which is why I didn't show. So I was in the process of trying to figure out how to add `UserLike` class to this model, but then I thought hedgesky's answer might be more suitable. Do you agree? – AnthonyGalli.com Apr 15 '15 at 16:16

1 Answers1

2

I think you could create additional model:

class CommentLike
   belongs_to :comment
   belongs_to :user
   validates :user, uniqueness: { scope: :comment}
end

class User
  has_many :comment_likes
end

class Comment
  has_many :comment_likes
end

def like
  @comment = Comment.find(params[:id])
  if current_user.comment_likes.create(comment: @comment)
    @comment.increment!(:likes)
    @comment.create_activity :like       
    flash[:success] = 'Thanks for liking!'
  else
    flash[:error] = 'Two many likes'
  end
  redirect_to(:back)
end

It'll solves you problem. If storing likes_count in comment is neccessary, you could use Rails' counter_cache.

hedgesky
  • 3,271
  • 1
  • 21
  • 36
  • I created the model and added the code, but I can still "like" a comment an unlimited amount of times. Any ideas? It shows number of likes next to comment so would counter_cache be necessary? – AnthonyGalli.com Apr 15 '15 at 16:14
  • Of course you should not only create this model, but rewrite rest part of app (user model and controllers). With you current approach it is impossible to solve problem. – hedgesky Apr 15 '15 at 16:21
  • Add changes in answer. – hedgesky Apr 15 '15 at 16:26
  • This will raise if the user has already "liked" a comment. Not sure that raising an exception is the best idea in this case. I think something like `if current_user.comment_likes.new(comment: @comment).save` would be better for this use case. Also this will increment likes even on a failure. I would suggest changing the controller logic completely to be reliant on passing validation. – engineersmnky Apr 15 '15 at 16:30
  • @hedgesky thanks for updating the answer! When I click on "like" we get `ActiveModel::MissingAttributeError in CommentsController#like can't write unknown attribute 'comment_id'` for line `current_user.comment_likes.create!(comment: @comment)` – AnthonyGalli.com Apr 15 '15 at 17:43
  • @engineersmnky I also get the same error with your line. How would you change the controller logic completely to be reliant on a passing validation? Thanks again! – AnthonyGalli.com Apr 15 '15 at 17:47
  • Did you create CommentLike model properly? Please show corresponding lines from schema.rb. – hedgesky Apr 15 '15 at 21:08
  • Hi @hedgesky I still got that error message with your updated code :/ Here are the lines: `create_table "comment_likes", force: true do |t| t.datetime "created_at", null: false t.datetime "updated_at", null: false end` – AnthonyGalli.com Apr 16 '15 at 00:07
  • 1
    You created DB table improperly. This table should have user_id and comment_id columns. Maybe you should read Rails guides first? http://guides.rubyonrails.org/association_basics.html#the-belongs-to-association . – hedgesky Apr 16 '15 at 07:43
  • Sorry about that @hedgesky I use to update schema manually and have been working on fixing the bad habit :/ You're right adding those two lines fixed the error, but we can still "like" to infinity. – AnthonyGalli.com Apr 16 '15 at 16:15
  • This is the updated schema @hedgesky (I also tried various versions of it): `create_table "comment_likes", force: true do |t| t.integer "user_id" t.integer "comment_id" t.datetime "created_at", null: false t.datetime "updated_at", null: false end` – AnthonyGalli.com Apr 16 '15 at 16:24
  • Try repeat this line in Rails console:\n CommentLike.create!(user: User.first, comment: Comment.first)\n If models are set up properly, repeating it should raise an exception. – hedgesky Apr 16 '15 at 16:30
  • This is what we get back @hedgesky `User Load (0.3ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1 Comment Load (0.3ms) SELECT "comments".* FROM "comments" ORDER BY "comments"."id" ASC LIMIT 1 (0.1ms) begin transaction CommentLike Exists (0.2ms) SELECT 1 AS one FROM "comment_likes" WHERE ("comment_likes"."user_id" = 1 AND "comment_likes"."comment_id" = 1) LIMIT 1 (0.1ms) rollback transaction ActiveRecord::RecordInvalid: Validation failed: User has already been taken` – AnthonyGalli.com Apr 17 '15 at 14:45
  • So model is working correctly. Problem is in your controller, good luck in searching it. – hedgesky Apr 17 '15 at 15:17
  • Thanks for your help and moving the ball forward @hedgesky :) – AnthonyGalli.com Apr 17 '15 at 17:36