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.
This commit is contained in:
evazion
2021-11-16 02:12:49 -06:00
parent 43c2870664
commit 1a27b1d5eb
7 changed files with 220 additions and 35 deletions

View File

@@ -10,13 +10,15 @@ class PostVote < ApplicationRecord
scope :positive, -> { where("post_votes.score > 0") } scope :positive, -> { where("post_votes.score > 0") }
scope :negative, -> { 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) def self.visible(user)
user.is_admin? ? all : where(user: user) user.is_admin? ? all : where(user: user).or(public_votes)
end end
def self.search(params) def self.search(params)
q = search_attributes(params, :id, :created_at, :updated_at, :score, :user, :post) q = search_attributes(params, :id, :created_at, :updated_at, :score, :user, :post)
q.apply_default_order(params) q.apply_default_order(params)
end end

View File

@@ -152,6 +152,8 @@ class User < ApplicationRecord
scope :admins, -> { where(level: Levels::ADMIN) } scope :admins, -> { where(level: Levels::ADMIN) }
scope :has_blacklisted_tag, ->(name) { where_regex(:blacklisted_tags, "(^| )[~-]?#{Regexp.escape(name)}( |$)", flags: "ni") } 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 module BanMethods
def unban! def unban!

View File

@@ -8,6 +8,16 @@ class PostVotePolicy < ApplicationPolicy
end end
def show? 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
end end

View File

@@ -24,8 +24,12 @@
<div><%= time_ago_in_words_tagged(vote.post.created_at) %></div> <div><%= time_ago_in_words_tagged(vote.post.created_at) %></div>
<% end %> <% end %>
<% t.column "Voter" do |vote| %> <% t.column "Voter" do |vote| %>
<%= link_to_user vote.user %> <% if policy(vote).can_see_voter? %>
<%= link_to "»", post_votes_path(search: { user_name: vote.user.name }) %> <%= link_to_user vote.user %>
<%= link_to "»", post_votes_path(search: { user_name: vote.user.name }) %>
<% else %>
<i>hidden</i>
<% end %>
<div><%= time_ago_in_words_tagged(vote.created_at) %></div> <div><%= time_ago_in_words_tagged(vote.created_at) %></div>
<% end %> <% end %>
<% t.column column: "control" do |vote| %> <% t.column column: "control" do |vote| %>

View File

@@ -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 :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 :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 :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_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_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 %> <%= f.input :disable_post_tooltips, :as => :select, :hint => "Disable advanced tooltips when hovering over thumbnails", :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false %>

View File

@@ -9,59 +9,157 @@ class PostVotesControllerTest < ActionDispatch::IntegrationTest
context "index action" do context "index action" do
setup do setup do
@admin = create(:admin_user) @user = create(:user, enable_private_favorites: true)
as(@user) { @post_vote = create(:post_vote, post: @post, user: @user) } create(:post_vote, user: @user, score: 1)
as(@admin) { @admin_vote = create(:post_vote, post: @post, user: @admin) } create(:post_vote, user: @user, score: -1)
@unrelated_vote = create(:post_vote)
end end
should "render" do should "render" do
get_auth post_votes_path, @user get post_votes_path
assert_response :success assert_response :success
end end
context "as a user" do context "as a user" do
setup do should "show the user all their own votes" do
CurrentUser.user = @user 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 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 end
context "as a moderator" do context "as an admin" do
setup do should "show all votes by other users" do
CurrentUser.user = @admin @admin = create(:admin_user)
end
should respond_to_search({}).with { [@unrelated_vote, @admin_vote, @post_vote] } get_auth post_votes_path, @admin
should respond_to_search(score: 1).with { [@unrelated_vote, @admin_vote, @post_vote].select{ |v| v.score == 1 } } assert_response :success
assert_select "tbody tr", 2
context "using includes" do get_auth post_votes_path(search: { user_id: @user.id }), @admin
should respond_to_search(post_tags_match: "dragon").with { [@admin_vote, @post_vote] } assert_response :success
should respond_to_search(user_name: "meiling").with { @post_vote } assert_select "tbody tr", 2
should respond_to_search(user: {level: User::Levels::ADMIN}).with { @admin_vote }
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 end
end end
context "show action" do context "show action" do
setup do context "for a public upvote" do
@post_vote = create(:post_vote, post: @post, user: @user) 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 end
should "show the vote to the voter" do context "for a private upvote" do
get_auth post_vote_path(@post_vote), @user, as: :json setup do
assert_response :success @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 end
should "show the vote to admins" do context "for a downvote" do
get_auth post_vote_path(@post_vote), create(:admin_user), as: :json setup do
assert_response :success @user = create(:user, enable_private_favorites: false)
end @post_vote = create(:post_vote, user: @user, score: -1)
end
should "not show the vote to other users" do should "show the voter to themselves" do
get_auth post_vote_path(@post_vote), create(:user), as: :json get_auth post_vote_path(@post_vote), @user, as: :json
assert_response 403
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
end end

View File

@@ -973,6 +973,75 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match(all - [e], "-rating:e") assert_tag_match(all - [e], "-rating:e")
end end
context "for the upvote:<user> 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:<user> 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:<user>, downvote:<user> metatag" do should "return posts for a upvote:<user>, downvote:<user> metatag" do
CurrentUser.scoped(create(:mod_user)) do CurrentUser.scoped(create(:mod_user)) do
upvoted = create(:post, tag_string: "upvote:self") upvoted = create(:post, tag_string: "upvote:self")