posts: allow toggling between upvotes and downvotes.

Like 9efb374ae, allow users to toggle between upvoting and downvoting a
post without raising an error or having to manually remove the vote
first. If you upvote a post, then downvote it, the upvote is
automatically removed and replaced by the downvote.

Other changes:

* Tagging a post with `upvote:self` or `downvote:self` is now silently
  ignored when the user doesn't have permission to vote, instead of
  raising an error.
* Undoing a vote that doesn't exist now does nothing instead of
  returning an error. This can happen if you open the same post in two
  tabs, undo the vote in tab 1, then try to undo the vote again in tab 2.

Changes to the /post_votes API:

* `POST /post_votes` and `DELETE /post_votes` now return a post vote
  instead of a post.
* The `score` param in `POST /post_votes` is now 1 or -1, not `up` or
  `down`.
This commit is contained in:
evazion
2021-01-29 01:58:12 -06:00
parent ffdd5e6128
commit d0c9f6e0b8
11 changed files with 221 additions and 155 deletions

View File

@@ -2,9 +2,8 @@ class CommentVote < ApplicationRecord
belongs_to :comment
belongs_to :user
validates_presence_of :score
validates_uniqueness_of :user_id, :scope => :comment_id, :message => "have already voted for this comment"
validates_inclusion_of :score, :in => [-1, 1], :message => "must be 1 or -1"
validates :user_id, uniqueness: { scope: :comment_id, message: "have already voted for this comment" }
validates :score, inclusion: { in: [-1, 1], message: "must be 1 or -1" }
after_create :update_score_after_create
after_destroy :update_score_after_destroy

View File

@@ -633,7 +633,8 @@ class Post < ApplicationRecord
remove_favorite(CurrentUser.user)
when /^(up|down)vote:(.+)$/i
vote!($1)
score = ($1 == "up" ? 1 : -1)
vote!(score, CurrentUser.user)
when /^status:active$/i
raise User::PrivilegeError unless CurrentUser.is_approver?
@@ -779,8 +780,7 @@ class Post < ApplicationRecord
def add_favorite!(user)
Favorite.add(post: self, user: user)
vote!("up", user) if Pundit.policy!(user, PostVote).create?
rescue PostVote::Error
vote!(1, user)
end
def delete_user_from_fav_string(user_id)
@@ -789,8 +789,7 @@ class Post < ApplicationRecord
def remove_favorite!(user)
Favorite.remove(post: self, user: user)
unvote!(user) if Pundit.policy!(user, PostVote).create?
rescue PostVote::Error
unvote!(user)
end
def remove_favorite(user)
@@ -871,30 +870,22 @@ class Post < ApplicationRecord
end
module VoteMethods
def can_be_voted_by?(user)
!PostVote.exists?(:user_id => user.id, :post_id => id)
def vote!(score, voter)
# Ignore vote if user doesn't have permission to vote.
return unless Pundit.policy!(voter, PostVote).create?
with_lock do
votes.destroy_by(user: voter)
votes.create!(user: voter, score: score)
reload # PostVote.create modifies our score. Reload to get the new score.
end
end
def vote!(vote, voter = CurrentUser.user)
unless Pundit.policy!(voter, PostVote).create?
raise PostVote::Error.new("You do not have permission to vote")
end
def unvote!(voter)
return unless Pundit.policy!(voter, PostVote).create?
unless can_be_voted_by?(voter)
raise PostVote::Error.new("You have already voted for this post")
end
votes.create!(user: voter, vote: vote)
reload # PostVote.create modifies our score. Reload to get the new score.
end
def unvote!(voter = CurrentUser.user)
if can_be_voted_by?(voter)
raise PostVote::Error.new("You have not voted for this post")
else
votes.where(user: voter).destroy_all
reload
end
votes.destroy_by(user: voter)
reload
end
end

View File

@@ -1,15 +1,12 @@
class PostVote < ApplicationRecord
class Error < StandardError; end
belongs_to :post
belongs_to :user
attr_accessor :vote
after_initialize :initialize_attributes, if: :new_record?
validates_presence_of :score
validates_inclusion_of :score, in: [1, -1]
after_create :update_post_on_create
after_destroy :update_post_on_destroy
validates :user_id, uniqueness: { scope: :post_id, message: "have already voted for this post" }
validates :score, inclusion: { in: [1, -1], message: "must be 1 or -1" }
after_create :update_score_after_create
after_destroy :update_score_after_destroy
scope :positive, -> { where("post_votes.score > 0") }
scope :negative, -> { where("post_votes.score < 0") }
@@ -23,16 +20,6 @@ class PostVote < ApplicationRecord
q.apply_default_order(params)
end
def initialize_attributes
self.user_id ||= CurrentUser.id
if vote == "up"
self.score = 1
elsif vote == "down"
self.score = -1
end
end
def is_positive?
score > 0
end
@@ -41,19 +28,19 @@ class PostVote < ApplicationRecord
score < 0
end
def update_post_on_create
if score > 0
Post.where(:id => post_id).update_all("score = score + #{score}, up_score = up_score + #{score}")
def update_score_after_create
if is_positive?
Post.update_counters(post_id, { score: score, up_score: score })
else
Post.where(:id => post_id).update_all("score = score + #{score}, down_score = down_score + #{score}")
Post.update_counters(post_id, { score: score, down_score: score })
end
end
def update_post_on_destroy
if score > 0
Post.where(:id => post_id).update_all("score = score - #{score}, up_score = up_score - #{score}")
def update_score_after_destroy
if is_positive?
Post.update_counters(post_id, { score: -score, up_score: -score })
else
Post.where(:id => post_id).update_all("score = score - #{score}, down_score = down_score - #{score}")
Post.update_counters(post_id, { score: -score, down_score: -score })
end
end