diff --git a/app/controllers/moderation_reports_controller.rb b/app/controllers/moderation_reports_controller.rb
new file mode 100644
index 000000000..906ff9e36
--- /dev/null
+++ b/app/controllers/moderation_reports_controller.rb
@@ -0,0 +1,49 @@
+class ModerationReportsController < ApplicationController
+ respond_to :html, :xml, :json, :js
+ before_action :builder_only, only: [:new, :create]
+ before_action :moderator_only, only: [:index]
+
+ def new
+ check_privilege
+ @moderation_report = ModerationReport.new(moderation_report_params)
+ respond_with(@moderation_report)
+ end
+
+ def index
+ @moderation_reports = ModerationReport.paginated_search(params).includes(:creator, :model)
+ respond_with(@moderation_reports)
+ end
+
+ def create
+ check_privilege
+ @moderation_report = ModerationReport.create(moderation_report_params)
+ @moderation_report.create_forum_post!
+ respond_with(@moderation_report)
+ end
+
+ private
+
+ def model_type
+ params.fetch(:moderation_report, {}).fetch(:model_type)
+ end
+
+ def model_id
+ params.fetch(:moderation_report, {}).fetch(:model_id)
+ end
+
+ def check_privilege
+ case model_type
+ when "User"
+ return if User.find(model_id).reportable_by?(CurrentUser.user)
+ when "Comment"
+ return if Comment.find(model_id).reportable_by?(CurrentUser.user)
+ when "ForumPost"
+ return if ForumPost.find(model_id).reportable_by?(CurrentUser.user)
+ end
+ raise User::PrivilegeError
+ end
+
+ def moderation_report_params
+ params.fetch(:moderation_report, {}).permit(%i[model_type model_id reason])
+ end
+end
diff --git a/app/models/comment.rb b/app/models/comment.rb
index c9111dd16..005e3c68d 100644
--- a/app/models/comment.rb
+++ b/app/models/comment.rb
@@ -7,6 +7,7 @@ class Comment < ApplicationRecord
belongs_to :post
belongs_to_creator
belongs_to_updater
+ has_many :moderation_reports, as: :model
has_many :votes, :class_name => "CommentVote", :dependent => :destroy
after_create :update_last_commented_at_on_create
after_update(:if => ->(rec) {(!rec.is_deleted? || !rec.saved_change_to_is_deleted?) && CurrentUser.id != rec.creator_id}) do |rec|
@@ -127,6 +128,10 @@ class Comment < ApplicationRecord
updater_id == user.id || user.is_moderator?
end
+ def reportable_by?(user)
+ user.is_builder? && creator_id != user.id && !creator.is_moderator?
+ end
+
def voted_by?(user)
return false if user.is_anonymous?
user.id.in?(votes.map(&:user_id))
diff --git a/app/models/forum_post.rb b/app/models/forum_post.rb
index 6c797731b..860f3791c 100644
--- a/app/models/forum_post.rb
+++ b/app/models/forum_post.rb
@@ -6,6 +6,7 @@ class ForumPost < ApplicationRecord
belongs_to_updater
belongs_to :topic, :class_name => "ForumTopic"
has_many :dtext_links, as: :model, dependent: :destroy
+ has_many :moderation_reports, as: :model
has_many :votes, class_name: "ForumPostVote"
has_one :tag_alias
has_one :tag_implication
@@ -93,6 +94,10 @@ class ForumPost < ApplicationRecord
bulk_update_request || tag_alias || tag_implication
end
+ def reportable_by?(user)
+ user.is_builder? && creator_id != user.id && !creator.is_moderator?
+ end
+
def votable?
TagAlias.where(forum_post_id: id).exists? ||
TagImplication.where(forum_post_id: id).exists? ||
diff --git a/app/models/moderation_report.rb b/app/models/moderation_report.rb
new file mode 100644
index 000000000..fb0a1aa61
--- /dev/null
+++ b/app/models/moderation_report.rb
@@ -0,0 +1,57 @@
+class ModerationReport < ApplicationRecord
+ belongs_to :model, polymorphic: true
+ belongs_to_creator
+
+ scope :user, -> { where(model_type: "User") }
+ scope :comment, -> { where(model_type: "Comment") }
+ scope :forum_post, -> { where(model_type: "ForumPost") }
+
+ def forum_topic_title
+ "Reports requiring moderation"
+ end
+
+ def forum_topic_body
+ "This topic deals with moderation events as reported by Builders. Reports can be filed against users, comments, or forum posts."
+ end
+
+ def forum_topic
+ topic = ForumTopic.find_by_title(forum_topic_title)
+ if topic.nil?
+ topic = CurrentUser.as_system do
+ ForumTopic.create(title: forum_topic_title, category_id: 0, min_level: User::Levels::MODERATOR, original_post_attributes: {body: forum_topic_body})
+ end
+ end
+ topic
+ end
+
+ def forum_post_message
+ messages = ["[b]Submitted by:[/b] @#{creator.name}"]
+ case model_type
+ when "User"
+ messages << "[b]Submitted against:[/b] @#{model.name}"
+ when "Comment"
+ messages << "[b]Submitted against[/b]: comment ##{model_id}"
+ when "ForumPost"
+ messages << "[b]Submitted against[/b]: forum ##{model_id}"
+ end
+ messages << ""
+ messages << "[quote]"
+ messages << "[b]Reason:[/b]"
+ messages << ""
+ messages << reason
+ messages << "[/quote]"
+ messages.join("\n")
+ end
+
+ def create_forum_post!
+ updater = ForumUpdater.new(forum_topic)
+ updater.update(forum_post_message)
+ end
+
+ def self.search(params)
+ q = super
+ q = q.search_attributes(params, :model_type, :model_id, :creator_id)
+
+ q.apply_default_order(params)
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 411d29799..6733a8b15 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -93,6 +93,7 @@ class User < ApplicationRecord
has_many :wiki_page_versions, foreign_key: :updater_id
has_many :feedback, :class_name => "UserFeedback", :dependent => :destroy
has_many :forum_post_votes, dependent: :destroy, foreign_key: :creator_id
+ has_many :moderation_reports, as: :model
has_many :posts, :foreign_key => "uploader_id"
has_many :post_appeals, foreign_key: :creator_id
has_many :post_approvals, :dependent => :destroy
@@ -791,6 +792,10 @@ class User < ApplicationRecord
end
end
+ def reportable_by?(user)
+ user.is_builder? && id != user.id && !is_moderator?
+ end
+
def hide_favorites?
!CurrentUser.is_admin? && enable_private_favorites? && CurrentUser.user.id != id
end
diff --git a/app/views/comments/partials/show/_comment.html.erb b/app/views/comments/partials/show/_comment.html.erb
index f08b42014..acf2c60ad 100644
--- a/app/views/comments/partials/show/_comment.html.erb
+++ b/app/views/comments/partials/show/_comment.html.erb
@@ -53,6 +53,9 @@
+ <% if comment.reportable_by?(CurrentUser.user) %>
+ <%= link_to "Report comment", new_moderation_report_path(moderation_report: { model_type: "Comment", model_id: comment.id }), remote: true %>
+ <% end %>
<% if comment.editable_by?(CurrentUser.user) %>
<%= render "comments/form", comment: comment, hidden: true %>
diff --git a/app/views/forum_posts/_forum_post.html.erb b/app/views/forum_posts/_forum_post.html.erb
index 88cacfef8..73726f3bd 100644
--- a/app/views/forum_posts/_forum_post.html.erb
+++ b/app/views/forum_posts/_forum_post.html.erb
@@ -32,6 +32,9 @@
<%= link_to "Edit", edit_forum_post_path(forum_post.id), :id => "edit_forum_post_link_#{forum_post.id}", :class => "edit_forum_post_link" %>
<% end %>
<% end %>
+ <% if forum_post.reportable_by?(CurrentUser.user) %>
+ <%= link_to "Report forum", new_moderation_report_path(moderation_report: { model_type: "ForumPost", model_id: forum_post.id }), remote: true %>
+ <% end %>
<% if forum_post.votable? %>
<%= render "forum_post_votes/list", votes: forum_post.votes, forum_post: forum_post %>
diff --git a/app/views/moderation_reports/_new.html.erb b/app/views/moderation_reports/_new.html.erb
new file mode 100644
index 000000000..e0e948144
--- /dev/null
+++ b/app/views/moderation_reports/_new.html.erb
@@ -0,0 +1,14 @@
+
+
+ <%= format_text(WikiPage.titled(Danbooru.config.report_notice_wiki_page).first.try(&:body)) %>
+
+
+ <%# XXX dtext_field expects there to be a `moderation_report` instance variable. %>
+ <% @moderation_report = moderation_report %>
+ <%= edit_form_for(@moderation_report, format: :js, remote: true) do |f| %>
+ <%= f.hidden_field :model_type %>
+ <%= f.hidden_field :model_id %>
+ <%= dtext_field "moderation_report", "reason", preview_id: "dtext-preview-for-moderation-report", type: "string" %>
+ <%= dtext_preview_button "moderation_report", "reason", preview_id: "dtext-preview-for-moderation-report" %>
+ <% end %>
+
diff --git a/app/views/moderation_reports/create.js.erb b/app/views/moderation_reports/create.js.erb
new file mode 100644
index 000000000..755f3d075
--- /dev/null
+++ b/app/views/moderation_reports/create.js.erb
@@ -0,0 +1 @@
+Danbooru.notice("Report submitted.");
\ No newline at end of file
diff --git a/app/views/moderation_reports/index.html.erb b/app/views/moderation_reports/index.html.erb
new file mode 100644
index 000000000..93507dbd1
--- /dev/null
+++ b/app/views/moderation_reports/index.html.erb
@@ -0,0 +1,27 @@
+
+
+
Moderation reports
+ <%= table_for @moderation_reports, width: "100%" do |t| %>
+ <% t.column "Reported", width: "10%" do |report| %>
+ <% if report.model_type == "User" %>
+ <%= link_to_user report.model %>
+ <% elsif report.model_type == "Comment" %>
+ <%= link_to "comment ##{report.model_id}", comment_path(report.model_id) %>
+ <% elsif report.model_type == "ForumPost" %>
+ <%= link_to "forum ##{report.model_id}", forum_post_path(report.model_id) %>
+ <% end %>
+ <% end %>
+ <% t.column "Reason" do |report| %>
+
+ <%= format_text report.reason, inline: true %>
+
+ <% end %>
+ <% t.column "Creator", width: "10%" do |report| %>
+ <%= compact_time report.created_at %>
+
by <%= link_to_user report.creator %>
+ <% end %>
+ <% end %>
+
+ <%= numbered_paginator(@moderation_reports) %>
+
+
diff --git a/app/views/moderation_reports/new.js.erb b/app/views/moderation_reports/new.js.erb
new file mode 100644
index 000000000..8f83b844a
--- /dev/null
+++ b/app/views/moderation_reports/new.js.erb
@@ -0,0 +1 @@
+Danbooru.Utility.dialog("Send report", "<%= j render "moderation_reports/new", moderation_report: @moderation_report %>");
diff --git a/app/views/static/site_map.html.erb b/app/views/static/site_map.html.erb
index 8951e50ce..32a695924 100644
--- a/app/views/static/site_map.html.erb
+++ b/app/views/static/site_map.html.erb
@@ -155,6 +155,7 @@
<% end %>
<% if CurrentUser.is_moderator? %>
+ - <%= link_to("Moderation Reports", moderation_reports_path) %>
- <%= link_to("IP Addresses", ip_addresses_path) %>
- <%= link_to("IP Bans", ip_bans_path) %>
<% end %>
diff --git a/app/views/users/_secondary_links.html.erb b/app/views/users/_secondary_links.html.erb
index ab204888f..26571d8fb 100644
--- a/app/views/users/_secondary_links.html.erb
+++ b/app/views/users/_secondary_links.html.erb
@@ -22,6 +22,9 @@
<% if !@user.is_platinum? %>
<%= subnav_link_to "Gift upgrade", new_user_upgrade_path(:user_id => @user.id) %>
<% end %>
+ <% if @user.reportable_by?(CurrentUser.user) %>
+ <%= subnav_link_to "Report user", new_moderation_report_path(moderation_report: { model_type: "User", model_id: @user.id }), remote: true %>
+ <% end %>
<% end %>
<% if CurrentUser.user.is_moderator? %>
diff --git a/config/danbooru_default_config.rb b/config/danbooru_default_config.rb
index 409d9f35f..42b134975 100644
--- a/config/danbooru_default_config.rb
+++ b/config/danbooru_default_config.rb
@@ -305,6 +305,10 @@ module Danbooru
"help:appeal_notice"
end
+ def report_notice_wiki_page
+ "help:report_notice"
+ end
+
def replacement_notice_wiki_page
"help:replacement_notice"
end
diff --git a/config/routes.rb b/config/routes.rb
index a0e692beb..f7a750ca3 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -160,6 +160,7 @@ Rails.application.routes.draw do
end
end
resources :mod_actions
+ resources :moderation_reports, only: [:new, :create, :index]
resources :news_updates
resources :notes do
collection do
diff --git a/db/migrate/20200117220602_create_moderation_reports.rb b/db/migrate/20200117220602_create_moderation_reports.rb
new file mode 100644
index 000000000..38f83f26e
--- /dev/null
+++ b/db/migrate/20200117220602_create_moderation_reports.rb
@@ -0,0 +1,12 @@
+class CreateModerationReports < ActiveRecord::Migration[6.0]
+ def change
+ create_table :moderation_reports do |t|
+ t.timestamps
+ t.references :model, polymorphic: true, null: false
+ t.integer :creator_id, null: false
+ t.text :reason, null: false
+ end
+
+ add_index :moderation_reports, :creator_id
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 5f00617b0..b545e73e6 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -2408,6 +2408,40 @@ CREATE SEQUENCE public.mod_actions_id_seq
ALTER SEQUENCE public.mod_actions_id_seq OWNED BY public.mod_actions.id;
+--
+-- Name: moderation_reports; Type: TABLE; Schema: public; Owner: -
+--
+
+CREATE TABLE public.moderation_reports (
+ id bigint NOT NULL,
+ created_at timestamp(6) without time zone NOT NULL,
+ updated_at timestamp(6) without time zone NOT NULL,
+ model_type character varying NOT NULL,
+ model_id bigint NOT NULL,
+ creator_id integer NOT NULL,
+ reason text NOT NULL
+);
+
+
+--
+-- Name: moderation_reports_id_seq; Type: SEQUENCE; Schema: public; Owner: -
+--
+
+CREATE SEQUENCE public.moderation_reports_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+
+--
+-- Name: moderation_reports_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
+--
+
+ALTER SEQUENCE public.moderation_reports_id_seq OWNED BY public.moderation_reports.id;
+
+
--
-- Name: news_updates; Type: TABLE; Schema: public; Owner: -
--
@@ -4080,6 +4114,13 @@ ALTER TABLE ONLY public.ip_bans ALTER COLUMN id SET DEFAULT nextval('public.ip_b
ALTER TABLE ONLY public.mod_actions ALTER COLUMN id SET DEFAULT nextval('public.mod_actions_id_seq'::regclass);
+--
+-- Name: moderation_reports id; Type: DEFAULT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.moderation_reports ALTER COLUMN id SET DEFAULT nextval('public.moderation_reports_id_seq'::regclass);
+
+
--
-- Name: news_updates id; Type: DEFAULT; Schema: public; Owner: -
--
@@ -4440,6 +4481,14 @@ ALTER TABLE ONLY public.mod_actions
ADD CONSTRAINT mod_actions_pkey PRIMARY KEY (id);
+--
+-- Name: moderation_reports moderation_reports_pkey; Type: CONSTRAINT; Schema: public; Owner: -
+--
+
+ALTER TABLE ONLY public.moderation_reports
+ ADD CONSTRAINT moderation_reports_pkey PRIMARY KEY (id);
+
+
--
-- Name: news_updates news_updates_pkey; Type: CONSTRAINT; Schema: public; Owner: -
--
@@ -6514,6 +6563,20 @@ CREATE INDEX index_mod_actions_on_created_at ON public.mod_actions USING btree (
CREATE INDEX index_mod_actions_on_creator_id ON public.mod_actions USING btree (creator_id);
+--
+-- Name: index_moderation_reports_on_creator_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_moderation_reports_on_creator_id ON public.moderation_reports USING btree (creator_id);
+
+
+--
+-- Name: index_moderation_reports_on_model_type_and_model_id; Type: INDEX; Schema: public; Owner: -
+--
+
+CREATE INDEX index_moderation_reports_on_model_type_and_model_id ON public.moderation_reports USING btree (model_type, model_id);
+
+
--
-- Name: index_news_updates_on_created_at; Type: INDEX; Schema: public; Owner: -
--
@@ -7415,6 +7478,7 @@ INSERT INTO "schema_migrations" (version) VALUES
('20191223032633'),
('20200114204550'),
('20200115010442'),
+('20200117220602'),
('20200118015014');