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

@@ -34,7 +34,7 @@ class PostVotesComponentTest < ViewComponent::TestCase
context "for a downvoted post" do
should "highlight the downvote button as active" do
@post.vote!("down", @user)
@post.vote!(-1, @user)
render_post_votes(@post, current_user: @user)
assert_css(".post-upvote-link.inactive-link")
@@ -44,7 +44,7 @@ class PostVotesComponentTest < ViewComponent::TestCase
context "for an upvoted post" do
should "highlight the upvote button as active" do
@post.vote!("up", @user)
@post.vote!(1, @user)
render_post_votes(@post, current_user: @user)
assert_css(".post-upvote-link.active-link")

View File

@@ -46,52 +46,94 @@ class PostVotesControllerTest < ActionDispatch::IntegrationTest
context "create action" do
should "not allow anonymous users to vote" do
post post_post_votes_path(post_id: @post.id), params: {:score => "up", :format => "js"}
post post_post_votes_path(post_id: @post.id), params: { score: 1, format: "js" }
assert_response 403
assert_equal(0, @post.reload.score)
end
should "not allow banned users to vote" do
@banned = create(:user)
@ban = create(:ban, user: @banned)
post_auth post_post_votes_path(post_id: @post.id), @banned, params: {:score => "up", :format => "js"}
post_auth post_post_votes_path(post_id: @post.id), create(:banned_user), params: { score: 1, format: "js"}
assert_response 403
assert_equal(0, @post.reload.score)
end
should "not allow members to vote" do
@member = create(:member_user)
post_auth post_post_votes_path(post_id: @post.id), @member, params: {:score => "up", :format => "js"}
post_auth post_post_votes_path(post_id: @post.id), create(:user), params: { score: 1, format: "js" }
assert_response 403
assert_equal(0, @post.reload.score)
end
should "not allow invalid scores" do
post_auth post_post_votes_path(post_id: @post.id), @user, params: { score: 3, format: "js" }
assert_response 200
assert_equal(0, @post.reload.score)
assert_equal(0, @post.up_score)
assert_equal(0, @post.votes.count)
end
should "increment a post's score if the score is positive" do
post_auth post_post_votes_path(post_id: @post.id), @user, params: {:score => "up", :format => "js"}
post_auth post_post_votes_path(post_id: @post.id), @user, params: { score: 1, format: "js" }
assert_response :success
@post.reload
assert_equal(1, @post.score)
assert_equal(1, @post.reload.score)
assert_equal(1, @post.up_score)
assert_equal(1, @post.votes.count)
end
should "decrement a post's score if the score is negative" do
post_auth post_post_votes_path(post_id: @post.id), @user, params: { score: -1, format: "js" }
assert_response :success
assert_equal(-1, @post.reload.score)
assert_equal(-1, @post.down_score)
assert_equal(1, @post.votes.count)
end
context "for a post that has already been voted on" do
should "not create another vote" do
@post.vote!("up", @user)
assert_no_difference("PostVote.count") do
post_auth post_post_votes_path(post_id: @post.id), @user, params: { score: "up", format: "js" }
assert_response 422
should "replace the vote" do
@post.vote!(1, @user)
assert_no_difference("@post.votes.count") do
post_auth post_post_votes_path(post_id: @post.id), @user, params: { score: -1, format: "js" }
assert_response :success
assert_equal(-1, @post.reload.score)
assert_equal(0, @post.up_score)
assert_equal(-1, @post.down_score)
end
end
end
end
context "destroy action" do
should "remove a vote" do
as(@user) { create(:post_vote, post_id: @post.id, user_id: @user.id) }
should "do nothing for anonymous users" do
delete post_post_votes_path(post_id: @post.id), xhr: true
assert_difference("PostVote.count", -1) do
delete_auth post_post_votes_path(post_id: @post.id), @user, as: :javascript
assert_redirected_to @post
end
assert_response 200
assert_equal(0, @post.reload.score)
end
should "do nothing if the post hasn't been voted on" do
delete_auth post_post_votes_path(post_id: @post.id), @user, xhr: true
assert_response :success
assert_equal(0, @post.reload.score)
assert_equal(0, @post.down_score)
assert_equal(0, @post.votes.count)
end
should "remove a vote" do
@post.vote!(1, @user)
delete_auth post_post_votes_path(post_id: @post.id), @user, xhr: true
assert_response :success
assert_equal(0, @post.reload.score)
assert_equal(0, @post.down_score)
assert_equal(0, @post.votes.count)
end
end
end

View File

@@ -1162,19 +1162,19 @@ class PostTest < ActiveSupport::TestCase
context "upvote:self or downvote:self" do
context "by a member" do
should "not upvote the post" do
assert_raises PostVote::Error do
@post.update(:tag_string => "upvote:self")
assert_no_difference("PostVote.count") do
@post.update(tag_string: "upvote:self")
end
assert_equal(0, @post.score)
assert_equal(0, @post.reload.score)
end
should "not downvote the post" do
assert_raises PostVote::Error do
@post.update(:tag_string => "downvote:self")
assert_no_difference("PostVote.count") do
@post.update(tag_string: "downvote:self")
end
assert_equal(0, @post.score)
assert_equal(0, @post.reload.score)
end
end
@@ -1832,50 +1832,63 @@ class PostTest < ActiveSupport::TestCase
context "Voting:" do
should "not allow members to vote" do
@user = FactoryBot.create(:user)
@post = FactoryBot.create(:post)
as(@user) do
assert_raises(PostVote::Error) { @post.vote!("up") }
end
user = create(:user)
post = create(:post)
assert_nothing_raised { post.vote!(1, user) }
assert_equal(0, post.votes.count)
assert_equal(0, post.reload.score)
end
should "not allow duplicate votes" do
user = FactoryBot.create(:gold_user)
post = FactoryBot.create(:post)
CurrentUser.scoped(user, "127.0.0.1") do
assert_nothing_raised {post.vote!("up")}
assert_raises(PostVote::Error) {post.vote!("up")}
post.reload
assert_equal(1, PostVote.count)
assert_equal(1, post.score)
end
user = create(:gold_user)
post = create(:post)
post.vote!(1, user)
post.vote!(1, user)
assert_equal(1, post.reload.score)
assert_equal(1, post.votes.count)
end
should "allow undoing of votes" do
user = FactoryBot.create(:gold_user)
post = FactoryBot.create(:post)
user = create(:gold_user)
post = create(:post)
# We deliberately don't call post.reload until the end to verify that
# post.unvote! returns the correct score even when not forcibly reloaded.
CurrentUser.scoped(user, "127.0.0.1") do
post.vote!("up")
assert_equal(1, post.score)
post.vote!(1, user)
assert_equal(1, post.score)
assert_equal(1, post.up_score)
assert_equal(0, post.down_score)
assert_equal(1, post.votes.positive.count)
post.unvote!
assert_equal(0, post.score)
post.unvote!(user)
assert_equal(0, post.score)
assert_equal(0, post.up_score)
assert_equal(0, post.down_score)
assert_equal(0, post.votes.count)
assert_nothing_raised {post.vote!("down")}
assert_equal(-1, post.score)
post.vote!(-1, user)
assert_equal(-1, post.score)
assert_equal(0, post.up_score)
assert_equal(-1, post.down_score)
assert_equal(1, post.votes.negative.count)
post.unvote!
assert_equal(0, post.score)
post.unvote!(user)
assert_equal(0, post.score)
assert_equal(0, post.up_score)
assert_equal(0, post.down_score)
assert_equal(0, post.votes.count)
assert_nothing_raised {post.vote!("up")}
assert_equal(1, post.score)
post.vote!(1, user)
assert_equal(1, post.score)
assert_equal(1, post.up_score)
assert_equal(0, post.down_score)
assert_equal(1, post.votes.positive.count)
post.reload
assert_equal(1, post.score)
end
post.reload
assert_equal(1, post.score)
end
end

View File

@@ -1,46 +1,74 @@
require 'test_helper'
class PostVoteTest < ActiveSupport::TestCase
def setup
super
@user = FactoryBot.create(:user)
CurrentUser.user = @user
CurrentUser.ip_addr = "127.0.0.1"
@post = FactoryBot.create(:post)
end
context "Voting for a post" do
should "interpret up as +1 score" do
vote = PostVote.create(:post_id => @post.id, :vote => "up")
assert_equal(1, vote.score)
context "A PostVote" do
setup do
@post = create(:post)
end
should "interpret down as -1 score" do
vote = PostVote.create(:post_id => @post.id, :vote => "down")
assert_equal(-1, vote.score)
context "during validation" do
subject { build(:post_vote, post: @post) }
should validate_uniqueness_of(:user_id).scoped_to(:post_id).with_message("have already voted for this post")
should validate_inclusion_of(:score).in_array([-1, 1]).with_message("must be 1 or -1")
end
should "not accept any other scores" do
vote = PostVote.create(:post_id => @post.id, :vote => "xxx")
assert(vote.errors.any?)
context "creating" do
context "an upvote" do
should "increment the post's score" do
vote = create(:post_vote, post: @post, score: 1)
assert_equal(1, @post.reload.score)
assert_equal(1, @post.up_score)
assert_equal(0, @post.down_score)
assert_equal(1, @post.votes.positive.count)
end
end
context "a downvote" do
should "decrement the post's score" do
vote = create(:post_vote, post: @post, score: -1)
assert_equal(-1, @post.reload.score)
assert_equal(0, @post.up_score)
assert_equal(-1, @post.down_score)
assert_equal(1, @post.votes.negative.count)
end
end
end
should "increase the score of the post" do
@post.votes.create(vote: "up")
@post.reload
context "destroying" do
context "an upvote" do
should "decrement the post's score" do
vote = create(:post_vote, post: @post, score: 1)
assert_equal(1, @post.reload.score)
assert_equal(1, @post.up_score)
assert_equal(0, @post.down_score)
assert_equal(1, @post.votes.count)
assert_equal(1, @post.score)
assert_equal(1, @post.up_score)
end
vote.destroy
assert_equal(0, @post.reload.score)
assert_equal(0, @post.up_score)
assert_equal(0, @post.down_score)
assert_equal(0, @post.votes.count)
end
end
should "decrease the score of the post when removed" do
@post.votes.create(vote: "up").destroy
@post.reload
context "a downvote" do
should "increment the post's score" do
vote = create(:post_vote, post: @post, score: -1)
assert_equal(-1, @post.reload.score)
assert_equal(0, @post.up_score)
assert_equal(-1, @post.down_score)
assert_equal(1, @post.votes.count)
assert_equal(0, @post.score)
assert_equal(0, @post.up_score)
vote.destroy
assert_equal(0, @post.reload.score)
assert_equal(0, @post.up_score)
assert_equal(0, @post.down_score)
assert_equal(0, @post.votes.count)
end
end
end
end
end