diff --git a/app/models/post_vote.rb b/app/models/post_vote.rb
index 4a571344c..777e55427 100644
--- a/app/models/post_vote.rb
+++ b/app/models/post_vote.rb
@@ -10,13 +10,15 @@ class PostVote < ApplicationRecord
scope :positive, -> { where("post_votes.score > 0") }
scope :negative, -> { where("post_votes.score < 0") }
+ scope :public_votes, -> { positive.where(user: User.has_public_favorites) }
def self.visible(user)
- user.is_admin? ? all : where(user: user)
+ user.is_admin? ? all : where(user: user).or(public_votes)
end
def self.search(params)
q = search_attributes(params, :id, :created_at, :updated_at, :score, :user, :post)
+
q.apply_default_order(params)
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 0281fbe59..4ff18780c 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -152,6 +152,8 @@ class User < ApplicationRecord
scope :admins, -> { where(level: Levels::ADMIN) }
scope :has_blacklisted_tag, ->(name) { where_regex(:blacklisted_tags, "(^| )[~-]?#{Regexp.escape(name)}( |$)", flags: "ni") }
+ scope :has_private_favorites, -> { bit_prefs_match(:enable_private_favorites, true) }
+ scope :has_public_favorites, -> { bit_prefs_match(:enable_private_favorites, false) }
module BanMethods
def unban!
diff --git a/app/policies/post_vote_policy.rb b/app/policies/post_vote_policy.rb
index 3b385d517..279e10d5c 100644
--- a/app/policies/post_vote_policy.rb
+++ b/app/policies/post_vote_policy.rb
@@ -8,6 +8,16 @@ class PostVotePolicy < ApplicationPolicy
end
def show?
- user.is_admin? || record.user == user
+ user.is_admin? || record.user == user || (record.is_positive? && !record.user.enable_private_favorites?)
+ end
+
+ def can_see_voter?
+ show?
+ end
+
+ def api_attributes
+ attributes = super
+ attributes -= [:user_id] unless can_see_voter?
+ attributes
end
end
diff --git a/app/views/post_votes/index.html.erb b/app/views/post_votes/index.html.erb
index 609ef2f7b..c93eb5e77 100644
--- a/app/views/post_votes/index.html.erb
+++ b/app/views/post_votes/index.html.erb
@@ -24,8 +24,12 @@
<%= time_ago_in_words_tagged(vote.post.created_at) %>
<% end %>
<% t.column "Voter" do |vote| %>
- <%= link_to_user vote.user %>
- <%= link_to "»", post_votes_path(search: { user_name: vote.user.name }) %>
+ <% if policy(vote).can_see_voter? %>
+ <%= link_to_user vote.user %>
+ <%= link_to "»", post_votes_path(search: { user_name: vote.user.name }) %>
+ <% else %>
+ hidden
+ <% end %>
<%= time_ago_in_words_tagged(vote.created_at) %>
<% end %>
<% t.column column: "control" do |vote| %>
diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb
index 557927ea3..fe556b763 100644
--- a/app/views/users/edit.html.erb
+++ b/app/views/users/edit.html.erb
@@ -63,7 +63,7 @@
<%= f.input :hide_deleted_posts, :as => :select, :label => "Deleted post filter", :hint => "Remove deleted posts from search results", :include_blank => false, :collection => [["Yes", "true"], ["No", "false"]] %>
<%= f.input :show_deleted_children, :as => :select, :label => "Show deleted children", :hint => "Show thumbnail borders on parent posts even if the children are deleted", :include_blank => false, :collection => [["Yes", "true"], ["No", "false"]] %>
<%= f.input :disable_categorized_saved_searches, :hint => "Don't show dialog box when creating a new saved search", :as => :select, :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false %>
- <%= f.input :enable_private_favorites, :as => :select, :hint => "Make your favorites private", :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false %>
+ <%= f.input :enable_private_favorites, :label => "Private favorites and votes", :as => :select, :hint => "Make your favorites and upvotes private", :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false %>
<%= f.input :disable_tagged_filenames, :as => :select, :hint => "Don't include tags in image filenames", :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false %>
<%= f.input :disable_mobile_gestures, :as => :select, :hint => "Disable swipe left / swipe right gestures on mobile", :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false %>
<%= f.input :disable_post_tooltips, :as => :select, :hint => "Disable advanced tooltips when hovering over thumbnails", :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false %>
diff --git a/test/functional/post_votes_controller_test.rb b/test/functional/post_votes_controller_test.rb
index ffe2f92e8..43687de23 100644
--- a/test/functional/post_votes_controller_test.rb
+++ b/test/functional/post_votes_controller_test.rb
@@ -9,59 +9,157 @@ class PostVotesControllerTest < ActionDispatch::IntegrationTest
context "index action" do
setup do
- @admin = create(:admin_user)
- as(@user) { @post_vote = create(:post_vote, post: @post, user: @user) }
- as(@admin) { @admin_vote = create(:post_vote, post: @post, user: @admin) }
- @unrelated_vote = create(:post_vote)
+ @user = create(:user, enable_private_favorites: true)
+ create(:post_vote, user: @user, score: 1)
+ create(:post_vote, user: @user, score: -1)
end
should "render" do
- get_auth post_votes_path, @user
+ get post_votes_path
assert_response :success
end
context "as a user" do
- setup do
- CurrentUser.user = @user
+ should "show the user all their own votes" do
+ get_auth post_votes_path, @user
+ assert_response :success
+ assert_select "tbody tr", 2
+
+ get_auth post_votes_path(search: { user_id: @user.id }), @user
+ assert_response :success
+ assert_select "tbody tr", 2
+
+ get_auth post_votes_path(search: { user_name: @user.name }), @user
+ assert_response :success
+ assert_select "tbody tr", 2
end
- should respond_to_search({}).with { @post_vote }
+ should "not show private upvotes to other users" do
+ get_auth post_votes_path, create(:user)
+ assert_response :success
+ assert_select "tbody tr", 0
+
+ get_auth post_votes_path(search: { user_id: @user.id }), create(:user)
+ assert_response :success
+ assert_select "tbody tr", 0
+
+ get_auth post_votes_path(search: { user_name: @user.name }), create(:user)
+ assert_response :success
+ assert_select "tbody tr", 0
+ end
+
+ should "not show downvotes to other users" do
+ @user.update!(enable_private_favorites: false)
+
+ get_auth post_votes_path, create(:user)
+ assert_response :success
+ assert_select "tbody tr[data-score=1]", 1
+ assert_select "tbody tr[data-score=-1]", 0
+
+ get_auth post_votes_path(search: { user_id: @user.id }), create(:user)
+ assert_response :success
+ assert_select "tbody tr[data-score=1]", 1
+ assert_select "tbody tr[data-score=-1]", 0
+
+ get_auth post_votes_path(search: { user_name: @user.name }), create(:user)
+ assert_response :success
+ assert_select "tbody tr[data-score=1]", 1
+ assert_select "tbody tr[data-score=-1]", 0
+ end
end
- context "as a moderator" do
- setup do
- CurrentUser.user = @admin
- end
+ context "as an admin" do
+ should "show all votes by other users" do
+ @admin = create(:admin_user)
- should respond_to_search({}).with { [@unrelated_vote, @admin_vote, @post_vote] }
- should respond_to_search(score: 1).with { [@unrelated_vote, @admin_vote, @post_vote].select{ |v| v.score == 1 } }
+ get_auth post_votes_path, @admin
+ assert_response :success
+ assert_select "tbody tr", 2
- context "using includes" do
- should respond_to_search(post_tags_match: "dragon").with { [@admin_vote, @post_vote] }
- should respond_to_search(user_name: "meiling").with { @post_vote }
- should respond_to_search(user: {level: User::Levels::ADMIN}).with { @admin_vote }
+ get_auth post_votes_path(search: { user_id: @user.id }), @admin
+ assert_response :success
+ assert_select "tbody tr", 2
+
+ get_auth post_votes_path(search: { user_name: @user.name }), @admin
+ assert_response :success
+ assert_select "tbody tr", 2
+
+ get_auth post_votes_path(search: { user: { level: @user.level }}), @admin
+ assert_response :success
+ assert_select "tbody tr", 2
end
end
end
context "show action" do
- setup do
- @post_vote = create(:post_vote, post: @post, user: @user)
+ context "for a public upvote" do
+ setup do
+ @user = create(:user, enable_private_favorites: false)
+ @post_vote = create(:post_vote, user: @user, score: 1)
+ end
+
+ should "show the voter to everyone" do
+ get post_vote_path(@post_vote), as: :json
+
+ assert_response :success
+ assert_equal(@user.id, response.parsed_body["user_id"])
+ end
end
- should "show the vote to the voter" do
- get_auth post_vote_path(@post_vote), @user, as: :json
- assert_response :success
+ context "for a private upvote" do
+ setup do
+ @user = create(:user, enable_private_favorites: true)
+ @post_vote = create(:post_vote, user: @user, score: 1)
+ end
+
+ should "show the voter to themselves" do
+ get_auth post_vote_path(@post_vote), @user, as: :json
+
+ assert_response :success
+ assert_equal(@user.id, response.parsed_body["user_id"])
+ end
+
+ should "show the voter to admins" do
+ get_auth post_vote_path(@post_vote), create(:admin_user), as: :json
+
+ assert_response :success
+ assert_equal(@user.id, response.parsed_body["user_id"])
+ end
+
+ should "not show the voter to other users" do
+ get post_vote_path(@post_vote), as: :json
+
+ assert_response 403
+ assert_nil(response.parsed_body["user_id"])
+ end
end
- should "show the vote to admins" do
- get_auth post_vote_path(@post_vote), create(:admin_user), as: :json
- assert_response :success
- end
+ context "for a downvote" do
+ setup do
+ @user = create(:user, enable_private_favorites: false)
+ @post_vote = create(:post_vote, user: @user, score: -1)
+ end
- should "not show the vote to other users" do
- get_auth post_vote_path(@post_vote), create(:user), as: :json
- assert_response 403
+ should "show the voter to themselves" do
+ get_auth post_vote_path(@post_vote), @user, as: :json
+
+ assert_response :success
+ assert_equal(@user.id, response.parsed_body["user_id"])
+ end
+
+ should "show the voter to admins" do
+ get_auth post_vote_path(@post_vote), create(:admin_user), as: :json
+
+ assert_response :success
+ assert_equal(@user.id, response.parsed_body["user_id"])
+ end
+
+ should "not show the voter to other users" do
+ get post_vote_path(@post_vote), as: :json
+
+ assert_response 403
+ assert_nil(response.parsed_body["user_id"])
+ end
end
end
diff --git a/test/unit/post_query_builder_test.rb b/test/unit/post_query_builder_test.rb
index 5bd7fcb10..94958fda6 100644
--- a/test/unit/post_query_builder_test.rb
+++ b/test/unit/post_query_builder_test.rb
@@ -973,6 +973,75 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match(all - [e], "-rating:e")
end
+ context "for the upvote: metatag" do
+ setup do
+ @user = create(:user)
+ @upvote = create(:post_vote, user: @user, score: 1)
+ @downvote = create(:post_vote, user: @user, score: -1)
+ end
+
+ should "show public upvotes to all users" do
+ as(User.anonymous) do
+ assert_tag_match([@upvote.post], "upvote:#{@user.name}")
+ assert_tag_match([@downvote.post], "-upvote:#{@user.name}")
+ end
+ end
+
+ should "not show private upvotes to other users" do
+ @user.update!(enable_private_favorites: true)
+
+ as(User.anonymous) do
+ assert_tag_match([], "upvote:#{@user.name}")
+ assert_tag_match([@downvote.post, @upvote.post], "-upvote:#{@user.name}")
+ end
+ end
+
+ should "show private upvotes to admins" do
+ @user.update!(enable_private_favorites: true)
+
+ as(create(:admin_user)) do
+ assert_tag_match([@upvote.post], "upvote:#{@user.name}")
+ assert_tag_match([@downvote.post], "-upvote:#{@user.name}")
+ end
+ end
+
+ should "show private upvotes to the voter themselves" do
+ as(@user) do
+ assert_tag_match([@upvote.post], "upvote:#{@user.name}")
+ assert_tag_match([@downvote.post], "-upvote:#{@user.name}")
+ end
+ end
+ end
+
+ context "for the downvote: metatag" do
+ setup do
+ @user = create(:user, enable_private_favorites: true)
+ @upvote = create(:post_vote, user: @user, score: 1)
+ @downvote = create(:post_vote, user: @user, score: -1)
+ end
+
+ should "not show downvotes to other users" do
+ as(User.anonymous) do
+ assert_tag_match([], "downvote:#{@user.name}")
+ assert_tag_match([@downvote.post, @upvote.post], "-downvote:#{@user.name}")
+ end
+ end
+
+ should "show downvotes to admins" do
+ as(create(:admin_user)) do
+ assert_tag_match([@downvote.post], "downvote:#{@user.name}")
+ assert_tag_match([@upvote.post], "-downvote:#{@user.name}")
+ end
+ end
+
+ should "show downvotes to the voter themselves" do
+ as(@user) do
+ assert_tag_match([@downvote.post], "downvote:#{@user.name}")
+ assert_tag_match([@upvote.post], "-downvote:#{@user.name}")
+ end
+ end
+ end
+
should "return posts for a upvote:, downvote: metatag" do
CurrentUser.scoped(create(:mod_user)) do
upvoted = create(:post, tag_string: "upvote:self")