Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

Already on GitHub? Sign in to your account

AO3-5234 Moderate Works #3162

Merged
merged 7 commits into from Nov 17, 2017
@@ -26,7 +26,7 @@ def admin_setting_params
:invite_from_queue_at, :suspend_filter_counts, :suspend_filter_counts_at,
:enable_test_caching, :cache_expiration, :tag_wrangling_off, :guest_downloading_off,
:disable_filtering, :request_invite_enabled, :creation_requires_invite,
- :downloads_enabled
+ :downloads_enabled, :hide_spam
)
end
end
@@ -0,0 +1,30 @@
+class Admin::SpamController < ApplicationController
+ before_action :admin_only
+
+ def index
+ conditions = case params[:show]
+ when "reviewed"
+ { reviewed: true, approved: false }
+ when "approved"
+ { approved: true }
+ else
+ { reviewed: false, approved: false }
+ end
+ @works = ModeratedWork.where(conditions).order(:created_at).page(params[:page])
+ end
+
+ def bulk_update
+ if ModeratedWork.bulk_update(spam_params)
+ flash[:notice] = "Works were successfully updated"
+ else
+ flash[:error] = "Sorry, please try again"
+ end
+ redirect_to admin_spam_index_path
+ end
+
+ private
+
+ def spam_params
+ params.slice(:spam, :ham)
+ end
+end
@@ -31,16 +31,17 @@ def hide
elsif @creation_class == ExternalWork || @creation_class == Bookmark
redirect_to(request.env["HTTP_REFERER"] || root_path)
else
- unless action == "unhide"
+ unless action == "unhide" || @creation_class == Work
# Email users so they're aware of Abuse action
+ # Emails for works are handled in the work class
orphan_account = User.orphan_account
users = @creation.pseuds.map(&:user).uniq
users.each do |user|
unless user == orphan_account
UserMailer.admin_hidden_work_notification(@creation.id, user.id).deliver
end
end
- end
+ end
redirect_to(@creation)
end
end
@@ -0,0 +1,73 @@
+class ModeratedWork < ApplicationRecord
+ belongs_to :work
+ validates :work_id, uniqueness: true
+
+ delegate :title, :revised_at, to: :work
+
+ def self.register(work)
+ find_or_create_by(work_id: work.id)
+ end
+
+ def self.mark_reviewed(work)
+ register(work).mark_reviewed!
+ end
+
+ def self.mark_approved(work)
+ register(work).mark_approved!
+ end
+
+ def self.bulk_update(params = {})
+ ids = processed_bulk_ids(params)
+ transaction do
+ bulk_review(ids[:spam])
+ bulk_approve(ids[:ham])
+ end
+ end
+
+ def self.processed_bulk_ids(params = {})
+ spam_ids = params[:spam] || []
+ ham_ids = params[:ham] || []
+ # Ensure no overlap
+ admin_confusion = spam_ids & ham_ids
+ if admin_confusion
+ spam_ids -= admin_confusion
+ ham_ids -= admin_confusion
+ end
+ { spam: spam_ids, ham: ham_ids }
+ end
+
+ def self.bulk_review(ids)
+ return unless ids.present?
+ where(id: ids).update_all("reviewed = 1")
+ admin_settings = Rails.cache.fetch("admin_settings"){ AdminSetting.first }
+ # If spam isn't hidden by default, hide it now
+ unless admin_settings.hide_spam?
+ Work.joins(:moderated_work).where("moderated_works.id IN (?)", ids).each do |work|
+ work.update_attribute(:hidden_by_admin, true)
+ end
+ end
+ end
+
+ def self.bulk_approve(ids)
+ return unless ids.present?
+ where(id: ids).update_all("approved = 1")
+ Work.joins(:moderated_work).where("moderated_works.id IN (?)", ids).each do |work|
+ work.mark_as_ham!
+ end
+ end
+
+ def mark_reviewed!
+ update_attribute(:reviewed, true)
+ end
+
+ def mark_approved!
+ update_attribute(:approved, true)
+ end
+
+ # Easy access to the creators of spam works
+ def admin_user_links
+ work.users.map do |u|
+ "<a href='/admin/users/#{u.to_param}'>#{u.login}</a>"
+ end.join(", ").html_safe
+ end
+end
View
@@ -69,7 +69,8 @@ def create_stat_counter
counter = self.build_stat_counter
counter.save
end
-
+ # moderation
+ has_one :moderated_work, dependent: :destroy
########################################################################
# VIRTUAL ATTRIBUTES
@@ -195,6 +196,10 @@ def check_for_invalid_chapters
before_update :validate_tags, :notify_before_update
after_update :adjust_series_restriction
+ before_save :hide_spam
+ after_save :moderate_spam
+ after_save :notify_of_hiding
+
after_save :notify_recipients, :expire_caches
after_destroy :expire_caches
before_destroy :before_destroy
@@ -1388,18 +1393,39 @@ def check_for_spam
save
end
+ def hide_spam
+ return unless spam?
+ admin_settings = Rails.cache.fetch("admin_settings"){ AdminSetting.first }
+ if admin_settings.hide_spam?
+ self.hidden_by_admin = true
+ end
+ end
+
+ def moderate_spam
+ ModeratedWork.register(self) if spam?
+ end
+
def mark_as_spam!
update_attribute(:spam, true)
+ ModeratedWork.mark_reviewed(self)
# don't submit spam reports unless in production mode
Rails.env.production? && Akismetor.submit_spam(akismet_attributes)
end
def mark_as_ham!
- update_attribute(:spam, false)
+ update_attributes(spam: false, hidden_by_admin: false)
+ ModeratedWork.mark_approved(self)
# don't submit ham reports unless in production mode
Rails.env.production? && Akismetor.submit_ham(akismet_attributes)
end
+ def notify_of_hiding
+ return unless hidden_by_admin? && saved_change_to_hidden_by_admin?
+ users.each do |user|
+ UserMailer.admin_hidden_work_notification(id, user.id).deliver
+ end
+ end
+
#############################################################################
#
# SEARCH INDEX
@@ -26,6 +26,7 @@
</ul>
</li>
<li><%= link_to ts("Blacklist", key: "header"), admin_blacklisted_emails_path %></li>
+ <li><%= link_to ts("Spam", key: "header"), admin_spam_index_path %></li>
<li><%= link_to ts("Settings", key: "header"), admin_settings_path %></li>
<li><%= link_to ts("Banners", key: "header"), admin_banners_path %></li>
<li class="dropdown">
@@ -28,6 +28,9 @@
<dt><%= f.label :days_to_purge_unactivated, ts("How many weeks you have to activate your account before we purge it") %></dt>
<dd><%= f.text_field :days_to_purge_unactivated, :size => "3" %></dd>
+
+ <dt><%= f.check_box :hide_spam %></dt>
+ <dd><%= f.label :hide_spam, ts("Automatically hide spam works") %></dd>
</dl>
</fieldset>
@@ -0,0 +1,78 @@
+<!--Descriptive page name, messages and instructions-->
+<h2 class="heading"><%= ts("Works Marked as Spam") %></h2>
+<!--/descriptions-->
+
+<!--subnav-->
+<!--/subnav-->
+
+<!--main content-->
+<%= form_tag "/admin/spam/bulk_update", method: :post do %>
+ <%= submit_button nil, ts("Update Works") %>
+ <table id="spam_works" summary="<%= ts("Titles, creators, and revision dates for works that have been automatically marked as spam, along with options to verify or correct their spam status.") %>">
+ <caption><%= ts("Works Marked as Spam") %></caption>
+ <thead>
+ <tr>
+ <th scope="col"><%= ts("Title") %></th>
+ <th scope="col"><%= ts("Creator") %></th>
+ <th scope="col"><%= ts("Revised At") %></th>
+ <th scope="col">
+ <%= ts("Mark Reviewed") %>
+ <ul class="actions">
+ <li>
+ <button id="spam_all_select" type="button">
+ <%= ts("All") %>
+ </button>
+ </li>
+ <li>
+ <button id="spam_all_deselect" type="button">
+ <%= ts("None") %>
+ </button>
+ </li>
+ </ul>
+ </th>
+ <th scope="col">
+ <%= ts("Mark As Not Spam") %>
+ <ul class="actions">
+ <li>
+ <button id="ham_all_select" type="button">
+ <%= ts("All") %>
+ </button>
+ </li>
+ <li>
+ <button id="ham_all_deselect" type="button">
+ <%= ts("None") %>
+ </button>
+ </li>
+ </ul>
+ </th>
+ </tr>
+ </thead>
+ <tbody>
+ <% @works.each do |work| %>
+ <tr>
+ <th scope="row">
+ <%= link_to work.title, work_path(id: work.work_id) %>
+ </th>
+ <td><%= work.admin_user_links %></td>
+ <td><%= work.revised_at %></td>
+ <td>
+ <%= check_box_tag 'spam[]', work.id, nil, id: "spam_#{work.id}" %>
+ <%= label_tag "spam_#{work.id}", "Spam" %>
+ </td>
+ <td>
+ <%= check_box_tag 'ham[]', work.id, nil, id: "ham_#{work.id}" %>
+ <%= label_tag "ham_#{work.id}", "Not Spam" %>
+ </td>
+ </tr>
+ <% end %>
+ </tbody>
+ </table>
+ <%= submit_button nil, ts("Update Works") %>
+<% end %>
+
+<%= will_paginate @works %>
+<!--/content-->
+
+<% content_for :footer_js do %>
+ <%= javascript_include_tag "select_all" %>
+<% end %>
View
@@ -146,6 +146,11 @@
get :index_approved
end
end
+ resources :spam, only: [:index] do
+ collection do
+ post :bulk_update
+ end
+ end
resources :user_creations, only: [:destroy] do
member do
put :hide
@@ -0,0 +1,11 @@
+class CreateModeratedWorks < ActiveRecord::Migration[5.1]
+ def change
+ create_table :moderated_works do |t|
+ t.references :work, null: false #, foreign_key: true
+ t.boolean :approved, null: false, default: false
+ t.boolean :reviewed, null: false, default: false
+
+ t.timestamps
+ end
+ end
+end
@@ -0,0 +1,5 @@
+class AddSpamHidingToAdminSettings < ActiveRecord::Migration[5.1]
+ def change
+ add_column :admin_settings, :hide_spam, :boolean, default: false, null: false
+ end
+end
@@ -0,0 +1,22 @@
+@admin
+Feature: Admin spam management
+ In order to manage spam works
+ As an an admin
+ I want to be able to view and update works marked as spam
+
+Scenario: Review spam
+ Given the spam work "Spammity Spam"
+ And the spam work "Totally Legit"
+ And I am logged in as an admin
+ Then I should see "Spam"
+ When I follow "Spam"
+ Then I should see "Works Marked as Spam"
+ And I should see "Spammity"
+ And I should see "Totally Legit"
+ When I check "spam_1"
+ And I check "ham_2"
+ And I press "Update Works"
+ Then I should not see "Spammity"
+ And I should not see "Totally Legit"
+ And the work "Spammity Spam" should be hidden
+ And the work "Totally Legit" should not be hidden
@@ -1,4 +1,6 @@
-// To be included only where needed, currently tag_wranglings/index and tags/wrangle
+// To be included only where needed:
+// tag_wranglings/index, tags/wrangle, admin/spam
+// TODO: Refactor to be less repetitive
$j(document).ready(function(){
$j("#wrangle_all_select").click(function() {
@@ -21,4 +23,24 @@ $j(document).ready(function(){
$j(ticky).prop("checked", false);
});
});
+ $j("#spam_all_select").click(function() {
@houndci-bot

houndci-bot Nov 16, 2017

'$j' is not defined.

+ $j("#spam_works").find(":checkbox[name='spam[]']").each(function(index, ticky) {
@houndci-bot

houndci-bot Nov 16, 2017

Line is too long.
'$j' is not defined.

+ $j(ticky).prop("checked", true);
@houndci-bot

houndci-bot Nov 16, 2017

'$j' is not defined.

+ });
+ });
+ $j("#spam_all_deselect").click(function() {
@houndci-bot

houndci-bot Nov 16, 2017

'$j' is not defined.

+ $j("#spam_works").find(":checkbox[name='spam[]']").each(function(index, ticky) {
@houndci-bot

houndci-bot Nov 16, 2017

Line is too long.
'$j' is not defined.

+ $j(ticky).prop("checked", false);
@houndci-bot

houndci-bot Nov 16, 2017

'$j' is not defined.

+ });
+ });
+ $j("#ham_all_select").click(function() {
@houndci-bot

houndci-bot Nov 16, 2017

'$j' is not defined.

+ $j("#spam_works").find(":checkbox[name='ham[]']").each(function(index, ticky) {
@houndci-bot

houndci-bot Nov 16, 2017

Line is too long.
'$j' is not defined.

+ $j(ticky).prop("checked", true);
@houndci-bot

houndci-bot Nov 16, 2017

'$j' is not defined.

+ });
+ });
+ $j("#ham_all_deselect").click(function() {
@houndci-bot

houndci-bot Nov 16, 2017

'$j' is not defined.

+ $j("#spam_works").find(":checkbox[name='ham[]']").each(function(index, ticky) {
@houndci-bot

houndci-bot Nov 16, 2017

Line is too long.
'$j' is not defined.

+ $j(ticky).prop("checked", false);
@houndci-bot

houndci-bot Nov 16, 2017

'$j' is not defined.

+ });
+ });
})
@@ -47,6 +47,7 @@ ul.actions {
}
button {
+ font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Arial Unicode MS', 'GNU Unifont', sans-serif;
box-sizing: content-box;
}
Oops, something went wrong.