From 1a27b1d5eb280f8d62cc16a5d95eb3790bc317dc Mon Sep 17 00:00:00 2001 From: evazion Date: Tue, 16 Nov 2021 02:12:49 -0600 Subject: [PATCH] votes: make upvotes visible to everyone by default. Make upvotes public the same way favorites are public: * Rename the "Private favorites" account setting to "Private favorites and upvotes". * Make upvotes public, unless the user has private upvotes enabled. Note that private upvotes are still visible to admins. Downvotes are still hidden to everyone except for admins. * Make https://danbooru.donmai.us/post_votes visible to all users. This page shows all public upvotes. Private upvotes and downvotes are only visible on the page to admins and to the voter themselves. * Make votes searchable with the `upvote:username` and `downvote:username` metatags. These already existed before, but they were only usable by admins and by people searching for their own votes. Upvotes are public to discourage users from upvoting with multiple accounts. Upvote abuse is obvious to everyone when upvotes are public. The other reason is to make upvotes consistent with favorites, which are already public. --- app/models/post_vote.rb | 4 +- app/models/user.rb | 2 + app/policies/post_vote_policy.rb | 12 +- app/views/post_votes/index.html.erb | 8 +- app/views/users/edit.html.erb | 2 +- test/functional/post_votes_controller_test.rb | 158 ++++++++++++++---- test/unit/post_query_builder_test.rb | 69 ++++++++ 7 files changed, 220 insertions(+), 35 deletions(-) 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")