Merge branch 'master' into attribute-searching

This commit is contained in:
evazion
2020-08-17 14:23:00 -05:00
committed by GitHub
155 changed files with 2834 additions and 2169 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,8 +3,8 @@
}
header#top {
h1#app-name-header {
font-size: 2em;
#app-name-header {
font-size: var(--text-xxl);
margin: 0 30px;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -7,10 +7,6 @@ div#c-static {
section {
flex: 1;
h1 {
font-size: $h3_size;
}
ul {
margin-bottom: 1.5em;
}

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View 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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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"]] }]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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