Merge branch 'master' into attribute-searching
This commit is contained in:
46
Gemfile.lock
46
Gemfile.lock
@@ -77,8 +77,8 @@ GEM
|
||||
ansi (1.5.0)
|
||||
ast (2.4.1)
|
||||
aws-eventstream (1.1.0)
|
||||
aws-partitions (1.341.0)
|
||||
aws-sdk-core (3.103.0)
|
||||
aws-partitions (1.356.0)
|
||||
aws-sdk-core (3.104.3)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
aws-partitions (~> 1, >= 1.239.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
@@ -86,10 +86,10 @@ GEM
|
||||
aws-sdk-sqs (1.30.0)
|
||||
aws-sdk-core (~> 3, >= 3.99.0)
|
||||
aws-sigv4 (~> 1.1)
|
||||
aws-sigv4 (1.2.1)
|
||||
aws-sigv4 (1.2.2)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
bcrypt (3.1.13)
|
||||
bootsnap (1.4.6)
|
||||
bcrypt (3.1.15)
|
||||
bootsnap (1.4.8)
|
||||
msgpack (~> 1.0)
|
||||
builder (3.2.4)
|
||||
byebug (11.1.3)
|
||||
@@ -98,7 +98,7 @@ GEM
|
||||
i18n
|
||||
rake (>= 10.0.0)
|
||||
sshkit (>= 1.9.0)
|
||||
capistrano-bundler (2.0.0)
|
||||
capistrano-bundler (2.0.1)
|
||||
capistrano (~> 3.1)
|
||||
capistrano-deploytags (1.0.7)
|
||||
capistrano (>= 3.7.0)
|
||||
@@ -120,13 +120,13 @@ GEM
|
||||
xpath (~> 3.2)
|
||||
childprocess (3.0.0)
|
||||
chronic (0.10.2)
|
||||
codecov (0.2.0)
|
||||
codecov (0.2.5)
|
||||
colorize
|
||||
json
|
||||
simplecov
|
||||
coderay (1.1.3)
|
||||
colorize (0.8.1)
|
||||
concurrent-ruby (1.1.6)
|
||||
concurrent-ruby (1.1.7)
|
||||
crass (1.0.6)
|
||||
daemons (1.3.1)
|
||||
delayed_job (4.1.8)
|
||||
@@ -147,7 +147,7 @@ GEM
|
||||
activesupport (>= 5.0.0)
|
||||
faraday (1.0.1)
|
||||
multipart-post (>= 1.2, < 3)
|
||||
ffaker (2.15.0)
|
||||
ffaker (2.16.0)
|
||||
ffi (1.13.1)
|
||||
ffi-compiler (1.0.1)
|
||||
ffi (>= 1.0.0)
|
||||
@@ -167,7 +167,7 @@ GEM
|
||||
http-form_data (2.3.0)
|
||||
http-parser (1.2.1)
|
||||
ffi-compiler (>= 1.0, < 2.0)
|
||||
i18n (1.8.3)
|
||||
i18n (1.8.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
ipaddress_2 (0.13.0)
|
||||
jmespath (1.4.0)
|
||||
@@ -212,7 +212,7 @@ GEM
|
||||
net-sftp (3.0.0)
|
||||
net-ssh (>= 5.0.0, < 7.0.0)
|
||||
net-ssh (6.1.0)
|
||||
newrelic_rpm (6.11.0.365)
|
||||
newrelic_rpm (6.12.0.367)
|
||||
nio4r (2.5.2)
|
||||
nokogiri (1.10.10)
|
||||
mini_portile2 (~> 2.4.0)
|
||||
@@ -244,7 +244,7 @@ GEM
|
||||
rack (2.2.3)
|
||||
rack-contrib (2.2.0)
|
||||
rack (~> 2.0)
|
||||
rack-mini-profiler (2.0.2)
|
||||
rack-mini-profiler (2.0.4)
|
||||
rack (>= 1.2.0)
|
||||
rack-proxy (0.6.5)
|
||||
rack
|
||||
@@ -293,21 +293,21 @@ GEM
|
||||
actionpack (>= 5.0)
|
||||
railties (>= 5.0)
|
||||
rexml (3.2.4)
|
||||
rubocop (0.88.0)
|
||||
rubocop (0.89.1)
|
||||
parallel (~> 1.10)
|
||||
parser (>= 2.7.1.1)
|
||||
rainbow (>= 2.2.2, < 4.0)
|
||||
regexp_parser (>= 1.7)
|
||||
rexml
|
||||
rubocop-ast (>= 0.1.0, < 1.0)
|
||||
rubocop-ast (>= 0.3.0, < 1.0)
|
||||
ruby-progressbar (~> 1.7)
|
||||
unicode-display_width (>= 1.4.0, < 2.0)
|
||||
rubocop-ast (0.1.0)
|
||||
parser (>= 2.7.0.1)
|
||||
rubocop-rails (2.6.0)
|
||||
rubocop-ast (0.3.0)
|
||||
parser (>= 2.7.1.4)
|
||||
rubocop-rails (2.7.1)
|
||||
activesupport (>= 4.2.0)
|
||||
rack (>= 1.1)
|
||||
rubocop (>= 0.82.0)
|
||||
rubocop (>= 0.87.0)
|
||||
ruby-progressbar (1.10.1)
|
||||
ruby-vips (2.0.17)
|
||||
ffi (~> 1.9)
|
||||
@@ -329,7 +329,7 @@ GEM
|
||||
simple_form (5.0.2)
|
||||
actionpack (>= 5.0)
|
||||
activemodel (>= 5.0)
|
||||
simplecov (0.18.5)
|
||||
simplecov (0.19.0)
|
||||
docile (~> 1.1)
|
||||
simplecov-html (~> 0.11)
|
||||
simplecov-html (0.12.2)
|
||||
@@ -346,7 +346,7 @@ GEM
|
||||
stackprof (0.2.15)
|
||||
streamio-ffmpeg (3.0.2)
|
||||
multi_json (~> 1.8)
|
||||
stripe (5.22.0)
|
||||
stripe (5.23.1)
|
||||
thor (1.0.1)
|
||||
thread_safe (0.3.6)
|
||||
tzinfo (1.2.7)
|
||||
@@ -355,13 +355,13 @@ GEM
|
||||
unf_ext
|
||||
unf_ext (0.0.7.7)
|
||||
unicode-display_width (1.7.0)
|
||||
unicorn (5.5.5)
|
||||
unicorn (5.6.0)
|
||||
kgio (~> 2.6)
|
||||
raindrops (~> 0.7)
|
||||
unicorn-worker-killer (0.4.4)
|
||||
get_process_mem (~> 0)
|
||||
unicorn (>= 4, < 6)
|
||||
webpacker (5.1.1)
|
||||
webpacker (5.2.0)
|
||||
activesupport (>= 5.2)
|
||||
rack-proxy (>= 0.6.1)
|
||||
railties (>= 5.2)
|
||||
@@ -373,7 +373,7 @@ GEM
|
||||
chronic (>= 0.6.3)
|
||||
xpath (3.2.0)
|
||||
nokogiri (~> 1.8)
|
||||
zeitwerk (2.3.1)
|
||||
zeitwerk (2.4.0)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
@@ -21,7 +21,7 @@ class EmailsController < ApplicationController
|
||||
end
|
||||
|
||||
if @user.errors.none?
|
||||
flash[:notice] = "Email updated"
|
||||
flash[:notice] = "Email updated. Check your email to confirm your new address"
|
||||
UserMailer.email_change_confirmation(@user).deliver_later
|
||||
respond_with(@user, location: settings_url)
|
||||
else
|
||||
@@ -31,10 +31,27 @@ class EmailsController < ApplicationController
|
||||
end
|
||||
|
||||
def verify
|
||||
@email_address = authorize EmailAddress.find_by_user_id!(params[:user_id])
|
||||
@email_address.update!(is_verified: true)
|
||||
@user = User.find(params[:user_id])
|
||||
@email_address = @user.email_address
|
||||
|
||||
flash[:notice] = "Email address verified"
|
||||
redirect_to @email_address.user
|
||||
if @email_address.blank?
|
||||
redirect_to edit_user_email_path(@user)
|
||||
elsif params[:email_verification_key].present?
|
||||
authorize @email_address
|
||||
@email_address.update!(is_verified: true)
|
||||
flash[:notice] = "Email address verified"
|
||||
redirect_to @email_address.user
|
||||
else
|
||||
authorize @email_address
|
||||
respond_with(@user)
|
||||
end
|
||||
end
|
||||
|
||||
def send_confirmation
|
||||
@user = authorize User.find(params[:user_id]), policy_class: EmailAddressPolicy
|
||||
UserMailer.welcome_user(@user).deliver_later
|
||||
|
||||
flash[:notice] = "Confirmation email sent to #{@user.email_address.address}. Check your email to confirm your address"
|
||||
redirect_to @user
|
||||
end
|
||||
end
|
||||
|
||||
@@ -4,18 +4,6 @@ module Moderator
|
||||
skip_before_action :api_check
|
||||
respond_to :html, :json, :xml, :js
|
||||
|
||||
def confirm_delete
|
||||
@post = ::Post.find(params[:id])
|
||||
end
|
||||
|
||||
def delete
|
||||
@post = authorize ::Post.find(params[:id])
|
||||
if params[:commit] == "Delete"
|
||||
@post.delete!(params[:reason], :move_favorites => params[:move_favorites].present?)
|
||||
end
|
||||
redirect_to(post_path(@post))
|
||||
end
|
||||
|
||||
def confirm_move_favorites
|
||||
@post = ::Post.find(params[:id])
|
||||
end
|
||||
@@ -44,7 +32,7 @@ module Moderator
|
||||
def unban
|
||||
@post = authorize ::Post.find(params[:id])
|
||||
@post.unban!
|
||||
flash[:notice] = "Post was banned"
|
||||
flash[:notice] = "Post was unbanned"
|
||||
|
||||
respond_with(@post)
|
||||
end
|
||||
|
||||
@@ -4,14 +4,15 @@ class ModqueueController < ApplicationController
|
||||
|
||||
def index
|
||||
authorize :modqueue
|
||||
@posts = Post.includes(:appeals, :disapprovals, :uploader, flags: [:creator]).pending_or_flagged.available_for_moderation(CurrentUser.user, hidden: search_params[:hidden])
|
||||
@posts = @posts.paginated_search(params, order: "modqueue", count_pages: true)
|
||||
@posts = Post.includes(:appeals, :disapprovals, :uploader, flags: [:creator]).in_modqueue.available_for_moderation(CurrentUser.user, hidden: search_params[:hidden])
|
||||
@modqueue_posts = @posts.reselect(nil).reorder(nil).offset(nil).limit(nil)
|
||||
@posts = @posts.paginated_search(params, order: "modqueue", count_pages: true, count: @modqueue_posts.to_a.size)
|
||||
|
||||
@modqueue_posts = @posts.except(:offset, :limit, :order)
|
||||
@pending_post_count = @modqueue_posts.pending.count
|
||||
@flagged_post_count = @modqueue_posts.flagged.count
|
||||
@disapproval_reasons = PostDisapproval.where(post: @modqueue_posts).where.not(reason: "disinterest").group(:reason).order(count: :desc).distinct.count(:post_id)
|
||||
@uploaders = @modqueue_posts.group(:uploader).order(count: :desc).limit(20).count
|
||||
@pending_post_count = @modqueue_posts.select(&:is_pending?).count
|
||||
@flagged_post_count = @modqueue_posts.select(&:is_flagged?).count
|
||||
@appealed_post_count = @modqueue_posts.select(&:is_appealed?).count
|
||||
@disapproval_reasons = PostDisapproval.where(post_id: @modqueue_posts.map(&:id)).where.not(reason: "disinterest").group(:reason).order(count: :desc).distinct.count(:post_id)
|
||||
@uploaders = @modqueue_posts.map(&:uploader).tally.sort_by(&:last).reverse.take(20).to_h
|
||||
|
||||
@tags = RelatedTagCalculator.frequent_tags_for_post_relation(@modqueue_posts)
|
||||
@artist_tags = @tags.select(&:artist?).sort_by(&:overlap_count).reverse.take(10)
|
||||
|
||||
@@ -56,6 +56,18 @@ class PostsController < ApplicationController
|
||||
respond_with_post_after_update(@post)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@post = authorize Post.find(params[:id])
|
||||
|
||||
if params[:commit] == "Delete"
|
||||
move_favorites = params.dig(:post, :move_favorites).to_s.truthy?
|
||||
@post.delete!(params.dig(:post, :reason), move_favorites: move_favorites, user: CurrentUser.user)
|
||||
flash[:notice] = "Post deleted"
|
||||
end
|
||||
|
||||
respond_with_post_after_update(@post)
|
||||
end
|
||||
|
||||
def revert
|
||||
@post = authorize Post.find(params[:id])
|
||||
@version = @post.versions.find(params[:version_id])
|
||||
|
||||
@@ -16,14 +16,10 @@ require("jquery-ui/ui/effects/effect-shake");
|
||||
require("jquery-ui/ui/widgets/autocomplete");
|
||||
require("jquery-ui/ui/widgets/button");
|
||||
require("jquery-ui/ui/widgets/dialog");
|
||||
require("jquery-ui/ui/widgets/draggable");
|
||||
require("jquery-ui/ui/widgets/resizable");
|
||||
require("jquery-ui/themes/base/core.css");
|
||||
require("jquery-ui/themes/base/autocomplete.css");
|
||||
require("jquery-ui/themes/base/button.css");
|
||||
require("jquery-ui/themes/base/dialog.css");
|
||||
require("jquery-ui/themes/base/draggable.css");
|
||||
require("jquery-ui/themes/base/resizable.css");
|
||||
require("jquery-ui/themes/base/theme.css");
|
||||
|
||||
require("@fortawesome/fontawesome-free/css/fontawesome.css");
|
||||
@@ -47,6 +43,7 @@ export { default as PostTooltip } from '../src/javascripts/post_tooltips.js';
|
||||
export { default as PostVersion } from '../src/javascripts/post_version.js';
|
||||
export { default as RelatedTag } from '../src/javascripts/related_tag.js';
|
||||
export { default as Shortcuts } from '../src/javascripts/shortcuts.js';
|
||||
export { default as TagCounter } from '../src/javascripts/tag_counter.js';
|
||||
export { default as Upload } from '../src/javascripts/uploads.js.erb';
|
||||
export { default as UserTooltip } from '../src/javascripts/user_tooltips.js';
|
||||
export { default as Utility } from '../src/javascripts/utility.js';
|
||||
|
||||
@@ -9,7 +9,7 @@ Autocomplete.ORDER_METATAGS = <%= PostQueryBuilder::ORDER_METATAGS.to_json.html_
|
||||
Autocomplete.DISAPPROVAL_REASONS = <%= PostDisapproval::REASONS.to_json.html_safe %>;
|
||||
/* eslint-enable */
|
||||
|
||||
Autocomplete.MISC_STATUSES = ["deleted", "active", "pending", "flagged", "banned", "modqueue", "unmoderated"];
|
||||
Autocomplete.MISC_STATUSES = ["deleted", "active", "pending", "flagged", "banned", "modqueue", "unmoderated", "appealed"];
|
||||
Autocomplete.TAG_PREFIXES = "-|~|" + Object.keys(Autocomplete.TAG_CATEGORIES).map(category => category + ":").join("|");
|
||||
Autocomplete.METATAGS_REGEX = Autocomplete.METATAGS.concat(Object.keys(Autocomplete.TAG_CATEGORIES)).join("|");
|
||||
Autocomplete.TERM_REGEX = new RegExp(`([-~]*)(?:(${Autocomplete.METATAGS_REGEX}):)?(\\S*)$`, "i");
|
||||
|
||||
@@ -15,6 +15,12 @@ $(function() {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$("#hide-verify-account-notice").on("click.danbooru", function(e) {
|
||||
$("#verify-account-notice").hide();
|
||||
Cookie.put('hide_verify_account_notice', '1', 3);
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
$("#close-notice-link").on("click.danbooru", function(e) {
|
||||
$('#notice').fadeOut("fast");
|
||||
e.preventDefault();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@ PostTooltip.initialize = function () {
|
||||
|
||||
delegate("body", {
|
||||
allowHTML: true,
|
||||
appendTo: document.body,
|
||||
appendTo: document.querySelector("#post-tooltips"),
|
||||
delay: [PostTooltip.SHOW_DELAY, PostTooltip.HIDE_DELAY],
|
||||
duration: PostTooltip.DURATION,
|
||||
interactive: true,
|
||||
|
||||
@@ -14,6 +14,7 @@ Post.SWIPE_VELOCITY = 0.6;
|
||||
Post.MAX_RECOMMENDATIONS = 45; // 3 rows of 9 posts at 1920x1080.
|
||||
Post.LOW_TAG_COUNT = 10;
|
||||
Post.HIGH_TAG_COUNT = 20;
|
||||
Post.EDIT_DIALOG_WIDTH = 720;
|
||||
|
||||
Post.initialize_all = function() {
|
||||
|
||||
@@ -42,8 +43,6 @@ Post.initialize_all = function() {
|
||||
}
|
||||
|
||||
var $fields_multiple = $('[data-autocomplete="tag-edit"]');
|
||||
$fields_multiple.on("keypress.danbooru", Post.update_tag_count);
|
||||
$fields_multiple.on("click", Post.update_tag_count);
|
||||
|
||||
$(window).on('danbooru:initialize_saved_seraches', () => {
|
||||
Post.initialize_saved_searches();
|
||||
@@ -105,14 +104,13 @@ Post.open_edit_dialog = function() {
|
||||
$("#post-edit-link").parent("li").addClass("active");
|
||||
|
||||
var $tag_string = $("#post_tag_string,#upload_tag_string");
|
||||
$("div.input").has($tag_string).prevAll().hide();
|
||||
$("#open-edit-dialog").hide();
|
||||
|
||||
var dialog = $("<div/>").attr("id", "edit-dialog");
|
||||
$("#form").appendTo(dialog);
|
||||
dialog.dialog({
|
||||
title: "Edit tags",
|
||||
width: $(window).width() * 0.6,
|
||||
width: Post.EDIT_DIALOG_WIDTH,
|
||||
position: {
|
||||
my: "right",
|
||||
at: "right-20",
|
||||
@@ -307,10 +305,11 @@ Post.view_original = function(e = null) {
|
||||
}
|
||||
|
||||
var $image = $("#image");
|
||||
var $post = $(".image-container");
|
||||
$image.attr("src", $(".image-view-original-link").attr("href"));
|
||||
$image.css("filter", "blur(8px)");
|
||||
$image.width($image.data("original-width"));
|
||||
$image.height($image.data("original-height"));
|
||||
$image.width($post.data("width"));
|
||||
$image.height($post.data("height"));
|
||||
$image.on("load.danbooru", function() {
|
||||
$image.css("animation", "sharpen 0.5s forwards");
|
||||
});
|
||||
@@ -326,10 +325,11 @@ Post.view_large = function(e = null) {
|
||||
}
|
||||
|
||||
var $image = $("#image");
|
||||
var $post = $(".image-container");
|
||||
$image.attr("src", $(".image-view-large-link").attr("href"));
|
||||
$image.css("filter", "blur(8px)");
|
||||
$image.width($image.data("large-width"));
|
||||
$image.height($image.data("large-height"));
|
||||
$image.width($post.data("large-width"));
|
||||
$image.height($post.data("large-height"));
|
||||
$image.on("load.danbooru", function() {
|
||||
$image.css("animation", "sharpen 0.5s forwards");
|
||||
});
|
||||
@@ -400,7 +400,6 @@ Post.initialize_post_sections = function() {
|
||||
$("#post_tag_string").focus().selectEnd().height($("#post_tag_string")[0].scrollHeight);
|
||||
$("#recommended").hide();
|
||||
$(document).trigger("danbooru:open-post-edit-tab");
|
||||
Post.update_tag_count({target: $("#post_tag_string")});
|
||||
} else if (e.target.hash === "#recommended") {
|
||||
$("#comments").hide();
|
||||
$("#edit").hide();
|
||||
@@ -511,32 +510,6 @@ Post.initialize_recommended = function() {
|
||||
});
|
||||
};
|
||||
|
||||
Post.update_tag_count = function(event) {
|
||||
let string = "0 tags";
|
||||
let count = 0;
|
||||
|
||||
if (event) {
|
||||
let tags = Utility.regexp_split($(event.target).val());
|
||||
if (tags.length) {
|
||||
count = tags.length;
|
||||
string = (count === 1) ? (count + " tag") : (count + " tags")
|
||||
}
|
||||
}
|
||||
|
||||
$("#tags-container .count").html(string);
|
||||
let klass = "";
|
||||
|
||||
if (count < Post.LOW_TAG_COUNT) {
|
||||
klass = "frown";
|
||||
} else if (count >= Post.LOW_TAG_COUNT && count < Post.HIGH_TAG_COUNT) {
|
||||
klass = "meh";
|
||||
} else {
|
||||
klass = "smile";
|
||||
}
|
||||
|
||||
$("#tags-container .options #face").removeClass().addClass(`far fa-${klass}`);
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
Post.initialize_all();
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import Uploads from './uploads.js.erb';
|
||||
import Utility from './utility';
|
||||
import Post from './posts.js.erb';
|
||||
|
||||
let RelatedTag = {};
|
||||
|
||||
@@ -121,7 +120,8 @@ RelatedTag.toggle_tag = function(e) {
|
||||
setTimeout(function () { $field.prop('selectionStart', $field.val().length);}, 100);
|
||||
e.preventDefault();
|
||||
|
||||
Post.update_tag_count({ target: $field });
|
||||
// Artificially trigger input event so the tag counter updates.
|
||||
$field.trigger("input");
|
||||
}
|
||||
|
||||
RelatedTag.show = function(e) {
|
||||
|
||||
49
app/javascript/src/javascripts/tag_counter.js
Normal file
49
app/javascript/src/javascripts/tag_counter.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { h, Component, render } from "preact";
|
||||
import { observable, computed, action } from "mobx";
|
||||
import { observer } from "mobx-react";
|
||||
|
||||
import Utility from "./utility";
|
||||
|
||||
export default @observer class TagCounter extends Component {
|
||||
static lowCount = 10;
|
||||
static highCount = 20;
|
||||
|
||||
@observable tagCount = 0;
|
||||
|
||||
componentDidMount() {
|
||||
$(this.props.tags).on("input", this.updateCount);
|
||||
this.updateCount();
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<span class="tag-counter">
|
||||
<span class="tag-count">{this.tagCount}</span> / {TagCounter.highCount} tags
|
||||
<img src={`/images/${this.iconName}.png`}/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
@action.bound updateCount() {
|
||||
this.tagCount = Utility.regexp_split($(this.props.tags).val()).length;
|
||||
}
|
||||
|
||||
@computed get iconName() {
|
||||
if (this.tagCount < TagCounter.lowCount) {
|
||||
return "blobglare";
|
||||
} else if (this.tagCount >= TagCounter.lowCount && this.tagCount < TagCounter.highCount) {
|
||||
return "blobthinkingglare";
|
||||
} else {
|
||||
return "blobaww";
|
||||
}
|
||||
}
|
||||
|
||||
static initialize() {
|
||||
$("[data-tag-counter]").toArray().forEach(element => {
|
||||
let target = $($(element).attr("data-for")).get(0);
|
||||
render(h(TagCounter, { tags: target }), element);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
$(TagCounter.initialize);
|
||||
@@ -143,10 +143,13 @@ Upload.toggle_size = function(e) {
|
||||
|
||||
Upload.update_scale = function() {
|
||||
let $image = $("#image");
|
||||
let natural_width = $image.get(0).naturalWidth;
|
||||
let natural_height = $image.get(0).naturalHeight;
|
||||
let scale_percentage = Math.round(100 * $image.width() / natural_width);
|
||||
$("#upload-image-metadata-resolution").html(`(${natural_width}x${natural_height}, resized to ${scale_percentage}%)`);
|
||||
|
||||
if ($image.length) {
|
||||
let natural_width = $image.get(0).naturalWidth;
|
||||
let natural_height = $image.get(0).naturalHeight;
|
||||
let scale_percentage = Math.round(100 * $image.width() / natural_width);
|
||||
$("#upload-image-metadata-resolution").html(`(${natural_width}x${natural_height}, resized to ${scale_percentage}%)`);
|
||||
}
|
||||
}
|
||||
|
||||
Upload.fetch_data_manual = function(e) {
|
||||
|
||||
@@ -11,9 +11,9 @@ UserTooltip.DURATION = 250;
|
||||
UserTooltip.MAX_WIDTH = 600;
|
||||
|
||||
UserTooltip.initialize = function () {
|
||||
delegate("body", {
|
||||
delegate("#page", {
|
||||
allowHTML: true,
|
||||
appendTo: document.body,
|
||||
appendTo: document.querySelector("#user-tooltips"),
|
||||
delay: [UserTooltip.SHOW_DELAY, UserTooltip.HIDE_DELAY],
|
||||
duration: UserTooltip.DURATION,
|
||||
interactive: true,
|
||||
@@ -26,7 +26,7 @@ UserTooltip.initialize = function () {
|
||||
onHide: UserTooltip.on_hide,
|
||||
});
|
||||
|
||||
delegate("body", {
|
||||
delegate("#user-tooltips", {
|
||||
allowHTML: true,
|
||||
interactive: true,
|
||||
theme: "common-tooltip",
|
||||
|
||||
@@ -3,6 +3,10 @@ import Rails from '@rails/ujs';
|
||||
|
||||
let Utility = {};
|
||||
|
||||
export function clamp(value, low, high) {
|
||||
return Math.max(low, Math.min(value, high));
|
||||
}
|
||||
|
||||
Utility.delay = function(milliseconds) {
|
||||
return new Promise(resolve => setTimeout(resolve, milliseconds));
|
||||
}
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
$h1_size: 2em;
|
||||
$h2_size: 1.5em;
|
||||
$h3_size: 1.16667em;
|
||||
$h4_size: 1em;
|
||||
$dtext_h1_size: 2em;
|
||||
$dtext_h2_size: 1.8em;
|
||||
$dtext_h3_size: 1.6em;
|
||||
$dtext_h4_size: 1.4em;
|
||||
$dtext_h5_size: 1.2em;
|
||||
$dtext_h6_size: 1em;
|
||||
:root {
|
||||
--text-xs: 0.8em;
|
||||
--text-sm: 0.9em;
|
||||
--text-md: 1em;
|
||||
--text-lg: 1.16667em;
|
||||
--text-xl: 1.5em;
|
||||
--text-xxl: 2em;
|
||||
--header-font: Tahoma, Verdana, Helvetica, sans-serif;
|
||||
--body-font: Verdana, Helvetica, sans-serif;
|
||||
--monospace-font: 1.2em monospace;
|
||||
}
|
||||
|
||||
$h1_padding: 0.8em 0 0.25em 0;
|
||||
$h2_padding: 0.8em 0 0.25em 0;
|
||||
$h3_padding: 0.8em 0 0.25em 0;
|
||||
$h4_padding: 0.8em 0 0.25em 0;
|
||||
|
||||
/* stylelint-disable-next-line value-keyword-case */
|
||||
$base_font_family: Verdana, Helvetica, sans-serif;
|
||||
|
||||
@mixin animated-icon {
|
||||
content: "►";
|
||||
position: absolute;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
@import "../base/000_vars.scss";
|
||||
|
||||
body {
|
||||
color: var(--text-color);
|
||||
background-color: var(--body-background-color);
|
||||
font-family: $base_font_family;
|
||||
font-family: var(--body-font);
|
||||
font-size: 87.5%;
|
||||
line-height: 1.25em;
|
||||
}
|
||||
@@ -12,11 +10,6 @@ abbr[title=required] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: monospace;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
dd {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
@@ -25,22 +18,23 @@ dt {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
font-family: Tahoma, Verdana, Helvetica, sans-serif;
|
||||
h1, h2, h3, h4, h5, h6, .heading {
|
||||
font-family: var(--header-font);
|
||||
font-weight: bold;
|
||||
line-height: 1.5em;
|
||||
color: var(--header-color);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: $h1_size;
|
||||
font-size: var(--text-xxl);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: $h2_size;
|
||||
font-size: var(--text-lg);
|
||||
}
|
||||
|
||||
h3, h4, h5, h6 {
|
||||
font-size: $h3_size;
|
||||
font-size: var(--text-md);
|
||||
}
|
||||
|
||||
fieldset {
|
||||
@@ -62,6 +56,11 @@ input, select, textarea {
|
||||
border: var(--form-input-border);
|
||||
color: var(--form-input-text-color);
|
||||
padding-left: 0.25em;
|
||||
font: var(--body-font);
|
||||
}
|
||||
|
||||
textarea {
|
||||
font-size: var(--text-sm);
|
||||
}
|
||||
|
||||
input[type="button"], input[type="submit"], button {
|
||||
|
||||
@@ -104,10 +104,8 @@
|
||||
--post-parent-notice-background: var(--success-background-color);
|
||||
--post-child-notice-background: var(--warning-background-color);
|
||||
--post-pending-notice-background: #D8D8FC;
|
||||
--post-flagged-notice-background: var(--error-background-color);
|
||||
--post-banned-notice-background: var(--error-background-color);
|
||||
--post-deleted-notice-background: var(--error-background-color);
|
||||
--post-appealed-notice-background: #D8F2FC;
|
||||
--post-resized-notice-background: #EED8FC;
|
||||
--post-search-notice-background: #EEE;
|
||||
|
||||
@@ -145,10 +143,6 @@
|
||||
--tag-count-color: var(--muted-text-color);
|
||||
--low-post-count-color: red;
|
||||
|
||||
--tag-count-indicator-frown-color: red;
|
||||
--tag-count-indicator-meh-color: darkkhaki;
|
||||
--tag-count-indicator-smile-color: green;
|
||||
|
||||
--remove-favorite-button: deeppink;
|
||||
|
||||
--ugoira-seek-slider-background: #EEE;
|
||||
@@ -387,10 +381,8 @@ body[data-current-user-theme="dark"] {
|
||||
--post-parent-notice-background: var(--green-0);
|
||||
--post-resized-notice-background: var(--purple-0);
|
||||
--post-pending-notice-background: var(--indigo-0);
|
||||
--post-flagged-notice-background: var(--red-0);
|
||||
--post-deleted-notice-background: var(--red-0);
|
||||
--post-banned-notice-background: var(--red-0);
|
||||
--post-appealed-notice-background: var(--blue-0);
|
||||
|
||||
--post-tooltip-background-color: var(--grey-3);
|
||||
--post-tooltip-border-color: var(--grey-4);
|
||||
@@ -417,10 +409,6 @@ body[data-current-user-theme="dark"] {
|
||||
|
||||
--target-background: var(--blue-0);
|
||||
|
||||
--tag-count-indicator-frown-color: var(--red-1);
|
||||
--tag-count-indicator-meh-color: var(--yellow-1);
|
||||
--tag-count-indicator-smile-color: var(--green-1);
|
||||
|
||||
--uploads-dropzone-background: var(--grey-3);
|
||||
--uploads-dropzone-progress-bar-foreground-color: var(--link-color);
|
||||
--uploads-dropzone-progress-bar-background-color: var(--link-hover-color);
|
||||
|
||||
@@ -9,32 +9,32 @@ div.prose {
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: $dtext_h1_size;
|
||||
font-size: var(--text-xl);
|
||||
padding: $h1_padding;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: $dtext_h2_size;
|
||||
font-size: var(--text-xl);
|
||||
padding: $h2_padding;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: $dtext_h3_size;
|
||||
font-size: var(--text-xl);
|
||||
padding: $h3_padding;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: $dtext_h4_size;
|
||||
font-size: var(--text-xl);
|
||||
padding: $h4_padding;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: $dtext_h5_size;
|
||||
font-size: var(--text-lg);
|
||||
padding: $h4_padding;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: $dtext_h6_size;
|
||||
font-size: var(--text-md);
|
||||
padding: $h4_padding;
|
||||
}
|
||||
|
||||
@@ -57,12 +57,14 @@ div.prose {
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
code, pre {
|
||||
font: var(--monospace-font);
|
||||
background: var(--dtext-code-background);
|
||||
}
|
||||
|
||||
pre {
|
||||
font-family: monospace;
|
||||
font-size: 1.2em;
|
||||
margin: 0.5em 0;
|
||||
padding: 0.5em 1em;
|
||||
background: var(--dtext-code-background);
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@@ -73,11 +75,6 @@ div.prose {
|
||||
background: var(--dtext-blockquote-background);
|
||||
}
|
||||
|
||||
code {
|
||||
font-family: monospace;
|
||||
background: var(--dtext-code-background);
|
||||
}
|
||||
|
||||
.tn {
|
||||
font-size: 0.8em;
|
||||
color: var(--muted-text-color);
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
@import "../base/000_vars.scss";
|
||||
|
||||
.ui-widget {
|
||||
font-family: $base_font_family;
|
||||
font-family: var(--body-font);
|
||||
|
||||
input, select, textarea, button {
|
||||
font-family: $base_font_family;
|
||||
font-family: var(--body-font);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,6 @@ div#page {
|
||||
padding: 0 10px;
|
||||
|
||||
aside#sidebar {
|
||||
h1 {
|
||||
font-size: $h3_size;
|
||||
}
|
||||
|
||||
#options-box i.fa-bookmark {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ div.list-of-messages {
|
||||
width: 12em;
|
||||
margin-right: 1em;
|
||||
|
||||
div.author-name {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
a.message-timestamp {
|
||||
font-style: italic;
|
||||
font-size: 0.90em;
|
||||
@@ -42,7 +46,7 @@ div.list-of-messages {
|
||||
margin: 0 0 1em;
|
||||
width: auto;
|
||||
|
||||
h4 {
|
||||
div.author-name {
|
||||
display: inline;
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
}
|
||||
|
||||
header#top {
|
||||
h1#app-name-header {
|
||||
font-size: 2em;
|
||||
#app-name-header {
|
||||
font-size: var(--text-xxl);
|
||||
margin: 0 30px;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,6 @@ form.simple_form {
|
||||
|
||||
textarea {
|
||||
width: 70%;
|
||||
font-size: 1.2em;
|
||||
}
|
||||
|
||||
label {
|
||||
@@ -107,10 +106,31 @@ form.one-line-form {
|
||||
}
|
||||
|
||||
div.ui-dialog {
|
||||
div.input {
|
||||
input[type="text"] {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
textarea, input[type="text"] {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
form.simple_form {
|
||||
margin-bottom: 0;
|
||||
|
||||
div.input {
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
|
||||
div.input.hidden {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* the submit and close buttons */
|
||||
.ui-dialog-buttonpane {
|
||||
margin-top: 0;
|
||||
padding: 1em 1em 1em 0;
|
||||
|
||||
.ui-button {
|
||||
margin: 0 0.25em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
ul.backtrace {
|
||||
font-family: monospace;
|
||||
font-size: 1.2em;
|
||||
font: var(--monospace-font);
|
||||
background: var(--dtext-code-background);
|
||||
padding: 1em;
|
||||
margin-bottom: 1em;
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
}
|
||||
|
||||
div.note-body {
|
||||
display: none;
|
||||
position: absolute;
|
||||
font-size: 14px;
|
||||
border: var(--note-body-border);
|
||||
background: var(--note-body-background);
|
||||
color: var(--note-body-text-color);
|
||||
@@ -81,7 +83,7 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
position: absolute !important;
|
||||
border: var(--note-box-border);
|
||||
min-width: 5px;
|
||||
min-height: 5px;
|
||||
@@ -93,6 +95,11 @@
|
||||
opacity: 0.5;
|
||||
z-index: 100;
|
||||
|
||||
/* Raise notes on hover so overlapping embedded notes are readable. */
|
||||
&:hover {
|
||||
z-index: 200;
|
||||
}
|
||||
|
||||
&.unsaved {
|
||||
border: var(--unsaved-note-box-border);
|
||||
}
|
||||
@@ -106,7 +113,7 @@
|
||||
border: 1px solid transparent;
|
||||
opacity: 1;
|
||||
|
||||
&.hovering {
|
||||
&:hover {
|
||||
border: var(--note-box-border);
|
||||
box-shadow: var(--note-box-shadow);
|
||||
|
||||
@@ -114,10 +121,6 @@
|
||||
&.movable {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
div.ui-resizable-handle {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
&.editing,
|
||||
@@ -134,24 +137,30 @@
|
||||
border: var(--movable-note-box-border);
|
||||
}
|
||||
|
||||
div.ui-resizable-handle {
|
||||
display: none;
|
||||
&:not(:hover) div.ui-resizable-handle {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.note-box-highlighted {
|
||||
outline: 2px solid var(--note-highlight-color);
|
||||
}
|
||||
|
||||
div.ui-resizable-handle {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* the box that appears when dragging to create a new note. */
|
||||
div#note-preview {
|
||||
position: absolute;
|
||||
cursor: crosshair;
|
||||
border: var(--note-preview-border);
|
||||
opacity: 0.6;
|
||||
display: none;
|
||||
background: var(--note-preview-background);
|
||||
z-index: 100;
|
||||
z-index: 250;
|
||||
}
|
||||
|
||||
div.note-edit-dialog {
|
||||
|
||||
@@ -27,17 +27,9 @@ div#add-to-pool-dialog {
|
||||
margin-left: 1em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: $h3_size;
|
||||
}
|
||||
}
|
||||
|
||||
div#c-pools {
|
||||
h1 {
|
||||
font-size: $h2_size;
|
||||
}
|
||||
|
||||
textarea {
|
||||
height: 10em;
|
||||
}
|
||||
@@ -50,10 +42,6 @@ div#c-pools {
|
||||
}
|
||||
|
||||
div#c-pool-orders, div#c-favorite-group-orders {
|
||||
h1 {
|
||||
font-size: $h2_size;
|
||||
}
|
||||
|
||||
div#a-edit {
|
||||
ul.ui-sortable {
|
||||
list-style-type: none;
|
||||
|
||||
@@ -80,8 +80,13 @@ table article.post-preview {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
#edit-dialog textarea {
|
||||
margin-bottom: 0.25em;
|
||||
#edit-dialog {
|
||||
/* Hide everything but the rating and tags fields. */
|
||||
.post_has_embedded_notes_fieldset, .post_lock_fieldset, .post_parent_id,
|
||||
.post_source, #filedropzone, .upload_as_pending, .upload_source_container,
|
||||
.upload_parent_id, .upload_artist_commentary_container, .upload_commentary_translation_container {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.post-preview {
|
||||
@@ -258,26 +263,15 @@ div#c-posts {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.resolved {
|
||||
margin-left: 0.5em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&.post-notice-parent { background: var(--post-parent-notice-background); }
|
||||
&.post-notice-child { background: var(--post-child-notice-background); }
|
||||
&.post-notice-pending { background: var(--post-pending-notice-background); }
|
||||
&.post-notice-flagged { background: var(--post-flagged-notice-background); }
|
||||
&.post-notice-banned { background: var(--post-banned-notice-background); }
|
||||
&.post-notice-deleted { background: var(--post-deleted-notice-background); }
|
||||
&.post-notice-appealed { background: var(--post-appealed-notice-background); }
|
||||
&.post-notice-resized { background: var(--post-resized-notice-background); }
|
||||
&.post-notice-search { background: var(--post-search-notice-background); }
|
||||
}
|
||||
|
||||
aside#sidebar #tag-list h2 {
|
||||
font-size: $h4_size;
|
||||
}
|
||||
|
||||
aside#sidebar > section > ul {
|
||||
margin-bottom: 1em;
|
||||
|
||||
@@ -308,7 +302,7 @@ div#c-posts {
|
||||
div#a-index {
|
||||
menu#post-sections {
|
||||
margin-bottom: 0.5em;
|
||||
font-size: $h3_size;
|
||||
font-size: var(--text-lg);
|
||||
|
||||
li {
|
||||
padding: 0 1em 0.5em 0;
|
||||
@@ -331,7 +325,7 @@ div#c-posts {
|
||||
|
||||
menu#post-sections {
|
||||
margin: 0;
|
||||
font-size: $h3_size;
|
||||
font-size: var(--text-lg);
|
||||
|
||||
li {
|
||||
padding: 0 1em 0 0;
|
||||
@@ -434,12 +428,27 @@ div#c-posts {
|
||||
}
|
||||
}
|
||||
|
||||
body[data-post-current-image-size="large"] .image-view-large-link,
|
||||
body[data-post-current-image-size="original"] .image-view-original-link,
|
||||
body[data-post-current-image-size="large"] #post-options .image-view-large-link,
|
||||
body[data-post-current-image-size="original"] #post-options .image-view-original-link,
|
||||
body[data-post-current-image-size="original"] #image-resize-notice {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Always show the "Resized to X% of original" notice on mobile when it exists. */
|
||||
#image-resize-notice {
|
||||
@media screen and (max-width: 660px) {
|
||||
display: block !important;
|
||||
}
|
||||
}
|
||||
|
||||
body.mode-translation .note-container {
|
||||
cursor: crosshair;
|
||||
}
|
||||
|
||||
body:not(.mode-translation) div#c-posts div#a-show #mark-as-translated-section {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div#c-post-versions, div#c-artist-versions {
|
||||
div#a-index {
|
||||
a {
|
||||
@@ -474,6 +483,33 @@ div#c-posts, div#c-uploads {
|
||||
}
|
||||
}
|
||||
|
||||
/* Container for the tag edit <textarea>, header, and related tags buttons. */
|
||||
#tags-container {
|
||||
div.header {
|
||||
line-height: 1.5em;
|
||||
|
||||
label {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
i.fa-external-link-alt {
|
||||
font-size: var(--text-xs);
|
||||
}
|
||||
|
||||
span[data-tag-counter] {
|
||||
float: right;
|
||||
color: var(--muted-text-color);
|
||||
font-size: var(--text-sm);
|
||||
|
||||
img {
|
||||
margin-left: 0.5em;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div#c-explore-posts {
|
||||
a.desc {
|
||||
font-weight: bold;
|
||||
@@ -499,10 +535,6 @@ div#unapprove-dialog {
|
||||
}
|
||||
}
|
||||
|
||||
textarea[data-autocomplete="tag-edit"] {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
#add-commentary-dialog {
|
||||
input {
|
||||
width: 70%;
|
||||
|
||||
@@ -7,10 +7,6 @@ div#c-static {
|
||||
section {
|
||||
flex: 1;
|
||||
|
||||
h1 {
|
||||
font-size: $h3_size;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
@@ -5,45 +5,3 @@ div#c-tags {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#tags-container {
|
||||
div.header {
|
||||
margin: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 50% 50%;
|
||||
width: 100%;
|
||||
|
||||
label {
|
||||
grid-column: 1;
|
||||
}
|
||||
|
||||
.options {
|
||||
grid-column: 2;
|
||||
justify-self: end;
|
||||
|
||||
.count {
|
||||
color: var(--tag-count-color);
|
||||
text-decoration: italic;
|
||||
margin-left: 0.25em;
|
||||
padding-bottom: 0.2em;
|
||||
}
|
||||
|
||||
i {
|
||||
margin-left: 0.25em;
|
||||
font-size: 11pt;
|
||||
}
|
||||
|
||||
.fa-frown {
|
||||
color: var(--tag-count-indicator-frown-color);
|
||||
}
|
||||
|
||||
.fa-meh {
|
||||
color: var(--tag-count-indicator-meh-color);
|
||||
}
|
||||
|
||||
.fa-smile {
|
||||
color: var(--tag-count-indicator-smile-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,20 +4,12 @@
|
||||
div#page {
|
||||
margin: 0 0.5rem;
|
||||
padding: 0;
|
||||
|
||||
aside#sidebar {
|
||||
font-size: $h3_size;
|
||||
}
|
||||
}
|
||||
|
||||
header#top {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
|
||||
h1#app-name-header {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
#maintoggle {
|
||||
display: block;
|
||||
font-weight: bold;
|
||||
@@ -28,7 +20,6 @@
|
||||
}
|
||||
|
||||
nav#nav {
|
||||
font-size: $h3_size;
|
||||
line-height: 2em;
|
||||
display: none;
|
||||
|
||||
@@ -53,7 +44,7 @@
|
||||
}
|
||||
|
||||
div.paginator {
|
||||
font-size: $h2_size;
|
||||
font-size: var(--text-lg);
|
||||
padding: 1em 0 0;
|
||||
|
||||
li {
|
||||
|
||||
@@ -163,7 +163,10 @@ module Searchable
|
||||
type = column.type || reflect_on_association(name)&.class_name
|
||||
|
||||
if column.try(:array?)
|
||||
return search_array_attribute(name, type, params)
|
||||
subtype = type
|
||||
type = :array
|
||||
elsif defined_enums.has_key?(name.to_s)
|
||||
type = :enum
|
||||
end
|
||||
|
||||
case type
|
||||
@@ -181,6 +184,10 @@ module Searchable
|
||||
numeric_attribute_matches(name, params[name])
|
||||
when :inet
|
||||
search_inet_attribute(name, params)
|
||||
when :enum
|
||||
search_enum_attribute(name, params)
|
||||
when :array
|
||||
search_array_attribute(name, subtype, params)
|
||||
else
|
||||
raise NotImplementedError, "unhandled attribute type: #{name}" if type.blank?
|
||||
search_includes(name, params, type)
|
||||
@@ -279,6 +286,19 @@ module Searchable
|
||||
relation
|
||||
end
|
||||
|
||||
def search_enum_attribute(name, params)
|
||||
relation = all
|
||||
|
||||
if params[name].present?
|
||||
value = params[name].split(/[, ]+/).map(&:downcase)
|
||||
relation = relation.where(name => value)
|
||||
elsif params["#{name}_id"].present?
|
||||
relation = relation.numeric_attribute_matches(name, params["#{name}_id"])
|
||||
end
|
||||
|
||||
relation
|
||||
end
|
||||
|
||||
def search_array_attribute(name, type, params)
|
||||
relation = all
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
require "danbooru/http/html_adapter"
|
||||
require "danbooru/http/xml_adapter"
|
||||
require "danbooru/http/cache"
|
||||
require "danbooru/http/logger"
|
||||
require "danbooru/http/redirector"
|
||||
require "danbooru/http/retriable"
|
||||
require "danbooru/http/session"
|
||||
|
||||
35
app/logical/danbooru/http/logger.rb
Normal file
35
app/logical/danbooru/http/logger.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
module Danbooru
|
||||
class Http
|
||||
class Logger < HTTP::Feature
|
||||
HTTP::Options.register_feature :logger, self
|
||||
|
||||
attr_reader :logger
|
||||
|
||||
def initialize(logger: ::Logger.new(STDOUT))
|
||||
@logger = logger
|
||||
end
|
||||
|
||||
def perform(request, &block)
|
||||
log_request(request)
|
||||
response = yield request
|
||||
log_response(request, response)
|
||||
response
|
||||
end
|
||||
|
||||
def log_request(request)
|
||||
logger.info do
|
||||
verb = request.verb.to_s.upcase
|
||||
headers = request.headers.map { |name, value| "#{name}: #{value}" }.join("\n")
|
||||
"> #{verb} #{request.uri}\n#{headers}\n"
|
||||
end
|
||||
end
|
||||
|
||||
def log_response(request, response)
|
||||
logger.info do
|
||||
headers = response.headers.map { |name, value| "#{name}: #{value}" }.join("\n")
|
||||
"< #{response.status.to_i} | #{request.uri}\n#{headers}\n"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,13 +3,14 @@ module DanbooruMaintenance
|
||||
|
||||
def hourly
|
||||
safely { Upload.prune! }
|
||||
safely { PostPruner.prune! }
|
||||
safely { PostAppealForumUpdater.update_forum! }
|
||||
safely { regenerate_post_counts! }
|
||||
end
|
||||
|
||||
def daily
|
||||
safely { PostPruner.new.prune! }
|
||||
safely { Delayed::Job.where('created_at < ?', 45.days.ago).delete_all }
|
||||
safely { PostDisapproval.prune! }
|
||||
safely { regenerate_post_counts! }
|
||||
safely { TokenBucket.prune! }
|
||||
safely { BulkUpdateRequestPruner.warn_old }
|
||||
safely { BulkUpdateRequestPruner.reject_expired }
|
||||
@@ -35,8 +36,12 @@ module DanbooruMaintenance
|
||||
|
||||
def safely(&block)
|
||||
ActiveRecord::Base.connection.execute("set statement_timeout = 0")
|
||||
yield
|
||||
|
||||
CurrentUser.scoped(User.system, "127.0.0.1") do
|
||||
yield
|
||||
end
|
||||
rescue StandardError => exception
|
||||
DanbooruLogger.log(exception)
|
||||
raise exception if Rails.env.test?
|
||||
end
|
||||
end
|
||||
|
||||
26
app/logical/post_appeal_forum_updater.rb
Normal file
26
app/logical/post_appeal_forum_updater.rb
Normal file
@@ -0,0 +1,26 @@
|
||||
module PostAppealForumUpdater
|
||||
APPEAL_TOPIC_TITLE = "Deletion appeal thread"
|
||||
|
||||
def self.update_forum!
|
||||
return if pending_appeals.empty?
|
||||
|
||||
CurrentUser.scoped(User.system) do
|
||||
topic = ForumTopic.order(:id).create_with(creator: User.system).find_or_create_by!(title: APPEAL_TOPIC_TITLE)
|
||||
ForumPost.create!(creator: User.system, topic: topic, body: forum_post_body)
|
||||
end
|
||||
end
|
||||
|
||||
def self.pending_appeals
|
||||
PostAppeal.pending.where(created_at: (1.hour.ago..Time.zone.now)).order(post_id: :asc)
|
||||
end
|
||||
|
||||
def self.forum_post_body
|
||||
pending_appeals.map do |appeal|
|
||||
if appeal.reason.present?
|
||||
"post ##{appeal.post_id}: #{appeal.reason}"
|
||||
else
|
||||
"post ##{appeal.post_id}"
|
||||
end
|
||||
end.join("\n")
|
||||
end
|
||||
end
|
||||
@@ -1,37 +1,27 @@
|
||||
class PostPruner
|
||||
module PostPruner
|
||||
module_function
|
||||
|
||||
def prune!
|
||||
prune_pending!
|
||||
prune_flagged!
|
||||
prune_mod_actions!
|
||||
prune_appealed!
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def prune_pending!
|
||||
CurrentUser.scoped(User.system, "127.0.0.1") do
|
||||
Post.where("is_deleted = ? and is_pending = ? and created_at < ?", false, true, 3.days.ago).each do |post|
|
||||
post.delete!("Unapproved in three days")
|
||||
rescue PostFlag::Error
|
||||
# swallow
|
||||
end
|
||||
Post.pending.expired.each do |post|
|
||||
post.delete!("Unapproved in three days", user: User.system)
|
||||
end
|
||||
end
|
||||
|
||||
def prune_flagged!
|
||||
CurrentUser.scoped(User.system, "127.0.0.1") do
|
||||
Post.where("is_deleted = ? and is_flagged = ?", false, true).each do |post|
|
||||
if post.flags.unresolved.old.any?
|
||||
begin
|
||||
post.delete!("Unapproved in three days after returning to moderation queue")
|
||||
rescue PostFlag::Error
|
||||
# swallow
|
||||
end
|
||||
end
|
||||
end
|
||||
PostFlag.expired.each do |flag|
|
||||
flag.post.delete!("Unapproved in three days after returning to moderation queue", user: User.system)
|
||||
end
|
||||
end
|
||||
|
||||
def prune_mod_actions!
|
||||
ModAction.where(["creator_id = ? and description like ?", User.system.id, "deleted post %"]).destroy_all
|
||||
def prune_appealed!
|
||||
PostAppeal.expired.each do |appeal|
|
||||
appeal.post.delete!("Unapproved in three days after returning to moderation queue", user: User.system)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -6,7 +6,7 @@ class PostQueryBuilder
|
||||
COUNT_METATAGS = %w[
|
||||
comment_count deleted_comment_count active_comment_count
|
||||
note_count deleted_note_count active_note_count
|
||||
flag_count resolved_flag_count unresolved_flag_count
|
||||
flag_count
|
||||
child_count deleted_child_count active_child_count
|
||||
pool_count deleted_pool_count active_pool_count series_pool_count collection_pool_count
|
||||
appeal_count approval_count replacement_count
|
||||
@@ -274,8 +274,10 @@ class PostQueryBuilder
|
||||
Post.pending
|
||||
when "flagged"
|
||||
Post.flagged
|
||||
when "appealed"
|
||||
Post.appealed
|
||||
when "modqueue"
|
||||
Post.pending_or_flagged
|
||||
Post.in_modqueue
|
||||
when "deleted"
|
||||
Post.deleted
|
||||
when "banned"
|
||||
@@ -283,7 +285,7 @@ class PostQueryBuilder
|
||||
when "active"
|
||||
Post.active
|
||||
when "unmoderated"
|
||||
Post.pending_or_flagged.available_for_moderation(current_user, hidden: false)
|
||||
Post.in_modqueue.available_for_moderation(current_user, hidden: false)
|
||||
when "all", "any"
|
||||
Post.all
|
||||
else
|
||||
@@ -307,7 +309,7 @@ class PostQueryBuilder
|
||||
Post.where(parent: nil)
|
||||
when "any"
|
||||
Post.where.not(parent: nil)
|
||||
when /pending|flagged|modqueue|deleted|banned|active|unmoderated/
|
||||
when "pending", "flagged", "appealed", "modqueue", "deleted", "banned", "active", "unmoderated"
|
||||
Post.where.not(parent: nil).where(parent: status_matches(parent))
|
||||
when /\A\d+\z/
|
||||
Post.where(id: parent).or(Post.where(parent: parent))
|
||||
@@ -322,7 +324,7 @@ class PostQueryBuilder
|
||||
Post.where(has_children: false)
|
||||
when "any"
|
||||
Post.where(has_children: true)
|
||||
when /pending|flagged|modqueue|deleted|banned|active|unmoderated/
|
||||
when "pending", "flagged", "appealed", "modqueue", "deleted", "banned", "active", "unmoderated"
|
||||
Post.where(has_children: true).where(children: status_matches(child))
|
||||
else
|
||||
Post.none
|
||||
@@ -330,8 +332,9 @@ class PostQueryBuilder
|
||||
end
|
||||
|
||||
def source_matches(source, quoted = false)
|
||||
case source.downcase
|
||||
in "none" unless quoted
|
||||
if source.empty?
|
||||
Post.where_like(:source, "")
|
||||
elsif source.downcase == "none" && !quoted
|
||||
Post.where_like(:source, "")
|
||||
else
|
||||
Post.where_ilike(:source, source + "*")
|
||||
@@ -606,10 +609,10 @@ class PostQueryBuilder
|
||||
.order("contributor_fav_count DESC, posts.fav_count DESC, posts.id DESC")
|
||||
|
||||
when "modqueue", "modqueue_desc"
|
||||
relation = relation.left_outer_joins(:flags).order(Arel.sql("GREATEST(posts.created_at, post_flags.created_at) DESC, posts.id DESC"))
|
||||
relation = relation.with_queued_at.order("queued_at DESC, posts.id DESC")
|
||||
|
||||
when "modqueue_asc"
|
||||
relation = relation.left_outer_joins(:flags).order(Arel.sql("GREATEST(posts.created_at, post_flags.created_at) ASC, posts.id ASC"))
|
||||
relation = relation.with_queued_at.order("queued_at ASC, posts.id ASC")
|
||||
|
||||
when "none"
|
||||
relation = relation.reorder(nil)
|
||||
@@ -642,14 +645,7 @@ class PostQueryBuilder
|
||||
if scanner.scan(/(-)?(#{METATAGS.join("|")}):/io)
|
||||
operator = scanner.captures.first
|
||||
metatag = scanner.captures.second.downcase
|
||||
|
||||
if scanner.scan(/"(.+)"/) || scanner.scan(/'(.+)'/)
|
||||
value = scanner.captures.first
|
||||
quoted = true
|
||||
else
|
||||
value = scanner.scan(/[^ ]*/)
|
||||
quoted = false
|
||||
end
|
||||
value, quoted = scan_string(scanner)
|
||||
|
||||
if metatag.in?(COUNT_METATAG_SYNONYMS)
|
||||
metatag = metatag.singularize + "_count"
|
||||
@@ -673,23 +669,41 @@ class PostQueryBuilder
|
||||
terms
|
||||
end
|
||||
|
||||
def scan_string(scanner)
|
||||
if scanner.scan(/"((?:\\"|[^"])*)"/)
|
||||
value = scanner.captures.first.gsub(/\\(.)/) { $1 }
|
||||
quoted = true
|
||||
elsif scanner.scan(/'((?:\\'|[^'])*)'/)
|
||||
value = scanner.captures.first.gsub(/\\(.)/) { $1 }
|
||||
quoted = true
|
||||
else
|
||||
value = scanner.scan(/(\\ |[^ ])*/)
|
||||
value = value.gsub(/\\ /) { " " }
|
||||
quoted = false
|
||||
end
|
||||
|
||||
[value, quoted]
|
||||
end
|
||||
|
||||
def split_query
|
||||
terms.map do |term|
|
||||
if term.type == :metatag && !term.negated && !term.quoted
|
||||
"#{term.name}:#{term.value}"
|
||||
elsif term.type == :metatag && !term.negated && term.quoted
|
||||
"#{term.name}:\"#{term.value}\""
|
||||
elsif term.type == :metatag && term.negated && !term.quoted
|
||||
"-#{term.name}:#{term.value}"
|
||||
elsif term.type == :metatag && term.negated && term.quoted
|
||||
"-#{term.name}:\"#{term.value}\""
|
||||
elsif term.type == :tag && term.negated
|
||||
"-#{term.name}"
|
||||
elsif term.type == :tag && term.optional
|
||||
"~#{term.name}"
|
||||
elsif term.type == :tag
|
||||
term.name
|
||||
type, name, value = term.type, term.name, term.value
|
||||
|
||||
str = ""
|
||||
str += "-" if term.negated
|
||||
str += "~" if term.optional
|
||||
|
||||
if type == :tag
|
||||
str += name
|
||||
elsif type == :metatag && (term.quoted || value.include?(" "))
|
||||
value = value.gsub(/\\/) { '\\\\' }
|
||||
value = value.gsub(/"/) { '\\"' }
|
||||
str += "#{name}:\"#{value}\""
|
||||
elsif type == :metatag
|
||||
str += "#{name}:#{value}"
|
||||
end
|
||||
|
||||
str
|
||||
end
|
||||
end
|
||||
|
||||
@@ -898,8 +912,9 @@ class PostQueryBuilder
|
||||
metatags
|
||||
end
|
||||
|
||||
# XXX unify with PostSets::Post#show_deleted?
|
||||
def hide_deleted?
|
||||
has_status_metatag = select_metatags(:status).any? { |metatag| metatag.value.downcase.in?(%w[deleted active any all]) }
|
||||
has_status_metatag = select_metatags(:status).any? { |metatag| metatag.value.downcase.in?(%w[deleted active any all unmoderated modqueue appealed]) }
|
||||
hide_deleted_posts? && !has_status_metatag
|
||||
end
|
||||
end
|
||||
|
||||
@@ -59,6 +59,12 @@ module PostSets
|
||||
posts.any? {|x| x.rating == "e"}
|
||||
end
|
||||
|
||||
def shown_posts
|
||||
shown_posts = posts.select(&:visible?)
|
||||
shown_posts = shown_posts.reject(&:is_deleted?) unless show_deleted?
|
||||
shown_posts
|
||||
end
|
||||
|
||||
def hidden_posts
|
||||
posts.reject(&:visible?)
|
||||
end
|
||||
@@ -136,24 +142,22 @@ module PostSets
|
||||
|
||||
def post_previews_html(template)
|
||||
html = ""
|
||||
if none_shown
|
||||
if shown_posts.empty?
|
||||
return template.render("post_sets/blank")
|
||||
end
|
||||
|
||||
posts.each do |post|
|
||||
html << PostPresenter.preview(post, show_cropped: true, tags: tag_string)
|
||||
shown_posts.each do |post|
|
||||
html << PostPresenter.preview(post, show_deleted: show_deleted?, show_cropped: true, tags: tag_string)
|
||||
html << "\n"
|
||||
end
|
||||
|
||||
html.html_safe
|
||||
end
|
||||
|
||||
def not_shown(post)
|
||||
post.is_deleted? && tag_string !~ /status:(?:all|any|deleted|banned)/
|
||||
end
|
||||
|
||||
def none_shown
|
||||
posts.reject {|post| not_shown(post) }.empty?
|
||||
def show_deleted?
|
||||
query.select_metatags("status").any? do |metatag|
|
||||
metatag.value.in?(%w[all any active unmoderated modqueue deleted appealed])
|
||||
end
|
||||
end
|
||||
|
||||
concerning :TagListMethods do
|
||||
|
||||
@@ -77,6 +77,10 @@ module Sources
|
||||
|
||||
FAVME = %r{\Ahttps?://(?:www\.)?fav\.me/d(?<base36_deviation_id>[a-z0-9]+)\z}i
|
||||
|
||||
def self.enabled?
|
||||
Danbooru.config.deviantart_client_id.present? && Danbooru.config.deviantart_client_secret.present?
|
||||
end
|
||||
|
||||
def domains
|
||||
["deviantart.net", "deviantart.com", "fav.me"]
|
||||
end
|
||||
|
||||
@@ -50,6 +50,10 @@ module Sources
|
||||
|
||||
PROFILE_PAGE = %r{\Ahttps?://seiga\.nicovideo\.jp/user/illust/(?<artist_id>\d+)}i
|
||||
|
||||
def self.enabled?
|
||||
Danbooru.config.nico_seiga_login.present? && Danbooru.config.nico_seiga_password.present?
|
||||
end
|
||||
|
||||
def domains
|
||||
["nicoseiga.jp", "nicovideo.jp"]
|
||||
end
|
||||
|
||||
@@ -64,6 +64,10 @@ module Sources
|
||||
DIR = %r{(?:\d+/)?(?:__rs_\w+/)?nijie_picture(?:/diff/main)?}
|
||||
IMAGE_URL = %r{#{IMAGE_BASE_URL}/#{DIR}/#{Regexp.union(FILENAME1, FILENAME2, FILENAME3)}\.\w+\z}i
|
||||
|
||||
def self.enabled?
|
||||
Danbooru.config.nijie_login.present? && Danbooru.config.nijie_password.present?
|
||||
end
|
||||
|
||||
def domains
|
||||
["nijie.info", "nijie.net"]
|
||||
end
|
||||
@@ -176,23 +180,37 @@ module Sources
|
||||
end
|
||||
|
||||
def page
|
||||
return nil if page_url.blank?
|
||||
return nil if page_url.blank? || client.blank?
|
||||
|
||||
http = Danbooru::Http.new
|
||||
form = { email: Danbooru.config.nijie_login, password: Danbooru.config.nijie_password }
|
||||
|
||||
# XXX `retriable` must come after `cache` so that retries don't return cached error responses.
|
||||
response = http.cache(1.hour).use(retriable: { max_retries: 20 }).post("https://nijie.info/login_int.php", form: form)
|
||||
DanbooruLogger.info "Nijie login failed (#{url}, #{response.status})" if response.status != 200
|
||||
return nil unless response.status == 200
|
||||
|
||||
response = http.cookies(R18: 1).cache(1.minute).get(page_url)
|
||||
response = client.cache(1.minute).get(page_url)
|
||||
return nil unless response.status == 200
|
||||
|
||||
response&.parse
|
||||
end
|
||||
|
||||
memoize :page
|
||||
|
||||
def client
|
||||
http = Danbooru::Http.new.timeout(60)
|
||||
|
||||
cookie = Cache.get("nijie-session-cookie", 1.week) do
|
||||
login_page = http.use(retriable: { max_retries: 20 }).get("https://nijie.info/login.php").parse
|
||||
form = {
|
||||
email: Danbooru.config.nijie_login,
|
||||
password: Danbooru.config.nijie_password,
|
||||
url: login_page.at("input[name='url']")["value"],
|
||||
save: "on",
|
||||
ticket: ""
|
||||
}
|
||||
response = http.use(retriable: { max_retries: 20 }).post("https://nijie.info/login_int.php", form: form)
|
||||
DanbooruLogger.info "Nijie login failed (#{url}, #{response.status})" if response.status != 200
|
||||
return nil unless response.status == 200
|
||||
|
||||
response.cookies.select { |c| c.name == "NIJIEIJIEID" }.compact.first
|
||||
end
|
||||
|
||||
http.cookies(NIJIEIJIEID: cookie, R18: 1)
|
||||
end
|
||||
memoize :client
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -24,6 +24,10 @@ module Sources::Strategies
|
||||
STATUS1 = %r{\A#{HOST}/web/statuses/(?<status_id>\d+)}
|
||||
STATUS2 = %r{\A#{NAMED_PROFILE}/(?<status_id>\d+)}
|
||||
|
||||
def self.enabled?
|
||||
Danbooru.config.pawoo_client_id.present? && Danbooru.config.pawoo_client_secret.present?
|
||||
end
|
||||
|
||||
def domains
|
||||
["pawoo.net"]
|
||||
end
|
||||
|
||||
@@ -65,6 +65,10 @@ module Sources
|
||||
STACC_PAGE = %r{\A#{WEB}/stacc/#{MONIKER}/?\z}i
|
||||
NOVEL_PAGE = %r{(?:\Ahttps?://www\.pixiv\.net/novel/show\.php\?id=(\d+))}
|
||||
|
||||
def self.enabled?
|
||||
Danbooru.config.pixiv_login.present? && Danbooru.config.pixiv_password.present?
|
||||
end
|
||||
|
||||
def self.to_dtext(text)
|
||||
if text.nil?
|
||||
return nil
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
# https://66.media.tumblr.com/5a2c3fe25c977e2281392752ab971c90/3dbfaec9b9e0c2e3-92/s500x750/4f92bbaaf95c0b4e7970e62b1d2e1415859dd659.png
|
||||
#
|
||||
# https://superboin.tumblr.com/post/141169066579/photoset_iframe/superboin/tumblr_o45miiAOts1u6rxu8/500/false
|
||||
#
|
||||
# https://make-do5.tumblr.com/post/619663949657423872 (extremely high res, extractable)
|
||||
|
||||
module Sources::Strategies
|
||||
class Tumblr < Base
|
||||
@@ -26,6 +28,11 @@ module Sources::Strategies
|
||||
VIDEO = %r{\Ahttps?://(?:vtt|ve|va\.media)\.tumblr\.com/}i
|
||||
POST = %r{\Ahttps?://(?<blog_name>[^.]+)\.tumblr\.com/(?:post|image)/(?<post_id>\d+)}i
|
||||
|
||||
NEW_HEADERS = {
|
||||
"user-agent": Danbooru.config.canonical_app_name,
|
||||
"accept": "text/html"
|
||||
}
|
||||
|
||||
def self.enabled?
|
||||
Danbooru.config.tumblr_consumer_key.present?
|
||||
end
|
||||
@@ -161,14 +168,22 @@ module Sources::Strategies
|
||||
# http://media.tumblr.com/tumblr_m24kbxqKAX1rszquso1_1280.jpg
|
||||
# => https://media.tumblr.com/tumblr_m24kbxqKAX1rszquso1_1280.jpg
|
||||
def find_largest(url, sizes: SIZES)
|
||||
return url unless url =~ OLD_IMAGE
|
||||
if url =~ OLD_IMAGE
|
||||
candidates = sizes.map do |size|
|
||||
"https://media.tumblr.com/#{$~[:dir]}#{$~[:filename]}_#{size}.#{$~[:ext]}"
|
||||
end
|
||||
|
||||
candidates = sizes.map do |size|
|
||||
"https://media.tumblr.com/#{$~[:dir]}#{$~[:filename]}_#{size}.#{$~[:ext]}"
|
||||
end
|
||||
candidates.find do |candidate|
|
||||
http_exists?(candidate)
|
||||
end
|
||||
elsif url =~ %r{/s\d+x\d+/(\w+\.\w+)$}i
|
||||
max_size = Integer.sqrt(Danbooru.config.max_image_resolution)
|
||||
url = url.gsub(%r{/s\d+x\d+/\w+\.\w+$}i, "/s#{max_size}x#{max_size}/#{$1}")
|
||||
|
||||
candidates.find do |candidate|
|
||||
http_exists?(candidate)
|
||||
resp = Danbooru::Http.cache(1.minute).get(url, headers: NEW_HEADERS).parse
|
||||
resp.at("img[src*='/s#{max_size}x#{max_size}/']")["src"]
|
||||
else
|
||||
url
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -25,11 +25,6 @@ class TagCategory
|
||||
@@short_name_mapping ||= Hash[Danbooru.config.full_tag_config_info.map { |k, v| [v["short"], k] }]
|
||||
end
|
||||
|
||||
# Returns a hash mapping for split_tag_list_html (presenters/tag_set_presenter.rb)
|
||||
def header_mapping
|
||||
@@header_mapping ||= Hash[Danbooru.config.full_tag_config_info.map { |k, v| [k, v["header"]] }]
|
||||
end
|
||||
|
||||
# Returns a hash mapping for related tag buttons (javascripts/related_tag.js.erb)
|
||||
def related_button_mapping
|
||||
@@related_button_mapping ||= Hash[Danbooru.config.full_tag_config_info.map { |k, v| [k, v["relatedbutton"]] }]
|
||||
|
||||
@@ -3,6 +3,8 @@ class UploadLimit
|
||||
|
||||
INITIAL_POINTS = 1000
|
||||
MAXIMUM_POINTS = 10_000
|
||||
APPEAL_COST = 3
|
||||
DELETION_COST = 5
|
||||
|
||||
attr_reader :user
|
||||
|
||||
@@ -30,11 +32,20 @@ class UploadLimit
|
||||
end
|
||||
end
|
||||
|
||||
def used_upload_slots
|
||||
pending = user.posts.pending
|
||||
early_deleted = user.posts.deleted.where("created_at >= ?", 3.days.ago)
|
||||
def maxed?
|
||||
user.upload_points >= MAXIMUM_POINTS
|
||||
end
|
||||
|
||||
pending.or(early_deleted).count
|
||||
def used_upload_slots
|
||||
pending_count = user.posts.pending.count
|
||||
appealed_count = user.post_appeals.pending.count
|
||||
early_deleted_count = user.posts.deleted.where("created_at >= ?", Danbooru.config.moderation_period.ago).count
|
||||
|
||||
pending_count + (early_deleted_count * DELETION_COST) + (appealed_count * APPEAL_COST)
|
||||
end
|
||||
|
||||
def free_upload_slots
|
||||
upload_slots - used_upload_slots
|
||||
end
|
||||
|
||||
def upload_slots
|
||||
@@ -111,6 +122,4 @@ class UploadLimit
|
||||
points_for_next_level(n - 1)
|
||||
end.sum
|
||||
end
|
||||
|
||||
memoize :used_upload_slots
|
||||
end
|
||||
|
||||
@@ -64,6 +64,8 @@ class UploadService
|
||||
end
|
||||
|
||||
def start!
|
||||
raise NotImplementedError, "No login credentials configured for #{strategy.site_name}." unless strategy.class.enabled?
|
||||
|
||||
if Utils.is_downloadable?(source)
|
||||
if Post.system_tag_match("source:#{canonical_source}").where.not(id: original_post_id).exists?
|
||||
raise ActiveRecord::RecordNotUnique, "A post with source #{canonical_source} already exists"
|
||||
|
||||
@@ -72,6 +72,8 @@ class UploadService
|
||||
raise "No file or source URL provided" if upload.source_url.blank?
|
||||
|
||||
strategy = Sources::Strategies.find(upload.source_url, upload.referer_url)
|
||||
raise NotImplementedError, "No login credentials configured for #{strategy.site_name}." unless strategy.class.enabled?
|
||||
|
||||
file = strategy.download_file!
|
||||
|
||||
if strategy.data[:ugoira_frame_data].present?
|
||||
|
||||
@@ -127,6 +127,10 @@ class ApplicationRecord < ActiveRecord::Base
|
||||
ensure
|
||||
connection.execute("SET STATEMENT_TIMEOUT = #{CurrentUser.user.try(:statement_timeout) || 3_000}") unless Rails.env == "test"
|
||||
end
|
||||
|
||||
def update!(*args)
|
||||
all.each { |record| record.update!(*args) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -6,11 +6,15 @@ class Ban < ApplicationRecord
|
||||
after_destroy :create_unban_mod_action
|
||||
belongs_to :user
|
||||
belongs_to :banner, :class_name => "User"
|
||||
|
||||
validates_presence_of :reason, :duration
|
||||
validate :user, :validate_user_is_bannable, on: :create
|
||||
|
||||
scope :unexpired, -> { where("bans.expires_at > ?", Time.now) }
|
||||
scope :expired, -> { where("bans.expires_at <= ?", Time.now) }
|
||||
|
||||
attr_reader :duration
|
||||
|
||||
def self.is_banned?(user)
|
||||
exists?(["user_id = ? AND expires_at > ?", user.id, Time.now])
|
||||
end
|
||||
@@ -48,6 +52,10 @@ class Ban < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def validate_user_is_bannable
|
||||
self.errors[:user] << "is already banned" if user.is_banned?
|
||||
end
|
||||
|
||||
def update_user_on_create
|
||||
user.update!(is_banned: true)
|
||||
end
|
||||
@@ -69,8 +77,6 @@ class Ban < ApplicationRecord
|
||||
@duration = dur
|
||||
end
|
||||
|
||||
attr_reader :duration
|
||||
|
||||
def humanized_duration
|
||||
ApplicationController.helpers.distance_of_time_in_words(created_at, expires_at)
|
||||
end
|
||||
|
||||
@@ -7,7 +7,8 @@ class Post < ApplicationRecord
|
||||
class TimeoutError < StandardError; end
|
||||
|
||||
# Tags to copy when copying notes.
|
||||
NOTE_COPY_TAGS = %w[translated partially_translated check_translation translation_request reverse_translation]
|
||||
NOTE_COPY_TAGS = %w[translated partially_translated check_translation translation_request reverse_translation
|
||||
annotated partially_annotated check_annotation annotation_request]
|
||||
|
||||
deletable
|
||||
|
||||
@@ -61,8 +62,10 @@ class Post < ApplicationRecord
|
||||
scope :pending, -> { where(is_pending: true) }
|
||||
scope :flagged, -> { where(is_flagged: true) }
|
||||
scope :banned, -> { where(is_banned: true) }
|
||||
scope :active, -> { where(is_pending: false, is_deleted: false, is_flagged: false) }
|
||||
scope :pending_or_flagged, -> { pending.or(flagged) }
|
||||
scope :active, -> { where(is_pending: false, is_deleted: false, is_flagged: false).where.not(id: PostAppeal.pending) }
|
||||
scope :appealed, -> { deleted.where(id: PostAppeal.pending.select(:post_id)) }
|
||||
scope :in_modqueue, -> { pending.or(flagged).or(appealed) }
|
||||
scope :expired, -> { pending.where("posts.created_at < ?", Danbooru.config.moderation_period.ago) }
|
||||
|
||||
scope :unflagged, -> { where(is_flagged: false) }
|
||||
scope :has_notes, -> { where.not(last_noted_at: nil) }
|
||||
@@ -237,9 +240,9 @@ class Post < ApplicationRecord
|
||||
|
||||
def large_image_width
|
||||
if has_large?
|
||||
[Danbooru.config.large_image_width, image_width].min
|
||||
[Danbooru.config.large_image_width, image_width.to_i].min
|
||||
else
|
||||
image_width
|
||||
image_width.to_i
|
||||
end
|
||||
end
|
||||
|
||||
@@ -269,6 +272,7 @@ class Post < ApplicationRecord
|
||||
end
|
||||
|
||||
def resize_percentage
|
||||
return 100 if image_width.to_i == 0
|
||||
100 * large_image_width.to_f / image_width.to_f
|
||||
end
|
||||
|
||||
@@ -279,12 +283,28 @@ class Post < ApplicationRecord
|
||||
end
|
||||
|
||||
module ApprovalMethods
|
||||
def in_modqueue?
|
||||
is_pending? || is_flagged? || is_appealed?
|
||||
end
|
||||
|
||||
def is_active?
|
||||
!is_deleted? && !in_modqueue?
|
||||
end
|
||||
|
||||
def is_appealed?
|
||||
is_deleted? && appeals.any?(&:pending?)
|
||||
end
|
||||
|
||||
def is_appealable?
|
||||
is_deleted? && !is_appealed?
|
||||
end
|
||||
|
||||
def is_approvable?(user = CurrentUser.user)
|
||||
!is_status_locked? && (is_pending? || is_flagged? || is_deleted?) && uploader != user
|
||||
!is_status_locked? && !is_active? && uploader != user
|
||||
end
|
||||
|
||||
def flag!(reason, is_deletion: false)
|
||||
flag = flags.create(reason: reason, is_resolved: false, is_deletion: is_deletion, creator: CurrentUser.user)
|
||||
flag = flags.create(reason: reason, is_deletion: is_deletion, creator: CurrentUser.user)
|
||||
|
||||
if flag.errors.any?
|
||||
raise PostFlag::Error.new(flag.errors.full_messages.join("; "))
|
||||
@@ -375,12 +395,6 @@ class Post < ApplicationRecord
|
||||
def update_tag_post_counts
|
||||
decrement_tags = tag_array_was - tag_array
|
||||
|
||||
decrement_tags_except_requests = decrement_tags.reject {|tag| tag == "tagme" || tag.end_with?("_request")}
|
||||
if !decrement_tags_except_requests.empty? && !CurrentUser.is_builder? && CurrentUser.created_at > 1.week.ago
|
||||
self.errors.add(:updater_id, "must have an account at least 1 week old to remove tags")
|
||||
return false
|
||||
end
|
||||
|
||||
increment_tags = tag_array - tag_array_was
|
||||
if increment_tags.any?
|
||||
Tag.increment_post_counts(increment_tags)
|
||||
@@ -398,24 +412,16 @@ class Post < ApplicationRecord
|
||||
set_tag_count(category, self.send("tag_count_#{category}") + 1)
|
||||
end
|
||||
|
||||
def set_tag_counts(disable_cache = true)
|
||||
def set_tag_counts
|
||||
self.tag_count = 0
|
||||
TagCategory.categories.each {|x| set_tag_count(x, 0)}
|
||||
categories = Tag.categories_for(tag_array, :disable_caching => disable_cache)
|
||||
categories = Tag.categories_for(tag_array, disable_caching: true)
|
||||
categories.each_value do |category|
|
||||
self.tag_count += 1
|
||||
inc_tag_count(TagCategory.reverse_mapping[category])
|
||||
end
|
||||
end
|
||||
|
||||
def fix_post_counts(post)
|
||||
post.set_tag_counts(false)
|
||||
if post.changes_saved?
|
||||
args = Hash[TagCategory.categories.map {|x| ["tag_count_#{x}", post.send("tag_count_#{x}")]}].update(:tag_count => post.tag_count)
|
||||
post.update_columns(args)
|
||||
end
|
||||
end
|
||||
|
||||
def merge_old_changes
|
||||
reset_tag_array_cache
|
||||
@removed_tags = []
|
||||
@@ -932,14 +938,7 @@ class Post < ApplicationRecord
|
||||
end
|
||||
|
||||
def update_children_on_destroy
|
||||
return unless children.present?
|
||||
|
||||
eldest = children[0]
|
||||
siblings = children[1..-1]
|
||||
|
||||
eldest.update(parent_id: nil)
|
||||
Post.where(id: siblings).find_each { |p| p.update(parent_id: eldest.id) }
|
||||
# Post.where(id: siblings).update(parent_id: eldest.id) # XXX rails 5
|
||||
children.update(parent: nil)
|
||||
end
|
||||
|
||||
def update_parent_on_save
|
||||
@@ -949,7 +948,7 @@ class Post < ApplicationRecord
|
||||
Post.find(parent_id_before_last_save).update_has_children_flag if parent_id_before_last_save.present?
|
||||
end
|
||||
|
||||
def give_favorites_to_parent(options = {})
|
||||
def give_favorites_to_parent
|
||||
return if parent.nil?
|
||||
|
||||
transaction do
|
||||
@@ -959,9 +958,7 @@ class Post < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
unless options[:without_mod_action]
|
||||
ModAction.log("moved favorites from post ##{id} to post ##{parent.id}", :post_move_favorites)
|
||||
end
|
||||
ModAction.log("moved favorites from post ##{id} to post ##{parent.id}", :post_move_favorites)
|
||||
end
|
||||
|
||||
def has_visible_children?
|
||||
@@ -985,9 +982,8 @@ class Post < ApplicationRecord
|
||||
|
||||
transaction do
|
||||
Post.without_timeout do
|
||||
ModAction.log("permanently deleted post ##{id}", :post_permanent_delete)
|
||||
ModAction.log("permanently deleted post ##{id} (md5=#{md5})", :post_permanent_delete)
|
||||
|
||||
give_favorites_to_parent
|
||||
update_children_on_destroy
|
||||
decrement_tag_post_counts
|
||||
remove_from_all_pools
|
||||
@@ -1009,29 +1005,22 @@ class Post < ApplicationRecord
|
||||
ModAction.log("unbanned post ##{id}", :post_unban)
|
||||
end
|
||||
|
||||
def delete!(reason, options = {})
|
||||
if is_status_locked?
|
||||
self.errors.add(:is_status_locked, "; cannot delete post")
|
||||
return false
|
||||
end
|
||||
def delete!(reason, move_favorites: false, user: CurrentUser.user)
|
||||
transaction do
|
||||
automated = (user == User.system)
|
||||
|
||||
Post.transaction do
|
||||
flag!(reason, is_deletion: true)
|
||||
flags.pending.update!(status: :succeeded)
|
||||
appeals.pending.update!(status: :rejected)
|
||||
|
||||
update(
|
||||
is_deleted: true,
|
||||
is_pending: false,
|
||||
is_flagged: false,
|
||||
is_banned: is_banned || options[:ban] || has_tag?("banned_artist")
|
||||
)
|
||||
flags.create!(reason: reason, is_deletion: true, creator: user, status: :succeeded)
|
||||
update!(is_deleted: true, is_pending: false, is_flagged: false)
|
||||
|
||||
# XXX This must happen *after* the `is_deleted` flag is set to true (issue #3419).
|
||||
give_favorites_to_parent(options) if options[:move_favorites]
|
||||
give_favorites_to_parent if move_favorites
|
||||
|
||||
is_automatic = (reason == "Unapproved in three days")
|
||||
uploader.upload_limit.update_limit!(self, incremental: is_automatic)
|
||||
uploader.upload_limit.update_limit!(self, incremental: automated)
|
||||
|
||||
unless options[:without_mod_action]
|
||||
unless automated
|
||||
ModAction.log("deleted post ##{id}, reason: #{reason}", :post_delete)
|
||||
end
|
||||
end
|
||||
@@ -1213,8 +1202,6 @@ class Post < ApplicationRecord
|
||||
def with_flag_stats
|
||||
relation = left_outer_joins(:flags).group(:id).select("posts.*")
|
||||
relation = relation.select("COUNT(post_flags.id) AS flag_count")
|
||||
relation = relation.select("COUNT(post_flags.id) FILTER (WHERE post_flags.is_resolved = TRUE) AS resolved_flag_count")
|
||||
relation = relation.select("COUNT(post_flags.id) FILTER (WHERE post_flags.is_resolved = FALSE) AS unresolved_flag_count")
|
||||
relation
|
||||
end
|
||||
|
||||
@@ -1256,6 +1243,14 @@ class Post < ApplicationRecord
|
||||
relation
|
||||
end
|
||||
|
||||
def with_queued_at
|
||||
relation = group(:id)
|
||||
relation = relation.left_outer_joins(:flags, :appeals)
|
||||
relation = relation.select("posts.*")
|
||||
relation = relation.select(Arel.sql("MAX(GREATEST(posts.created_at, post_flags.created_at, post_appeals.created_at)) AS queued_at"))
|
||||
relation
|
||||
end
|
||||
|
||||
def with_stats(tables)
|
||||
return all if tables.empty?
|
||||
|
||||
|
||||
@@ -1,57 +1,38 @@
|
||||
class PostAppeal < ApplicationRecord
|
||||
class Error < StandardError; end
|
||||
|
||||
MAX_APPEALS_PER_DAY = 1
|
||||
|
||||
belongs_to :creator, :class_name => "User"
|
||||
belongs_to :post
|
||||
validates_presence_of :reason
|
||||
validates :reason, presence: true, length: { in: 1..140 }
|
||||
validate :validate_post_is_inactive
|
||||
validate :validate_creator_is_not_limited
|
||||
validates_uniqueness_of :creator_id, :scope => :post_id, :message => "have already appealed this post"
|
||||
|
||||
scope :resolved, -> { where(post: Post.undeleted.unflagged) }
|
||||
scope :unresolved, -> { where(post: Post.deleted.or(Post.flagged)) }
|
||||
scope :recent, -> { where("post_appeals.created_at >= ?", 1.day.ago) }
|
||||
validates :reason, length: { maximum: 140 }
|
||||
validate :validate_post_is_appealable, on: :create
|
||||
validate :validate_creator_is_not_limited, on: :create
|
||||
validates :creator, uniqueness: { scope: :post, message: "have already appealed this post" }, on: :create
|
||||
|
||||
enum status: {
|
||||
pending: 0,
|
||||
succeeded: 1,
|
||||
rejected: 2
|
||||
}
|
||||
|
||||
scope :expired, -> { pending.where("post_appeals.created_at < ?", Danbooru.config.moderation_period.ago) }
|
||||
|
||||
module SearchMethods
|
||||
def search(params)
|
||||
q = super
|
||||
q = q.search_attributes(params, :reason)
|
||||
q = q.search_attributes(params, :reason, :status)
|
||||
q = q.text_attribute_matches(:reason, params[:reason_matches])
|
||||
|
||||
q = q.resolved if params[:is_resolved].to_s.truthy?
|
||||
q = q.unresolved if params[:is_resolved].to_s.falsy?
|
||||
|
||||
q.apply_default_order(params)
|
||||
end
|
||||
end
|
||||
|
||||
extend SearchMethods
|
||||
|
||||
def resolved?
|
||||
post.present? && !post.is_deleted? && !post.is_flagged?
|
||||
end
|
||||
|
||||
def is_resolved
|
||||
resolved?
|
||||
end
|
||||
|
||||
def validate_creator_is_not_limited
|
||||
if appeal_count_for_creator >= MAX_APPEALS_PER_DAY
|
||||
errors[:creator] << "can appeal at most #{MAX_APPEALS_PER_DAY} post a day"
|
||||
end
|
||||
errors[:creator] << "have reached your appeal limit" if creator.is_appeal_limited?
|
||||
end
|
||||
|
||||
def validate_post_is_inactive
|
||||
if resolved?
|
||||
errors[:post] << "is active"
|
||||
end
|
||||
end
|
||||
|
||||
def appeal_count_for_creator
|
||||
creator.post_appeals.recent.count
|
||||
def validate_post_is_appealable
|
||||
errors[:post] << "cannot be appealed" if post.is_status_locked? || !post.is_appealable?
|
||||
end
|
||||
|
||||
def self.searchable_includes
|
||||
|
||||
@@ -12,7 +12,7 @@ class PostApproval < ApplicationRecord
|
||||
errors.add(:post, "is locked and cannot be approved")
|
||||
end
|
||||
|
||||
if post.status == "active"
|
||||
if post.is_active?
|
||||
errors.add(:post, "is already active and cannot be approved")
|
||||
end
|
||||
|
||||
@@ -28,7 +28,9 @@ class PostApproval < ApplicationRecord
|
||||
def approve_post
|
||||
is_undeletion = post.is_deleted
|
||||
|
||||
post.flags.each(&:resolve!)
|
||||
post.flags.pending.update!(status: :rejected)
|
||||
post.appeals.pending.update!(status: :succeeded)
|
||||
|
||||
post.update(approver: user, is_flagged: false, is_pending: false, is_deleted: false)
|
||||
ModAction.log("undeleted post ##{post_id}", :post_undelete) if is_undeletion
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class PostDisapproval < ApplicationRecord
|
||||
end
|
||||
|
||||
def validate_disapproval
|
||||
if post.status == "active"
|
||||
if post.is_active?
|
||||
errors[:post] << "is already active and cannot be disapproved"
|
||||
end
|
||||
end
|
||||
|
||||
@@ -30,10 +30,6 @@ class PostEvent
|
||||
event.try(:reason) || ""
|
||||
end
|
||||
|
||||
def is_resolved
|
||||
event.try(:is_resolved) || false
|
||||
end
|
||||
|
||||
def creator_id
|
||||
event.try(:creator_id) || event.try(:user_id)
|
||||
end
|
||||
@@ -42,6 +38,18 @@ class PostEvent
|
||||
event.try(:creator) || event.try(:user)
|
||||
end
|
||||
|
||||
def status
|
||||
if event.is_a?(PostApproval)
|
||||
"approved"
|
||||
elsif (event.is_a?(PostAppeal) && event.succeeded?) || (event.is_a?(PostFlag) && event.rejected?)
|
||||
"approved"
|
||||
elsif (event.is_a?(PostAppeal) && event.rejected?) || (event.is_a?(PostFlag) && event.succeeded?)
|
||||
"deleted"
|
||||
else
|
||||
"pending"
|
||||
end
|
||||
end
|
||||
|
||||
def is_creator_visible?(user = CurrentUser.user)
|
||||
case event
|
||||
when PostAppeal, PostApproval
|
||||
@@ -57,7 +65,7 @@ class PostEvent
|
||||
"creator_id": nil,
|
||||
"created_at": nil,
|
||||
"reason": nil,
|
||||
"is_resolved": nil,
|
||||
"status": nil,
|
||||
"type": nil
|
||||
}
|
||||
end
|
||||
|
||||
@@ -6,24 +6,26 @@ class PostFlag < ApplicationRecord
|
||||
REJECTED = "Unapproved in three days after returning to moderation queue%"
|
||||
end
|
||||
|
||||
COOLDOWN_PERIOD = 3.days
|
||||
|
||||
belongs_to :creator, class_name: "User"
|
||||
belongs_to :post
|
||||
validates :reason, presence: true, length: { in: 1..140 }
|
||||
validate :validate_creator_is_not_limited, on: :create
|
||||
validate :validate_post
|
||||
validates_uniqueness_of :creator_id, :scope => :post_id, :on => :create, :unless => :is_deletion, :message => "have already flagged this post"
|
||||
validate :validate_post, on: :create
|
||||
validates_uniqueness_of :creator_id, scope: :post_id, on: :create, unless: :is_deletion, message: "have already flagged this post"
|
||||
before_save :update_post
|
||||
attr_accessor :is_deletion
|
||||
|
||||
enum status: {
|
||||
pending: 0,
|
||||
succeeded: 1,
|
||||
rejected: 2
|
||||
}
|
||||
|
||||
scope :by_users, -> { where.not(creator: User.system) }
|
||||
scope :by_system, -> { where(creator: User.system) }
|
||||
scope :in_cooldown, -> { by_users.where("created_at >= ?", COOLDOWN_PERIOD.ago) }
|
||||
scope :resolved, -> { where(is_resolved: true) }
|
||||
scope :unresolved, -> { where(is_resolved: false) }
|
||||
scope :recent, -> { where("post_flags.created_at >= ?", 1.day.ago) }
|
||||
scope :old, -> { where("post_flags.created_at <= ?", 3.days.ago) }
|
||||
scope :in_cooldown, -> { by_users.where("created_at >= ?", Danbooru.config.moderation_period.ago) }
|
||||
scope :expired, -> { pending.where("post_flags.created_at < ?", Danbooru.config.moderation_period.ago) }
|
||||
scope :active, -> { pending.or(rejected.in_cooldown) }
|
||||
|
||||
module SearchMethods
|
||||
def creator_matches(creator, searcher)
|
||||
@@ -56,7 +58,7 @@ class PostFlag < ApplicationRecord
|
||||
def search(params)
|
||||
q = super
|
||||
|
||||
q = q.search_attributes(params, :is_resolved, :reason)
|
||||
q = q.search_attributes(params, :reason, :status)
|
||||
q = q.text_attribute_matches(:reason, params[:reason_matches])
|
||||
|
||||
if params[:creator_id].present?
|
||||
@@ -93,36 +95,18 @@ class PostFlag < ApplicationRecord
|
||||
end
|
||||
|
||||
def validate_creator_is_not_limited
|
||||
return if is_deletion
|
||||
|
||||
if creator.can_approve_posts?
|
||||
# do nothing
|
||||
elsif creator.created_at > 1.week.ago
|
||||
errors[:creator] << "cannot flag within the first week of sign up"
|
||||
elsif creator.is_gold? && flag_count_for_creator >= 10
|
||||
errors[:creator] << "can flag 10 posts a day"
|
||||
elsif !creator.is_gold? && flag_count_for_creator >= 1
|
||||
errors[:creator] << "can flag 1 post a day"
|
||||
end
|
||||
|
||||
flag = post.flags.in_cooldown.last
|
||||
if flag.present?
|
||||
errors[:post] << "cannot be flagged more than once every #{COOLDOWN_PERIOD.inspect} (last flagged: #{flag.created_at.to_s(:long)})"
|
||||
end
|
||||
errors[:creator] << "have reached your flag limit" if creator.is_flag_limited? && !is_deletion
|
||||
end
|
||||
|
||||
def validate_post
|
||||
errors[:post] << "is pending and cannot be flagged" if post.is_pending? && !is_deletion
|
||||
errors[:post] << "is deleted and cannot be flagged" if post.is_deleted? && !is_deletion
|
||||
errors[:post] << "is locked and cannot be flagged" if post.is_status_locked?
|
||||
errors[:post] << "is deleted" if post.is_deleted?
|
||||
end
|
||||
|
||||
def resolve!
|
||||
update_column(:is_resolved, true)
|
||||
end
|
||||
|
||||
def flag_count_for_creator
|
||||
creator.post_flags.recent.count
|
||||
flag = post.flags.in_cooldown.last
|
||||
if !is_deletion && flag.present?
|
||||
errors[:post] << "cannot be flagged more than once every #{Danbooru.config.moderation_period.inspect} (last flagged: #{flag.created_at.to_s(:long)})"
|
||||
end
|
||||
end
|
||||
|
||||
def uploader_id
|
||||
|
||||
@@ -15,12 +15,6 @@ class PostVersion < ApplicationRecord
|
||||
|
||||
establish_connection database_url if enabled?
|
||||
|
||||
def self.check_for_retry(msg)
|
||||
if msg =~ /can't get socket descriptor/ && msg =~ /post_versions/
|
||||
connection.reconnect!
|
||||
end
|
||||
end
|
||||
|
||||
module SearchMethods
|
||||
def changed_tags_include(tag)
|
||||
where_array_includes_all(:added_tags, [tag]).or(where_array_includes_all(:removed_tags, [tag]))
|
||||
@@ -32,6 +26,10 @@ class PostVersion < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def changed_tags_include_any(tags)
|
||||
where_array_includes_any(:added_tags, tags).or(where_array_includes_any(:removed_tags, tags))
|
||||
end
|
||||
|
||||
def tag_matches(string)
|
||||
tag = string.match(/\S+/)[0]
|
||||
return all if tag.nil?
|
||||
@@ -47,6 +45,14 @@ class PostVersion < ApplicationRecord
|
||||
q = q.changed_tags_include_all(params[:changed_tags].scan(/[^[:space:]]+/))
|
||||
end
|
||||
|
||||
if params[:all_changed_tags]
|
||||
q = q.changed_tags_include_all(params[:all_changed_tags].scan(/[^[:space:]]+/))
|
||||
end
|
||||
|
||||
if params[:any_changed_tags]
|
||||
q = q.changed_tags_include_any(params[:any_changed_tags].scan(/[^[:space:]]+/))
|
||||
end
|
||||
|
||||
if params[:tag_matches]
|
||||
q = q.tag_matches(params[:tag_matches])
|
||||
end
|
||||
|
||||
@@ -11,8 +11,8 @@ class Tag < ApplicationRecord
|
||||
validates :name, tag_name: true, on: :name
|
||||
validates_inclusion_of :category, in: TagCategory.category_ids
|
||||
|
||||
before_save :update_category_cache, if: :category_changed?
|
||||
before_save :update_category_post_counts, if: :category_changed?
|
||||
after_save :update_category_cache, if: :saved_change_to_category?
|
||||
after_save :update_category_post_counts, if: :saved_change_to_category?
|
||||
|
||||
scope :empty, -> { where("tags.post_count <= 0") }
|
||||
scope :nonempty, -> { where("tags.post_count > 0") }
|
||||
@@ -163,12 +163,10 @@ class Tag < ApplicationRecord
|
||||
end
|
||||
|
||||
def update_category_post_counts
|
||||
Post.with_timeout(30_000, nil, :tags => name) do
|
||||
Post.raw_tag_match(name).where("true /* Tag#update_category_post_counts */").find_each do |post|
|
||||
post.reload
|
||||
post.set_tag_counts(false)
|
||||
args = TagCategory.categories.map {|x| ["tag_count_#{x}", post.send("tag_count_#{x}")]}.to_h.update(:tag_count => post.tag_count)
|
||||
Post.where(:id => post.id).update_all(args)
|
||||
Post.with_timeout(30_000) do
|
||||
Post.raw_tag_match(name).find_each do |post|
|
||||
post.set_tag_counts
|
||||
post.save!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -265,6 +265,10 @@ class User < ApplicationRecord
|
||||
name.match?(/\Auser_[0-9]+~*\z/)
|
||||
end
|
||||
|
||||
def is_restricted?
|
||||
requires_verification? && !is_verified?
|
||||
end
|
||||
|
||||
def is_anonymous?
|
||||
level == Levels::ANONYMOUS
|
||||
end
|
||||
@@ -343,6 +347,26 @@ class User < ApplicationRecord
|
||||
end
|
||||
end
|
||||
|
||||
def is_appeal_limited?
|
||||
return false if can_upload_free?
|
||||
upload_limit.free_upload_slots < UploadLimit::APPEAL_COST
|
||||
end
|
||||
|
||||
def is_flag_limited?
|
||||
return false if has_unlimited_flags?
|
||||
post_flags.active.count >= 5
|
||||
end
|
||||
|
||||
# Flags are unlimited if you're an approver or you have at least 30 flags
|
||||
# in the last 3 months and have a 70% flag success rate.
|
||||
def has_unlimited_flags?
|
||||
return true if can_approve_posts?
|
||||
|
||||
recent_flags = post_flags.where("created_at >= ?", 3.months.ago)
|
||||
flag_ratio = recent_flags.succeeded.count / recent_flags.count.to_f
|
||||
recent_flags.count >= 30 && flag_ratio >= 0.70
|
||||
end
|
||||
|
||||
def upload_limit
|
||||
@upload_limit ||= UploadLimit.new(self)
|
||||
end
|
||||
|
||||
@@ -9,6 +9,15 @@ class EmailAddressPolicy < ApplicationPolicy
|
||||
end
|
||||
|
||||
def verify?
|
||||
record.valid_key?(request.params[:email_verification_key])
|
||||
if request.params[:email_verification_key].present?
|
||||
record.valid_key?(request.params[:email_verification_key])
|
||||
else
|
||||
record.user_id == user.id
|
||||
end
|
||||
end
|
||||
|
||||
def send_confirmation?
|
||||
# XXX record is a user, not the email address.
|
||||
record.id == user.id
|
||||
end
|
||||
end
|
||||
|
||||
@@ -31,6 +31,10 @@ class PostPolicy < ApplicationPolicy
|
||||
user.is_approver? && !record.is_deleted?
|
||||
end
|
||||
|
||||
def destroy?
|
||||
delete?
|
||||
end
|
||||
|
||||
def ban?
|
||||
user.is_approver? && !record.is_banned?
|
||||
end
|
||||
|
||||
@@ -2,12 +2,12 @@ class PostPresenter
|
||||
attr_reader :pool, :next_post_in_pool
|
||||
delegate :tag_list_html, :split_tag_list_html, :split_tag_list_text, :inline_tag_list_html, to: :tag_set_presenter
|
||||
|
||||
def self.preview(post, options = {})
|
||||
def self.preview(post, show_deleted: false, tags: "", **options)
|
||||
if post.nil?
|
||||
return "<em>none</em>".html_safe
|
||||
end
|
||||
|
||||
if !options[:show_deleted] && post.is_deleted? && options[:tags] !~ /status:(?:all|any|deleted|banned)/
|
||||
if post.is_deleted? && !show_deleted
|
||||
return ""
|
||||
end
|
||||
|
||||
@@ -31,8 +31,8 @@ class PostPresenter
|
||||
locals[:link_target] = options[:link_target] || post
|
||||
|
||||
locals[:link_params] = {}
|
||||
if options[:tags].present? && !CurrentUser.is_anonymous?
|
||||
locals[:link_params]["q"] = options[:tags]
|
||||
if tags.present? && !CurrentUser.is_anonymous?
|
||||
locals[:link_params]["q"] = tags
|
||||
end
|
||||
if options[:pool_id]
|
||||
locals[:link_params]["pool_id"] = options[:pool_id]
|
||||
@@ -116,6 +116,8 @@ class PostPresenter
|
||||
"data-pools" => post.pool_string,
|
||||
"data-approver-id" => post.approver_id,
|
||||
"data-rating" => post.rating,
|
||||
"data-large-width" => post.large_image_width,
|
||||
"data-large-height" => post.large_image_height,
|
||||
"data-width" => post.image_width,
|
||||
"data-height" => post.image_height,
|
||||
"data-flags" => post.status_flags,
|
||||
|
||||
@@ -35,7 +35,10 @@ class TagSetPresenter
|
||||
typetags = tags_for_category(category)
|
||||
|
||||
if typetags.any?
|
||||
html << TagCategory.header_mapping[category] if headers
|
||||
if headers
|
||||
html << %{<h3 class="#{category}-tag-list">#{category.capitalize.pluralize(typetags.size)}</h3>}
|
||||
end
|
||||
|
||||
html << %{<ul class="#{category}-tag-list">}
|
||||
typetags.each do |tag|
|
||||
html << build_list_item(tag, current_query: current_query, show_extra_links: show_extra_links, name_only: name_only, humanize_tags: humanize_tags)
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
</ul>
|
||||
|
||||
<div style="margin: 1em 0;">
|
||||
<h2>Script</h2>
|
||||
<div class="prose">
|
||||
<%= format_text @bulk_update_request.processor.to_dtext %>
|
||||
</div>
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
<% end %>
|
||||
data-is-voted="<%= comment.voted_by?(CurrentUser.user) %>">
|
||||
<div class="author">
|
||||
<h4>
|
||||
<div class="author-name">
|
||||
<%= link_to_user comment.creator %>
|
||||
<% if comment.is_deleted? %>
|
||||
(deleted)
|
||||
<% end %>
|
||||
</h4>
|
||||
</div>
|
||||
<%= link_to time_ago_in_words_tagged(comment.created_at), post_path(comment.post, anchor: "comment_#{comment.id}"), class: "message-timestamp" %>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
||||
@@ -4,8 +4,7 @@
|
||||
<div id="c-dmails">
|
||||
<div id="a-show">
|
||||
<div class="dmail">
|
||||
<h1>Show Message</h1>
|
||||
<h2><%= @dmail.title %></h2>
|
||||
<h1><%= @dmail.title %></h1>
|
||||
|
||||
<ul style="margin-bottom: 1em;">
|
||||
<li><strong>Sender</strong>: <%= link_to_user @dmail.from %></li>
|
||||
@@ -13,7 +12,6 @@
|
||||
<li><strong>Date</strong>: <%= compact_time(@dmail.created_at) %></li>
|
||||
</ul>
|
||||
|
||||
<h3>Body</h3>
|
||||
<div class="prose">
|
||||
<%= format_text(@dmail.body) %>
|
||||
|
||||
|
||||
@@ -1,14 +1,30 @@
|
||||
<% page_title "Change Email" %>
|
||||
|
||||
<div id="c-emails">
|
||||
<div id="a-edit">
|
||||
<h1>Change Email</h1>
|
||||
<div id="a-edit" class="fixed-width-container">
|
||||
<% if @user.email_address.present? %>
|
||||
<% page_title "Change Email" %>
|
||||
<h1>Change Email</h1>
|
||||
|
||||
<p>You must confirm your password in order to change your email address.</p>
|
||||
<p>Your current email address is <strong><%= @user.email_address.address %></strong>.
|
||||
You must re-enter your password in order to update your email address.</p>
|
||||
<% else %>
|
||||
<% page_title "Add Email" %>
|
||||
<h1>Add Email</h1>
|
||||
|
||||
<p>Add a new email address below. You must re-enter your password in
|
||||
order to update your email address.</p>
|
||||
<% end %>
|
||||
|
||||
<% if @user.is_restricted? %>
|
||||
<p>Your account is restricted because you signed up from a proxy or VPN.
|
||||
You can still use the site, but you won't be able to leave comments, edit
|
||||
tags, or upload posts until you add a verified email address to your
|
||||
account. Disposable or throwaway email addresses can't be used to verify
|
||||
your account.</p>
|
||||
<% end %>
|
||||
|
||||
<%= edit_form_for(@user, url: user_email_path(@user)) do |f| %>
|
||||
<%= f.input :email, as: :email, label: "New Email", input_html: { value: "" } %>
|
||||
<%= f.input :password %>
|
||||
<%= f.input :email, as: :email, input_html: { value: "" } %>
|
||||
<%= f.submit "Save" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
20
app/views/emails/verify.html.erb
Normal file
20
app/views/emails/verify.html.erb
Normal file
@@ -0,0 +1,20 @@
|
||||
<% page_title "Verify account" %>
|
||||
|
||||
<div id="c-emails">
|
||||
<div id="a-verify" class="fixed-width-container">
|
||||
<h1>Verify account</h1>
|
||||
|
||||
<% if @user.is_restricted? %>
|
||||
<p>Your account is restricted because you signed up from a VPN or proxy.
|
||||
You can still use the site, but you won't be able to leave comments, edit
|
||||
tags, or upload posts until you verify your account.</p>
|
||||
<% end %>
|
||||
|
||||
<p>Click below to send an email to <strong><%= @email_address.address %></strong>
|
||||
to verify your account.</p>
|
||||
|
||||
<%= edit_form_for(@user, method: :post, url: send_confirmation_user_email_path(@user)) do |f| %>
|
||||
<%= f.submit "Send confirmation email" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
@@ -6,12 +6,12 @@
|
||||
<% end %>
|
||||
data-creator="<%= forum_post.creator.name %>">
|
||||
<div class="author">
|
||||
<h4>
|
||||
<div class="author-name">
|
||||
<%= link_to_user forum_post.creator %>
|
||||
<% if forum_post.is_deleted? %>
|
||||
(deleted)
|
||||
<% end %>
|
||||
</h4>
|
||||
</div>
|
||||
<%= link_to time_ago_in_words_tagged(forum_post.created_at), forum_post, class: "message-timestamp" %>
|
||||
</div>
|
||||
<div class="content">
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
|
||||
$("article[data-forum-post-id=<%= @forum_post.id %>] div.author h4").append(" (deleted)");
|
||||
$("article[data-forum-post-id=<%= @forum_post.id %>] div.author div.author-name").append(" (deleted)");
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
<%= render "news_updates/listing" %>
|
||||
|
||||
<header id="top">
|
||||
<h1 id="app-name-header"><%= link_to Danbooru.config.app_name, "/" %></h1>
|
||||
<%= link_to Danbooru.config.app_name, root_path, id: "app-name-header", class: "heading" %>
|
||||
|
||||
<div id="maintoggle" class="mobile-only">
|
||||
<a href="#"><i id="maintoggle-on" class="fas fa-bars"></i></a>
|
||||
@@ -68,6 +68,8 @@
|
||||
</header>
|
||||
|
||||
<div id="page">
|
||||
<%= render "users/verification_notice" %>
|
||||
|
||||
<% if !CurrentUser.is_anonymous? && !CurrentUser.is_gold? && cookies[:hide_upgrade_account_notice].blank? && params[:action] != "upgrade_information" %>
|
||||
<%= render "users/upgrade_notice" %>
|
||||
<% end %>
|
||||
@@ -88,6 +90,11 @@
|
||||
<%= yield :layout %>
|
||||
</div>
|
||||
|
||||
<div id="tooltips">
|
||||
<div id="post-tooltips"></div>
|
||||
<div id="user-tooltips"></div>
|
||||
</div>
|
||||
|
||||
<script type="application/javascript">
|
||||
if (typeof window.Danbooru !== "object") {
|
||||
window.Danbooru = {};
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
<table class="striped">
|
||||
<caption>Appeals</caption>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Post</th>
|
||||
<th>User</th>
|
||||
<th>Flags</th>
|
||||
<th>Appeals</th>
|
||||
<th>Score</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @dashboard.appeals.each do |post| %>
|
||||
<tr>
|
||||
<td><%= PostPresenter.preview(post, show_deleted: true) %></td>
|
||||
<td><%= mod_link_to_user post.uploader, :negative %></td>
|
||||
<td><%= render "post_flags/reasons", flags: post.flags %></td>
|
||||
<td><%= render "post_appeals/reasons", appeals: post.appeals %></td>
|
||||
<td><%= post.score %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<p><%= link_to "View all appeals", post_appeals_path %></p>
|
||||
@@ -11,14 +11,12 @@
|
||||
<div id="column-left" class="column column-expand">
|
||||
<div class="activity"><%= render "activity_upload" %></div>
|
||||
<div class="activity"><%= render "activity_note" %></div>
|
||||
<div class="activity"><%= render "activity_tag" %></div>
|
||||
<div class="activity"><%= render "activity_wiki_page" %></div>
|
||||
<div class="activity"><%= render "activity_artist" %></div>
|
||||
<div class="activity"><%= render "activity_comment" %></div>
|
||||
</div>
|
||||
|
||||
<div id="column-right" class="column column-expand">
|
||||
<div class="activity"><%= render "activity_appeal" %></div>
|
||||
<div class="activity"><%= render "activity_comment" %></div>
|
||||
<div class="activity"><%= render "activity_user_feedback" %></div>
|
||||
<div class="activity"><%= render "activity_mod_action" %></div>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<h1>Delete Post</h1>
|
||||
|
||||
<div>
|
||||
<%= PostPresenter.preview(@post, show_deleted: true) %>
|
||||
</div>
|
||||
|
||||
<%= form_tag(delete_moderator_post_post_path, :style => "clear: both;", :class => "simple_form") do %>
|
||||
<% if @post.parent_id %>
|
||||
<div class="input">
|
||||
<label for="move_favorites">
|
||||
<%= check_box_tag "move_favorites" %>
|
||||
Move favorites to parent?
|
||||
</label>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<p style="font-weight: bold;">Note: If the reason you are planning to delete this post is because it is from a banned artist, please <%= link_to "ban", confirm_ban_moderator_post_post_path(@post) %> this post instead of deleting it.</p>
|
||||
|
||||
<div class="input">
|
||||
<label for="reason">Reason</label>
|
||||
<%= text_area_tag "reason" %>
|
||||
</div>
|
||||
|
||||
<%= submit_tag "Delete" %>
|
||||
<%= submit_tag "Cancel" %>
|
||||
<% end %>
|
||||
@@ -1,6 +1,6 @@
|
||||
<%= content_tag(:div, { id: "post-#{post.id}", class: ["post", "mod-queue-preview", "column-container", *PostPresenter.preview_class(post)].join(" ") }.merge(PostPresenter.data_attributes(post))) do %>
|
||||
<aside class="column column-shrink">
|
||||
<%= PostPresenter.preview(post, size: true) %>
|
||||
<%= PostPresenter.preview(post, size: true, show_deleted: true) %>
|
||||
</aside>
|
||||
|
||||
<section class="column column-expand">
|
||||
@@ -64,14 +64,14 @@
|
||||
<% if post.is_flagged? %>
|
||||
<span class="info">
|
||||
<strong>Flagged</strong>
|
||||
<%= render "post_flags/reasons", flags: post.flags %>
|
||||
<%= render "post_flags/reasons", flag: post.flags.select(&:pending?).last %>
|
||||
</span>
|
||||
<% end %>
|
||||
|
||||
<% if (post.is_flagged? || post.is_deleted?) && post.appeals.any? %>
|
||||
<% if post.is_appealed? %>
|
||||
<span class="info">
|
||||
<strong>Appeals</strong>
|
||||
<%= render "post_appeals/reasons", appeals: post.appeals %>
|
||||
<%= render "post_appeals/reasons", appeal: post.appeals.select(&:pending?).last %>
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<%= render "posts/partials/index/blacklist" %>
|
||||
|
||||
<p id="modqueue-sidebar-status" class="sidebar-section">
|
||||
<h6>Status</h6>
|
||||
<h2>Status</h2>
|
||||
<ul>
|
||||
<li>
|
||||
<%= link_to "status:pending", modqueue_index_path(search: { tags: "status:pending" }) %>
|
||||
@@ -22,6 +22,10 @@
|
||||
<%= link_to "status:flagged", modqueue_index_path(search: { tags: "status:flagged" }) %>
|
||||
<span class="post-count"><%= @flagged_post_count %></span>
|
||||
</li>
|
||||
<li>
|
||||
<%= link_to "status:appealed", modqueue_index_path(search: { tags: "status:appealed" }) %>
|
||||
<span class="post-count"><%= @appealed_post_count %></span>
|
||||
</li>
|
||||
|
||||
<% @disapproval_reasons.each do |reason, count| %>
|
||||
<li>
|
||||
@@ -33,7 +37,7 @@
|
||||
</p>
|
||||
|
||||
<p id="modqueue-sidebar-uploaders" class="sidebar-section">
|
||||
<h6>Uploaders</h6>
|
||||
<h2>Uploaders</h2>
|
||||
<ul>
|
||||
<% @uploaders.each do |uploader, count| %>
|
||||
<li>
|
||||
@@ -46,7 +50,7 @@
|
||||
</p>
|
||||
|
||||
<p id="modqueue-sidebar-tags" class="sidebar-section">
|
||||
<h6>Tags</h6>
|
||||
<h2>Tags</h2>
|
||||
|
||||
<%= render "tag_list", tags: @artist_tags %>
|
||||
<%= render "tag_list", tags: @copyright_tags %>
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
<%= edit_form_for(post_appeal, format: :js, remote: true) do |f| %>
|
||||
<%= f.hidden_field :post_id %>
|
||||
<%= f.input :reason, as: :dtext, inline: true %>
|
||||
<%= dtext_preview_button "post_appeal_reason" %>
|
||||
<%= f.input :reason, as: :dtext, inline: true, placeholder: "Optional" %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
<ul class="post-appeal-reasons list-bulleted">
|
||||
<% appeals.each do |appeal| %>
|
||||
<li class="post-appeal-reason">
|
||||
<ul class="post-appeal-reason list-bulleted">
|
||||
<li>
|
||||
<% if appeal.reason.present? %>
|
||||
<span class="prose"><%= format_text(appeal.reason, inline: true) %></span>
|
||||
- <%= link_to_user(appeal.creator) %>
|
||||
- <%= time_ago_in_words_tagged(appeal.created_at) %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<span class="prose"><em>no reason</em></span>
|
||||
<% end %>
|
||||
|
||||
(<%= link_to_user(appeal.creator) %>, <%= time_ago_in_words_tagged(appeal.created_at) %>)
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
<%= f.input :post_tags_match, label: "Tags", input_html: { value: params[:search][:post_tags_match], data: { autocomplete: "tag-query" } } %>
|
||||
<%= f.input :post_id, label: "Post ID", input_html: { value: params[:search][:post_id] } %>
|
||||
<%= f.input :creator_name, label: "Creator", input_html: { value: params[:search][:creator_name], data: { autocomplete: "user" } } %>
|
||||
<%= f.input :is_resolved, label: "Resolved?", collection: [["Yes", true], ["No", false]], include_blank: true, selected: params[:search][:is_resolved] %>
|
||||
<%= f.input :status, collection: PostAppeal.statuses, include_blank: true, selected: params[:search][:status] %>
|
||||
<%= f.submit "Search" %>
|
||||
<% end %>
|
||||
|
||||
@@ -16,8 +16,8 @@
|
||||
<% t.column "Appeals", width: "1%" do |post_appeal| %>
|
||||
<%= link_to post_appeal.post.appeals.size, post_appeals_path(search: { post_id: post_appeal.post_id }) %>
|
||||
<% end %>
|
||||
<% t.column "Resolved?", width: "5%" do |post_appeal| %>
|
||||
<%= link_to post_appeal.is_resolved.to_s, post_appeals_path(search: params[:search].merge(is_resolved: post_appeal.is_resolved)) %>
|
||||
<% t.column "Status", width: "5%" do |post_appeal| %>
|
||||
<%= link_to post_appeal.status, post_appeals_path(search: { status: post_appeal.status }) %>
|
||||
<% end %>
|
||||
<% t.column "Uploaded", width: "15%" do |post_appeal| %>
|
||||
<%= compact_time post_appeal.post.created_at %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<% if (CurrentUser.can_approve_posts? || post.created_at < 3.days.ago) && disapprovals.length > 0 %>
|
||||
<% if (CurrentUser.can_approve_posts? || post.created_at < Danbooru.config.moderation_period.ago) && disapprovals.length > 0 %>
|
||||
<% if disapprovals.map(&:reason).grep("breaks_rules").count > 0 %>
|
||||
(breaks rules: <%= disapprovals.map(&:reason).grep("breaks_rules").count %>)
|
||||
<% end %>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<% if (CurrentUser.can_approve_posts? || post.created_at < 3.days.ago) && disapprovals.length > 0 %>
|
||||
<% if (CurrentUser.can_approve_posts? || post.created_at < Danbooru.config.moderation_period.ago) && disapprovals.length > 0 %>
|
||||
<p>
|
||||
It has been reviewed by <%= pluralize disapprovals.length, "approver" %>.
|
||||
|
||||
|
||||
@@ -4,6 +4,14 @@
|
||||
|
||||
<%= table_for @events, class: "striped autofit", width: "100%" do |t| %>
|
||||
<% t.column :type_name, name: "Type" %>
|
||||
<% t.column "Description", td: { class: "col-expand" } do |event| %>
|
||||
<div class="prose">
|
||||
<%= format_text event.reason %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% t.column "Status" do |event| %>
|
||||
<%= event.status %>
|
||||
<% end %>
|
||||
<% t.column "User" do |event| %>
|
||||
<% if event.is_creator_visible? %>
|
||||
<%= link_to_user event.creator %>
|
||||
@@ -12,12 +20,6 @@
|
||||
<% end %>
|
||||
<br><%= time_ago_in_words_tagged event.created_at %>
|
||||
<% end %>
|
||||
<% t.column "Description", td: { class: "col-expand" } do |event| %>
|
||||
<div class="prose">
|
||||
<%= format_text event.reason %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% t.column :is_resolved, name: "Resolved" %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
<ul class="post-flag-reasons list-bulleted">
|
||||
<% flags.each do |flag| %>
|
||||
<li class="post-flag-reason">
|
||||
<span class="prose"><%= format_text(flag.reason, inline: true) %></span>
|
||||
<ul class="post-flag-reason list-bulleted">
|
||||
<li>
|
||||
<span class="prose"><%= format_text(flag.reason, inline: true) %></span>
|
||||
|
||||
<% if policy(flag).can_view_flagger? %>
|
||||
- <%= link_to_user(flag.creator) %>
|
||||
<% end %>
|
||||
|
||||
- <%= time_ago_in_words_tagged(flag.created_at) %>
|
||||
|
||||
<% if flag.is_resolved? %>
|
||||
<span class="resolved">RESOLVED</span>
|
||||
<% end %>
|
||||
</li>
|
||||
<% end %>
|
||||
<% if policy(flag).can_view_flagger? %>
|
||||
(<%= link_to_user(flag.creator) %>, <%= time_ago_in_words_tagged(flag.created_at) %>)
|
||||
<% else %>
|
||||
(<%= time_ago_in_words_tagged(flag.created_at) %>)
|
||||
<% end %>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<% if policy(PostFlag).can_search_flagger? %>
|
||||
<%= f.input :creator_name, label: "Creator", input_html: { value: params[:search][:creator_name], data: { autocomplete: "user" } } %>
|
||||
<% end %>
|
||||
<%= f.input :is_resolved, label: "Resolved?", collection: [["Yes", true], ["No", false]], include_blank: true, selected: params[:search][:is_resolved] %>
|
||||
<%= f.input :category, label: "Category", collection: ["normal", "unapproved", "rejected", "deleted"], include_blank: true, selected: params[:search][:category] %>
|
||||
<%= f.input :status, collection: PostFlag.statuses, include_blank: true, selected: params[:search][:status] %>
|
||||
<%= f.submit "Search" %>
|
||||
<% end %>
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
<% t.column "Category", width: "1%" do |post_flag| %>
|
||||
<%= link_to post_flag.category.to_s, post_flags_path(search: params[:search].merge(category: post_flag.category)) %>
|
||||
<% end %>
|
||||
<% t.column "Resolved?", width: "1%" do |post_flag| %>
|
||||
<%= link_to post_flag.is_resolved?.to_s, post_flags_path(search: params[:search].merge(is_resolved: post_flag.is_resolved?)) %>
|
||||
<% t.column "Status", width: "5%" do |post_flag| %>
|
||||
<%= link_to post_flag.status, post_flags_path(search: { status: post_flag.status }) %>
|
||||
<% end %>
|
||||
<% t.column "Uploaded", width: "15%" do |post_flag| %>
|
||||
<%= compact_time post_flag.post.created_at %>
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
<%= f.input :updater_name, label: "Updater", input_html: { "data-autocomplete": "user", value: params.dig(:search, :updater_name) } %>
|
||||
<%= f.input :added_tags_include_all, label: "Added Tags", input_html: { "data-autocomplete": "tag-query", value: params.dig(:search, :added_tags_include_all) } %>
|
||||
<%= f.input :removed_tags_include_all, label: "Removed Tags", input_html: { "data-autocomplete": "tag-query", value: params.dig(:search, :removed_tags_include_all) } %>
|
||||
<%= f.input :changed_tags, label: "Changed Tags", input_html: { "data-autocomplete": "tag-query", value: params.dig(:search, :changed_tags) } %>
|
||||
<%= f.input :all_changed_tags, label: "All Changed Tags", input_html: { "data-autocomplete": "tag-query", value: params.dig(:search, :all_changed_tags) }, hint: "All tags must appear in either tag adds or removes" %>
|
||||
<%= f.input :any_changed_tags, label: "Any Changed Tags", input_html: { "data-autocomplete": "tag-query", value: params.dig(:search, :any_changed_tags) }, hint: "Any tag must appear in either tag adds or removes" %>
|
||||
<%= f.submit "Search" %>
|
||||
<%= link_to "Advanced", search_post_versions_path(params.except(:controller, :action, :index, :commit, :type).permit!), class: "advanced-search-link" %>
|
||||
<% end %>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<%= f.input :updater_name, label: "Updater", input_html: { value: params.dig(:search, :updater_name), "data-autocomplete": "user" } %>
|
||||
<%= f.input :added_tags_include_all, label: "Added tags", input_html: { value: params.dig(:search, :added_tags_include_all), "data-autocomplete": "tag-query" } %>
|
||||
<%= f.input :removed_tags_include_all, label: "Removed tags", input_html: { value: params.dig(:search, :removed_tags_include_all), "data-autocomplete": "tag-query" } %>
|
||||
<%= f.input :changed_tags, label: "Changed tags", input_html: { value: params.dig(:search, :changed_tags), "data-autocomplete": "tag-query" } %>
|
||||
<%= f.input :changed_tags, label: "Changed Tags", input_html: { "data-autocomplete": "tag-query", value: params.dig(:search, :changed_tags) }, hint: "Added or removed tags" %>
|
||||
<%= f.input :post_id, input_html: { value: params.dig(:search, :post_id) } %>
|
||||
<%= f.input :parent_id, input_html: { value: params.dig(:search, :parent_id) } %>
|
||||
<%= f.input :rating, input_html: { value: params.dig(:search, :rating) } %>
|
||||
|
||||
5
app/views/posts/destroy.js.erb
Normal file
5
app/views/posts/destroy.js.erb
Normal file
@@ -0,0 +1,5 @@
|
||||
<% if params[:commit] == "Delete" %>
|
||||
location.reload();
|
||||
<% else %>
|
||||
Danbooru.Utility.dialog("Delete Post", "<%= j render "posts/partials/show/delete_dialog", post: @post %>");
|
||||
<% end %>
|
||||
@@ -6,7 +6,7 @@
|
||||
<%= render "posts/partials/index/blacklist" %>
|
||||
|
||||
<section id="tag-box">
|
||||
<h1>Tags</h1>
|
||||
<h2>Tags</h2>
|
||||
<%= @post_set.tag_list_html(current_query: params[:tags], show_extra_links: policy(Post).show_extra_links?) %>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<%# path, tags %>
|
||||
|
||||
<section id="search-box">
|
||||
<h1>Search</h1>
|
||||
<h2>Search</h2>
|
||||
<%= form_tag(path, method: "get", id: "search-box-form") do %>
|
||||
<% if params[:random] %>
|
||||
<%= hidden_field_tag :random, params[:random] %>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div id="blacklist-box" class="sidebar-blacklist">
|
||||
<h1>Blacklisted (<%= link_to_wiki "help", "help:blacklists" %>)</h1>
|
||||
<h2>Blacklisted (<%= link_to_wiki "help", "help:blacklists" %>)</h2>
|
||||
<ul id="blacklist-list" class="list-bulleted"></ul>
|
||||
<%= link_to "Disable all", "#", :id => "disable-all-blacklists", :style => "display: none;" %>
|
||||
<%= link_to "Re-enable all", "#", :id => "re-enable-all-blacklists", :style => "display: none;" %>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<div id="quick-edit-div" style="display: none;">
|
||||
<h1>Edit</h1>
|
||||
<h2>Edit</h2>
|
||||
|
||||
<%= edit_form_for(:post, html: { id: "quick-edit-form" }) do |f| %>
|
||||
<%= f.input :tag_string, label: "Tags", as: :text, input_html: { "data-autocomplete": "tag-edit" } %>
|
||||
|
||||
@@ -40,13 +40,13 @@
|
||||
<% end %>
|
||||
<% elsif post_set.pool.present? %>
|
||||
<% post_set.pool.tap do |pool| %>
|
||||
<h4>
|
||||
<h2>
|
||||
<%= pool.pretty_category %>:
|
||||
<%= link_to pool.pretty_name, pool_path(pool), :class => "pool-category-#{pool.category}" %>
|
||||
<% if pool.is_deleted? %>
|
||||
<span class="inactive">(deleted)</span>
|
||||
<% end %>
|
||||
</h4>
|
||||
</h2>
|
||||
|
||||
<div id="description" class="prose">
|
||||
<%= format_text(post_set.pool.description) %>
|
||||
@@ -57,10 +57,10 @@
|
||||
</p>
|
||||
<% end %>
|
||||
<% elsif post_set.favgroup.present? %>
|
||||
<h4>
|
||||
<h2>
|
||||
Favorite Group:
|
||||
<%= link_to post_set.favgroup.pretty_name, favorite_group_path(post_set.favgroup) %>
|
||||
</h4>
|
||||
</h2>
|
||||
Creator: <%= link_to_user post_set.favgroup.creator %>
|
||||
<% elsif post_set.has_blank_wiki? %>
|
||||
<p>There is currently no wiki page for the tag <%= link_to_wiki post_set.tag.pretty_name %>. You can <%= link_to "create one", new_wiki_page_path(wiki_page: { title: post_set.tag.name }), rel: "nofollow" %>.</p>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<% if policy(Post).can_use_mode_menu? %>
|
||||
<section id="mode-box">
|
||||
<h1>Mode</h1>
|
||||
<h2>Mode</h2>
|
||||
<form action="/">
|
||||
<select name="mode">
|
||||
<option value="view">View</option>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<section id="options-box">
|
||||
<h1>Options</h1>
|
||||
<h2>Options</h2>
|
||||
<ul>
|
||||
<% if policy(SavedSearch).create? %>
|
||||
<li><%= button_tag(tag.i(class: "fas fa-bookmark") + " Save search", id: "save-search", class: "ui-button ui-widget ui-corner-all sub") %></li>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<section id="related-box">
|
||||
<h1>Related</h1>
|
||||
<h2>Related</h2>
|
||||
<ul id="related-list">
|
||||
<% if discover_mode? %>
|
||||
<li id="secondary-links-posts-hot"><%= link_to "Hot", posts_path(:tags => "order:rank") %></li>
|
||||
|
||||
9
app/views/posts/partials/show/_delete_dialog.html.erb
Normal file
9
app/views/posts/partials/show/_delete_dialog.html.erb
Normal file
@@ -0,0 +1,9 @@
|
||||
<div class="delete-post-dialog-body">
|
||||
<%= edit_form_for(post, method: :delete, remote: true) do |f| %>
|
||||
<input type="hidden" name="commit" value="Delete">
|
||||
<%= f.input :reason, as: :dtext, inline: true, input_html: { value: "" } %>
|
||||
<% if post.parent_id.present? %>
|
||||
<%= f.input :move_favorites, label: "Move favorites to parent", as: :boolean, input_html: { checked: false } %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user