From 1a27b1d5eb280f8d62cc16a5d95eb3790bc317dc Mon Sep 17 00:00:00 2001 From: evazion Date: Tue, 16 Nov 2021 02:12:49 -0600 Subject: [PATCH 01/11] 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") From 055e5939b4abd52d3b110d0fb85a34daf264e44e Mon Sep 17 00:00:00 2001 From: evazion Date: Tue, 16 Nov 2021 05:11:04 -0600 Subject: [PATCH 02/11] votes: allow Members to vote. * Allow Member-level users to vote. * Don't allow Banned or Restricted users to create favorites any more. Banned and Restricted users aren't allowed to upvote or favorite any more to prevent sockpuppet accounts from upvoting even after they're banned. --- app/policies/favorite_policy.rb | 2 +- app/policies/post_vote_policy.rb | 2 +- test/factories/user.rb | 1 + test/functional/favorites_controller_test.rb | 15 ++++++++++++--- test/functional/post_votes_controller_test.rb | 11 +++++++++-- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/app/policies/favorite_policy.rb b/app/policies/favorite_policy.rb index b604f1ea1..29784ceb7 100644 --- a/app/policies/favorite_policy.rb +++ b/app/policies/favorite_policy.rb @@ -1,6 +1,6 @@ class FavoritePolicy < ApplicationPolicy def create? - !user.is_anonymous? + unbanned? && user.is_member? end def destroy? diff --git a/app/policies/post_vote_policy.rb b/app/policies/post_vote_policy.rb index 279e10d5c..730cbe46c 100644 --- a/app/policies/post_vote_policy.rb +++ b/app/policies/post_vote_policy.rb @@ -1,6 +1,6 @@ class PostVotePolicy < ApplicationPolicy def create? - unbanned? && user.is_gold? + unbanned? && user.is_member? end def destroy? diff --git a/test/factories/user.rb b/test/factories/user.rb index dc6cf0942..a9bbe1c77 100644 --- a/test/factories/user.rb +++ b/test/factories/user.rb @@ -8,6 +8,7 @@ FactoryBot.define do factory(:banned_user) do transient { ban_duration {3} } is_banned {true} + active_ban factory: :ban end factory(:restricted_user) do diff --git a/test/functional/favorites_controller_test.rb b/test/functional/favorites_controller_test.rb index 61e94cffe..f714e8393 100644 --- a/test/functional/favorites_controller_test.rb +++ b/test/functional/favorites_controller_test.rb @@ -48,12 +48,21 @@ class FavoritesControllerTest < ActionDispatch::IntegrationTest end end - should "allow banned users to create favorites" do + should "not allow banned users to create favorites" do @banned_user = create(:banned_user) - assert_difference [-> { @post.favorites.count }, -> { @post.reload.fav_count }, -> { @banned_user.reload.favorite_count }], 1 do + assert_difference [-> { @post.favorites.count }, -> { @post.reload.fav_count }, -> { @banned_user.reload.favorite_count }], 0 do post_auth favorites_path(post_id: @post.id), @banned_user, as: :javascript - assert_response :redirect + assert_response 403 + end + end + + should "not allow restricted users to create favorites" do + @restricted_user = create(:restricted_user) + + assert_difference [-> { @post.favorites.count }, -> { @post.reload.fav_count }, -> { @restricted_user.reload.favorite_count }], 0 do + post_auth favorites_path(post_id: @post.id), @restricted_user, as: :javascript + assert_response 403 end end diff --git a/test/functional/post_votes_controller_test.rb b/test/functional/post_votes_controller_test.rb index 43687de23..d329cda62 100644 --- a/test/functional/post_votes_controller_test.rb +++ b/test/functional/post_votes_controller_test.rb @@ -185,13 +185,20 @@ class PostVotesControllerTest < ActionDispatch::IntegrationTest assert_equal(0, @post.reload.score) end - should "not allow members to vote" do - post_auth post_post_votes_path(post_id: @post.id), create(:user), params: { score: 1, format: "js" } + should "not allow restricted users to vote" do + post_auth post_post_votes_path(post_id: @post.id), create(:restricted_user), params: { score: 1, format: "js"} assert_response 403 assert_equal(0, @post.reload.score) end + should "allow members to vote" do + post_auth post_post_votes_path(post_id: @post.id), create(:user), params: { score: 1, format: "js" } + + assert_response :success + assert_equal(1, @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" } From bc96eb864bb6803f167f91e04f513e0cebd4714d Mon Sep 17 00:00:00 2001 From: evazion Date: Tue, 16 Nov 2021 17:09:06 -0600 Subject: [PATCH 03/11] votes: make private favorites and upvotes a Gold-only option. Make private favorites and upvotes a Gold-only account option. Existing Members with private favorites enabled are allowed to keep it enabled, as long as they don't disable it. If they disable it, then they can't re-enable it again without upgrading to Gold first. This is a Gold-only option to prevent uploaders from creating multiple accounts to upvote their own posts. If private upvotes were allowed for Members, then it would be too easy to use fake accounts and private upvotes to upvote your own posts. --- app/logical/concerns/has_bit_flags.rb | 9 ++++++++ app/models/user.rb | 9 ++++++++ app/policies/user_policy.rb | 4 ++++ app/views/users/edit.html.erb | 8 ++++++- test/functional/users_controller_test.rb | 28 ++++++++++++++++++++++++ 5 files changed, 57 insertions(+), 1 deletion(-) diff --git a/app/logical/concerns/has_bit_flags.rb b/app/logical/concerns/has_bit_flags.rb index c05b6d54a..9364c042f 100644 --- a/app/logical/concerns/has_bit_flags.rb +++ b/app/logical/concerns/has_bit_flags.rb @@ -6,6 +6,7 @@ module HasBitFlags def has_bit_flags(attributes, field: :bit_flags) attributes.each.with_index do |attribute, i| bit_flag = 1 << i + field_was = "#{field}_was" define_method(attribute) do send(field) & bit_flag > 0 @@ -15,6 +16,14 @@ module HasBitFlags send(field) & bit_flag > 0 end + define_method("#{attribute}_was") do + send(field_was) & bit_flag > 0 + end + + define_method("#{attribute}_was?") do + send(field_was) & bit_flag > 0 + end + define_method("#{attribute}=") do |val| if val.to_s =~ /[t1y]/ send("#{field}=", send(field) | bit_flag) diff --git a/app/models/user.rb b/app/models/user.rb index 4ff18780c..74b21d966 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -103,6 +103,7 @@ class User < ApplicationRecord validates :per_page, inclusion: { in: (1..PostSets::Post::MAX_PER_PAGE) } validates :password, confirmation: true validates :comment_threshold, inclusion: { in: (-100..5) } + validate :validate_enable_private_favorites, on: :update before_validation :normalize_blacklisted_tags before_create :promote_to_owner_if_first_user has_many :artist_versions, foreign_key: :updater_id @@ -192,6 +193,14 @@ class User < ApplicationRecord end end + concerning :ValidationMethods do + def validate_enable_private_favorites + if enable_private_favorites_was == false && enable_private_favorites == true && !Pundit.policy!(self, self).can_enable_private_favorites? + errors.add(:base, "Can't enable privacy mode without a Gold account") + end + end + end + concerning :AuthenticationMethods do def password=(new_password) @password = new_password diff --git a/app/policies/user_policy.rb b/app/policies/user_policy.rb index ebf97d350..75ae4deb5 100644 --- a/app/policies/user_policy.rb +++ b/app/policies/user_policy.rb @@ -31,6 +31,10 @@ class UserPolicy < ApplicationPolicy user.is_admin? || record.id == user.id || !record.enable_private_favorites? end + def can_enable_private_favorites? + user.is_gold? + end + def permitted_attributes_for_create [:name, :password, :password_confirmation, { email_address_attributes: [:address] }] end diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb index fe556b763..0d7487658 100644 --- a/app/views/users/edit.html.erb +++ b/app/views/users/edit.html.erb @@ -63,7 +63,13 @@ <%= 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, :label => "Private favorites and votes", :as => :select, :hint => "Make your favorites and upvotes private", :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false %> + <% if policy(@user).can_enable_private_favorites? %> + <%= 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 %> + <% elsif @user.enable_private_favorites? %> + <%= f.input :enable_private_favorites, label: "Private favorites and votes", as: :select, hint: "Make your favorites and upvotes private. (Warning: if you disable this, you can't re-enable it without ".html_safe + link_to("upgrading to a Gold account", new_user_upgrade_path) + " first. (".html_safe + link_to_wiki("learn more", "help:privacy_mode") + ")".html_safe, collection: { "No" => false, "Yes" => true }, include_blank: false %> + <% else %> + <%= f.input :enable_private_favorites, label: "Private favorites and votes", as: :select, hint: link_to("Upgrade to Danbooru Gold to enable private favorites and upvotes", new_user_upgrade_path), collection: { "No" => false, "Yes" => true }, include_blank: false %> + <% end %> <%= 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/users_controller_test.rb b/test/functional/users_controller_test.rb index e71d27c12..8053f0ddb 100644 --- a/test/functional/users_controller_test.rb +++ b/test/functional/users_controller_test.rb @@ -420,6 +420,34 @@ class UsersControllerTest < ActionDispatch::IntegrationTest assert_equal("xyz", @user.favorite_tags) end + context "for a Member-level user" do + should "allow disabling the private favorites option" do + @user = create(:user, enable_private_favorites: true) + put_auth user_path(@user), @user, params: { user: { enable_private_favorites: false }} + + assert_redirected_to edit_user_path(@user) + assert_equal(false, @user.reload.enable_private_favorites) + end + + should "not allow enabling the private favorites option" do + @user = create(:user, enable_private_favorites: false) + put_auth user_path(@user), @user, params: { user: { enable_private_favorites: true }} + + assert_redirected_to edit_user_path(@user) + assert_equal(false, @user.reload.enable_private_favorites) + end + end + + context "for a Gold-level user" do + should "allow enabling the private favorites option" do + @user = create(:gold_user, enable_private_favorites: false) + put_auth user_path(@user), @user, params: { user: { enable_private_favorites: true }} + + assert_redirected_to edit_user_path(@user) + assert_equal(true, @user.reload.enable_private_favorites) + end + end + context "changing the level" do should "not work" do @owner = create(:owner_user) From a9997d0d2ba01f386579f7d8b4c7d9b471c5ee43 Mon Sep 17 00:00:00 2001 From: evazion Date: Wed, 17 Nov 2021 20:11:06 -0600 Subject: [PATCH 04/11] favgroups: make private favgroups a Gold-only option. Make private favgroups a Gold-only option. This is for consistency with private favorites and upvotes being Gold-only options. Existing Members with private favgroups are allowed to keep them, as long as they don't disable privacy. If they disable it, then they can't re-enable it again without upgrading to Gold first. --- app/controllers/favorite_groups_controller.rb | 2 +- app/models/favorite_group.rb | 21 ++++++++++++++++++- app/policies/favorite_group_policy.rb | 6 +++++- app/views/favorite_groups/_form.html.erb | 12 +++++++++++ app/views/favorite_groups/edit.html.erb | 7 +------ app/views/favorite_groups/new.html.erb | 7 +------ test/factories/favorite_group.rb | 5 +++++ .../favorite_groups_controller_test.rb | 15 +++++++++---- 8 files changed, 56 insertions(+), 19 deletions(-) create mode 100644 app/views/favorite_groups/_form.html.erb diff --git a/app/controllers/favorite_groups_controller.rb b/app/controllers/favorite_groups_controller.rb index 3f3b02f9e..f221e97e5 100644 --- a/app/controllers/favorite_groups_controller.rb +++ b/app/controllers/favorite_groups_controller.rb @@ -19,7 +19,7 @@ class FavoriteGroupsController < ApplicationController end def new - @favorite_group = authorize FavoriteGroup.new + @favorite_group = authorize FavoriteGroup.new(creator: CurrentUser.user) respond_with(@favorite_group) end diff --git a/app/models/favorite_group.rb b/app/models/favorite_group.rb index 2cbaacc05..eedde972d 100644 --- a/app/models/favorite_group.rb +++ b/app/models/favorite_group.rb @@ -10,6 +10,7 @@ class FavoriteGroup < ApplicationRecord validate :creator_can_create_favorite_groups, :on => :create validate :validate_number_of_posts validate :validate_posts + validate :validate_can_enable_privacy array_attribute :post_ids, parse: /\d+/, cast: :to_i @@ -83,6 +84,12 @@ class FavoriteGroup < ApplicationRecord end end + def validate_can_enable_privacy + if is_public_change == [true, false] && !Pundit.policy!(creator, self).can_enable_privacy? + errors.add(:base, "Can't enable privacy without a Gold account") + end + end + def self.normalize_name(name) name.gsub(/[[:space:]]+/, "_") end @@ -112,7 +119,7 @@ class FavoriteGroup < ApplicationRecord end def pretty_name - name.tr("_", " ") + name&.tr("_", " ") end def posts @@ -166,6 +173,18 @@ class FavoriteGroup < ApplicationRecord post_ids.include?(post_id) end + def is_private=(value) + self.is_public = !ActiveModel::Type::Boolean.new.cast(value) + end + + def is_private + !is_public? + end + + def is_private? + !is_public? + end + def self.available_includes [:creator] end diff --git a/app/policies/favorite_group_policy.rb b/app/policies/favorite_group_policy.rb index 05cefc298..8926a1f8b 100644 --- a/app/policies/favorite_group_policy.rb +++ b/app/policies/favorite_group_policy.rb @@ -15,7 +15,11 @@ class FavoriteGroupPolicy < ApplicationPolicy update? end + def can_enable_privacy? + record.creator.is_gold? + end + def permitted_attributes - [:name, :post_ids_string, :is_public, :post_ids, { post_ids: [] }] + [:name, :post_ids_string, :is_public, :is_private, :post_ids, { post_ids: [] }] end end diff --git a/app/views/favorite_groups/_form.html.erb b/app/views/favorite_groups/_form.html.erb new file mode 100644 index 000000000..ae4069218 --- /dev/null +++ b/app/views/favorite_groups/_form.html.erb @@ -0,0 +1,12 @@ +<%= edit_form_for(@favorite_group) do |f| %> + <%= f.input :name, as: :string, required: true, input_html: { value: @favorite_group.pretty_name } %> + <%= f.input :post_ids_string, label: "Posts", as: :text %> + <% if policy(@favorite_group).can_enable_privacy? %> + <%= f.input :is_private, label: "Private", as: :boolean, hint: "Don't allow others to view this favgroup." %> + <% elsif @favorite_group.is_private? %> + <%= f.input :is_private, label: "Private", as: :boolean, hint: "Don't allow others to view this favgroup. Warning: if you disable this, you can't re-enable it without ".html_safe + link_to("upgrading to Danbooru Gold", new_user_upgrade_path) + ". (".html_safe + link_to_wiki("learn more", "help:privacy_mode") + ")".html_safe %> + <% else %> + <%= f.input :is_private, label: "Private", as: :boolean, hint: link_to("Upgrade to Danbooru Gold to enable private favgroups", new_user_upgrade_path), input_html: { disabled: true } %> + <% end %> + <%= f.submit "Submit" %> +<% end %> diff --git a/app/views/favorite_groups/edit.html.erb b/app/views/favorite_groups/edit.html.erb index 29d7369eb..c74ea5149 100644 --- a/app/views/favorite_groups/edit.html.erb +++ b/app/views/favorite_groups/edit.html.erb @@ -2,12 +2,7 @@

Edit Favorite Group: <%= @favorite_group.pretty_name %>

- <%= edit_form_for(@favorite_group) do |f| %> - <%= f.input :name, :as => :string, :input_html => { :value => @favorite_group.pretty_name } %> - <%= f.input :post_ids_string, label: "Posts", as: :text %> - <%= f.input :is_public %> - <%= f.button :submit, "Submit" %> - <% end %> + <%= render "form" %>
diff --git a/app/views/favorite_groups/new.html.erb b/app/views/favorite_groups/new.html.erb index b3245edaa..f0ca86e04 100644 --- a/app/views/favorite_groups/new.html.erb +++ b/app/views/favorite_groups/new.html.erb @@ -2,12 +2,7 @@

New Favorite Group

- <%= edit_form_for(@favorite_group) do |f| %> - <%= f.input :name, as: :string, required: true %> - <%= f.input :post_ids_string, label: "Posts", as: :text %> - <%= f.input :is_public, label: "Public" %> - <%= f.submit "Submit" %> - <% end %> + <%= render "form" %>
diff --git a/test/factories/favorite_group.rb b/test/factories/favorite_group.rb index 73104b22a..f03473a07 100644 --- a/test/factories/favorite_group.rb +++ b/test/factories/favorite_group.rb @@ -2,5 +2,10 @@ FactoryBot.define do factory :favorite_group do creator name { SecureRandom.uuid } + + factory :private_favorite_group do + creator factory: :gold_user + is_public { false } + end end end diff --git a/test/functional/favorite_groups_controller_test.rb b/test/functional/favorite_groups_controller_test.rb index f9d0d32bb..bba7a91ce 100644 --- a/test/functional/favorite_groups_controller_test.rb +++ b/test/functional/favorite_groups_controller_test.rb @@ -10,7 +10,7 @@ class FavoriteGroupsControllerTest < ActionDispatch::IntegrationTest context "index action" do setup do @mod_favgroup = create(:favorite_group, name: "monochrome", creator: build(:moderator_user, name: "fumimi")) - @private_favgroup = create(:favorite_group, creator: @user, is_public: false) + @private_favgroup = create(:private_favorite_group) end should "render" do @@ -28,7 +28,7 @@ class FavoriteGroupsControllerTest < ActionDispatch::IntegrationTest context "for private favorite groups as the creator" do setup do - CurrentUser.user = @user + CurrentUser.user = @private_favgroup.creator end should respond_to_search(is_public: "false").with { @private_favgroup } @@ -42,13 +42,13 @@ class FavoriteGroupsControllerTest < ActionDispatch::IntegrationTest end should "show private favgroups to the creator" do - @favgroup.update!(is_public: false) + @favgroup.update_columns(is_public: false) get_auth favorite_group_path(@favgroup), @user assert_response :success end should "not show private favgroups to other users" do - @favgroup = create(:favorite_group, is_public: false) + @favgroup = create(:private_favorite_group) get_auth favorite_group_path(@favgroup), create(:user) assert_response 403 end @@ -91,6 +91,13 @@ class FavoriteGroupsControllerTest < ActionDispatch::IntegrationTest assert_response 403 assert_not_equal("foo", @favgroup.reload.name) end + + should "not allow non-Gold users to enable private favgroups" do + put_auth favorite_group_path(@favgroup), @user, params: { favorite_group: { is_private: true } } + + assert_response :success + assert_equal(false, @favgroup.is_private?) + end end context "destroy action" do From 5585d1f7d6908f32c91d3638d2c8ecaec3f90841 Mon Sep 17 00:00:00 2001 From: evazion Date: Thu, 18 Nov 2021 01:05:24 -0600 Subject: [PATCH 05/11] votes: show votes when hovering over post score. Make it so you can hover over a post's score to see the list of public upvotes. Also show the upvote count, the downvote count, and the upvote ratio. --- .../post_votes_component.html.erb | 4 +- .../post_votes_component.scss | 6 ++ .../post_votes_tooltip_component.rb | 34 ++++++++++ .../post_votes_tooltip_component.html.erb | 13 ++++ .../post_votes_tooltip_component.js | 65 +++++++++++++++++++ .../post_votes_tooltip_component.scss | 16 +++++ app/controllers/post_votes_controller.rb | 1 + app/helpers/application_helper.rb | 4 +- app/helpers/components_helper.rb | 4 ++ app/javascript/packs/application.js | 2 + app/javascript/src/styles/base/040_colors.css | 6 ++ .../src/styles/common/utilities.scss | 63 ++++++++++++++++++ .../src/styles/specific/post_tooltips.scss | 45 ------------- app/views/layouts/default.html.erb | 1 + app/views/post_votes/_search.html.erb | 7 ++ app/views/post_votes/index.html+compact.erb | 24 +++++++ app/views/post_votes/index.html+tooltip.erb | 1 + app/views/post_votes/index.html.erb | 8 +-- app/views/posts/show.html+tooltip.erb | 2 +- test/functional/post_votes_controller_test.rb | 14 +++- 20 files changed, 262 insertions(+), 58 deletions(-) create mode 100644 app/components/post_votes_tooltip_component.rb create mode 100644 app/components/post_votes_tooltip_component/post_votes_tooltip_component.html.erb create mode 100644 app/components/post_votes_tooltip_component/post_votes_tooltip_component.js create mode 100644 app/components/post_votes_tooltip_component/post_votes_tooltip_component.scss create mode 100644 app/views/post_votes/_search.html.erb create mode 100644 app/views/post_votes/index.html+compact.erb create mode 100644 app/views/post_votes/index.html+tooltip.erb diff --git a/app/components/post_votes_component/post_votes_component.html.erb b/app/components/post_votes_component/post_votes_component.html.erb index f9186994a..5934c1a42 100644 --- a/app/components/post_votes_component/post_votes_component.html.erb +++ b/app/components/post_votes_component/post_votes_component.html.erb @@ -7,7 +7,9 @@ <% end %> <% end %> - <%= post.score %> + + <%= link_to post.score, post_votes_path(search: { post_id: post.id }, variant: :compact) %> + <% if can_vote? %> <% if downvoted? %> diff --git a/app/components/post_votes_component/post_votes_component.scss b/app/components/post_votes_component/post_votes_component.scss index 4d2fd99ac..0dcd21510 100644 --- a/app/components/post_votes_component/post_votes_component.scss +++ b/app/components/post_votes_component/post_votes_component.scss @@ -6,5 +6,11 @@ text-align: center; min-width: 1.25em; white-space: nowrap; + vertical-align: middle; + + a { + color: var(--text-color); + &:hover { text-decoration: underline; } + } } } diff --git a/app/components/post_votes_tooltip_component.rb b/app/components/post_votes_tooltip_component.rb new file mode 100644 index 000000000..b8b1f11a4 --- /dev/null +++ b/app/components/post_votes_tooltip_component.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# This component represents the tooltip that displays when you hover over a post's score. +class PostVotesTooltipComponent < ApplicationComponent + attr_reader :post, :current_user + delegate :upvote_icon, :downvote_icon, to: :helpers + + def initialize(post:, current_user:) + super + @post = post + @current_user = current_user + end + + def votes + @post.votes.includes(:user).order(id: :desc) + end + + def vote_icon(vote) + vote.is_positive? ? upvote_icon : downvote_icon + end + + def upvote_ratio + return nil if votes.length == 0 + sprintf("(%.1f%%)", 100.0 * votes.select(&:is_positive?).length / votes.length) + end + + def voter_name(vote) + if policy(vote).can_see_voter? + link_to_user(vote.user, classes: "align-middle") + else + tag.i("hidden", class: "align-middle") + end + end +end diff --git a/app/components/post_votes_tooltip_component/post_votes_tooltip_component.html.erb b/app/components/post_votes_tooltip_component/post_votes_tooltip_component.html.erb new file mode 100644 index 000000000..24937ae58 --- /dev/null +++ b/app/components/post_votes_tooltip_component/post_votes_tooltip_component.html.erb @@ -0,0 +1,13 @@ +
+
+ +<%= post.up_score %> / <%= post.down_score %> <%= upvote_ratio %> +
+ +
+ <% votes.each do |vote| %> +
+ <%= vote_icon(vote) %> <%= voter_name(vote) %> +
+ <% end %> +
+
diff --git a/app/components/post_votes_tooltip_component/post_votes_tooltip_component.js b/app/components/post_votes_tooltip_component/post_votes_tooltip_component.js new file mode 100644 index 000000000..d9223d9ab --- /dev/null +++ b/app/components/post_votes_tooltip_component/post_votes_tooltip_component.js @@ -0,0 +1,65 @@ +import Utility from "../../javascript/src/javascripts/utility.js"; +import { delegate, hideAll } from 'tippy.js'; +import 'tippy.js/dist/tippy.css'; + +class PostVotesTooltipComponent { + // Trigger on the post score link; see PostVotesComponent. + static TARGET_SELECTOR = "span.post-votes span.post-score a"; + static SHOW_DELAY = 125; + static HIDE_DELAY = 125; + static DURATION = 250; + static instance = null; + + static initialize() { + if ($(PostVotesTooltipComponent.TARGET_SELECTOR).length === 0) { + return; + } + + PostVotesTooltipComponent.instance = delegate("body", { + allowHTML: true, + appendTo: document.querySelector("#post-votes-tooltips"), + delay: [PostVotesTooltipComponent.SHOW_DELAY, PostVotesTooltipComponent.HIDE_DELAY], + duration: PostVotesTooltipComponent.DURATION, + interactive: true, + maxWidth: "none", + target: PostVotesTooltipComponent.TARGET_SELECTOR, + theme: "common-tooltip", + touch: false, + + onShow: PostVotesTooltipComponent.onShow, + onHide: PostVotesTooltipComponent.onHide, + }); + } + + static async onShow(instance) { + let $target = $(instance.reference); + let $tooltip = $(instance.popper); + let postId = $target.parents("[data-id]").data("id"); + + hideAll({ exclude: instance }); + + try { + $tooltip.addClass("tooltip-loading"); + + instance._request = $.get(`/post_votes?search[post_id]=${postId}`, { variant: "tooltip" }); + let html = await instance._request; + instance.setContent(html); + + $tooltip.removeClass("tooltip-loading"); + } catch (error) { + if (error.status !== 0 && error.statusText !== "abort") { + Utility.error(`Error displaying votes for post #${postId} (error: ${error.status} ${error.statusText})`); + } + } + } + + static async onHide(instance) { + if (instance._request?.state() === "pending") { + instance._request.abort(); + } + } +} + +$(document).ready(PostVotesTooltipComponent.initialize); + +export default PostVotesTooltipComponent; diff --git a/app/components/post_votes_tooltip_component/post_votes_tooltip_component.scss b/app/components/post_votes_tooltip_component/post_votes_tooltip_component.scss new file mode 100644 index 000000000..2cc839b5d --- /dev/null +++ b/app/components/post_votes_tooltip_component/post_votes_tooltip_component.scss @@ -0,0 +1,16 @@ +.post-votes-tooltip { + font-size: var(--text-xs); + max-height: 240px; + + .upvote-icon { + color: var(--post-upvote-color); + } + + .downvote-icon { + color: var(--post-downvote-color); + } + + .post-voter { + max-width: 160px; + } +} diff --git a/app/controllers/post_votes_controller.rb b/app/controllers/post_votes_controller.rb index c21c1d102..24484deaa 100644 --- a/app/controllers/post_votes_controller.rb +++ b/app/controllers/post_votes_controller.rb @@ -4,6 +4,7 @@ class PostVotesController < ApplicationController def index @post_votes = authorize PostVote.visible(CurrentUser.user).paginated_search(params, count_pages: true) @post_votes = @post_votes.includes(:user, post: [:uploader, :media_asset]) if request.format.html? + @post = Post.find(params.dig(:search, :post_id)) if params.dig(:search, :post_id).present? respond_with(@post_votes) end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 10f23b3a0..0b63f33b3 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -193,10 +193,10 @@ module ApplicationHelper to_sentence(links, **options) end - def link_to_user(user, text = nil) + def link_to_user(user, text = nil, classes: nil, **options) return "anonymous" if user.blank? - user_class = "user user-#{user.level_string.downcase}" + user_class = "user user-#{user.level_string.downcase} #{classes}" user_class += " user-post-approver" if user.can_approve_posts? user_class += " user-post-uploader" if user.can_upload_free? user_class += " user-banned" if user.is_banned? diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb index 9191c17b9..db731eb71 100644 --- a/app/helpers/components_helper.rb +++ b/app/helpers/components_helper.rb @@ -20,6 +20,10 @@ module ComponentsHelper render PostVotesComponent.new(post: post, **options) end + def render_post_votes_tooltip(post, **options) + render PostVotesTooltipComponent.new(post: post, **options) + end + def render_post_navbar(post, **options) render PostNavbarComponent.new(post: post, **options) end diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 7daf6f1cd..094c96578 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -43,6 +43,7 @@ import PopupMenuComponent from "../../components/popup_menu_component/popup_menu import Post from "../src/javascripts/posts.js"; import PostModeMenu from "../src/javascripts/post_mode_menu.js"; import PostTooltip from "../src/javascripts/post_tooltips.js"; +import PostVotesTooltipComponent from "../../components/post_votes_tooltip_component/post_votes_tooltip_component.js"; import RelatedTag from "../src/javascripts/related_tag.js"; import Shortcuts from "../src/javascripts/shortcuts.js"; import TagCounter from "../src/javascripts/tag_counter.js"; @@ -64,6 +65,7 @@ Danbooru.PopupMenuComponent = PopupMenuComponent; Danbooru.Post = Post; Danbooru.PostModeMenu = PostModeMenu; Danbooru.PostTooltip = PostTooltip; +Danbooru.PostVotesTooltipComponent = PostVotesTooltipComponent; Danbooru.RelatedTag = RelatedTag; Danbooru.Shortcuts = Shortcuts; Danbooru.TagCounter = TagCounter; diff --git a/app/javascript/src/styles/base/040_colors.css b/app/javascript/src/styles/base/040_colors.css index 8aca7d161..ea7860aa3 100644 --- a/app/javascript/src/styles/base/040_colors.css +++ b/app/javascript/src/styles/base/040_colors.css @@ -218,6 +218,9 @@ html { --post-artist-commentary-container-background: var(--grey-0); --post-artist-commentary-container-border-color: var(--grey-1); + --post-upvote-color: var(--link-color); + --post-downvote-color: var(--red-5); + --note-body-background: #FFE; --note-body-text-color: var(--black); --note-body-border-color: var(--black); @@ -418,6 +421,9 @@ body[data-current-user-theme="dark"] { --post-artist-commentary-container-background: var(--grey-8); --post-artist-commentary-container-border-color: var(--grey-7); + --post-upvote-color: var(--link-color); + --post-downvote-color: var(--red-4); + --unsaved-note-box-border-color: var(--red-5); --movable-note-box-border-color: var(--blue-5); --note-preview-border-color: var(--red-5); diff --git a/app/javascript/src/styles/common/utilities.scss b/app/javascript/src/styles/common/utilities.scss index 2b5a2f4e7..6433d2967 100644 --- a/app/javascript/src/styles/common/utilities.scss +++ b/app/javascript/src/styles/common/utilities.scss @@ -22,6 +22,12 @@ $spacer: 0.25rem; /* 4px */ .text-xs { font-size: var(--text-xs); } .text-sm { font-size: var(--text-sm); } +.truncate { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + .leading-none { line-height: 1; } .absolute { position: absolute; } @@ -52,6 +58,9 @@ $spacer: 0.25rem; /* 4px */ .p-0\.5 { padding: 0.5 * $spacer; } .p-4 { padding: 4 * $spacer; } +.pr-2 { padding-right: 2 * $spacer; } +.pr-4 { padding-right: 4 * $spacer; } + .w-1\/4 { width: 25%; } .w-full { width: 100%; } @@ -73,6 +82,60 @@ $spacer: 0.25rem; /* 4px */ .items-center { align-items: center; } .justify-center { justify-content: center; } +.thin-scrollbar { + overflow-x: hidden; + overflow-y: auto; + padding-right: 2 * $spacer; + overscroll-behavior: contain; // https://caniuse.com/css-overscroll-behavior + + // Firefox only + // https://caniuse.com/?search=scrollbar-width + // https://developer.mozilla.org/en-US/docs/Web/CSS/scrollbar-width + scrollbar-width: thin; + + &::-webkit-scrollbar { + width: 5px; + height: 5px; + } + + &::-webkit-scrollbar-button { + width: 0; + height: 0; + } + + &::-webkit-scrollbar-thumb { + background: var(--post-tooltip-scrollbar-background); + border: none; + border-radius: 0; + } + + &::-webkit-scrollbar-thumb:hover { + background: var(--post-tooltip-scrollbar-thumb-color); + } + + &::-webkit-scrollbar-thumb:active { + background: var(--post-tooltip-scrollbar-thumb-color); + } + + &::-webkit-scrollbar-track { + background: var(--post-tooltip-scrollbar-track-background); + border: none; + border-radius: 0; + } + + &::-webkit-scrollbar-track:hover { + background: var(--post-tooltip-scrollbar-track-background); + } + + &::-webkit-scrollbar-track:active { + background: var(--post-tooltip-scrollbar-track-background); + } + + &::-webkit-scrollbar-corner { + background: transparent; + } +} + @media screen and (min-width: 660px) { .md\:inline-block { display: inline-block; } .md\:flex { display: flex; } diff --git a/app/javascript/src/styles/specific/post_tooltips.scss b/app/javascript/src/styles/specific/post_tooltips.scss index 7d947c54c..c23ddfcc3 100644 --- a/app/javascript/src/styles/specific/post_tooltips.scss +++ b/app/javascript/src/styles/specific/post_tooltips.scss @@ -1,50 +1,6 @@ $tooltip-line-height: 16px; $tooltip-body-height: $tooltip-line-height * 4; // 4 lines high. -@mixin thin-scrollbar { - &::-webkit-scrollbar { - width: 5px; - height: 5px; - } - - &::-webkit-scrollbar-button { - width: 0; - height: 0; - } - - &::-webkit-scrollbar-thumb { - background: var(--post-tooltip-scrollbar-background); - border: none; - border-radius: 0; - } - - &::-webkit-scrollbar-thumb:hover { - background: var(--post-tooltip-scrollbar-thumb-color); - } - - &::-webkit-scrollbar-thumb:active { - background: var(--post-tooltip-scrollbar-thumb-color); - } - - &::-webkit-scrollbar-track { - background: var(--post-tooltip-scrollbar-track-background); - border: none; - border-radius: 0; - } - - &::-webkit-scrollbar-track:hover { - background: var(--post-tooltip-scrollbar-track-background); - } - - &::-webkit-scrollbar-track:active { - background: var(--post-tooltip-scrollbar-track-background); - } - - &::-webkit-scrollbar-corner { - background: transparent; - } -} - .tippy-box[data-theme~="post-tooltip"] { min-width: 20em; max-width: 40em !important; @@ -59,7 +15,6 @@ $tooltip-body-height: $tooltip-line-height * 4; // 4 lines high. } .post-tooltip-body { - @include thin-scrollbar; max-height: $tooltip-body-height; overflow-y: auto; display: flex; diff --git a/app/views/layouts/default.html.erb b/app/views/layouts/default.html.erb index d33f6bd77..e927f4ab8 100644 --- a/app/views/layouts/default.html.erb +++ b/app/views/layouts/default.html.erb @@ -101,6 +101,7 @@
+
diff --git a/app/views/post_votes/_search.html.erb b/app/views/post_votes/_search.html.erb new file mode 100644 index 000000000..c9f4071e0 --- /dev/null +++ b/app/views/post_votes/_search.html.erb @@ -0,0 +1,7 @@ +<%= search_form_for(post_votes_path) do |f| %> + <%= f.input :user_name, label: "Voter", input_html: { value: params[:search][:user_name], "data-autocomplete": "user" } %> + <%= f.input :post_id, label: "Post", input_html: { value: params[:search][:post_id] } %> + <%= f.input :post_tags_match, label: "Tags", input_html: { value: params[:search][:post_tags_match], "data-autocomplete": "tag-query" } %> + <%= f.input :score, collection: [["+3", "3"], ["+1", "1"], ["-1", "-1"], ["-3", "-3"]], include_blank: true, selected: params[:search][:score] %> + <%= f.submit "Search" %> +<% end %> diff --git a/app/views/post_votes/index.html+compact.erb b/app/views/post_votes/index.html+compact.erb new file mode 100644 index 000000000..cd861d79e --- /dev/null +++ b/app/views/post_votes/index.html+compact.erb @@ -0,0 +1,24 @@ +<%= render "posts/partials/common/secondary_links" %> + +
+
+ <%= render "search" %> + + <%= table_for @post_votes, class: "striped autofit" do |t| %> + <% t.column "Score" do |vote| %> + <%= link_to sprintf("%+d", vote.score), post_votes_path(search: { score: vote.score }) %> + <% end %> + + <% t.column "Voter" do |vote| %> + <%= link_to_user vote.user %> + <%= link_to "»", post_votes_path(search: { user_name: vote.user.name }) %> + <% end %> + + <% t.column "Created" do |vote| %> + <%= time_ago_in_words_tagged(vote.created_at) %> + <% end %> + <% end %> + + <%= numbered_paginator(@post_votes) %> +
+
diff --git a/app/views/post_votes/index.html+tooltip.erb b/app/views/post_votes/index.html+tooltip.erb new file mode 100644 index 000000000..b351de668 --- /dev/null +++ b/app/views/post_votes/index.html+tooltip.erb @@ -0,0 +1 @@ +<%= render_post_votes_tooltip(@post, current_user: CurrentUser.user) %> diff --git a/app/views/post_votes/index.html.erb b/app/views/post_votes/index.html.erb index c93eb5e77..096c683d3 100644 --- a/app/views/post_votes/index.html.erb +++ b/app/views/post_votes/index.html.erb @@ -1,12 +1,6 @@
- <%= search_form_for(post_votes_path) do |f| %> - <%= f.input :user_name, label: "Voter", input_html: { value: params[:search][:user_name], "data-autocomplete": "user" } %> - <%= f.input :post_id, label: "Post", input_html: { value: params[:search][:post_id] } %> - <%= f.input :post_tags_match, label: "Tags", input_html: { value: params[:search][:post_tags_match], "data-autocomplete": "tag-query" } %> - <%= f.input :score, collection: [["+3", "3"], ["+1", "1"], ["-1", "-1"], ["-3", "-3"]], include_blank: true, selected: params[:search][:score] %> - <%= f.submit "Search" %> - <% end %> + <%= render "search" %> <%= table_for @post_votes, class: "striped autofit" do |t| %> <% t.column "Post" do |vote| %> diff --git a/app/views/posts/show.html+tooltip.erb b/app/views/posts/show.html+tooltip.erb index ca24f442e..bbaa83ad8 100644 --- a/app/views/posts/show.html+tooltip.erb +++ b/app/views/posts/show.html+tooltip.erb @@ -40,7 +40,7 @@ <% end %>
-
"> +
">
<% if params[:preview].truthy? %> <%= post_preview(@post, show_deleted: true, compact: true) %> diff --git a/test/functional/post_votes_controller_test.rb b/test/functional/post_votes_controller_test.rb index d329cda62..ff5c47605 100644 --- a/test/functional/post_votes_controller_test.rb +++ b/test/functional/post_votes_controller_test.rb @@ -10,8 +10,8 @@ class PostVotesControllerTest < ActionDispatch::IntegrationTest context "index action" do setup do @user = create(:user, enable_private_favorites: true) - create(:post_vote, user: @user, score: 1) - create(:post_vote, user: @user, score: -1) + @upvote = create(:post_vote, user: @user, score: 1) + @downvote = create(:post_vote, user: @user, score: -1) end should "render" do @@ -19,6 +19,16 @@ class PostVotesControllerTest < ActionDispatch::IntegrationTest assert_response :success end + should "render for a compact view" do + get post_votes_path(variant: "compact") + assert_response :success + end + + should "render for a tooltip" do + get post_votes_path(search: { post_id: @upvote.post_id }, variant: "tooltip") + assert_response :success + end + context "as a user" do should "show the user all their own votes" do get_auth post_votes_path, @user From bd8672681f54b9db01644438edbce942932e9fb7 Mon Sep 17 00:00:00 2001 From: evazion Date: Thu, 18 Nov 2021 01:49:50 -0600 Subject: [PATCH 06/11] votes: add vote buttons beneath thumbnails. Add upvote and downvote buttons beneath thumbnails on the post index page. This is disabled by default. To enable it, click the "..." menu in the top right of the page, then click "Show scores". This is currently a per-search setting, not an account setting. If you enable it in one tab, it won't be enabled in other tabs. --- .../popup_menu_component.scss | 2 +- app/components/post_preview_component.rb | 7 ++++--- .../post_preview_component.html.erb | 20 ++++++++++--------- .../post_preview_component.scss | 1 - app/components/post_votes_component.rb | 2 +- app/components/tag_list_component.rb | 5 +++-- .../tag_list_component.html+search.erb | 6 +++--- app/controllers/posts_controller.rb | 2 +- app/javascript/src/styles/base/020_base.scss | 1 - .../src/styles/common/utilities.scss | 2 ++ app/javascript/src/styles/specific/posts.scss | 2 -- app/logical/post_sets/post.rb | 19 +++++++++++++++--- app/models/post.rb | 1 + app/views/posts/index.html.erb | 16 +++++++++++++-- .../posts/partials/common/_search.html.erb | 3 +++ .../posts/partials/index/_posts.html.erb | 2 +- 16 files changed, 61 insertions(+), 30 deletions(-) diff --git a/app/components/popup_menu_component/popup_menu_component.scss b/app/components/popup_menu_component/popup_menu_component.scss index dcc28222f..83ac457ec 100644 --- a/app/components/popup_menu_component/popup_menu_component.scss +++ b/app/components/popup_menu_component/popup_menu_component.scss @@ -28,7 +28,7 @@ div.popup-menu { li a { display: block; - padding: 0.125em 2em 0.125em 0; + padding: 0.125em 0 0.125em 0; .icon { width: 1rem; diff --git a/app/components/post_preview_component.rb b/app/components/post_preview_component.rb index 838cfc729..44ef74963 100644 --- a/app/components/post_preview_component.rb +++ b/app/components/post_preview_component.rb @@ -3,18 +3,19 @@ class PostPreviewComponent < ApplicationComponent with_collection_parameter :post - attr_reader :post, :tags, :show_deleted, :show_cropped, :link_target, :pool, :similarity, :recommended, :compact, :size, :current_user, :options + attr_reader :post, :tags, :show_deleted, :show_cropped, :link_target, :pool, :similarity, :recommended, :show_votes, :compact, :size, :current_user, :options - delegate :external_link_to, :time_ago_in_words_tagged, :duration_to_hhmmss, :empty_heart_icon, :sound_icon, to: :helpers + delegate :external_link_to, :time_ago_in_words_tagged, :duration_to_hhmmss, :render_post_votes, :empty_heart_icon, :sound_icon, to: :helpers delegate :image_width, :image_height, :file_ext, :file_size, :duration, :is_animated?, to: :media_asset delegate :media_asset, to: :post - def initialize(post:, tags: "", show_deleted: false, show_cropped: true, link_target: post, pool: nil, similarity: nil, recommended: nil, compact: nil, size: nil, current_user: CurrentUser.user, **options) + def initialize(post:, tags: "", show_deleted: false, show_cropped: true, show_votes: true, link_target: post, pool: nil, similarity: nil, recommended: nil, compact: nil, size: nil, current_user: CurrentUser.user, **options) super @post = post @tags = tags.presence @show_deleted = show_deleted @show_cropped = show_cropped + @show_votes = show_votes @link_target = link_target @pool = pool @similarity = similarity.round(1) if similarity.present? diff --git a/app/components/post_preview_component/post_preview_component.html.erb b/app/components/post_preview_component/post_preview_component.html.erb index da2c7f1b9..2fdf0e55a 100644 --- a/app/components/post_preview_component/post_preview_component.html.erb +++ b/app/components/post_preview_component/post_preview_component.html.erb @@ -23,8 +23,7 @@

<%= link_to pool.pretty_name.truncate(80), pool %>

- <% end -%> - <% if similarity -%> + <% elsif similarity -%>

<% if post.source =~ %r!\Ahttps?://!i %> <%= external_link_to post.normalized_source, post.source_domain %> @@ -33,20 +32,19 @@ <%= time_ago_in_words_tagged(post.created_at, compact: true) %> <% end %>

- <% end %> - <% if size -%>

<%= link_to number_to_human_size(size), post.file_url %> (<%= post.image_width %>x<%= post.image_height %>)

- <% end -%> - <% if similarity -%>

<%= link_to "#{similarity}%", iqdb_queries_path(post_id: post.id) %> similarity

- <% end -%> - - <% if recommended -%> + <% elsif size -%> +

+ <%= link_to number_to_human_size(size), post.file_url %> + (<%= post.image_width %>x<%= post.image_height %>) +

+ <% elsif recommended -%> + <% elsif show_votes -%> +

+ <%= render_post_votes post, current_user: current_user %> +

<% end -%> <% end -%> diff --git a/app/components/post_preview_component/post_preview_component.scss b/app/components/post_preview_component/post_preview_component.scss index 28315bcf6..e24646a54 100644 --- a/app/components/post_preview_component/post_preview_component.scss +++ b/app/components/post_preview_component/post_preview_component.scss @@ -1,7 +1,6 @@ @import "../../javascript/src/styles/base/000_vars.scss"; article.post-preview { - height: 154px; width: 154px; margin: 0 10px 10px 0; text-align: center; diff --git a/app/components/post_votes_component.rb b/app/components/post_votes_component.rb index 5734425f3..295c5ea1a 100644 --- a/app/components/post_votes_component.rb +++ b/app/components/post_votes_component.rb @@ -15,7 +15,7 @@ class PostVotesComponent < ApplicationComponent end def current_vote - post.votes.find_by(user: current_user) + post.vote_by_current_user end def upvoted? diff --git a/app/components/tag_list_component.rb b/app/components/tag_list_component.rb index 9c5f3abd3..03a88f977 100644 --- a/app/components/tag_list_component.rb +++ b/app/components/tag_list_component.rb @@ -1,15 +1,16 @@ # frozen_string_literal: true class TagListComponent < ApplicationComponent - attr_reader :tags, :current_query, :show_extra_links + attr_reader :tags, :current_query, :show_extra_links, :search_params delegate :humanized_number, to: :helpers - def initialize(tags: [], current_query: nil, show_extra_links: false) + def initialize(tags: [], current_query: nil, show_extra_links: false, search_params: {}) super @tags = tags @current_query = current_query @show_extra_links = show_extra_links + @search_params = search_params end def self.tags_from_names(tag_names) diff --git a/app/components/tag_list_component/tag_list_component.html+search.erb b/app/components/tag_list_component/tag_list_component.html+search.erb index 72888f6e5..1f648db46 100644 --- a/app/components/tag_list_component/tag_list_component.html+search.erb +++ b/app/components/tag_list_component/tag_list_component.html+search.erb @@ -11,11 +11,11 @@ <% end %> <% if show_extra_links && current_query.present? %> - <%= link_to "+", posts_path(tags: "#{current_query} #{t.name}"), class: "search-inc-tag" %> - <%= link_to "-", posts_path(tags: "#{current_query} -#{t.name}"), class: "search-exl-tag" %> + <%= link_to "+", posts_path(tags: "#{current_query} #{t.name}", **search_params), class: "search-inc-tag" %> + <%= link_to "-", posts_path(tags: "#{current_query} -#{t.name}", **search_params), class: "search-exl-tag" %> <% end %> - <%= link_to t.pretty_name, posts_path(tags: t.name), class: "search-tag" %> + <%= link_to t.pretty_name, posts_path(tags: t.name, **search_params), class: "search-tag" %> <%= tag.span humanized_number(t.post_count), class: "post-count", title: t.post_count %> <% end %> diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index 12c212dce..7ed34a00e 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -10,7 +10,7 @@ class PostsController < ApplicationController end else tag_query = params[:tags] || params.dig(:post, :tags) - @post_set = PostSets::Post.new(tag_query, params[:page], params[:limit], random: params[:random], format: params[:format]) + @post_set = PostSets::Post.new(tag_query, params[:page], params[:limit], random: params[:random], format: params[:format], view: params[:view]) @posts = authorize @post_set.posts, policy_class: PostPolicy @post_set.log! respond_with(@posts) do |format| diff --git a/app/javascript/src/styles/base/020_base.scss b/app/javascript/src/styles/base/020_base.scss index ae17ce8c0..1672b74d4 100644 --- a/app/javascript/src/styles/base/020_base.scss +++ b/app/javascript/src/styles/base/020_base.scss @@ -98,7 +98,6 @@ menu { > li { margin: 0; - padding: 0 0.2em; display: inline; } } diff --git a/app/javascript/src/styles/common/utilities.scss b/app/javascript/src/styles/common/utilities.scss index 6433d2967..beae54a77 100644 --- a/app/javascript/src/styles/common/utilities.scss +++ b/app/javascript/src/styles/common/utilities.scss @@ -82,6 +82,8 @@ $spacer: 0.25rem; /* 4px */ .items-center { align-items: center; } .justify-center { justify-content: center; } +.float-right { float: right; } + .thin-scrollbar { overflow-x: hidden; overflow-y: auto; diff --git a/app/javascript/src/styles/specific/posts.scss b/app/javascript/src/styles/specific/posts.scss index a8eb4a795..169ff14df 100644 --- a/app/javascript/src/styles/specific/posts.scss +++ b/app/javascript/src/styles/specific/posts.scss @@ -135,8 +135,6 @@ div#c-posts { font-size: var(--text-lg); li { - padding: 0 1em 0 0; - &.active { font-weight: bold; } diff --git a/app/logical/post_sets/post.rb b/app/logical/post_sets/post.rb index e292ed1dd..5702133b9 100644 --- a/app/logical/post_sets/post.rb +++ b/app/logical/post_sets/post.rb @@ -7,10 +7,10 @@ module PostSets MAX_PER_PAGE = 200 MAX_SIDEBAR_TAGS = 25 - attr_reader :page, :random, :format, :tag_string, :query, :normalized_query + attr_reader :page, :random, :format, :tag_string, :query, :normalized_query, :view delegate :post_count, to: :normalized_query - def initialize(tags, page = 1, per_page = nil, user: CurrentUser.user, random: false, format: "html") + def initialize(tags, page = 1, per_page = nil, user: CurrentUser.user, random: false, format: "html", view: "simple") @query = PostQueryBuilder.new(tags, user, tag_limit: user.tag_query_limit, safe_mode: CurrentUser.safe_mode?, hide_deleted_posts: user.hide_deleted_posts?) @normalized_query = query.normalized_query @tag_string = tags @@ -18,6 +18,7 @@ module PostSets @per_page = per_page @random = random.to_s.truthy? @format = format.to_s + @view = view.presence || "simple" end def humanized_tag_string @@ -107,7 +108,7 @@ module PostSets if is_random? get_random_posts.paginate(page, search_count: false, limit: per_page, max_limit: max_per_page).load else - normalized_query.paginated_posts(page, includes: :media_asset, count: post_count, search_count: !post_count.nil?, limit: per_page, max_limit: max_per_page).load + normalized_query.paginated_posts(page, includes: includes, count: post_count, search_count: !post_count.nil?, limit: per_page, max_limit: max_per_page).load end end end @@ -139,6 +140,18 @@ module PostSets end end + def show_votes? + view == "score" + end + + def includes + if show_votes? + [:media_asset, :vote_by_current_user] + else + [:media_asset] + end + end + def search_stats { query: normalized_query.to_s, diff --git a/app/models/post.rb b/app/models/post.rb index 7d803790c..8e8e8beee 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -40,6 +40,7 @@ class Post < ApplicationRecord has_one :upload, :dependent => :destroy has_one :artist_commentary, :dependent => :destroy has_one :pixiv_ugoira_frame_data, class_name: "PixivUgoiraFrameData", foreign_key: :md5, primary_key: :md5 + has_one :vote_by_current_user, -> { where(user_id: CurrentUser.id) }, class_name: "PostVote" # XXX using current user here is wrong has_many :flags, :class_name => "PostFlag", :dependent => :destroy has_many :appeals, :class_name => "PostAppeal", :dependent => :destroy has_many :votes, :class_name => "PostVote", :dependent => :destroy diff --git a/app/views/posts/index.html.erb b/app/views/posts/index.html.erb index ce0673544..aa470ba93 100644 --- a/app/views/posts/index.html.erb +++ b/app/views/posts/index.html.erb @@ -7,7 +7,7 @@

Tags

- <%= render_search_tag_list(@post_set.related_tags, current_query: params[:tags], show_extra_links: policy(Post).show_extra_links?) %> + <%= render_search_tag_list(@post_set.related_tags, current_query: params[:tags], show_extra_links: policy(Post).show_extra_links?, search_params: { view: params[:view] }) %>
<%= render "posts/partials/index/options" %> @@ -16,7 +16,7 @@ <% end %> <% content_for(:content) do %> - +
  • Posts
  • <% if @post_set.artist.present? %> @@ -31,6 +31,18 @@ <% end %> +
  • + <%= render PopupMenuComponent.new do |menu| %> + <% menu.item do %> + <% if params[:view] == "score" %> + <%= link_to "Hide scores", posts_path(tags: params[:tags], view: nil) %> + <% else %> + <%= link_to "Show scores", posts_path(tags: params[:tags], view: "score") %> + <% end %> + <% end %> + <% end %> +
  • +
    diff --git a/app/views/posts/partials/common/_search.html.erb b/app/views/posts/partials/common/_search.html.erb index e1077018d..26cb3547b 100644 --- a/app/views/posts/partials/common/_search.html.erb +++ b/app/views/posts/partials/common/_search.html.erb @@ -6,6 +6,9 @@ <% if params[:random] %> <%= hidden_field_tag :random, params[:random] %> <% end %> + <% if params[:view] %> + <%= hidden_field_tag :view, params[:view] %> + <% end %> <%= text_field_tag("tags", tags, :id => tags_dom_id, :class => "flex-auto", :"data-shortcut" => "q", :"data-autocomplete" => "tag-query") %> <% end %> diff --git a/app/views/posts/partials/index/_posts.html.erb b/app/views/posts/partials/index/_posts.html.erb index 26c911433..129d6f5e8 100644 --- a/app/views/posts/partials/index/_posts.html.erb +++ b/app/views/posts/partials/index/_posts.html.erb @@ -3,7 +3,7 @@ <% if post_set.shown_posts.empty? %> <%= render "post_sets/blank" %> <% else %> - <%= post_previews_html(post_set.posts, show_deleted: post_set.show_deleted?, show_cropped: true, tags: post_set.tag_string) %> + <%= post_previews_html(post_set.posts, show_deleted: post_set.show_deleted?, show_cropped: true, tags: post_set.tag_string, show_votes: post_set.show_votes?) %> <% end %>
    From e28da733d7a1bc34acd7b461e6ffc4a155f14079 Mon Sep 17 00:00:00 2001 From: evazion Date: Thu, 18 Nov 2021 01:59:47 -0600 Subject: [PATCH 07/11] votes: show vote buttons to logged-out users. Show upvote and downvote buttons to logged-out users. Clicking them sends you to the login page. --- .../post_votes_component.html.erb | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/app/components/post_votes_component/post_votes_component.html.erb b/app/components/post_votes_component/post_votes_component.html.erb index 5934c1a42..65dfdd071 100644 --- a/app/components/post_votes_component/post_votes_component.html.erb +++ b/app/components/post_votes_component/post_votes_component.html.erb @@ -1,21 +1,21 @@ - <% if can_vote? %> - <% if upvoted? %> - <%= link_to upvote_icon, post_post_votes_path(post_id: post.id), class: "post-upvote-link post-unvote-link active-link", method: :delete, remote: true %> - <% else %> - <%= link_to upvote_icon, post_post_votes_path(post_id: post.id, score: 1), class: "post-upvote-link inactive-link", method: :post, remote: true %> - <% end %> + <% if current_user.is_anonymous? %> + <%= link_to upvote_icon, login_path(url: request.fullpath), class: "post-upvote-link inactive-link" %> + <% elsif upvoted? %> + <%= link_to upvote_icon, post_post_votes_path(post_id: post.id), class: "post-upvote-link post-unvote-link active-link", method: :delete, remote: true %> + <% else %> + <%= link_to upvote_icon, post_post_votes_path(post_id: post.id, score: 1), class: "post-upvote-link inactive-link", method: :post, remote: true %> <% end %> <%= link_to post.score, post_votes_path(search: { post_id: post.id }, variant: :compact) %> - <% if can_vote? %> - <% if downvoted? %> - <%= link_to downvote_icon, post_post_votes_path(post_id: post.id), class: "post-downvote-link post-unvote-link active-link", method: :delete, remote: true %> - <% else %> - <%= link_to downvote_icon, post_post_votes_path(post_id: post.id, score: -1), class: "post-downvote-link inactive-link", method: :post, remote: true %> - <% end %> + <% if current_user.is_anonymous? %> + <%= link_to downvote_icon, login_path(url: request.fullpath), class: "post-downvote-link inactive-link" %> + <% elsif downvoted? %> + <%= link_to downvote_icon, post_post_votes_path(post_id: post.id), class: "post-downvote-link post-unvote-link active-link", method: :delete, remote: true %> + <% else %> + <%= link_to downvote_icon, post_post_votes_path(post_id: post.id, score: -1), class: "post-downvote-link inactive-link", method: :post, remote: true %> <% end %> From 4a20014e5c1621f8cd9306b02c0e796ff0677170 Mon Sep 17 00:00:00 2001 From: Lily Date: Mon, 15 Nov 2021 16:26:48 -0400 Subject: [PATCH 08/11] mod dashboard: minor cleanup * only show the [+] and [-] feedback buttons to gold users * only show the [promote] button to moderators, and only when targetting users builder or below * don't show either button if the target is the current user --- app/helpers/application_helper.rb | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 0b63f33b3..e458d263f 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -210,14 +210,16 @@ module ApplicationHelper html = "" html << link_to_user(user) - if positive_or_negative == :positive - html << " [" + link_to("+", new_user_feedback_path(:user_feedback => {:category => "positive", :user_id => user.id})) + "]" + if CurrentUser.is_gold? && user != CurrentUser.user + if positive_or_negative == :positive + html << " [" + link_to("+", new_user_feedback_path(:user_feedback => {:category => "positive", :user_id => user.id})) + "]" - unless user.is_gold? - html << " [" + link_to("promote", edit_admin_user_path(user)) + "]" + if CurrentUser.is_moderator? && !user.is_builder? + html << " [" + link_to("promote", edit_admin_user_path(user)) + "]" + end + else + html << " [" + link_to("–".html_safe, new_user_feedback_path(:user_feedback => {:category => "negative", :user_id => user.id})) + "]" end - else - html << " [" + link_to("–".html_safe, new_user_feedback_path(:user_feedback => {:category => "negative", :user_id => user.id})) + "]" end html.html_safe From c4ad50bbba9b1c8279eb9973f932f92018523cca Mon Sep 17 00:00:00 2001 From: evazion Date: Fri, 19 Nov 2021 17:37:55 -0600 Subject: [PATCH 09/11] Fix #4924: Moderator dashboard shouldn't show a "Promote" link that's unusable by non-moderators Remove these links entirely. --- app/helpers/application_helper.rb | 19 ------------------- .../dashboards/_activity_artist.html.erb | 2 +- .../dashboards/_activity_comment.html.erb | 2 +- .../dashboards/_activity_note.html.erb | 2 +- .../dashboards/_activity_tag.html.erb | 2 +- .../dashboards/_activity_upload.html.erb | 2 +- .../dashboards/_activity_wiki_page.html.erb | 2 +- 7 files changed, 6 insertions(+), 25 deletions(-) diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e458d263f..f4261c06a 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -206,25 +206,6 @@ module ApplicationHelper link_to(text, user, class: user_class, data: data) end - def mod_link_to_user(user, positive_or_negative) - html = "" - html << link_to_user(user) - - if CurrentUser.is_gold? && user != CurrentUser.user - if positive_or_negative == :positive - html << " [" + link_to("+", new_user_feedback_path(:user_feedback => {:category => "positive", :user_id => user.id})) + "]" - - if CurrentUser.is_moderator? && !user.is_builder? - html << " [" + link_to("promote", edit_admin_user_path(user)) + "]" - end - else - html << " [" + link_to("–".html_safe, new_user_feedback_path(:user_feedback => {:category => "negative", :user_id => user.id})) + "]" - end - end - - html.html_safe - end - def embed_wiki(title, **options) wiki = WikiPage.find_by(title: title) text = format_text(wiki&.body) diff --git a/app/views/moderator/dashboards/_activity_artist.html.erb b/app/views/moderator/dashboards/_activity_artist.html.erb index 8317ad790..cbfd3e996 100644 --- a/app/views/moderator/dashboards/_activity_artist.html.erb +++ b/app/views/moderator/dashboards/_activity_artist.html.erb @@ -9,7 +9,7 @@ <% @dashboard.artists.each do |activity| %> - <%= mod_link_to_user(activity.user, :positive) %> + <%= link_to_user(activity.user) %> <%= link_to activity.count, artist_versions_path(:search => {:updater_id => activity.user.id}) %> <% end %> diff --git a/app/views/moderator/dashboards/_activity_comment.html.erb b/app/views/moderator/dashboards/_activity_comment.html.erb index 55e03031d..f14731771 100644 --- a/app/views/moderator/dashboards/_activity_comment.html.erb +++ b/app/views/moderator/dashboards/_activity_comment.html.erb @@ -21,7 +21,7 @@ <% end %>
    - <%= mod_link_to_user(activity.comment.creator, :negative) %> + <%= link_to_user(activity.comment.creator) %> <%= activity.count %> <%= activity.comment.score %> diff --git a/app/views/moderator/dashboards/_activity_note.html.erb b/app/views/moderator/dashboards/_activity_note.html.erb index 2da47f35c..87143821e 100644 --- a/app/views/moderator/dashboards/_activity_note.html.erb +++ b/app/views/moderator/dashboards/_activity_note.html.erb @@ -9,7 +9,7 @@ <% @dashboard.notes.each do |activity| %> - <%= mod_link_to_user(activity.user, :positive) %> + <%= link_to_user(activity.user) %> <%= link_to activity.count, note_versions_path(:search => {:updater_id => activity.user.id}) %> <% end %> diff --git a/app/views/moderator/dashboards/_activity_tag.html.erb b/app/views/moderator/dashboards/_activity_tag.html.erb index f08670016..847f5a314 100644 --- a/app/views/moderator/dashboards/_activity_tag.html.erb +++ b/app/views/moderator/dashboards/_activity_tag.html.erb @@ -9,7 +9,7 @@ <% @dashboard.tags.each do |activity| %> - <%= mod_link_to_user(activity.user, :positive) %> + <%= link_to_user(activity.user) %> <%= link_to activity.count, post_versions_path(:search => {:updater_id => activity.user.id}) %> <% end %> diff --git a/app/views/moderator/dashboards/_activity_upload.html.erb b/app/views/moderator/dashboards/_activity_upload.html.erb index 8d2bae2ee..7613b1d6f 100644 --- a/app/views/moderator/dashboards/_activity_upload.html.erb +++ b/app/views/moderator/dashboards/_activity_upload.html.erb @@ -9,7 +9,7 @@ <% @dashboard.posts.each do |activity| %> - <%= mod_link_to_user(activity.user, :positive) %> + <%= link_to_user(activity.user) %> <%= link_to activity.count, posts_path(:tags => "user:#{activity.user.name}") %> <% end %> diff --git a/app/views/moderator/dashboards/_activity_wiki_page.html.erb b/app/views/moderator/dashboards/_activity_wiki_page.html.erb index 6d2cc4c58..6cb6360b6 100644 --- a/app/views/moderator/dashboards/_activity_wiki_page.html.erb +++ b/app/views/moderator/dashboards/_activity_wiki_page.html.erb @@ -9,7 +9,7 @@ <% @dashboard.wiki_pages.each do |activity| %> - <%= mod_link_to_user(activity.user, :positive) %> + <%= link_to_user(activity.user) %> <%= link_to activity.count, wiki_page_versions_path(:search => {:updater_id => activity.user.id}) %> <% end %> From 3ae62d08eb16d203ce7e3d87316b79ff2dbb7602 Mon Sep 17 00:00:00 2001 From: evazion Date: Fri, 19 Nov 2021 20:54:02 -0600 Subject: [PATCH 10/11] favorites: show favlist when hovering over favcount. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes: * Make it so you can click or hover over a post's favorite count to see the list of public favorites. * Remove the "Show »" button next to the favorite count. * Make the favorites list visible to all users. Before favorites were only visible to Gold users. * Make the /favorites page show the list of all public favorites, instead of redirecting to the current user's favorites. * Add /posts/:id/favorites endpoint. * Add /users/:id/favorites endpoint. This is for several reasons: * To make viewing favorites work the same way as viewing upvotes. * To make posts load faster for Gold users. Before, we loaded all the favorites when viewing a post, even when the user didn't look at them. This made pageloads slower for posts that had hundreds or thousands of favorites. Now we only load the favlist if the user hovers over the favcount. * To make the favorite list visible to all users. Before, it wasn't visible to non-Gold users, because of the performance issue listed above. * To make it more obvious that favorites are public by default. Before, since regular users could only see the favcount, they may have mistakenly believed other users couldn't see their favorites. --- app/components/favorites_tooltip_component.rb | 24 +++++++ .../favorites_tooltip_component.html.erb | 9 +++ .../favorites_tooltip_component.js | 65 +++++++++++++++++++ .../favorites_tooltip_component.scss | 13 ++++ app/controllers/favorites_controller.rb | 23 +++---- app/helpers/components_helper.rb | 4 ++ app/javascript/packs/application.js | 2 + app/javascript/src/javascripts/posts.js | 8 --- app/javascript/src/styles/specific/posts.scss | 4 -- app/models/favorite.rb | 10 ++- app/models/post.rb | 8 --- app/policies/favorite_policy.rb | 4 ++ app/policies/post_policy.rb | 4 -- app/views/favorites/_search.html.erb | 6 ++ app/views/favorites/_update.js.erb | 13 +--- app/views/favorites/index.html+tooltip.erb | 3 + app/views/favorites/index.html.erb | 44 +++++++++++++ app/views/layouts/default.html.erb | 1 + .../partials/show/_favorite_list.html.erb | 2 - .../posts/partials/show/_information.html.erb | 14 ++-- config/routes.rb | 2 + test/functional/favorites_controller_test.rb | 40 +++++++----- .../moderator/post/posts_controller_test.rb | 4 +- 23 files changed, 229 insertions(+), 78 deletions(-) create mode 100644 app/components/favorites_tooltip_component.rb create mode 100644 app/components/favorites_tooltip_component/favorites_tooltip_component.html.erb create mode 100644 app/components/favorites_tooltip_component/favorites_tooltip_component.js create mode 100644 app/components/favorites_tooltip_component/favorites_tooltip_component.scss create mode 100644 app/views/favorites/_search.html.erb create mode 100644 app/views/favorites/index.html+tooltip.erb create mode 100644 app/views/favorites/index.html.erb delete mode 100644 app/views/posts/partials/show/_favorite_list.html.erb diff --git a/app/components/favorites_tooltip_component.rb b/app/components/favorites_tooltip_component.rb new file mode 100644 index 000000000..bd69821b8 --- /dev/null +++ b/app/components/favorites_tooltip_component.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# This component represents the tooltip that displays when you hover over a post's favorite count. +class FavoritesTooltipComponent < ApplicationComponent + attr_reader :post, :current_user + + def initialize(post:, current_user:) + super + @post = post + @current_user = current_user + end + + def favorites + post.favorites.includes(:user).order(id: :desc) + end + + def favoriter_name(favorite) + if policy(favorite).can_see_favoriter? + link_to_user(favorite.user) + else + tag.i("hidden") + end + end +end diff --git a/app/components/favorites_tooltip_component/favorites_tooltip_component.html.erb b/app/components/favorites_tooltip_component/favorites_tooltip_component.html.erb new file mode 100644 index 000000000..13c1f5b32 --- /dev/null +++ b/app/components/favorites_tooltip_component/favorites_tooltip_component.html.erb @@ -0,0 +1,9 @@ +
    +
    + <% favorites.each do |favorite| %> +
    + <%= favoriter_name(favorite) %> +
    + <% end %> +
    +
    diff --git a/app/components/favorites_tooltip_component/favorites_tooltip_component.js b/app/components/favorites_tooltip_component/favorites_tooltip_component.js new file mode 100644 index 000000000..918f61786 --- /dev/null +++ b/app/components/favorites_tooltip_component/favorites_tooltip_component.js @@ -0,0 +1,65 @@ +import Utility from "../../javascript/src/javascripts/utility.js"; +import { delegate, hideAll } from 'tippy.js'; +import 'tippy.js/dist/tippy.css'; + +class FavoritesTooltipComponent { + // Trigger on the post favcount link. + static TARGET_SELECTOR = "span.post-favcount a"; + static SHOW_DELAY = 125; + static HIDE_DELAY = 125; + static DURATION = 250; + static instance = null; + + static initialize() { + if ($(FavoritesTooltipComponent.TARGET_SELECTOR).length === 0) { + return; + } + + FavoritesTooltipComponent.instance = delegate("body", { + allowHTML: true, + appendTo: document.querySelector("#post-favorites-tooltips"), + delay: [FavoritesTooltipComponent.SHOW_DELAY, FavoritesTooltipComponent.HIDE_DELAY], + duration: FavoritesTooltipComponent.DURATION, + interactive: true, + maxWidth: "none", + target: FavoritesTooltipComponent.TARGET_SELECTOR, + theme: "common-tooltip", + touch: false, + + onShow: FavoritesTooltipComponent.onShow, + onHide: FavoritesTooltipComponent.onHide, + }); + } + + static async onShow(instance) { + let $target = $(instance.reference); + let $tooltip = $(instance.popper); + let postId = $target.parents("[data-id]").data("id"); + + hideAll({ exclude: instance }); + + try { + $tooltip.addClass("tooltip-loading"); + + instance._request = $.get(`/posts/${postId}/favorites?variant=tooltip`); + let html = await instance._request; + instance.setContent(html); + + $tooltip.removeClass("tooltip-loading"); + } catch (error) { + if (error.status !== 0 && error.statusText !== "abort") { + Utility.error(`Error displaying favorites for post #${postId} (error: ${error.status} ${error.statusText})`); + } + } + } + + static async onHide(instance) { + if (instance._request?.state() === "pending") { + instance._request.abort(); + } + } +} + +$(document).ready(FavoritesTooltipComponent.initialize); + +export default FavoritesTooltipComponent; diff --git a/app/components/favorites_tooltip_component/favorites_tooltip_component.scss b/app/components/favorites_tooltip_component/favorites_tooltip_component.scss new file mode 100644 index 000000000..4876c8cdd --- /dev/null +++ b/app/components/favorites_tooltip_component/favorites_tooltip_component.scss @@ -0,0 +1,13 @@ +.favorites-tooltip { + font-size: var(--text-xs); + max-height: 240px; + + .post-favoriter { + max-width: 160px; + } +} + +span.post-favcount a { + color: var(--text-color); + &:hover { text-decoration: underline; } +} diff --git a/app/controllers/favorites_controller.rb b/app/controllers/favorites_controller.rb index cd740ca72..40378df79 100644 --- a/app/controllers/favorites_controller.rb +++ b/app/controllers/favorites_controller.rb @@ -1,19 +1,16 @@ class FavoritesController < ApplicationController - respond_to :html, :xml, :json, :js + respond_to :js, :json, :html, :xml def index - authorize Favorite - if !request.format.html? - @favorites = Favorite.visible(CurrentUser.user).paginated_search(params) - respond_with(@favorites) - elsif params[:user_id].present? - user = User.find(params[:user_id]) - redirect_to posts_path(tags: "ordfav:#{user.name}", format: request.format.symbol) - elsif !CurrentUser.is_anonymous? - redirect_to posts_path(tags: "ordfav:#{CurrentUser.user.name}", format: request.format.symbol) - else - redirect_to posts_path(format: request.format.symbol) - end + post_id = params[:post_id] || params[:search][:post_id] + user_id = params[:user_id] || params[:search][:user_id] + user_name = params[:search][:user_name] + @post = Post.find(post_id) if post_id + @user = User.find(user_id) if user_id + @user = User.find_by_name(user_name) if user_name + + @favorites = authorize Favorite.visible(CurrentUser.user).paginated_search(params, defaults: { post_id: @post&.id, user_id: @user&.id }) + respond_with(@favorites) end def create diff --git a/app/helpers/components_helper.rb b/app/helpers/components_helper.rb index db731eb71..ade8022d7 100644 --- a/app/helpers/components_helper.rb +++ b/app/helpers/components_helper.rb @@ -24,6 +24,10 @@ module ComponentsHelper render PostVotesTooltipComponent.new(post: post, **options) end + def render_favorites_tooltip(post, **options) + render FavoritesTooltipComponent.new(post: post, **options) + end + def render_post_navbar(post, **options) render PostNavbarComponent.new(post: post, **options) end diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 094c96578..acfb2096a 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -37,6 +37,7 @@ import Blacklist from "../src/javascripts/blacklists.js"; import CommentComponent from "../../components/comment_component/comment_component.js"; import CurrentUser from "../src/javascripts/current_user.js"; import Dtext from "../src/javascripts/dtext.js"; +import FavoritesTooltipComponent from "../../components/favorites_tooltip_component/favorites_tooltip_component.js"; import IqdbQuery from "../src/javascripts/iqdb_queries.js"; import Note from "../src/javascripts/notes.js"; import PopupMenuComponent from "../../components/popup_menu_component/popup_menu_component.js"; @@ -59,6 +60,7 @@ Danbooru.Blacklist = Blacklist; Danbooru.CommentComponent = CommentComponent; Danbooru.CurrentUser = CurrentUser; Danbooru.Dtext = Dtext; +Danbooru.FavoritesTooltipComponent = FavoritesTooltipComponent; Danbooru.IqdbQuery = IqdbQuery; Danbooru.Note = Note; Danbooru.PopupMenuComponent = PopupMenuComponent; diff --git a/app/javascript/src/javascripts/posts.js b/app/javascript/src/javascripts/posts.js index dc658c2a2..85fc8f070 100644 --- a/app/javascript/src/javascripts/posts.js +++ b/app/javascript/src/javascripts/posts.js @@ -30,7 +30,6 @@ Post.initialize_all = function() { if ($("#c-posts").length && $("#a-show").length) { this.initialize_links(); this.initialize_post_relationship_previews(); - this.initialize_favlist(); this.initialize_post_sections(); this.initialize_post_image_resize_links(); this.initialize_recommended(); @@ -242,13 +241,6 @@ Post.toggle_relationship_preview = function(preview, preview_link) { } } -Post.initialize_favlist = function() { - $("#show-favlist-link, #hide-favlist-link").on("click.danbooru", function(e) { - $("#favlist, #show-favlist-link, #hide-favlist-link").toggle(); - e.preventDefault(); - }); -} - Post.view_original = function(e = null) { if (Utility.test_max_width(660)) { // Do the default behavior (navigate to image) diff --git a/app/javascript/src/styles/specific/posts.scss b/app/javascript/src/styles/specific/posts.scss index 169ff14df..460f1de71 100644 --- a/app/javascript/src/styles/specific/posts.scss +++ b/app/javascript/src/styles/specific/posts.scss @@ -170,10 +170,6 @@ div#c-posts { } } - #favlist { - word-wrap: break-word; - } - #recommended.loading-recommended-posts { pointer-events: none; opacity: 0.5; diff --git a/app/models/favorite.rb b/app/models/favorite.rb index aa1c63aa0..6f8d472c2 100644 --- a/app/models/favorite.rb +++ b/app/models/favorite.rb @@ -6,10 +6,16 @@ class Favorite < ApplicationRecord after_create :upvote_post_on_create after_destroy :unvote_post_on_destroy - scope :public_favorites, -> { where(user: User.bit_prefs_match(:enable_private_favorites, false)) } + scope :public_favorites, -> { where(user: User.has_public_favorites) } def self.visible(user) - user.is_admin? ? all : where(user: user).or(public_favorites) + if user.is_admin? + all + elsif user.is_anonymous? + public_favorites + else + where(user: user).or(public_favorites) + end end def self.search(params) diff --git a/app/models/post.rb b/app/models/post.rb index 8e8e8beee..f0e6a0e7a 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -50,7 +50,6 @@ class Post < ApplicationRecord has_many :approvals, :class_name => "PostApproval", :dependent => :destroy has_many :disapprovals, :class_name => "PostDisapproval", :dependent => :destroy has_many :favorites, dependent: :destroy - has_many :favorited_users, through: :favorites, source: :user has_many :replacements, class_name: "PostReplacement", :dependent => :destroy attr_accessor :old_tag_string, :old_parent_id, :old_source, :old_rating, :has_constraints, :disable_versioning @@ -667,13 +666,6 @@ class Post < ApplicationRecord Favorite.exists?(post: self, user: user) end - # Users who publicly favorited this post, ordered by time of favorite. - def visible_favorited_users(viewer) - favorited_users.order("favorites.id DESC").select do |fav_user| - Pundit.policy!(viewer, fav_user).can_see_favorites? - end - end - def favorite_groups FavoriteGroup.for_post(id) end diff --git a/app/policies/favorite_policy.rb b/app/policies/favorite_policy.rb index 29784ceb7..4c3a5aef2 100644 --- a/app/policies/favorite_policy.rb +++ b/app/policies/favorite_policy.rb @@ -6,4 +6,8 @@ class FavoritePolicy < ApplicationPolicy def destroy? record.user_id == user.id end + + def can_see_favoriter? + user.is_admin? || record.user == user || !record.user.enable_private_favorites? + end end diff --git a/app/policies/post_policy.rb b/app/policies/post_policy.rb index 5612e1ce5..ae2592e42 100644 --- a/app/policies/post_policy.rb +++ b/app/policies/post_policy.rb @@ -59,10 +59,6 @@ class PostPolicy < ApplicationPolicy user.is_gold? end - def can_view_favlist? - user.is_gold? - end - # whether to show the + - links in the tag list. def show_extra_links? user.is_gold? diff --git a/app/views/favorites/_search.html.erb b/app/views/favorites/_search.html.erb new file mode 100644 index 000000000..1519c13ec --- /dev/null +++ b/app/views/favorites/_search.html.erb @@ -0,0 +1,6 @@ +<%= search_form_for(favorites_path) do |f| %> + <%= f.input :user_name, label: "Favoriter", input_html: { value: @user&.name, "data-autocomplete": "user" } %> + <%= f.input :post_id, label: "Post", input_html: { value: @post&.id } %> + <%= f.input :post_tags_match, label: "Tags", input_html: { value: params[:search][:post_tags_match], "data-autocomplete": "tag-query" } %> + <%= f.submit "Search" %> +<% end %> diff --git a/app/views/favorites/_update.js.erb b/app/views/favorites/_update.js.erb index b87eabc06..afd469fb8 100644 --- a/app/views/favorites/_update.js.erb +++ b/app/views/favorites/_update.js.erb @@ -4,19 +4,8 @@ $("#add-to-favorites, #add-fav-button, #remove-from-favorites, #remove-fav-button").toggle(); $("#remove-fav-button").addClass("animate"); $("span.post-votes[data-id=<%= @post.id %>]").replaceWith("<%= j render_post_votes @post, current_user: CurrentUser.user %>"); - $("#favcount-for-post-<%= @post.id %>").text(<%= @post.fav_count %>); + $("span.post-favcount[data-id=<%= @post.id %>]").html("<%= j link_to @post.fav_count, favorites_path(post_id: @post.id, variant: :compact) %>"); $(".fav-buttons").toggleClass("fav-buttons-false").toggleClass("fav-buttons-true"); - <% if policy(@post).can_view_favlist? %> - var fav_count = <%= @post.fav_count %>; - $("#favlist").html("<%= j render "posts/partials/show/favorite_list", post: @post %>"); - - if (fav_count === 0) { - $("#show-favlist-link, #hide-favlist-link, #favlist").hide(); - } else if (!$("#favlist").is(":visible")) { - $("#show-favlist-link").show(); - } - <% end %> - Danbooru.Utility.notice("<%= j flash[:notice] %>"); <% end %> diff --git a/app/views/favorites/index.html+tooltip.erb b/app/views/favorites/index.html+tooltip.erb new file mode 100644 index 000000000..288e797d7 --- /dev/null +++ b/app/views/favorites/index.html+tooltip.erb @@ -0,0 +1,3 @@ +<% if @post.present? %> + <%= render_favorites_tooltip(@post, current_user: CurrentUser.user) %> +<% end %> diff --git a/app/views/favorites/index.html.erb b/app/views/favorites/index.html.erb new file mode 100644 index 000000000..d25e903b4 --- /dev/null +++ b/app/views/favorites/index.html.erb @@ -0,0 +1,44 @@ +
    +
    + <% if @post %> +

    <%= link_to "Favorites", favorites_path %>/<%= link_to @post.dtext_shortlink, @post %>

    + <% elsif @user %> +

    <%= link_to "Favorites", favorites_path %>/<%= link_to_user @user %>

    + <% else %> +

    <%= link_to "Favorites", favorites_path %>

    + <% end %> + + <%= render "search" %> + + <%= table_for @favorites.includes(:user, post: [:uploader, :media_asset]), class: "striped autofit" do |t| %> + <% if @post.nil? %> + <% t.column "Post" do |favorite| %> + <%= post_preview(favorite.post, show_deleted: true) %> + <% end %> + + <% t.column "Tags", td: {class: "col-expand"} do |favorite| %> + <%= render_inline_tag_list(favorite.post) %> + <% end %> + + <% t.column "Uploader" do |favorite| %> + <%= link_to_user favorite.post.uploader %> + <%= link_to "»", favorites_path(search: { post_tags_match: "user:#{favorite.post.uploader.name}" }) %> +
    <%= time_ago_in_words_tagged(favorite.post.created_at) %>
    + <% end %> + <% end %> + + <% if @user.nil? %> + <% t.column "Favoriter" do |favorite| %> + <% if policy(favorite).can_see_favoriter? %> + <%= link_to_user favorite.user %> + <%= link_to "»", favorites_path(search: { user_name: favorite.user.name }) %> + <% else %> + hidden + <% end %> + <% end %> + <% end %> + <% end %> + + <%= numbered_paginator(@favorites) %> +
    +
    diff --git a/app/views/layouts/default.html.erb b/app/views/layouts/default.html.erb index e927f4ab8..3dee6ac89 100644 --- a/app/views/layouts/default.html.erb +++ b/app/views/layouts/default.html.erb @@ -102,6 +102,7 @@
    +
    diff --git a/app/views/posts/partials/show/_favorite_list.html.erb b/app/views/posts/partials/show/_favorite_list.html.erb deleted file mode 100644 index 863a24a7a..000000000 --- a/app/views/posts/partials/show/_favorite_list.html.erb +++ /dev/null @@ -1,2 +0,0 @@ -<%# post %> -<%= safe_join(post.visible_favorited_users(CurrentUser.user).map { |user| link_to_user(user) }, ", ") %> diff --git a/app/views/posts/partials/show/_information.html.erb b/app/views/posts/partials/show/_information.html.erb index a389d4be2..91f1c4e60 100644 --- a/app/views/posts/partials/show/_information.html.erb +++ b/app/views/posts/partials/show/_information.html.erb @@ -23,14 +23,12 @@
  • Score: <%= render_post_votes post, current_user: CurrentUser.user %>
  • -
  • Favorites: <%= post.fav_count %> - <% if policy(post).can_view_favlist? %> - <%= link_to "Show »", "#", id: "show-favlist-link", style: ("display: none;" if post.fav_count == 0) %> - <%= link_to "« Hide", "#", id: "hide-favlist-link", style: "display: none;" %> - - <% end %>
  • +
  • + Favorites: + <%= tag.span class: "post-favcount", "data-id": post.id do %> + <%= link_to post.fav_count, post_favorites_path(post) %> + <% end %> +
  • Status: <% if post.is_pending? %> diff --git a/config/routes.rb b/config/routes.rb index b7e2287df..7d104b16e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -194,6 +194,7 @@ Rails.application.routes.draw do # XXX Use `only: []` to avoid redefining post routes defined at top of file. resources :posts, only: [] do resources :events, :only => [:index], :controller => "post_events" + resources :favorites, only: [:index, :create, :destroy] resources :replacements, :only => [:index, :new, :create], :controller => "post_replacements" resource :artist_commentary, only: [:show] do collection { put :create_or_update } @@ -252,6 +253,7 @@ Rails.application.routes.draw do end end resources :users do + resources :favorites, only: [:index, :create, :destroy] resources :favorite_groups, controller: "favorite_groups", only: [:index], as: "favorite_groups" resource :email, only: [:show, :edit, :update] do get :verify diff --git a/test/functional/favorites_controller_test.rb b/test/functional/favorites_controller_test.rb index f714e8393..936e69e48 100644 --- a/test/functional/favorites_controller_test.rb +++ b/test/functional/favorites_controller_test.rb @@ -10,25 +10,35 @@ class FavoritesControllerTest < ActionDispatch::IntegrationTest end context "index action" do - should "redirect the user_id param to an ordfav: search" do - get favorites_path(user_id: @user.id) - assert_redirected_to posts_path(tags: "ordfav:#{@user.name}", format: "html") - end - - should "redirect members to an ordfav: search" do - get_auth favorites_path, @user - assert_redirected_to posts_path(tags: "ordfav:#{@user.name}", format: "html") - end - - should "redirect anonymous users to the posts index" do - get favorites_path - assert_redirected_to posts_path(format: "html") - end - should "render for json" do get favorites_path, as: :json assert_response :success end + + should "render for html" do + get favorites_path + assert_response :success + end + + should "render for /favorites?variant=tooltip" do + get post_favorites_path(@post, variant: "tooltip") + assert_response :success + end + + should "render for /users/:id/favorites" do + get user_favorites_path(@user) + assert_response :success + end + + should "render for /posts/:id/favorites" do + get post_favorites_path(@faved_post) + assert_response :success + end + + should "render for /favorites?search[user_name]=" do + get favorites_path(search: { user_name: @user.name }) + assert_response :success + end end context "create action" do diff --git a/test/functional/moderator/post/posts_controller_test.rb b/test/functional/moderator/post/posts_controller_test.rb index 7b13dc163..6aff77f78 100644 --- a/test/functional/moderator/post/posts_controller_test.rb +++ b/test/functional/moderator/post/posts_controller_test.rb @@ -43,8 +43,8 @@ module Moderator @parent.reload @child.reload as(@admin) do - assert_equal(users.map(&:id).sort, @parent.favorited_users.map(&:id).sort) - assert_equal([], @child.favorited_users.map(&:id)) + assert_equal(users.map(&:id).sort, @parent.favorites.map(&:user_id).sort) + assert_equal([], @child.favorites.map(&:user_id)) end end end From eda23c719a5a4900f229d8ad1c4808d93b79f387 Mon Sep 17 00:00:00 2001 From: evazion Date: Fri, 19 Nov 2021 22:40:34 -0600 Subject: [PATCH 11/11] votes: fixup various minor issues. * Add a gap between thumbnails on mobile. * Adjust CSS for scores and vote buttons. * Include "Private favorites" as an incentive on the user upgrade page. * Fix vote buttons not being visible beneath thumbnails on mobile. * Fix the "Show scores" link not preserving the current page number. * Fix vote buttons being unintentionally enabled for all thumbnails by default. * Fix banned and restricted users being able to favorite posts by tagging them with `fav:self`. * Fix search engines being able to crawl /posts?view=score pages. * Fix broken tests. --- .../favorites_tooltip_component.scss | 5 -- app/components/post_preview_component.rb | 2 +- .../post_preview_component.html.erb | 4 +- .../post_preview_component.scss | 46 +++++-------------- .../post_votes_component.scss | 13 ++++-- .../post_votes_tooltip_component.html.erb | 4 +- .../src/styles/common/utilities.scss | 3 ++ .../src/styles/specific/z_responsive.scss | 14 +++--- app/logical/post_sets/post.rb | 1 + app/models/post.rb | 2 + app/views/posts/index.html.erb | 4 +- .../posts/partials/index/_posts.html.erb | 4 +- .../partials/index/_seo_meta_tags.html.erb | 2 + app/views/user_upgrades/new.html.erb | 6 +++ test/components/post_navbar_component_test.rb | 7 ++- test/components/post_votes_component_test.rb | 14 +++--- test/factories/favorite.rb | 4 ++ test/unit/favorite_test.rb | 13 ++---- test/unit/post_query_builder_test.rb | 9 ++-- test/unit/post_test.rb | 45 ++++++++++++++---- 20 files changed, 108 insertions(+), 94 deletions(-) diff --git a/app/components/favorites_tooltip_component/favorites_tooltip_component.scss b/app/components/favorites_tooltip_component/favorites_tooltip_component.scss index 4876c8cdd..ccdc2b5a1 100644 --- a/app/components/favorites_tooltip_component/favorites_tooltip_component.scss +++ b/app/components/favorites_tooltip_component/favorites_tooltip_component.scss @@ -6,8 +6,3 @@ max-width: 160px; } } - -span.post-favcount a { - color: var(--text-color); - &:hover { text-decoration: underline; } -} diff --git a/app/components/post_preview_component.rb b/app/components/post_preview_component.rb index 44ef74963..8c9e7369a 100644 --- a/app/components/post_preview_component.rb +++ b/app/components/post_preview_component.rb @@ -9,7 +9,7 @@ class PostPreviewComponent < ApplicationComponent delegate :image_width, :image_height, :file_ext, :file_size, :duration, :is_animated?, to: :media_asset delegate :media_asset, to: :post - def initialize(post:, tags: "", show_deleted: false, show_cropped: true, show_votes: true, link_target: post, pool: nil, similarity: nil, recommended: nil, compact: nil, size: nil, current_user: CurrentUser.user, **options) + def initialize(post:, tags: "", show_deleted: false, show_cropped: true, show_votes: false, link_target: post, pool: nil, similarity: nil, recommended: nil, compact: nil, size: nil, current_user: CurrentUser.user, **options) super @post = post @tags = tags.presence diff --git a/app/components/post_preview_component/post_preview_component.html.erb b/app/components/post_preview_component/post_preview_component.html.erb index 2fdf0e55a..49d90e5c7 100644 --- a/app/components/post_preview_component/post_preview_component.html.erb +++ b/app/components/post_preview_component/post_preview_component.html.erb @@ -55,8 +55,8 @@ <% end %>

    <% elsif show_votes -%> -

    +

    <%= render_post_votes post, current_user: current_user %> -

    +
    <% end -%> <% end -%> diff --git a/app/components/post_preview_component/post_preview_component.scss b/app/components/post_preview_component/post_preview_component.scss index e24646a54..c31586e0f 100644 --- a/app/components/post_preview_component/post_preview_component.scss +++ b/app/components/post_preview_component/post_preview_component.scss @@ -1,12 +1,10 @@ @import "../../javascript/src/styles/base/000_vars.scss"; article.post-preview { - width: 154px; - margin: 0 10px 10px 0; text-align: center; display: inline-block; position: relative; - vertical-align: top; + overflow: hidden; a { display: inline-block; @@ -119,38 +117,16 @@ body[data-current-user-can-approve-posts="true"] .post-preview { } } -@media screen and (max-width: 660px) { +@media screen and (min-width: 660px) { article.post-preview { - margin: 0; - text-align: center; - vertical-align: middle; - display: inline-block; - - a { - margin: 0 auto; - } - - img { - max-width: 33.3vw; - max-height: 33.3vw; - border: none !important; - } - } - - .user-disable-cropped-false { - article.post-preview { - width: 33.3%; - height: 33.3vw; - overflow: hidden; - } - - img { - width: 33.3vw; - height: 33.3vw; - - &.has-cropped-false { - object-fit: cover; - } - } + width: 154px; + margin: 0 10px 10px 0; + vertical-align: top; + } +} + +@media screen and (max-width: 660px) { + article.post-preview img { + border: none !important; } } diff --git a/app/components/post_votes_component/post_votes_component.scss b/app/components/post_votes_component/post_votes_component.scss index 0dcd21510..b5abde985 100644 --- a/app/components/post_votes_component/post_votes_component.scss +++ b/app/components/post_votes_component/post_votes_component.scss @@ -1,3 +1,5 @@ +@import "../../javascript/src/styles/base/000_vars.scss"; + .post-votes { // Fix it so that the vote buttons don't move when the score changes width. // XXX duplicated from app/components/comment_component/comment_component.scss @@ -7,10 +9,11 @@ min-width: 1.25em; white-space: nowrap; vertical-align: middle; - - a { - color: var(--text-color); - &:hover { text-decoration: underline; } - } + } +} + +.posts-container { + .post-score a { + @include inactive-link; } } diff --git a/app/components/post_votes_tooltip_component/post_votes_tooltip_component.html.erb b/app/components/post_votes_tooltip_component/post_votes_tooltip_component.html.erb index 24937ae58..a50e1e3e0 100644 --- a/app/components/post_votes_tooltip_component/post_votes_tooltip_component.html.erb +++ b/app/components/post_votes_tooltip_component/post_votes_tooltip_component.html.erb @@ -1,6 +1,6 @@
    -
    - +<%= post.up_score %> / <%= post.down_score %> <%= upvote_ratio %> +
    + +<%= post.up_score %> / -<%= post.down_score.abs %> <%= upvote_ratio %>
    diff --git a/app/javascript/src/styles/common/utilities.scss b/app/javascript/src/styles/common/utilities.scss index beae54a77..fd09c2cc2 100644 --- a/app/javascript/src/styles/common/utilities.scss +++ b/app/javascript/src/styles/common/utilities.scss @@ -28,6 +28,8 @@ $spacer: 0.25rem; /* 4px */ white-space: nowrap; } +.whitespace-nowrap { white-space: nowrap; } + .leading-none { line-height: 1; } .absolute { position: absolute; } @@ -45,6 +47,7 @@ $spacer: 0.25rem; /* 4px */ .mx-0\.5 { margin-left: 0.5 * $spacer; margin-right: 0.5 * $spacer; } .mx-2 { margin-left: 2 * $spacer; margin-right: 2 * $spacer; } +.mt-1 { margin-top: 1 * $spacer; } .mt-2 { margin-top: 2 * $spacer; } .mt-4 { margin-top: 4 * $spacer; } .mt-8 { margin-top: 8 * $spacer; } diff --git a/app/javascript/src/styles/specific/z_responsive.scss b/app/javascript/src/styles/specific/z_responsive.scss index 80da5b38f..4bb6cc934 100644 --- a/app/javascript/src/styles/specific/z_responsive.scss +++ b/app/javascript/src/styles/specific/z_responsive.scss @@ -41,11 +41,13 @@ } } - #posts #posts-container { - width: 100%; - display: flex; - flex-wrap: wrap; - align-items: center; - justify-content: flex-start; + .posts-container { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 0.25rem; + + &.user-disable-cropped-false article.post-preview img.has-cropped-true { + object-fit: none; + } } } diff --git a/app/logical/post_sets/post.rb b/app/logical/post_sets/post.rb index 5702133b9..34f576e93 100644 --- a/app/logical/post_sets/post.rb +++ b/app/logical/post_sets/post.rb @@ -115,6 +115,7 @@ module PostSets def hide_from_crawler? return true if current_page > 50 + return true if show_votes? return true if artist.present? && artist.is_banned? return false if query.is_empty_search? || query.is_simple_tag? || query.is_metatag?(:order, :rank) true diff --git a/app/models/post.rb b/app/models/post.rb index f0e6a0e7a..4ac77452d 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -548,9 +548,11 @@ class Post < ApplicationRecord pool&.add!(self) when /^fav:(.+)$/i + raise User::PrivilegeError unless Pundit.policy!(CurrentUser.user, Favorite).create? Favorite.create(post: self, user: CurrentUser.user) when /^-fav:(.+)$/i + raise User::PrivilegeError unless Pundit.policy!(CurrentUser.user, Favorite).create? Favorite.destroy_by(post: self, user: CurrentUser.user) when /^(up|down)vote:(.+)$/i diff --git a/app/views/posts/index.html.erb b/app/views/posts/index.html.erb index aa470ba93..c2d3e3701 100644 --- a/app/views/posts/index.html.erb +++ b/app/views/posts/index.html.erb @@ -35,9 +35,9 @@ <%= render PopupMenuComponent.new do |menu| %> <% menu.item do %> <% if params[:view] == "score" %> - <%= link_to "Hide scores", posts_path(tags: params[:tags], view: nil) %> + <%= link_to "Hide scores", posts_path(tags: params[:tags], page: params[:page], limit: params[:limit], view: nil), rel: "nofollow" %> <% else %> - <%= link_to "Show scores", posts_path(tags: params[:tags], view: "score") %> + <%= link_to "Show scores", posts_path(tags: params[:tags], page: params[:page], limit: params[:limit], view: "score"), rel: "nofollow" %> <% end %> <% end %> <% end %> diff --git a/app/views/posts/partials/index/_posts.html.erb b/app/views/posts/partials/index/_posts.html.erb index 129d6f5e8..5bbf4ecbc 100644 --- a/app/views/posts/partials/index/_posts.html.erb +++ b/app/views/posts/partials/index/_posts.html.erb @@ -1,5 +1,5 @@ -
    -
    +
    +
    <% if post_set.shown_posts.empty? %> <%= render "post_sets/blank" %> <% else %> diff --git a/app/views/posts/partials/index/_seo_meta_tags.html.erb b/app/views/posts/partials/index/_seo_meta_tags.html.erb index 66755c3e0..382f13e4b 100644 --- a/app/views/posts/partials/index/_seo_meta_tags.html.erb +++ b/app/views/posts/partials/index/_seo_meta_tags.html.erb @@ -12,6 +12,8 @@ <% if params[:tags].blank? && @post_set.current_page == 1 %> <% canonical_url root_url(host: Danbooru.config.hostname) %> +<% else %> + <% canonical_url posts_url(host: Danbooru.config.hostname, tags: params[:tags], page: params[:page], limit: params[:limit]) %> <% end %> <% noindex if @post_set.hide_from_crawler? %> diff --git a/app/views/user_upgrades/new.html.erb b/app/views/user_upgrades/new.html.erb index f72ed032a..7e88565bb 100644 --- a/app/views/user_upgrades/new.html.erb +++ b/app/views/user_upgrades/new.html.erb @@ -70,6 +70,12 @@ 2,000 5,000 + + Private Favorites + no + yes + yes + Favorite Groups 3 diff --git a/test/components/post_navbar_component_test.rb b/test/components/post_navbar_component_test.rb index a27024ac0..60a37c0ec 100644 --- a/test/components/post_navbar_component_test.rb +++ b/test/components/post_navbar_component_test.rb @@ -7,7 +7,7 @@ class PostNavbarComponentTest < ViewComponent::TestCase setup do @post = create(:post) - @user = create(:user) + @user = create(:gold_user) end context "The PostNavbarComponent" do @@ -46,9 +46,8 @@ class PostNavbarComponentTest < ViewComponent::TestCase context "for a post with favgroups" do setup do as(@user) do - @favgroup1 = create(:favorite_group, creator: @user, is_public: true) - @favgroup2 = create(:favorite_group, creator: @user, is_public: false) - @post.update(tag_string: "favgroup:#{@favgroup1.id} favgroup:#{@favgroup2.id}") + @favgroup1 = create(:favorite_group, creator: @user, post_ids: [@post.id]) + @favgroup2 = create(:private_favorite_group, creator: @user, post_ids: [@post.id]) end end diff --git a/test/components/post_votes_component_test.rb b/test/components/post_votes_component_test.rb index df30aff6d..7a9118d56 100644 --- a/test/components/post_votes_component_test.rb +++ b/test/components/post_votes_component_test.rb @@ -11,12 +11,12 @@ class PostVotesComponentTest < ViewComponent::TestCase end context "for a user who can't vote" do - should "not show the vote buttons" do + should "show the vote buttons" do render_post_votes(@post, current_user: User.anonymous) assert_css(".post-score") - assert_no_css(".post-upvote-link") - assert_no_css(".post-downvote-link") + assert_css(".post-upvote-link.inactive-link") + assert_css(".post-downvote-link.inactive-link") end end @@ -34,8 +34,8 @@ class PostVotesComponentTest < ViewComponent::TestCase context "for a downvoted post" do should "highlight the downvote button as active" do - @post.vote!(-1, @user) - render_post_votes(@post, current_user: @user) + create(:post_vote, post: @post, user: @user, score: -1) + as(@user) { render_post_votes(@post, current_user: @user) } assert_css(".post-upvote-link.inactive-link") assert_css(".post-downvote-link.active-link") @@ -44,8 +44,8 @@ class PostVotesComponentTest < ViewComponent::TestCase context "for an upvoted post" do should "highlight the upvote button as active" do - @post.vote!(1, @user) - render_post_votes(@post, current_user: @user) + create(:post_vote, post: @post, user: @user, score: 1) + as(@user) { render_post_votes(@post, current_user: @user) } assert_css(".post-upvote-link.active-link") assert_css(".post-downvote-link.inactive-link") diff --git a/test/factories/favorite.rb b/test/factories/favorite.rb index 441c6f4a9..40ef97b84 100644 --- a/test/factories/favorite.rb +++ b/test/factories/favorite.rb @@ -2,5 +2,9 @@ FactoryBot.define do factory(:favorite) do user post + + factory(:private_favorite) do + user factory: :gold_user, enable_private_favorites: true + end end end diff --git a/test/unit/favorite_test.rb b/test/unit/favorite_test.rb index 9c0bed69b..8273dc8af 100644 --- a/test/unit/favorite_test.rb +++ b/test/unit/favorite_test.rb @@ -11,10 +11,13 @@ class FavoriteTest < ActiveSupport::TestCase context "Favorites: " do context "removing a favorite" do should "update the post and user favorite counts" do + @user1 = create(:restricted_user) fav = Favorite.create!(post: @p1, user: @user1) assert_equal(1, @user1.reload.favorite_count) assert_equal(1, @p1.reload.fav_count) + assert_equal(0, @p1.reload.score) + refute(PostVote.positive.exists?(post: @p1, user: @user)) Favorite.destroy_by(post: @p1, user: @user1) @@ -42,14 +45,8 @@ class FavoriteTest < ActiveSupport::TestCase end context "adding a favorite" do - should "update the post and user favorite counts" do - Favorite.create!(post: @p1, user: @user1) - - assert_equal(1, @user1.reload.favorite_count) - assert_equal(1, @p1.reload.fav_count) - end - should "not upvote the post if the user can't vote" do + @user1 = create(:restricted_user) Favorite.create!(post: @p1, user: @user1) assert_equal(1, @user1.reload.favorite_count) @@ -89,7 +86,7 @@ class FavoriteTest < ActiveSupport::TestCase assert_equal(["You have already favorited this post"], @f2.errors.full_messages) assert_equal(1, @user1.reload.favorite_count) assert_equal(1, @p1.reload.fav_count) - assert_equal(0, @p1.reload.score) + assert_equal(1, @p1.reload.score) end end end diff --git a/test/unit/post_query_builder_test.rb b/test/unit/post_query_builder_test.rb index 94958fda6..a0706f543 100644 --- a/test/unit/post_query_builder_test.rb +++ b/test/unit/post_query_builder_test.rb @@ -390,7 +390,7 @@ class PostQueryBuilderTest < ActiveSupport::TestCase favgroup1 = create(:favorite_group, creator: CurrentUser.user, post_ids: [post1.id]) favgroup2 = create(:favorite_group, creator: CurrentUser.user, post_ids: [post2.id]) - favgroup3 = create(:favorite_group, creator: create(:user), post_ids: [post3.id], is_public: false) + favgroup3 = create(:private_favorite_group, post_ids: [post3.id]) assert_tag_match([post1], "favgroup:#{favgroup1.id}") assert_tag_match([post2], "favgroup:#{favgroup2.name}") @@ -421,7 +421,7 @@ class PostQueryBuilderTest < ActiveSupport::TestCase post3 = create(:post) favgroup1 = create(:favorite_group, creator: CurrentUser.user, post_ids: [post1.id, post2.id]) - favgroup2 = create(:favorite_group, creator: create(:user), post_ids: [post2.id, post3.id], is_public: false) + favgroup2 = create(:private_favorite_group, post_ids: [post2.id, post3.id]) assert_tag_match([post1, post2], "ordfavgroup:#{favgroup1.id}") assert_tag_match([post1, post2], "ordfavgroup:#{favgroup1.name}") @@ -975,7 +975,7 @@ class PostQueryBuilderTest < ActiveSupport::TestCase context "for the upvote: metatag" do setup do - @user = create(:user) + @user = create(:gold_user) @upvote = create(:post_vote, user: @user, score: 1) @downvote = create(:post_vote, user: @user, score: -1) end @@ -1411,8 +1411,7 @@ class PostQueryBuilderTest < ActiveSupport::TestCase end should "return the correct favorite count for a fav: search for a user with private favorites" do - fav = create(:favorite) - fav.user.update!(favorite_count: 1, enable_private_favorites: true) + fav = create(:private_favorite) assert_fast_count(0, "fav:#{fav.user.name}") assert_fast_count(0, "ordfav:#{fav.user.name}") diff --git a/test/unit/post_test.rb b/test/unit/post_test.rb index a4f0a6f53..2721c52a9 100644 --- a/test/unit/post_test.rb +++ b/test/unit/post_test.rb @@ -699,10 +699,34 @@ class PostTest < ActiveSupport::TestCase context "for a fav" do should "add/remove the current user to the post's favorite listing" do @post.update(tag_string: "aaa fav:self") + assert_equal(1, @post.reload.score) assert_equal(1, @post.favorites.where(user: @user).count) + assert_equal(1, @post.votes.positive.where(user: @user).count) @post.update(tag_string: "aaa -fav:self") + assert_equal(0, @post.reload.score) assert_equal(0, @post.favorites.count) + assert_equal(0, @post.votes.positive.where(user: @user).count) + end + + should "not allow banned users to fav" do + assert_raises(User::PrivilegeError) do + as(create(:banned_user)) { @post.update(tag_string: "aaa fav:self") } + end + + assert_raises(User::PrivilegeError) do + as(create(:banned_user)) { @post.update(tag_string: "aaa -fav:self") } + end + end + + should "not allow restricted users to fav" do + assert_raises(User::PrivilegeError) do + as(create(:restricted_user)) { @post.update(tag_string: "aaa fav:self") } + end + + assert_raises(User::PrivilegeError) do + as(create(:restricted_user)) { @post.update(tag_string: "aaa -fav:self") } + end end should "not fail when the fav: metatag is used twice" do @@ -870,20 +894,20 @@ class PostTest < ActiveSupport::TestCase context "upvote:self or downvote:self" do context "by a member" do - should "not upvote the post" do - assert_no_difference("PostVote.count") do + should "upvote the post" do + assert_difference("PostVote.count") do @post.update(tag_string: "upvote:self") end - assert_equal(0, @post.reload.score) + assert_equal(1, @post.reload.score) end - should "not downvote the post" do - assert_no_difference("PostVote.count") do + should "downvote the post" do + assert_difference("PostVote.count") do @post.update(tag_string: "downvote:self") end - assert_equal(0, @post.reload.score) + assert_equal(-1, @post.reload.score) end end @@ -1476,7 +1500,8 @@ class PostTest < ActiveSupport::TestCase should "create a vote for each user who can vote" do assert(@parent.votes.where(user: @gold1).exists?) - assert_equal(1, @parent.score) + assert(@parent.votes.where(user: @user1).exists?) + assert_equal(2, @parent.score) end end end @@ -1523,13 +1548,13 @@ class PostTest < ActiveSupport::TestCase end context "Voting:" do - should "not allow members to vote" do + should "allow members to vote" do 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) + assert_equal(1, post.votes.count) + assert_equal(1, post.reload.score) end should "not allow duplicate votes" do