Merge pull request #4923 from danbooru/feat-member-voting

Allow Members to vote
This commit is contained in:
evazion
2021-11-20 02:44:16 -06:00
committed by GitHub
78 changed files with 1008 additions and 313 deletions

View File

@@ -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

View File

@@ -0,0 +1,9 @@
<div class="favorites-tooltip thin-scrollbar">
<div class="post-favoriters">
<% favorites.each do |favorite| %>
<div class="post-favoriter truncate">
<%= favoriter_name(favorite) %>
</div>
<% end %>
</div>
</div>

View File

@@ -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;

View File

@@ -0,0 +1,8 @@
.favorites-tooltip {
font-size: var(--text-xs);
max-height: 240px;
.post-favoriter {
max-width: 160px;
}
}

View File

@@ -28,7 +28,7 @@ div.popup-menu {
li a { li a {
display: block; display: block;
padding: 0.125em 2em 0.125em 0; padding: 0.125em 0 0.125em 0;
.icon { .icon {
width: 1rem; width: 1rem;

View File

@@ -3,18 +3,19 @@
class PostPreviewComponent < ApplicationComponent class PostPreviewComponent < ApplicationComponent
with_collection_parameter :post 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 :image_width, :image_height, :file_ext, :file_size, :duration, :is_animated?, to: :media_asset
delegate :media_asset, to: :post 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: false, link_target: post, pool: nil, similarity: nil, recommended: nil, compact: nil, size: nil, current_user: CurrentUser.user, **options)
super super
@post = post @post = post
@tags = tags.presence @tags = tags.presence
@show_deleted = show_deleted @show_deleted = show_deleted
@show_cropped = show_cropped @show_cropped = show_cropped
@show_votes = show_votes
@link_target = link_target @link_target = link_target
@pool = pool @pool = pool
@similarity = similarity.round(1) if similarity.present? @similarity = similarity.round(1) if similarity.present?

View File

@@ -23,8 +23,7 @@
<p class="desc"> <p class="desc">
<%= link_to pool.pretty_name.truncate(80), pool %> <%= link_to pool.pretty_name.truncate(80), pool %>
</p> </p>
<% end -%> <% elsif similarity -%>
<% if similarity -%>
<p class="desc"> <p class="desc">
<% if post.source =~ %r!\Ahttps?://!i %> <% if post.source =~ %r!\Ahttps?://!i %>
<%= external_link_to post.normalized_source, post.source_domain %> <%= external_link_to post.normalized_source, post.source_domain %>
@@ -33,20 +32,19 @@
<%= time_ago_in_words_tagged(post.created_at, compact: true) %> <%= time_ago_in_words_tagged(post.created_at, compact: true) %>
<% end %> <% end %>
</p> </p>
<% end %>
<% if size -%>
<p class="desc"> <p class="desc">
<%= link_to number_to_human_size(size), post.file_url %> <%= link_to number_to_human_size(size), post.file_url %>
(<%= post.image_width %>x<%= post.image_height %>) (<%= post.image_width %>x<%= post.image_height %>)
</p> </p>
<% end -%>
<% if similarity -%>
<p class="desc"> <p class="desc">
<%= link_to "#{similarity}%", iqdb_queries_path(post_id: post.id) %> similarity <%= link_to "#{similarity}%", iqdb_queries_path(post_id: post.id) %> similarity
</p> </p>
<% end -%> <% elsif size -%>
<p class="desc">
<% if recommended -%> <%= link_to number_to_human_size(size), post.file_url %>
(<%= post.image_width %>x<%= post.image_height %>)
</p>
<% elsif recommended -%>
<p class="desc recommended"> <p class="desc recommended">
<%= link_to recommended_posts_path(search: { post_id: post.id }), class: "more-recommended-posts", "data-post-id": post.id do %> <%= link_to recommended_posts_path(search: { post_id: post.id }), class: "more-recommended-posts", "data-post-id": post.id do %>
<%= post.fav_count %> <%= post.fav_count %>
@@ -56,5 +54,9 @@
<br>more » <br>more »
<% end %> <% end %>
</p> </p>
<% elsif show_votes -%>
<div class="post-preview-score text-sm mt-1">
<%= render_post_votes post, current_user: current_user %>
</div>
<% end -%> <% end -%>
<% end -%> <% end -%>

View File

@@ -1,13 +1,10 @@
@import "../../javascript/src/styles/base/000_vars.scss"; @import "../../javascript/src/styles/base/000_vars.scss";
article.post-preview { article.post-preview {
height: 154px;
width: 154px;
margin: 0 10px 10px 0;
text-align: center; text-align: center;
display: inline-block; display: inline-block;
position: relative; position: relative;
vertical-align: top; overflow: hidden;
a { a {
display: inline-block; display: inline-block;
@@ -120,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 { article.post-preview {
margin: 0; width: 154px;
text-align: center; margin: 0 10px 10px 0;
vertical-align: middle; vertical-align: top;
display: inline-block; }
a {
margin: 0 auto;
} }
img { @media screen and (max-width: 660px) {
max-width: 33.3vw; article.post-preview img {
max-height: 33.3vw;
border: none !important; 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;
}
}
}
}

View File

@@ -15,7 +15,7 @@ class PostVotesComponent < ApplicationComponent
end end
def current_vote def current_vote
post.votes.find_by(user: current_user) post.vote_by_current_user
end end
def upvoted? def upvoted?

View File

@@ -1,19 +1,21 @@
<span class="post-votes" data-id="<%= post.id %>"> <span class="post-votes" data-id="<%= post.id %>">
<% if can_vote? %> <% if current_user.is_anonymous? %>
<% if upvoted? %> <%= 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 %> <%= 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 %> <% 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 %> <%= 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 %> <% end %>
<% end %>
<span class="post-score"><%= post.score %></span> <span class="post-score">
<%= link_to post.score, post_votes_path(search: { post_id: post.id }, variant: :compact) %>
</span>
<% if can_vote? %> <% if current_user.is_anonymous? %>
<% if downvoted? %> <%= 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 %> <%= 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 %> <% 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 %> <%= 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 %> <% end %>
<% end %>
</span> </span>

View File

@@ -1,3 +1,5 @@
@import "../../javascript/src/styles/base/000_vars.scss";
.post-votes { .post-votes {
// Fix it so that the vote buttons don't move when the score changes width. // 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 // XXX duplicated from app/components/comment_component/comment_component.scss
@@ -6,5 +8,12 @@
text-align: center; text-align: center;
min-width: 1.25em; min-width: 1.25em;
white-space: nowrap; white-space: nowrap;
vertical-align: middle;
}
}
.posts-container {
.post-score a {
@include inactive-link;
} }
} }

View File

@@ -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

View File

@@ -0,0 +1,13 @@
<div class="post-votes-tooltip thin-scrollbar">
<div class="text-center text-muted whitespace-nowrap">
+<%= post.up_score %> / -<%= post.down_score.abs %> <%= upvote_ratio %>
</div>
<div class="post-voters">
<% votes.each do |vote| %>
<div class="post-voter truncate">
<%= vote_icon(vote) %> <%= voter_name(vote) %>
</div>
<% end %>
</div>
</div>

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -1,15 +1,16 @@
# frozen_string_literal: true # frozen_string_literal: true
class TagListComponent < ApplicationComponent 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 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 super
@tags = tags @tags = tags
@current_query = current_query @current_query = current_query
@show_extra_links = show_extra_links @show_extra_links = show_extra_links
@search_params = search_params
end end
def self.tags_from_names(tag_names) def self.tags_from_names(tag_names)

View File

@@ -11,11 +11,11 @@
<% end %> <% end %>
<% if show_extra_links && current_query.present? %> <% 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}", **search_params), 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-exl-tag" %>
<% end %> <% 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 %> <%= tag.span humanized_number(t.post_count), class: "post-count", title: t.post_count %>
</li> </li>
<% end %> <% end %>

View File

@@ -19,7 +19,7 @@ class FavoriteGroupsController < ApplicationController
end end
def new def new
@favorite_group = authorize FavoriteGroup.new @favorite_group = authorize FavoriteGroup.new(creator: CurrentUser.user)
respond_with(@favorite_group) respond_with(@favorite_group)
end end

View File

@@ -1,19 +1,16 @@
class FavoritesController < ApplicationController class FavoritesController < ApplicationController
respond_to :html, :xml, :json, :js respond_to :js, :json, :html, :xml
def index def index
authorize Favorite post_id = params[:post_id] || params[:search][:post_id]
if !request.format.html? user_id = params[:user_id] || params[:search][:user_id]
@favorites = Favorite.visible(CurrentUser.user).paginated_search(params) 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) 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
end end
def create def create

View File

@@ -4,6 +4,7 @@ class PostVotesController < ApplicationController
def index def index
@post_votes = authorize PostVote.visible(CurrentUser.user).paginated_search(params, count_pages: true) @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_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) respond_with(@post_votes)
end end

View File

@@ -10,7 +10,7 @@ class PostsController < ApplicationController
end end
else else
tag_query = params[:tags] || params.dig(:post, :tags) 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 @posts = authorize @post_set.posts, policy_class: PostPolicy
@post_set.log! @post_set.log!
respond_with(@posts) do |format| respond_with(@posts) do |format|

View File

@@ -193,10 +193,10 @@ module ApplicationHelper
to_sentence(links, **options) to_sentence(links, **options)
end end
def link_to_user(user, text = nil) def link_to_user(user, text = nil, classes: nil, **options)
return "anonymous" if user.blank? 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-approver" if user.can_approve_posts?
user_class += " user-post-uploader" if user.can_upload_free? user_class += " user-post-uploader" if user.can_upload_free?
user_class += " user-banned" if user.is_banned? user_class += " user-banned" if user.is_banned?

View File

@@ -20,6 +20,14 @@ module ComponentsHelper
render PostVotesComponent.new(post: post, **options) render PostVotesComponent.new(post: post, **options)
end end
def render_post_votes_tooltip(post, **options)
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) def render_post_navbar(post, **options)
render PostNavbarComponent.new(post: post, **options) render PostNavbarComponent.new(post: post, **options)
end end

View File

@@ -37,12 +37,14 @@ import Blacklist from "../src/javascripts/blacklists.js";
import CommentComponent from "../../components/comment_component/comment_component.js"; import CommentComponent from "../../components/comment_component/comment_component.js";
import CurrentUser from "../src/javascripts/current_user.js"; import CurrentUser from "../src/javascripts/current_user.js";
import Dtext from "../src/javascripts/dtext.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 IqdbQuery from "../src/javascripts/iqdb_queries.js";
import Note from "../src/javascripts/notes.js"; import Note from "../src/javascripts/notes.js";
import PopupMenuComponent from "../../components/popup_menu_component/popup_menu_component.js"; import PopupMenuComponent from "../../components/popup_menu_component/popup_menu_component.js";
import Post from "../src/javascripts/posts.js"; import Post from "../src/javascripts/posts.js";
import PostModeMenu from "../src/javascripts/post_mode_menu.js"; import PostModeMenu from "../src/javascripts/post_mode_menu.js";
import PostTooltip from "../src/javascripts/post_tooltips.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 RelatedTag from "../src/javascripts/related_tag.js";
import Shortcuts from "../src/javascripts/shortcuts.js"; import Shortcuts from "../src/javascripts/shortcuts.js";
import TagCounter from "../src/javascripts/tag_counter.js"; import TagCounter from "../src/javascripts/tag_counter.js";
@@ -58,12 +60,14 @@ Danbooru.Blacklist = Blacklist;
Danbooru.CommentComponent = CommentComponent; Danbooru.CommentComponent = CommentComponent;
Danbooru.CurrentUser = CurrentUser; Danbooru.CurrentUser = CurrentUser;
Danbooru.Dtext = Dtext; Danbooru.Dtext = Dtext;
Danbooru.FavoritesTooltipComponent = FavoritesTooltipComponent;
Danbooru.IqdbQuery = IqdbQuery; Danbooru.IqdbQuery = IqdbQuery;
Danbooru.Note = Note; Danbooru.Note = Note;
Danbooru.PopupMenuComponent = PopupMenuComponent; Danbooru.PopupMenuComponent = PopupMenuComponent;
Danbooru.Post = Post; Danbooru.Post = Post;
Danbooru.PostModeMenu = PostModeMenu; Danbooru.PostModeMenu = PostModeMenu;
Danbooru.PostTooltip = PostTooltip; Danbooru.PostTooltip = PostTooltip;
Danbooru.PostVotesTooltipComponent = PostVotesTooltipComponent;
Danbooru.RelatedTag = RelatedTag; Danbooru.RelatedTag = RelatedTag;
Danbooru.Shortcuts = Shortcuts; Danbooru.Shortcuts = Shortcuts;
Danbooru.TagCounter = TagCounter; Danbooru.TagCounter = TagCounter;

View File

@@ -30,7 +30,6 @@ Post.initialize_all = function() {
if ($("#c-posts").length && $("#a-show").length) { if ($("#c-posts").length && $("#a-show").length) {
this.initialize_links(); this.initialize_links();
this.initialize_post_relationship_previews(); this.initialize_post_relationship_previews();
this.initialize_favlist();
this.initialize_post_sections(); this.initialize_post_sections();
this.initialize_post_image_resize_links(); this.initialize_post_image_resize_links();
this.initialize_recommended(); 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) { Post.view_original = function(e = null) {
if (Utility.test_max_width(660)) { if (Utility.test_max_width(660)) {
// Do the default behavior (navigate to image) // Do the default behavior (navigate to image)

View File

@@ -98,7 +98,6 @@ menu {
> li { > li {
margin: 0; margin: 0;
padding: 0 0.2em;
display: inline; display: inline;
} }
} }

View File

@@ -218,6 +218,9 @@ html {
--post-artist-commentary-container-background: var(--grey-0); --post-artist-commentary-container-background: var(--grey-0);
--post-artist-commentary-container-border-color: var(--grey-1); --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-background: #FFE;
--note-body-text-color: var(--black); --note-body-text-color: var(--black);
--note-body-border-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-background: var(--grey-8);
--post-artist-commentary-container-border-color: var(--grey-7); --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); --unsaved-note-box-border-color: var(--red-5);
--movable-note-box-border-color: var(--blue-5); --movable-note-box-border-color: var(--blue-5);
--note-preview-border-color: var(--red-5); --note-preview-border-color: var(--red-5);

View File

@@ -22,6 +22,14 @@ $spacer: 0.25rem; /* 4px */
.text-xs { font-size: var(--text-xs); } .text-xs { font-size: var(--text-xs); }
.text-sm { font-size: var(--text-sm); } .text-sm { font-size: var(--text-sm); }
.truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.whitespace-nowrap { white-space: nowrap; }
.leading-none { line-height: 1; } .leading-none { line-height: 1; }
.absolute { position: absolute; } .absolute { position: absolute; }
@@ -39,6 +47,7 @@ $spacer: 0.25rem; /* 4px */
.mx-0\.5 { margin-left: 0.5 * $spacer; margin-right: 0.5 * $spacer; } .mx-0\.5 { margin-left: 0.5 * $spacer; margin-right: 0.5 * $spacer; }
.mx-2 { margin-left: 2 * $spacer; margin-right: 2 * $spacer; } .mx-2 { margin-left: 2 * $spacer; margin-right: 2 * $spacer; }
.mt-1 { margin-top: 1 * $spacer; }
.mt-2 { margin-top: 2 * $spacer; } .mt-2 { margin-top: 2 * $spacer; }
.mt-4 { margin-top: 4 * $spacer; } .mt-4 { margin-top: 4 * $spacer; }
.mt-8 { margin-top: 8 * $spacer; } .mt-8 { margin-top: 8 * $spacer; }
@@ -52,6 +61,9 @@ $spacer: 0.25rem; /* 4px */
.p-0\.5 { padding: 0.5 * $spacer; } .p-0\.5 { padding: 0.5 * $spacer; }
.p-4 { padding: 4 * $spacer; } .p-4 { padding: 4 * $spacer; }
.pr-2 { padding-right: 2 * $spacer; }
.pr-4 { padding-right: 4 * $spacer; }
.w-1\/4 { width: 25%; } .w-1\/4 { width: 25%; }
.w-full { width: 100%; } .w-full { width: 100%; }
@@ -73,6 +85,62 @@ $spacer: 0.25rem; /* 4px */
.items-center { align-items: center; } .items-center { align-items: center; }
.justify-center { justify-content: center; } .justify-center { justify-content: center; }
.float-right { float: right; }
.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) { @media screen and (min-width: 660px) {
.md\:inline-block { display: inline-block; } .md\:inline-block { display: inline-block; }
.md\:flex { display: flex; } .md\:flex { display: flex; }

View File

@@ -1,50 +1,6 @@
$tooltip-line-height: 16px; $tooltip-line-height: 16px;
$tooltip-body-height: $tooltip-line-height * 4; // 4 lines high. $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"] { .tippy-box[data-theme~="post-tooltip"] {
min-width: 20em; min-width: 20em;
max-width: 40em !important; max-width: 40em !important;
@@ -59,7 +15,6 @@ $tooltip-body-height: $tooltip-line-height * 4; // 4 lines high.
} }
.post-tooltip-body { .post-tooltip-body {
@include thin-scrollbar;
max-height: $tooltip-body-height; max-height: $tooltip-body-height;
overflow-y: auto; overflow-y: auto;
display: flex; display: flex;

View File

@@ -135,8 +135,6 @@ div#c-posts {
font-size: var(--text-lg); font-size: var(--text-lg);
li { li {
padding: 0 1em 0 0;
&.active { &.active {
font-weight: bold; font-weight: bold;
} }
@@ -172,10 +170,6 @@ div#c-posts {
} }
} }
#favlist {
word-wrap: break-word;
}
#recommended.loading-recommended-posts { #recommended.loading-recommended-posts {
pointer-events: none; pointer-events: none;
opacity: 0.5; opacity: 0.5;

View File

@@ -41,11 +41,13 @@
} }
} }
#posts #posts-container { .posts-container {
width: 100%; display: grid;
display: flex; grid-template-columns: repeat(3, minmax(0, 1fr));
flex-wrap: wrap; gap: 0.25rem;
align-items: center;
justify-content: flex-start; &.user-disable-cropped-false article.post-preview img.has-cropped-true {
object-fit: none;
}
} }
} }

View File

@@ -6,6 +6,7 @@ module HasBitFlags
def has_bit_flags(attributes, field: :bit_flags) def has_bit_flags(attributes, field: :bit_flags)
attributes.each.with_index do |attribute, i| attributes.each.with_index do |attribute, i|
bit_flag = 1 << i bit_flag = 1 << i
field_was = "#{field}_was"
define_method(attribute) do define_method(attribute) do
send(field) & bit_flag > 0 send(field) & bit_flag > 0
@@ -15,6 +16,14 @@ module HasBitFlags
send(field) & bit_flag > 0 send(field) & bit_flag > 0
end 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| define_method("#{attribute}=") do |val|
if val.to_s =~ /[t1y]/ if val.to_s =~ /[t1y]/
send("#{field}=", send(field) | bit_flag) send("#{field}=", send(field) | bit_flag)

View File

@@ -7,10 +7,10 @@ module PostSets
MAX_PER_PAGE = 200 MAX_PER_PAGE = 200
MAX_SIDEBAR_TAGS = 25 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 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?) @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 @normalized_query = query.normalized_query
@tag_string = tags @tag_string = tags
@@ -18,6 +18,7 @@ module PostSets
@per_page = per_page @per_page = per_page
@random = random.to_s.truthy? @random = random.to_s.truthy?
@format = format.to_s @format = format.to_s
@view = view.presence || "simple"
end end
def humanized_tag_string def humanized_tag_string
@@ -107,13 +108,14 @@ module PostSets
if is_random? if is_random?
get_random_posts.paginate(page, search_count: false, limit: per_page, max_limit: max_per_page).load get_random_posts.paginate(page, search_count: false, limit: per_page, max_limit: max_per_page).load
else 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 end
end end
def hide_from_crawler? def hide_from_crawler?
return true if current_page > 50 return true if current_page > 50
return true if show_votes?
return true if artist.present? && artist.is_banned? return true if artist.present? && artist.is_banned?
return false if query.is_empty_search? || query.is_simple_tag? || query.is_metatag?(:order, :rank) return false if query.is_empty_search? || query.is_simple_tag? || query.is_metatag?(:order, :rank)
true true
@@ -139,6 +141,18 @@ module PostSets
end end
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 def search_stats
{ {
query: normalized_query.to_s, query: normalized_query.to_s,

View File

@@ -6,10 +6,16 @@ class Favorite < ApplicationRecord
after_create :upvote_post_on_create after_create :upvote_post_on_create
after_destroy :unvote_post_on_destroy 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) 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 end
def self.search(params) def self.search(params)

View File

@@ -10,6 +10,7 @@ class FavoriteGroup < ApplicationRecord
validate :creator_can_create_favorite_groups, :on => :create validate :creator_can_create_favorite_groups, :on => :create
validate :validate_number_of_posts validate :validate_number_of_posts
validate :validate_posts validate :validate_posts
validate :validate_can_enable_privacy
array_attribute :post_ids, parse: /\d+/, cast: :to_i array_attribute :post_ids, parse: /\d+/, cast: :to_i
@@ -83,6 +84,12 @@ class FavoriteGroup < ApplicationRecord
end end
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) def self.normalize_name(name)
name.gsub(/[[:space:]]+/, "_") name.gsub(/[[:space:]]+/, "_")
end end
@@ -112,7 +119,7 @@ class FavoriteGroup < ApplicationRecord
end end
def pretty_name def pretty_name
name.tr("_", " ") name&.tr("_", " ")
end end
def posts def posts
@@ -166,6 +173,18 @@ class FavoriteGroup < ApplicationRecord
post_ids.include?(post_id) post_ids.include?(post_id)
end 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 def self.available_includes
[:creator] [:creator]
end end

View File

@@ -40,6 +40,7 @@ class Post < ApplicationRecord
has_one :upload, :dependent => :destroy has_one :upload, :dependent => :destroy
has_one :artist_commentary, :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 :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 :flags, :class_name => "PostFlag", :dependent => :destroy
has_many :appeals, :class_name => "PostAppeal", :dependent => :destroy has_many :appeals, :class_name => "PostAppeal", :dependent => :destroy
has_many :votes, :class_name => "PostVote", :dependent => :destroy has_many :votes, :class_name => "PostVote", :dependent => :destroy
@@ -49,7 +50,6 @@ class Post < ApplicationRecord
has_many :approvals, :class_name => "PostApproval", :dependent => :destroy has_many :approvals, :class_name => "PostApproval", :dependent => :destroy
has_many :disapprovals, :class_name => "PostDisapproval", :dependent => :destroy has_many :disapprovals, :class_name => "PostDisapproval", :dependent => :destroy
has_many :favorites, dependent: :destroy has_many :favorites, dependent: :destroy
has_many :favorited_users, through: :favorites, source: :user
has_many :replacements, class_name: "PostReplacement", :dependent => :destroy has_many :replacements, class_name: "PostReplacement", :dependent => :destroy
attr_accessor :old_tag_string, :old_parent_id, :old_source, :old_rating, :has_constraints, :disable_versioning attr_accessor :old_tag_string, :old_parent_id, :old_source, :old_rating, :has_constraints, :disable_versioning
@@ -548,9 +548,11 @@ class Post < ApplicationRecord
pool&.add!(self) pool&.add!(self)
when /^fav:(.+)$/i when /^fav:(.+)$/i
raise User::PrivilegeError unless Pundit.policy!(CurrentUser.user, Favorite).create?
Favorite.create(post: self, user: CurrentUser.user) Favorite.create(post: self, user: CurrentUser.user)
when /^-fav:(.+)$/i when /^-fav:(.+)$/i
raise User::PrivilegeError unless Pundit.policy!(CurrentUser.user, Favorite).create?
Favorite.destroy_by(post: self, user: CurrentUser.user) Favorite.destroy_by(post: self, user: CurrentUser.user)
when /^(up|down)vote:(.+)$/i when /^(up|down)vote:(.+)$/i
@@ -666,13 +668,6 @@ class Post < ApplicationRecord
Favorite.exists?(post: self, user: user) Favorite.exists?(post: self, user: user)
end 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 def favorite_groups
FavoriteGroup.for_post(id) FavoriteGroup.for_post(id)
end end

View File

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

View File

@@ -103,6 +103,7 @@ class User < ApplicationRecord
validates :per_page, inclusion: { in: (1..PostSets::Post::MAX_PER_PAGE) } validates :per_page, inclusion: { in: (1..PostSets::Post::MAX_PER_PAGE) }
validates :password, confirmation: true validates :password, confirmation: true
validates :comment_threshold, inclusion: { in: (-100..5) } validates :comment_threshold, inclusion: { in: (-100..5) }
validate :validate_enable_private_favorites, on: :update
before_validation :normalize_blacklisted_tags before_validation :normalize_blacklisted_tags
before_create :promote_to_owner_if_first_user before_create :promote_to_owner_if_first_user
has_many :artist_versions, foreign_key: :updater_id has_many :artist_versions, foreign_key: :updater_id
@@ -152,6 +153,8 @@ class User < ApplicationRecord
scope :admins, -> { where(level: Levels::ADMIN) } scope :admins, -> { where(level: Levels::ADMIN) }
scope :has_blacklisted_tag, ->(name) { where_regex(:blacklisted_tags, "(^| )[~-]?#{Regexp.escape(name)}( |$)", flags: "ni") } scope :has_blacklisted_tag, ->(name) { where_regex(:blacklisted_tags, "(^| )[~-]?#{Regexp.escape(name)}( |$)", flags: "ni") }
scope :has_private_favorites, -> { bit_prefs_match(:enable_private_favorites, true) }
scope :has_public_favorites, -> { bit_prefs_match(:enable_private_favorites, false) }
module BanMethods module BanMethods
def unban! def unban!
@@ -190,6 +193,14 @@ class User < ApplicationRecord
end end
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 concerning :AuthenticationMethods do
def password=(new_password) def password=(new_password)
@password = new_password @password = new_password

View File

@@ -15,7 +15,11 @@ class FavoriteGroupPolicy < ApplicationPolicy
update? update?
end end
def can_enable_privacy?
record.creator.is_gold?
end
def permitted_attributes 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
end end

View File

@@ -1,9 +1,13 @@
class FavoritePolicy < ApplicationPolicy class FavoritePolicy < ApplicationPolicy
def create? def create?
!user.is_anonymous? unbanned? && user.is_member?
end end
def destroy? def destroy?
record.user_id == user.id record.user_id == user.id
end end
def can_see_favoriter?
user.is_admin? || record.user == user || !record.user.enable_private_favorites?
end
end end

View File

@@ -59,10 +59,6 @@ class PostPolicy < ApplicationPolicy
user.is_gold? user.is_gold?
end end
def can_view_favlist?
user.is_gold?
end
# whether to show the + - links in the tag list. # whether to show the + - links in the tag list.
def show_extra_links? def show_extra_links?
user.is_gold? user.is_gold?

View File

@@ -1,6 +1,6 @@
class PostVotePolicy < ApplicationPolicy class PostVotePolicy < ApplicationPolicy
def create? def create?
unbanned? && user.is_gold? unbanned? && user.is_member?
end end
def destroy? def destroy?
@@ -8,6 +8,16 @@ class PostVotePolicy < ApplicationPolicy
end end
def show? def show?
user.is_admin? || record.user == user user.is_admin? || record.user == user || (record.is_positive? && !record.user.enable_private_favorites?)
end
def can_see_voter?
show?
end
def api_attributes
attributes = super
attributes -= [:user_id] unless can_see_voter?
attributes
end end
end end

View File

@@ -31,6 +31,10 @@ class UserPolicy < ApplicationPolicy
user.is_admin? || record.id == user.id || !record.enable_private_favorites? user.is_admin? || record.id == user.id || !record.enable_private_favorites?
end end
def can_enable_private_favorites?
user.is_gold?
end
def permitted_attributes_for_create def permitted_attributes_for_create
[:name, :password, :password_confirmation, { email_address_attributes: [:address] }] [:name, :password, :password_confirmation, { email_address_attributes: [:address] }]
end end

View File

@@ -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 %>

View File

@@ -2,12 +2,7 @@
<div id="a-edit"> <div id="a-edit">
<h1>Edit Favorite Group: <%= @favorite_group.pretty_name %></h1> <h1>Edit Favorite Group: <%= @favorite_group.pretty_name %></h1>
<%= edit_form_for(@favorite_group) do |f| %> <%= render "form" %>
<%= 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 %>
</div> </div>
</div> </div>

View File

@@ -2,12 +2,7 @@
<div id="a-new"> <div id="a-new">
<h1>New Favorite Group</h1> <h1>New Favorite Group</h1>
<%= edit_form_for(@favorite_group) do |f| %> <%= render "form" %>
<%= 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 %>
</div> </div>
</div> </div>

View File

@@ -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 %>

View File

@@ -4,19 +4,8 @@
$("#add-to-favorites, #add-fav-button, #remove-from-favorites, #remove-fav-button").toggle(); $("#add-to-favorites, #add-fav-button, #remove-from-favorites, #remove-fav-button").toggle();
$("#remove-fav-button").addClass("animate"); $("#remove-fav-button").addClass("animate");
$("span.post-votes[data-id=<%= @post.id %>]").replaceWith("<%= j render_post_votes @post, current_user: CurrentUser.user %>"); $("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"); $(".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] %>"); Danbooru.Utility.notice("<%= j flash[:notice] %>");
<% end %> <% end %>

View File

@@ -0,0 +1,3 @@
<% if @post.present? %>
<%= render_favorites_tooltip(@post, current_user: CurrentUser.user) %>
<% end %>

View File

@@ -0,0 +1,44 @@
<div id="c-favorites">
<div id="a-index">
<% if @post %>
<h1><%= link_to "Favorites", favorites_path %>/<%= link_to @post.dtext_shortlink, @post %></h1>
<% elsif @user %>
<h1><%= link_to "Favorites", favorites_path %>/<%= link_to_user @user %></h1>
<% else %>
<h1><%= link_to "Favorites", favorites_path %></h1>
<% 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}" }) %>
<div><%= time_ago_in_words_tagged(favorite.post.created_at) %></div>
<% 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 %>
<i>hidden</i>
<% end %>
<% end %>
<% end %>
<% end %>
<%= numbered_paginator(@favorites) %>
</div>
</div>

View File

@@ -101,6 +101,8 @@
<div id="tooltips"> <div id="tooltips">
<div id="post-tooltips"></div> <div id="post-tooltips"></div>
<div id="user-tooltips"></div> <div id="user-tooltips"></div>
<div id="post-votes-tooltips"></div>
<div id="post-favorites-tooltips"></div>
<div id="popup-menus"></div> <div id="popup-menus"></div>
</div> </div>

View File

@@ -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 %>

View File

@@ -0,0 +1,24 @@
<%= render "posts/partials/common/secondary_links" %>
<div id="c-post-votes">
<div id="a-index">
<%= 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) %>
</div>
</div>

View File

@@ -0,0 +1 @@
<%= render_post_votes_tooltip(@post, current_user: CurrentUser.user) %>

View File

@@ -1,12 +1,6 @@
<div id="c-post-votes"> <div id="c-post-votes">
<div id="a-index"> <div id="a-index">
<%= search_form_for(post_votes_path) do |f| %> <%= render "search" %>
<%= 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 %>
<%= table_for @post_votes, class: "striped autofit" do |t| %> <%= table_for @post_votes, class: "striped autofit" do |t| %>
<% t.column "Post" do |vote| %> <% t.column "Post" do |vote| %>
@@ -24,8 +18,12 @@
<div><%= time_ago_in_words_tagged(vote.post.created_at) %></div> <div><%= time_ago_in_words_tagged(vote.post.created_at) %></div>
<% end %> <% end %>
<% t.column "Voter" do |vote| %> <% t.column "Voter" do |vote| %>
<% if policy(vote).can_see_voter? %>
<%= link_to_user vote.user %> <%= link_to_user vote.user %>
<%= link_to "»", post_votes_path(search: { user_name: vote.user.name }) %> <%= link_to "»", post_votes_path(search: { user_name: vote.user.name }) %>
<% else %>
<i>hidden</i>
<% end %>
<div><%= time_ago_in_words_tagged(vote.created_at) %></div> <div><%= time_ago_in_words_tagged(vote.created_at) %></div>
<% end %> <% end %>
<% t.column column: "control" do |vote| %> <% t.column column: "control" do |vote| %>

View File

@@ -7,7 +7,7 @@
<section id="tag-box"> <section id="tag-box">
<h2>Tags</h2> <h2>Tags</h2>
<%= 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] }) %>
</section> </section>
<%= render "posts/partials/index/options" %> <%= render "posts/partials/index/options" %>
@@ -16,7 +16,7 @@
<% end %> <% end %>
<% content_for(:content) do %> <% content_for(:content) do %>
<menu id="post-sections" class="mb-4"> <menu id="post-sections" class="mb-4 space-x-2">
<li class="active"><a href="#" id="show-posts-link">Posts</a></li> <li class="active"><a href="#" id="show-posts-link">Posts</a></li>
<% if @post_set.artist.present? %> <% if @post_set.artist.present? %>
@@ -31,6 +31,18 @@
<li class="blank-wiki-excerpt-link"><%= link_to "Wiki", new_wiki_page_path(wiki_page: { title: @post_set.tag.name }), id: "show-excerpt-link" %></li> <li class="blank-wiki-excerpt-link"><%= link_to "Wiki", new_wiki_page_path(wiki_page: { title: @post_set.tag.name }), id: "show-excerpt-link" %></li>
<% end %> <% end %>
<li class="float-right">
<%= render PopupMenuComponent.new do |menu| %>
<% menu.item do %>
<% if params[:view] == "score" %>
<%= 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], page: params[:page], limit: params[:limit], view: "score"), rel: "nofollow" %>
<% end %>
<% end %>
<% end %>
</li>
<li id="searchbox-redirect-link" class="mobile-only"><a href="#search-box">Search &raquo;</a></li> <li id="searchbox-redirect-link" class="mobile-only"><a href="#search-box">Search &raquo;</a></li>
</menu> </menu>

View File

@@ -6,6 +6,9 @@
<% if params[:random] %> <% if params[:random] %>
<%= hidden_field_tag :random, params[:random] %> <%= hidden_field_tag :random, params[:random] %>
<% end %> <% 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") %> <%= text_field_tag("tags", tags, :id => tags_dom_id, :class => "flex-auto", :"data-shortcut" => "q", :"data-autocomplete" => "tag-query") %>
<button id="search-box-submit" type="submit"><%= search_icon %></button> <button id="search-box-submit" type="submit"><%= search_icon %></button>
<% end %> <% end %>

View File

@@ -1,9 +1,9 @@
<div id="posts" class="user-disable-cropped-<%= CurrentUser.user.disable_cropped_thumbnails? %>"> <div id="posts">
<div id="posts-container"> <div class="posts-container user-disable-cropped-<%= CurrentUser.user.disable_cropped_thumbnails? %>">
<% if post_set.shown_posts.empty? %> <% if post_set.shown_posts.empty? %>
<%= render "post_sets/blank" %> <%= render "post_sets/blank" %>
<% else %> <% 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 %> <% end %>
</div> </div>

View File

@@ -12,6 +12,8 @@
<% if params[:tags].blank? && @post_set.current_page == 1 %> <% if params[:tags].blank? && @post_set.current_page == 1 %>
<% canonical_url root_url(host: Danbooru.config.hostname) %> <% 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 %> <% end %>
<% noindex if @post_set.hide_from_crawler? %> <% noindex if @post_set.hide_from_crawler? %>

View File

@@ -1,2 +0,0 @@
<%# post %>
<%= safe_join(post.visible_favorited_users(CurrentUser.user).map { |user| link_to_user(user) }, ", ") %>

View File

@@ -23,14 +23,12 @@
<li id="post-info-score"> <li id="post-info-score">
Score: <%= render_post_votes post, current_user: CurrentUser.user %> Score: <%= render_post_votes post, current_user: CurrentUser.user %>
</li> </li>
<li id="post-info-favorites">Favorites: <span id="favcount-for-post-<%= post.id %>"><%= post.fav_count %></span> <li id="post-info-favorites">
<% if policy(post).can_view_favlist? %> Favorites:
<%= link_to "Show »", "#", id: "show-favlist-link", style: ("display: none;" if post.fav_count == 0) %> <%= tag.span class: "post-favcount", "data-id": post.id do %>
<%= link_to "« Hide", "#", id: "hide-favlist-link", style: "display: none;" %> <%= link_to post.fav_count, post_favorites_path(post) %>
<div id="favlist" style="display: none;" class="ml-4"> <% end %>
<%= render "posts/partials/show/favorite_list", post: post %> </li>
</div>
<% end %></li>
<li id="post-info-status"> <li id="post-info-status">
Status: Status:
<% if post.is_pending? %> <% if post.is_pending? %>

View File

@@ -40,7 +40,7 @@
<% end %> <% end %>
</div> </div>
<div class="post-tooltip-body <%= "has-preview" if params[:preview].truthy? %>"> <div class="post-tooltip-body thin-scrollbar <%= "has-preview" if params[:preview].truthy? %>">
<div class="post-tooltip-body-left"> <div class="post-tooltip-body-left">
<% if params[:preview].truthy? %> <% if params[:preview].truthy? %>
<%= post_preview(@post, show_deleted: true, compact: true) %> <%= post_preview(@post, show_deleted: true, compact: true) %>

View File

@@ -70,6 +70,12 @@
<td>2,000</td> <td>2,000</td>
<td>5,000</td> <td>5,000</td>
</tr> </tr>
<tr>
<td>Private Favorites</td>
<td>no</td>
<td>yes</td>
<td>yes</td>
</tr>
<tr> <tr>
<td>Favorite Groups</td> <td>Favorite Groups</td>
<td>3</td> <td>3</td>

View File

@@ -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 :hide_deleted_posts, :as => :select, :label => "Deleted post filter", :hint => "Remove deleted posts from search results", :include_blank => false, :collection => [["Yes", "true"], ["No", "false"]] %>
<%= f.input :show_deleted_children, :as => :select, :label => "Show deleted children", :hint => "Show thumbnail borders on parent posts even if the children are deleted", :include_blank => false, :collection => [["Yes", "true"], ["No", "false"]] %> <%= f.input :show_deleted_children, :as => :select, :label => "Show deleted children", :hint => "Show thumbnail borders on parent posts even if the children are deleted", :include_blank => false, :collection => [["Yes", "true"], ["No", "false"]] %>
<%= f.input :disable_categorized_saved_searches, :hint => "Don't show dialog box when creating a new saved search", :as => :select, :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false %> <%= f.input :disable_categorized_saved_searches, :hint => "Don't show dialog box when creating a new saved search", :as => :select, :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false %>
<%= f.input :enable_private_favorites, :as => :select, :hint => "Make your favorites private", :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false %> <% 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_tagged_filenames, :as => :select, :hint => "Don't include tags in image filenames", :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false %>
<%= f.input :disable_mobile_gestures, :as => :select, :hint => "Disable swipe left / swipe right gestures on mobile", :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false %> <%= f.input :disable_mobile_gestures, :as => :select, :hint => "Disable swipe left / swipe right gestures on mobile", :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false %>
<%= f.input :disable_post_tooltips, :as => :select, :hint => "Disable advanced tooltips when hovering over thumbnails", :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false %> <%= f.input :disable_post_tooltips, :as => :select, :hint => "Disable advanced tooltips when hovering over thumbnails", :collection => [["No", "false"], ["Yes", "true"]], :include_blank => false %>

View File

@@ -194,6 +194,7 @@ Rails.application.routes.draw do
# XXX Use `only: []` to avoid redefining post routes defined at top of file. # XXX Use `only: []` to avoid redefining post routes defined at top of file.
resources :posts, only: [] do resources :posts, only: [] do
resources :events, :only => [:index], :controller => "post_events" resources :events, :only => [:index], :controller => "post_events"
resources :favorites, only: [:index, :create, :destroy]
resources :replacements, :only => [:index, :new, :create], :controller => "post_replacements" resources :replacements, :only => [:index, :new, :create], :controller => "post_replacements"
resource :artist_commentary, only: [:show] do resource :artist_commentary, only: [:show] do
collection { put :create_or_update } collection { put :create_or_update }
@@ -252,6 +253,7 @@ Rails.application.routes.draw do
end end
end end
resources :users do resources :users do
resources :favorites, only: [:index, :create, :destroy]
resources :favorite_groups, controller: "favorite_groups", only: [:index], as: "favorite_groups" resources :favorite_groups, controller: "favorite_groups", only: [:index], as: "favorite_groups"
resource :email, only: [:show, :edit, :update] do resource :email, only: [:show, :edit, :update] do
get :verify get :verify

View File

@@ -7,7 +7,7 @@ class PostNavbarComponentTest < ViewComponent::TestCase
setup do setup do
@post = create(:post) @post = create(:post)
@user = create(:user) @user = create(:gold_user)
end end
context "The PostNavbarComponent" do context "The PostNavbarComponent" do
@@ -46,9 +46,8 @@ class PostNavbarComponentTest < ViewComponent::TestCase
context "for a post with favgroups" do context "for a post with favgroups" do
setup do setup do
as(@user) do as(@user) do
@favgroup1 = create(:favorite_group, creator: @user, is_public: true) @favgroup1 = create(:favorite_group, creator: @user, post_ids: [@post.id])
@favgroup2 = create(:favorite_group, creator: @user, is_public: false) @favgroup2 = create(:private_favorite_group, creator: @user, post_ids: [@post.id])
@post.update(tag_string: "favgroup:#{@favgroup1.id} favgroup:#{@favgroup2.id}")
end end
end end

View File

@@ -11,12 +11,12 @@ class PostVotesComponentTest < ViewComponent::TestCase
end end
context "for a user who can't vote" do 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) render_post_votes(@post, current_user: User.anonymous)
assert_css(".post-score") assert_css(".post-score")
assert_no_css(".post-upvote-link") assert_css(".post-upvote-link.inactive-link")
assert_no_css(".post-downvote-link") assert_css(".post-downvote-link.inactive-link")
end end
end end
@@ -34,8 +34,8 @@ class PostVotesComponentTest < ViewComponent::TestCase
context "for a downvoted post" do context "for a downvoted post" do
should "highlight the downvote button as active" do should "highlight the downvote button as active" do
@post.vote!(-1, @user) create(:post_vote, post: @post, user: @user, score: -1)
render_post_votes(@post, current_user: @user) as(@user) { render_post_votes(@post, current_user: @user) }
assert_css(".post-upvote-link.inactive-link") assert_css(".post-upvote-link.inactive-link")
assert_css(".post-downvote-link.active-link") assert_css(".post-downvote-link.active-link")
@@ -44,8 +44,8 @@ class PostVotesComponentTest < ViewComponent::TestCase
context "for an upvoted post" do context "for an upvoted post" do
should "highlight the upvote button as active" do should "highlight the upvote button as active" do
@post.vote!(1, @user) create(:post_vote, post: @post, user: @user, score: 1)
render_post_votes(@post, current_user: @user) as(@user) { render_post_votes(@post, current_user: @user) }
assert_css(".post-upvote-link.active-link") assert_css(".post-upvote-link.active-link")
assert_css(".post-downvote-link.inactive-link") assert_css(".post-downvote-link.inactive-link")

View File

@@ -2,5 +2,9 @@ FactoryBot.define do
factory(:favorite) do factory(:favorite) do
user user
post post
factory(:private_favorite) do
user factory: :gold_user, enable_private_favorites: true
end
end end
end end

View File

@@ -2,5 +2,10 @@ FactoryBot.define do
factory :favorite_group do factory :favorite_group do
creator creator
name { SecureRandom.uuid } name { SecureRandom.uuid }
factory :private_favorite_group do
creator factory: :gold_user
is_public { false }
end
end end
end end

View File

@@ -8,6 +8,7 @@ FactoryBot.define do
factory(:banned_user) do factory(:banned_user) do
transient { ban_duration {3} } transient { ban_duration {3} }
is_banned {true} is_banned {true}
active_ban factory: :ban
end end
factory(:restricted_user) do factory(:restricted_user) do

View File

@@ -10,7 +10,7 @@ class FavoriteGroupsControllerTest < ActionDispatch::IntegrationTest
context "index action" do context "index action" do
setup do setup do
@mod_favgroup = create(:favorite_group, name: "monochrome", creator: build(:moderator_user, name: "fumimi")) @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 end
should "render" do should "render" do
@@ -28,7 +28,7 @@ class FavoriteGroupsControllerTest < ActionDispatch::IntegrationTest
context "for private favorite groups as the creator" do context "for private favorite groups as the creator" do
setup do setup do
CurrentUser.user = @user CurrentUser.user = @private_favgroup.creator
end end
should respond_to_search(is_public: "false").with { @private_favgroup } should respond_to_search(is_public: "false").with { @private_favgroup }
@@ -42,13 +42,13 @@ class FavoriteGroupsControllerTest < ActionDispatch::IntegrationTest
end end
should "show private favgroups to the creator" do 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 get_auth favorite_group_path(@favgroup), @user
assert_response :success assert_response :success
end end
should "not show private favgroups to other users" do 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) get_auth favorite_group_path(@favgroup), create(:user)
assert_response 403 assert_response 403
end end
@@ -91,6 +91,13 @@ class FavoriteGroupsControllerTest < ActionDispatch::IntegrationTest
assert_response 403 assert_response 403
assert_not_equal("foo", @favgroup.reload.name) assert_not_equal("foo", @favgroup.reload.name)
end 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 end
context "destroy action" do context "destroy action" do

View File

@@ -10,25 +10,35 @@ class FavoritesControllerTest < ActionDispatch::IntegrationTest
end end
context "index action" do 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 should "render for json" do
get favorites_path, as: :json get favorites_path, as: :json
assert_response :success assert_response :success
end 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]=<name>" do
get favorites_path(search: { user_name: @user.name })
assert_response :success
end
end end
context "create action" do context "create action" do
@@ -48,12 +58,21 @@ class FavoritesControllerTest < ActionDispatch::IntegrationTest
end end
end end
should "allow banned users to create favorites" do should "not allow banned users to create favorites" do
@banned_user = create(:banned_user) @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 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
end end

View File

@@ -43,8 +43,8 @@ module Moderator
@parent.reload @parent.reload
@child.reload @child.reload
as(@admin) do as(@admin) do
assert_equal(users.map(&:id).sort, @parent.favorited_users.map(&:id).sort) assert_equal(users.map(&:id).sort, @parent.favorites.map(&:user_id).sort)
assert_equal([], @child.favorited_users.map(&:id)) assert_equal([], @child.favorites.map(&:user_id))
end end
end end
end end

View File

@@ -9,59 +9,167 @@ class PostVotesControllerTest < ActionDispatch::IntegrationTest
context "index action" do context "index action" do
setup do setup do
@admin = create(:admin_user) @user = create(:user, enable_private_favorites: true)
as(@user) { @post_vote = create(:post_vote, post: @post, user: @user) } @upvote = create(:post_vote, user: @user, score: 1)
as(@admin) { @admin_vote = create(:post_vote, post: @post, user: @admin) } @downvote = create(:post_vote, user: @user, score: -1)
@unrelated_vote = create(:post_vote)
end end
should "render" do should "render" do
get_auth post_votes_path, @user get post_votes_path
assert_response :success
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 assert_response :success
end end
context "as a user" do context "as a user" do
setup do should "show the user all their own votes" do
CurrentUser.user = @user get_auth post_votes_path, @user
assert_response :success
assert_select "tbody tr", 2
get_auth post_votes_path(search: { user_id: @user.id }), @user
assert_response :success
assert_select "tbody tr", 2
get_auth post_votes_path(search: { user_name: @user.name }), @user
assert_response :success
assert_select "tbody tr", 2
end end
should respond_to_search({}).with { @post_vote } should "not show private upvotes to other users" do
get_auth post_votes_path, create(:user)
assert_response :success
assert_select "tbody tr", 0
get_auth post_votes_path(search: { user_id: @user.id }), create(:user)
assert_response :success
assert_select "tbody tr", 0
get_auth post_votes_path(search: { user_name: @user.name }), create(:user)
assert_response :success
assert_select "tbody tr", 0
end end
context "as a moderator" do should "not show downvotes to other users" do
setup do @user.update!(enable_private_favorites: false)
CurrentUser.user = @admin
get_auth post_votes_path, create(:user)
assert_response :success
assert_select "tbody tr[data-score=1]", 1
assert_select "tbody tr[data-score=-1]", 0
get_auth post_votes_path(search: { user_id: @user.id }), create(:user)
assert_response :success
assert_select "tbody tr[data-score=1]", 1
assert_select "tbody tr[data-score=-1]", 0
get_auth post_votes_path(search: { user_name: @user.name }), create(:user)
assert_response :success
assert_select "tbody tr[data-score=1]", 1
assert_select "tbody tr[data-score=-1]", 0
end
end end
should respond_to_search({}).with { [@unrelated_vote, @admin_vote, @post_vote] } context "as an admin" do
should respond_to_search(score: 1).with { [@unrelated_vote, @admin_vote, @post_vote].select{ |v| v.score == 1 } } should "show all votes by other users" do
@admin = create(:admin_user)
context "using includes" do get_auth post_votes_path, @admin
should respond_to_search(post_tags_match: "dragon").with { [@admin_vote, @post_vote] } assert_response :success
should respond_to_search(user_name: "meiling").with { @post_vote } assert_select "tbody tr", 2
should respond_to_search(user: {level: User::Levels::ADMIN}).with { @admin_vote }
get_auth post_votes_path(search: { user_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 end
end end
context "show action" do context "show action" do
context "for a public upvote" do
setup do setup do
@post_vote = create(:post_vote, post: @post, user: @user) @user = create(:user, enable_private_favorites: false)
@post_vote = create(:post_vote, user: @user, score: 1)
end end
should "show the vote to the voter" do 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
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 get_auth post_vote_path(@post_vote), @user, as: :json
assert_response :success assert_response :success
assert_equal(@user.id, response.parsed_body["user_id"])
end end
should "show the vote to admins" do should "show the voter to admins" do
get_auth post_vote_path(@post_vote), create(:admin_user), as: :json get_auth post_vote_path(@post_vote), create(:admin_user), as: :json
assert_response :success assert_response :success
assert_equal(@user.id, response.parsed_body["user_id"])
end end
should "not show the vote to other users" do should "not show the voter to other users" do
get_auth post_vote_path(@post_vote), create(:user), as: :json get post_vote_path(@post_vote), as: :json
assert_response 403 assert_response 403
assert_nil(response.parsed_body["user_id"])
end
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 "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
end end
@@ -87,13 +195,20 @@ class PostVotesControllerTest < ActionDispatch::IntegrationTest
assert_equal(0, @post.reload.score) assert_equal(0, @post.reload.score)
end end
should "not allow members to vote" do should "not allow restricted users to vote" do
post_auth post_post_votes_path(post_id: @post.id), create(:user), params: { score: 1, format: "js" } post_auth post_post_votes_path(post_id: @post.id), create(:restricted_user), params: { score: 1, format: "js"}
assert_response 403 assert_response 403
assert_equal(0, @post.reload.score) assert_equal(0, @post.reload.score)
end 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 should "not allow invalid scores" do
post_auth post_post_votes_path(post_id: @post.id), @user, params: { score: 3, format: "js" } post_auth post_post_votes_path(post_id: @post.id), @user, params: { score: 3, format: "js" }

View File

@@ -420,6 +420,34 @@ class UsersControllerTest < ActionDispatch::IntegrationTest
assert_equal("xyz", @user.favorite_tags) assert_equal("xyz", @user.favorite_tags)
end 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 context "changing the level" do
should "not work" do should "not work" do
@owner = create(:owner_user) @owner = create(:owner_user)

View File

@@ -11,10 +11,13 @@ class FavoriteTest < ActiveSupport::TestCase
context "Favorites: " do context "Favorites: " do
context "removing a favorite" do context "removing a favorite" do
should "update the post and user favorite counts" do should "update the post and user favorite counts" do
@user1 = create(:restricted_user)
fav = Favorite.create!(post: @p1, user: @user1) fav = Favorite.create!(post: @p1, user: @user1)
assert_equal(1, @user1.reload.favorite_count) assert_equal(1, @user1.reload.favorite_count)
assert_equal(1, @p1.reload.fav_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) Favorite.destroy_by(post: @p1, user: @user1)
@@ -42,14 +45,8 @@ class FavoriteTest < ActiveSupport::TestCase
end end
context "adding a favorite" do 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 should "not upvote the post if the user can't vote" do
@user1 = create(:restricted_user)
Favorite.create!(post: @p1, user: @user1) Favorite.create!(post: @p1, user: @user1)
assert_equal(1, @user1.reload.favorite_count) 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(["You have already favorited this post"], @f2.errors.full_messages)
assert_equal(1, @user1.reload.favorite_count) assert_equal(1, @user1.reload.favorite_count)
assert_equal(1, @p1.reload.fav_count) assert_equal(1, @p1.reload.fav_count)
assert_equal(0, @p1.reload.score) assert_equal(1, @p1.reload.score)
end end
end end
end end

View File

@@ -390,7 +390,7 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
favgroup1 = create(:favorite_group, creator: CurrentUser.user, post_ids: [post1.id]) favgroup1 = create(:favorite_group, creator: CurrentUser.user, post_ids: [post1.id])
favgroup2 = create(:favorite_group, creator: CurrentUser.user, post_ids: [post2.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([post1], "favgroup:#{favgroup1.id}")
assert_tag_match([post2], "favgroup:#{favgroup2.name}") assert_tag_match([post2], "favgroup:#{favgroup2.name}")
@@ -421,7 +421,7 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
post3 = create(:post) post3 = create(:post)
favgroup1 = create(:favorite_group, creator: CurrentUser.user, post_ids: [post1.id, post2.id]) 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.id}")
assert_tag_match([post1, post2], "ordfavgroup:#{favgroup1.name}") assert_tag_match([post1, post2], "ordfavgroup:#{favgroup1.name}")
@@ -973,6 +973,75 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
assert_tag_match(all - [e], "-rating:e") assert_tag_match(all - [e], "-rating:e")
end end
context "for the upvote:<user> metatag" do
setup do
@user = create(:gold_user)
@upvote = create(:post_vote, user: @user, score: 1)
@downvote = create(:post_vote, user: @user, score: -1)
end
should "show public upvotes to all users" do
as(User.anonymous) do
assert_tag_match([@upvote.post], "upvote:#{@user.name}")
assert_tag_match([@downvote.post], "-upvote:#{@user.name}")
end
end
should "not show private upvotes to other users" do
@user.update!(enable_private_favorites: true)
as(User.anonymous) do
assert_tag_match([], "upvote:#{@user.name}")
assert_tag_match([@downvote.post, @upvote.post], "-upvote:#{@user.name}")
end
end
should "show private upvotes to admins" do
@user.update!(enable_private_favorites: true)
as(create(:admin_user)) do
assert_tag_match([@upvote.post], "upvote:#{@user.name}")
assert_tag_match([@downvote.post], "-upvote:#{@user.name}")
end
end
should "show private upvotes to the voter themselves" do
as(@user) do
assert_tag_match([@upvote.post], "upvote:#{@user.name}")
assert_tag_match([@downvote.post], "-upvote:#{@user.name}")
end
end
end
context "for the downvote:<user> metatag" do
setup do
@user = create(:user, enable_private_favorites: true)
@upvote = create(:post_vote, user: @user, score: 1)
@downvote = create(:post_vote, user: @user, score: -1)
end
should "not show downvotes to other users" do
as(User.anonymous) do
assert_tag_match([], "downvote:#{@user.name}")
assert_tag_match([@downvote.post, @upvote.post], "-downvote:#{@user.name}")
end
end
should "show downvotes to admins" do
as(create(:admin_user)) do
assert_tag_match([@downvote.post], "downvote:#{@user.name}")
assert_tag_match([@upvote.post], "-downvote:#{@user.name}")
end
end
should "show downvotes to the voter themselves" do
as(@user) do
assert_tag_match([@downvote.post], "downvote:#{@user.name}")
assert_tag_match([@upvote.post], "-downvote:#{@user.name}")
end
end
end
should "return posts for a upvote:<user>, downvote:<user> metatag" do should "return posts for a upvote:<user>, downvote:<user> metatag" do
CurrentUser.scoped(create(:mod_user)) do CurrentUser.scoped(create(:mod_user)) do
upvoted = create(:post, tag_string: "upvote:self") upvoted = create(:post, tag_string: "upvote:self")
@@ -1342,8 +1411,7 @@ class PostQueryBuilderTest < ActiveSupport::TestCase
end end
should "return the correct favorite count for a fav:<name> search for a user with private favorites" do should "return the correct favorite count for a fav:<name> search for a user with private favorites" do
fav = create(:favorite) fav = create(:private_favorite)
fav.user.update!(favorite_count: 1, enable_private_favorites: true)
assert_fast_count(0, "fav:#{fav.user.name}") assert_fast_count(0, "fav:#{fav.user.name}")
assert_fast_count(0, "ordfav:#{fav.user.name}") assert_fast_count(0, "ordfav:#{fav.user.name}")

View File

@@ -699,10 +699,34 @@ class PostTest < ActiveSupport::TestCase
context "for a fav" do context "for a fav" do
should "add/remove the current user to the post's favorite listing" do should "add/remove the current user to the post's favorite listing" do
@post.update(tag_string: "aaa fav:self") @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.favorites.where(user: @user).count)
assert_equal(1, @post.votes.positive.where(user: @user).count)
@post.update(tag_string: "aaa -fav:self") @post.update(tag_string: "aaa -fav:self")
assert_equal(0, @post.reload.score)
assert_equal(0, @post.favorites.count) 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 end
should "not fail when the fav: metatag is used twice" do 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 "upvote:self or downvote:self" do
context "by a member" do context "by a member" do
should "not upvote the post" do should "upvote the post" do
assert_no_difference("PostVote.count") do assert_difference("PostVote.count") do
@post.update(tag_string: "upvote:self") @post.update(tag_string: "upvote:self")
end end
assert_equal(0, @post.reload.score) assert_equal(1, @post.reload.score)
end end
should "not downvote the post" do should "downvote the post" do
assert_no_difference("PostVote.count") do assert_difference("PostVote.count") do
@post.update(tag_string: "downvote:self") @post.update(tag_string: "downvote:self")
end end
assert_equal(0, @post.reload.score) assert_equal(-1, @post.reload.score)
end end
end end
@@ -1476,7 +1500,8 @@ class PostTest < ActiveSupport::TestCase
should "create a vote for each user who can vote" do should "create a vote for each user who can vote" do
assert(@parent.votes.where(user: @gold1).exists?) 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 end
end end
@@ -1523,13 +1548,13 @@ class PostTest < ActiveSupport::TestCase
end end
context "Voting:" do context "Voting:" do
should "not allow members to vote" do should "allow members to vote" do
user = create(:user) user = create(:user)
post = create(:post) post = create(:post)
assert_nothing_raised { post.vote!(1, user) } assert_nothing_raised { post.vote!(1, user) }
assert_equal(0, post.votes.count) assert_equal(1, post.votes.count)
assert_equal(0, post.reload.score) assert_equal(1, post.reload.score)
end end
should "not allow duplicate votes" do should "not allow duplicate votes" do