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")