From 09c7c2f064b72f7feb6b30f9ab60d837bfd477f4 Mon Sep 17 00:00:00 2001
From: neuroalien <105230050+neuroalien@users.noreply.github.com>
Date: Thu, 6 Jul 2023 05:03:34 +0100
Subject: [PATCH 001/208] AO3-5901 Limit number of pseuds on profile and
sidebar (#4554)
* AO3-5901 Define a profile scope for profile and sidebar display
* AO3-5901 Limit number of pseuds shown in the user sidebar
* AO3-5901 Limit number of pseuds shown on the user's profile
* AO3-5901 Introduce a profile presenter
The view was a bit confusing, pulling information from various parts of the user entity, the profile, and the preferences.
In extracting it to a presenter, I realised that if the preference was set to show it, we were showing the birthday section even if the field itself was empty, so I changed that to not show the section.
Refactor some specs to remove duplication.
Update ignore file for Brakeman to ignore the newly introduced false positive.
* AO3-5901 Get rid of deprecation messages
These yaks aren't going to shave themselves...
* AO3-5901 Always show the currently selected pseud
Fixes a long-standing bug where the intention of the code was always to show the current pseud at the top of the pseud switcher.
* AO3-5901 pseud cukes
* AO3-5901 Remove profile request spec
The maximum number of pseuds on the profile page is now tested in `pseuds.feature`.
* AO3-5901 Expand cuke with user page scenarios
And remove user request spec, as per review.
* AO3-5901 Different text for profile pseud link
Show different link text, depending on whether there are more pseuds in total than there are shown on profile.
* AO3-5901 Newly surfaced Hydra barkings
I mean Hound. Yes, that's what I mean.
* Double quoting strings
* Using `let` like DHH intended us to instead of instance variables
* Assertive language in spec descriptions
* Explicit expectations ("Do not use `be` without an argument")
* Fixes from cesy's commit cf8568d0e422f0782cd5e23cf0b0b200b419287b which could not be cherry-picked
Co-authored-by: Cesy
* Dynamically load pseuds on profile page from commit 9e68b3e
Co-authored-by: tickinginstant
* Test fixes for dynamic pseud list on profile
* AO3-5901 Use verified double
---------
Co-authored-by: Cesy
Co-authored-by: Sarken
Co-authored-by: tickinginstant
---
app/controllers/profile_controller.rb | 31 ++++++++--
app/decorators/profile_presenter.rb | 13 +++++
app/helpers/pseuds_helper.rb | 43 ++++++++++++--
app/models/pseud.rb | 4 ++
app/views/profile/pseuds.js.erb | 2 +
app/views/profile/show.html.erb | 26 ++++-----
app/views/users/_sidebar.html.erb | 5 +-
config/locales/views/en.yml | 5 ++
config/routes.rb | 6 +-
features/other_a/preferences_edit.feature | 26 ++-------
features/other_a/profile_edit.feature | 6 ++
features/other_a/pseuds.feature | 30 ++++++++++
features/step_definitions/pseud_steps.rb | 10 +++-
spec/controllers/profile_controller_spec.rb | 28 ++++++---
spec/models/profile_presenter_spec.rb | 64 +++++++++++++++++++++
spec/models/pseud_spec.rb | 34 +++++++++++
16 files changed, 276 insertions(+), 57 deletions(-)
create mode 100644 app/decorators/profile_presenter.rb
create mode 100644 app/views/profile/pseuds.js.erb
create mode 100644 spec/models/profile_presenter_spec.rb
diff --git a/app/controllers/profile_controller.rb b/app/controllers/profile_controller.rb
index 5a3515ac4ff..1de8b547e35 100644
--- a/app/controllers/profile_controller.rb
+++ b/app/controllers/profile_controller.rb
@@ -1,14 +1,14 @@
class ProfileController < ApplicationController
+ before_action :load_user_and_pseuds
def show
@user = User.find_by(login: params[:user_id])
- if @user.nil?
- flash[:error] = ts("Sorry, there's no user by that name.")
- redirect_to '/' and return
- elsif @user.profile.nil?
+ if @user.profile.nil?
Profile.create(user_id: @user.id)
@user.reload
end
+
+ @profile = ProfilePresenter.new(@user.profile)
#code the same as the stuff in users_controller
if current_user.respond_to?(:subscriptions)
@subscription = current_user.subscriptions.where(subscribable_id: @user.id,
@@ -18,4 +18,27 @@ def show
@page_subtitle = ts("%{username} - Profile", username: @user.login)
end
+ def pseuds
+ respond_to do |format|
+ format.html do
+ redirect_to user_pseuds_path(@user)
+ end
+
+ format.js
+ end
+ end
+
+ private
+
+ def load_user_and_pseuds
+ @user = User.find_by(login: params[:user_id])
+
+ if @user.nil?
+ flash[:error] = ts("Sorry, there's no user by that name.")
+ redirect_to root_path
+ return
+ end
+
+ @pseuds = @user.pseuds.default_alphabetical.paginate(page: params[:page])
+ end
end
diff --git a/app/decorators/profile_presenter.rb b/app/decorators/profile_presenter.rb
new file mode 100644
index 00000000000..02af53628fe
--- /dev/null
+++ b/app/decorators/profile_presenter.rb
@@ -0,0 +1,13 @@
+class ProfilePresenter < SimpleDelegator
+ def created_at
+ user.created_at.to_date
+ end
+
+ def date_of_birth
+ super if user.preference.try(:date_of_birth_visible)
+ end
+
+ def email
+ user.email if user.preference.try(:email_visible)
+ end
+end
diff --git a/app/helpers/pseuds_helper.rb b/app/helpers/pseuds_helper.rb
index 67b7cd963de..cc3b1234193 100644
--- a/app/helpers/pseuds_helper.rb
+++ b/app/helpers/pseuds_helper.rb
@@ -1,11 +1,42 @@
module PseudsHelper
-
- # Prints array of pseuds with links to user pages
- # used on Profile page
- def print_pseud_list(pseuds)
- pseuds.includes(:user).collect { |pseud| span_if_current(pseud.name, [pseud.user, pseud]) }.join(", ").html_safe
+ # Returns a list of pseuds, with links to each pseud.
+ #
+ # Used on Profile page, and by ProfileController#pseuds.
+ #
+ # The pseuds argument should be a single page of the user's pseuds, generated
+ # by calling user.pseuds.paginate(page: 1) or similar. This allows us to
+ # insert a remote link to dynamically insert the next page of pseuds.
+ def print_pseud_list(user, pseuds, first: true)
+ links = pseuds.map do |pseud|
+ link_to(pseud.name, [user, pseud])
+ end
+
+ difference = pseuds.total_entries - pseuds.length - pseuds.offset
+
+ if difference.positive?
+ links << link_to(
+ t("profile.pseud_list.more_pseuds", count: difference),
+ pseuds_user_profile_path(user, page: pseuds.next_page),
+ remote: true, id: "more_pseuds"
+ )
+ end
+
+ more_pseuds_connector = tag.span(
+ t("support.array.last_word_connector"),
+ id: "more_pseuds_connector"
+ )
+
+ if first
+ to_sentence(links,
+ last_word_connector: more_pseuds_connector)
+ else
+ links.unshift("")
+ to_sentence(links,
+ last_word_connector: more_pseuds_connector,
+ two_words_connector: more_pseuds_connector)
+ end
end
-
+
# used in the sidebar
def print_pseud_selector(pseuds)
pseuds -= [@pseud] if @pseud && @pseud.new_record?
diff --git a/app/models/pseud.rb b/app/models/pseud.rb
index 0625b11c9f7..374675e2496 100644
--- a/app/models/pseud.rb
+++ b/app/models/pseud.rb
@@ -86,6 +86,10 @@ class Pseud < ApplicationRecord
after_update :expire_caches
after_commit :reindex_creations, :touch_comments
+ scope :alphabetical, -> { order(:name) }
+ scope :default_alphabetical, -> { order(is_default: :desc).alphabetical }
+ scope :abbreviated_list, -> { default_alphabetical.limit(ArchiveConfig.ITEMS_PER_PAGE) }
+
def self.not_orphaned
where("user_id != ?", User.orphan_account)
end
diff --git a/app/views/profile/pseuds.js.erb b/app/views/profile/pseuds.js.erb
new file mode 100644
index 00000000000..12e98e90ccd
--- /dev/null
+++ b/app/views/profile/pseuds.js.erb
@@ -0,0 +1,2 @@
+$j("#more_pseuds_connector").remove();
+$j("#more_pseuds").replaceWith("<%= j print_pseud_list(@user, @pseuds, first: false) %>")
diff --git a/app/views/profile/show.html.erb b/app/views/profile/show.html.erb
index c81094f1bdb..781f8b8ea81 100644
--- a/app/views/profile/show.html.erb
+++ b/app/views/profile/show.html.erb
@@ -2,37 +2,37 @@
<%= render 'users/header' %>
- <% unless @user.profile.title.blank? %>
- <%=h @user.profile.title %>
+ <% if @profile.title.present? %>
+ <%=h @profile.title %>
<% end %>
- <%= link_to ts("My pseuds:"), user_pseuds_path(@user) %>
- <%= print_pseud_list(@user.pseuds) %>
+ <%= ts("My pseuds:") %>
+ <%= print_pseud_list(@user, @pseuds) %>
<%= ts("I joined on:") %>
- <%= ts("%{date}", :date => l(@user.created_at.to_date)) %>
+ <%= l(@profile.created_at) %>
<%= ts("My user ID is:") %>
<%= @user.id %>
- <% if @user.profile.location? %>
+ <% if @profile.location %>
<%=h ts("I live in:") %>
- <%=h @user.profile.location %>
+ <%=h @profile.location %>
<% end %>
- <% if @user.preference.try(:date_of_birth_visible) %>
+ <% if @profile.date_of_birth %>
<%=h ts("My birthday:") %>
- <%=l(@user.profile.date_of_birth) unless @user.profile.date_of_birth.blank? %>
+ <%=l(@profile.date_of_birth) %>
<% end %>
- <% if @user.preference.try(:email_visible) %>
+ <% if @profile.email %>
<%=h ts("My email address:") %>
- <%= mail_to @user.email, nil, :encode => "hex" %>
+ <%= mail_to @profile.email, nil, :encode => "hex" %>
<% end %>
- <% unless @user.profile.about_me.blank? %>
+ <% if @profile.about_me.present? %>
<%=h ts("Bio") %>
-
<%=raw sanitize_field(@user.profile, :about_me) %>
+
<%=raw sanitize_field(@profile, :about_me) %>
<% end %>
diff --git a/app/views/users/_sidebar.html.erb b/app/views/users/_sidebar.html.erb
index 707fa2bb189..cbd8264ba54 100644
--- a/app/views/users/_sidebar.html.erb
+++ b/app/views/users/_sidebar.html.erb
@@ -5,9 +5,10 @@
<%= span_if_current ts("Profile"), user_profile_path(@user) %>
<% if @user.pseuds.size > 1 %>
- "><%= ts("Pseuds") %>
+ <% pseud_link_text = current_page?(@user) ? ts("Pseuds") : (@pseud ? @pseud.name : @user.login) %>
+ "><%= pseud_link_text %>
- <%= print_pseud_selector(@user.pseuds) %>
+ <%= print_pseud_selector(@user.pseuds.abbreviated_list) %>
<%= span_if_current ts("All Pseuds (%{pseud_number})", :pseud_number => @user.pseuds.count), user_pseuds_path(@user) %>
diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml
index 23f56e53ee9..507c541b3d8 100644
--- a/config/locales/views/en.yml
+++ b/config/locales/views/en.yml
@@ -507,6 +507,11 @@ en:
allow_collection_invitation: Allow others to invite my works to collections.
blocked_users: Blocked Users
muted_users: Muted Users
+ profile:
+ pseud_list:
+ more_pseuds:
+ one: "%{count} more pseud"
+ other: "%{count} more pseuds"
pseuds:
delete_preview:
cancel: Cancel
diff --git a/config/routes.rb b/config/routes.rb
index e21ea24cce3..e0c027fe8f0 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -280,7 +280,11 @@
end
resources :nominations, controller: "tag_set_nominations", only: [:index]
resources :preferences, only: [:index, :update]
- resource :profile, only: [:show], controller: "profile"
+ resource :profile, only: [:show], controller: "profile" do
+ collection do
+ get :pseuds
+ end
+ end
resources :pseuds do
resources :works
resources :series
diff --git a/features/other_a/preferences_edit.feature b/features/other_a/preferences_edit.feature
index e522e31b80a..c3d2742c33f 100644
--- a/features/other_a/preferences_edit.feature
+++ b/features/other_a/preferences_edit.feature
@@ -40,7 +40,7 @@ Feature: Edit preferences
And I should see "Turn off the banner showing on every page."
- Scenario: View and edit preferences for history, personal details, view entire work
+ Scenario: View and edit preferences for history, view entire work
Given the following activated user exists
| login | password |
@@ -50,16 +50,10 @@ Feature: Edit preferences
Then I should not see "My email address"
And I should not see "My birthday"
When I am logged in as "editname" with password "password"
- Then I should see "Hi, editname!"
- And I should see "Log Out"
- When I post the work "This has two chapters"
- And I follow "Add Chapter"
- And I fill in "content" with "Secondy chapter"
- And I press "Preview"
- And I press "Post"
- Then I should see "Secondy chapter"
+ And I post the 2 chapter work "This has two chapters"
+ Then I should be on the 2nd chapter of the work "This has two chapters"
And I follow "Previous Chapter"
- Then I should not see "Secondy chapter"
+ Then I should be on the 1st chapter of the work "This has two chapters"
When I follow "editname"
Then I should see "Dashboard" within "div#dashboard"
And I should see "History" within "div#dashboard"
@@ -79,22 +73,12 @@ Feature: Edit preferences
Then I should see "Edit My Profile"
When I uncheck "Turn on History"
And I check "Show the whole work by default."
- And I check "Show my email address to other people."
- And I check "Show my date of birth to other people."
And I press "Update"
Then I should see "Your preferences were successfully updated"
And I should not see "History" within "div#dashboard"
When I go to the works page
And I follow "This has two chapters"
- Then I should see "Secondy chapter"
- When I log out
- And I go to editname's user page
- And I follow "Profile"
- Then I should see "My email address"
- And I should see "My birthday"
- When I go to the works page
- And I follow "This has two chapters"
- Then I should not see "Secondy chapter"
+ Then I should not see "Next Chapter"
@javascript
Scenario: User can hide warning and freeform tags and reveal them on a case-
diff --git a/features/other_a/profile_edit.feature b/features/other_a/profile_edit.feature
index 9572cf2e5e4..d383bcf2941 100644
--- a/features/other_a/profile_edit.feature
+++ b/features/other_a/profile_edit.feature
@@ -112,6 +112,9 @@ Scenario: Changing email address and viewing
And the email should not contain "translation missing"
When I change my preferences to display my email address
Then I should see "My email address: valid2@archiveofourown.org"
+ When I log out
+ And I go to editname's profile page
+ Then I should see "My email address: valid2@archiveofourown.org"
Scenario: Changing email address after requesting password reset
@@ -153,6 +156,9 @@ Scenario: Entering date of birth and displaying
When I change my preferences to display my date of birth
Then I should see "My birthday: 1980-11-30"
And 0 emails should be delivered
+ When I log out
+ And I go to editname's profile page
+ Then I should see "My birthday: 1980-11-30"
Scenario: Change password - mistake in typing old password
diff --git a/features/other_a/pseuds.feature b/features/other_a/pseuds.feature
index 478a2df5a8e..ce6fe1cc767 100644
--- a/features/other_a/pseuds.feature
+++ b/features/other_a/pseuds.feature
@@ -134,3 +134,33 @@ Scenario: Comments reflect pseud changes immediately
And I view the work "Interesting" with comments
Then I should see "after (myself)" within ".comment h4.byline"
And I should not see "before (myself)"
+
+Scenario: Many pseuds
+
+ Given there are 3 pseuds per page
+ And "Zaphod" has the pseud "Slartibartfast"
+ And "Zaphod" has the pseud "Agrajag"
+ And "Zaphod" has the pseud "Betelgeuse"
+ And I am logged in as "Zaphod"
+
+ When I view my profile
+ Then I should see "Zaphod" within "dl.meta"
+ And I should see "Agrajag" within "dl.meta"
+ And I should see "Betelgeuse" within "dl.meta"
+ And I should not see "Slartibartfast" within "dl.meta"
+ And I should see "1 more pseud" within "dl.meta"
+
+ When I go to my user page
+ Then I should see "Zaphod" within "ul.expandable"
+ And I should see "Agrajag" within "ul.expandable"
+ And I should see "Betelgeuse" within "ul.expandable"
+ And I should not see "Slartibartfast" within "ul.expandable"
+ And I should see "All Pseuds (4)" within "ul.expandable"
+
+ When I go to my "Slartibartfast" pseud page
+ Then I should see "Slartibartfast" within "li.pseud > a"
+ And I should not see "Slartibartfast" within "ul.expandable"
+
+ When there are 10 pseuds per page
+ And I view my profile
+ Then I should see "Zaphod, Agrajag, Betelgeuse, and Slartibartfast" within "dl.meta"
diff --git a/features/step_definitions/pseud_steps.rb b/features/step_definitions/pseud_steps.rb
index 50a8d05a0c1..4448d1c81b1 100644
--- a/features/step_definitions/pseud_steps.rb
+++ b/features/step_definitions/pseud_steps.rb
@@ -4,18 +4,24 @@
step %{I start a new session}
end
+Given "there are {int} pseuds per page" do |amount|
+ stub_const("ArchiveConfig", OpenStruct.new(ArchiveConfig))
+ ArchiveConfig.ITEMS_PER_PAGE = amount.to_i
+ allow(Pseud).to receive(:per_page).and_return(amount)
+end
+
When /^I change the pseud "([^\"]*)" to "([^\"]*)"/ do |old_pseud, new_pseud|
step %{I edit the pseud "#{old_pseud}"}
fill_in("Name", with: new_pseud)
click_button("Update")
end
-When /^I edit the pseud "([^\"]*)"/ do |pseud|
+When /^I edit the pseud "([^\"]*)"/ do |pseud|
p = Pseud.where(name: pseud, user_id: User.current_user.id).first
visit edit_user_pseud_path(User.current_user, p)
end
-When /^I add the pseud "([^\"]*)"/ do |pseud|
+When /^I add the pseud "([^\"]*)"/ do |pseud|
visit new_user_pseud_path(User.current_user)
fill_in("Name", with: pseud)
click_button("Create")
diff --git a/spec/controllers/profile_controller_spec.rb b/spec/controllers/profile_controller_spec.rb
index 89b58f3d35e..d80a2cf14c5 100644
--- a/spec/controllers/profile_controller_spec.rb
+++ b/spec/controllers/profile_controller_spec.rb
@@ -1,20 +1,32 @@
require 'spec_helper'
describe ProfileController do
- describe 'show' do
- it 'should be an error for a non existent user' do
+ describe "show" do
+ let(:user) { create(:user) }
+
+ it "redirects and shows an error message for a non existent user" do
get :show, params: { user_id: 999_999_999_999 }
expect(response).to redirect_to(root_path)
expect(flash[:error]).to eq "Sorry, there's no user by that name."
end
- it 'should create a new profile if one does not exist' do
- @user = FactoryBot.create(:user)
- @user.profile.destroy
- @user.reload
- get :show, params: { user_id: @user }
- expect(@user.profile).to be
+ it "creates a new profile if one does not exist" do
+ user.profile.destroy
+ user.reload
+
+ get :show, params: { user_id: user }
+
+ expect(user.profile).not_to be_nil
+ end
+
+ it "uses the profile presenter for the profile" do
+ profile_presenter = instance_double(ProfilePresenter)
+ allow(ProfilePresenter).to receive(:new).and_return(profile_presenter)
+
+ get :show, params: { user_id: user }
+
+ expect(assigns(:profile)).to eq(profile_presenter)
end
end
end
diff --git a/spec/models/profile_presenter_spec.rb b/spec/models/profile_presenter_spec.rb
new file mode 100644
index 00000000000..da582c89586
--- /dev/null
+++ b/spec/models/profile_presenter_spec.rb
@@ -0,0 +1,64 @@
+require "spec_helper"
+
+describe ProfilePresenter do
+ let(:user) { create(:user, email: "example@example.com") }
+ let(:profile) { user.profile }
+ let(:preference) { user.preference }
+ let(:subject) { ProfilePresenter.new(profile) }
+
+ describe "email" do
+ context "for a user whose preference does not allow showing the email" do
+ it "returns nil" do
+ expect(subject.email).to be_nil
+ end
+ end
+
+ context "for a user whose preference allows showing the email" do
+ before do
+ allow(preference).to receive(:email_visible).and_return(true)
+ end
+
+ it "returns the email" do
+ expect(subject.email).to eq("example@example.com")
+ end
+ end
+ end
+
+ describe "created_at" do
+ it "returns the date part of the timestamp" do
+ allow(user).to receive(:created_at).and_return(DateTime.new(2010, 12, 31, 10, 14, 20))
+ expect(subject.created_at).to eq(Date.new(2010, 12, 31))
+ end
+ end
+
+ describe "date_of_birth" do
+ let(:date_of_birth) { Date.new(2000, 12, 31) }
+
+ context "for a user whose preference does not allow showing date of birth" do
+ before do
+ allow(preference).to receive(:date_of_birth_visible).and_return(false)
+ end
+
+ it "returns nil" do
+ profile.date_of_birth = date_of_birth
+ expect(subject.date_of_birth).to be_nil
+ end
+ end
+
+ context "for a user whose preference allows showing date of birth" do
+ before do
+ allow(preference).to receive(:date_of_birth_visible).and_return(true)
+ end
+
+ it "returns the date of birth if it's present" do
+ profile.date_of_birth = date_of_birth
+ expect(subject.date_of_birth).to eq(date_of_birth)
+ end
+
+ it "returns nil if it's not present" do
+ profile.date_of_birth = nil
+ expect(subject.date_of_birth).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/models/pseud_spec.rb b/spec/models/pseud_spec.rb
index 4cfb8d1d3a8..eac4a07b777 100644
--- a/spec/models/pseud_spec.rb
+++ b/spec/models/pseud_spec.rb
@@ -65,4 +65,38 @@
end.to change { comment.reload.updated_at }
end
end
+
+ describe ".default_alphabetical" do
+ let(:user) { create(:user, login: "Zaphod") }
+ let(:subject) { user.pseuds.default_alphabetical }
+
+ before do
+ create(:pseud, user: user, name: "Slartibartfast")
+ create(:pseud, user: user, name: "Agrajag")
+ create(:pseud, user: user, name: "Betelgeuse")
+ allow(ArchiveConfig).to receive(:ITEMS_PER_PAGE).and_return(3)
+ end
+
+ it "gets default pseud, then all pseuds in alphabetical order" do
+ expect(subject.map(&:name)).to eq(%w[Zaphod Agrajag Betelgeuse Slartibartfast])
+ end
+ end
+
+ describe ".abbreviated_list" do
+ let(:user) { create(:user, login: "Zaphod") }
+ let(:subject) { user.pseuds.abbreviated_list }
+
+ before do
+ create(:pseud, user: user, name: "Slartibartfast")
+ create(:pseud, user: user, name: "Agrajag")
+ create(:pseud, user: user, name: "Betelgeuse")
+ allow(ArchiveConfig).to receive(:ITEMS_PER_PAGE).and_return(3)
+ end
+
+ it "gets default pseud, then shortened alphabetical list of other pseuds" do
+ expect(subject.map(&:name)).to eq(%w[Zaphod Agrajag Betelgeuse])
+ expect(subject.map(&:name)).not_to include("Slartibartfast")
+ expect(subject.length).to eq(ArchiveConfig.ITEMS_PER_PAGE)
+ end
+ end
end
From ba94c04580de2bac4c7a932e33a11fad4de7e06d Mon Sep 17 00:00:00 2001
From: Brian Austin <13002992+brianjaustin@users.noreply.github.com>
Date: Sat, 8 Jul 2023 19:54:43 -0700
Subject: [PATCH 002/208] AO3-6535 Update linters and cops (#4521)
* AO3-6535 Update linters and cops
* Downgrades to please the Hound
* More downgrades
* Fix test issue and warning
* Revert unnecessary lockfile changes
* Normalize i18n
* Comment ignore include
---
.erb-lint.yml | 2 +
.hound.yml | 2 +-
.rubocop.yml | 36 +-
.rubocop_todo.yml | 4994 +++++++++++++++++++++++++++++++++++++++++
.rubocop_todo_erb.yml | 1495 ++++++++++++
Gemfile | 8 +-
Gemfile.lock | 46 +-
7 files changed, 6550 insertions(+), 33 deletions(-)
create mode 100644 .rubocop_todo.yml
create mode 100644 .rubocop_todo_erb.yml
diff --git a/.erb-lint.yml b/.erb-lint.yml
index 087ed4ebe6b..df80cf9a83f 100644
--- a/.erb-lint.yml
+++ b/.erb-lint.yml
@@ -11,6 +11,8 @@ linters:
rubocop_config:
inherit_from:
- .rubocop.yml
+ # Uncomment the following line to ignore known issues
+ # - .rubocop_todo_erb.yml
Layout/ArgumentAlignment:
EnforcedStyle: with_fixed_indentation
Layout/InitialIndentation:
diff --git a/.hound.yml b/.hound.yml
index 0795119c53e..f544a38eee4 100644
--- a/.hound.yml
+++ b/.hound.yml
@@ -10,5 +10,5 @@ jshint:
ignore_file: .jshintignore
rubocop:
- version: 0.83.0
+ version: 1.22.1
config_file: .rubocop.yml
diff --git a/.rubocop.yml b/.rubocop.yml
index f6e882ca2c1..d6fdc3dd71b 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1,16 +1,19 @@
+# Uncomment the following line to ignore known issues
+# inherit_from: .rubocop_todo.yml
+
# Options available at https://github.com/bbatsov/rubocop/blob/master/config/default.yml
require:
- rubocop-rails
- rubocop-rspec
+inherit_mode:
+ merge:
+ - Exclude
+
AllCops:
- RSpec:
- Patterns:
- - "(?:^|/)factories/"
- - "(?:^|/)features/"
- - "(?:^|/)spec/"
- TargetRubyVersion: 2.7
+ NewCops: enable
+ TargetRubyVersion: 3.0
Bundler/OrderedGems:
Enabled: false
@@ -18,6 +21,9 @@ Bundler/OrderedGems:
Layout/DotPosition:
EnforcedStyle: leading
+Layout/EmptyLinesAroundAttributeAccessor:
+ Enabled: false
+
Layout/FirstArrayElementIndentation:
EnforcedStyle: consistent
@@ -27,6 +33,9 @@ Layout/LineLength:
Layout/MultilineMethodCallIndentation:
EnforcedStyle: indented
+Layout/SingleLineBlockChain:
+ Enabled: true
+
Layout/TrailingWhitespace:
Enabled: false
@@ -78,6 +87,9 @@ Rails/DynamicFindBy:
# Exceptions for InboxComment.find_by_filters
- find_by_filters
+Rails/EnvironmentVariableAccess:
+ Enabled: true
+
# Allow all uses of html_safe, they're everywhere...
Rails/OutputSafety:
Enabled: false
@@ -90,6 +102,9 @@ Rails/Output:
Rails/RakeEnvironment:
Enabled: false
+Rails/ReversibleMigrationMethodDefinition:
+ Enabled: true
+
# Allow update_attribute, update_all, touch, etc.
Rails/SkipsModelValidations:
Enabled: false
@@ -101,6 +116,12 @@ Rails/UnknownEnv:
- staging
- production
+RSpec:
+ Include:
+ - "(?:^|/)factories/"
+ - "(?:^|/)features/"
+ - "(?:^|/)spec/"
+
# Allow "allow_any_instance_of"
RSpec/AnyInstance:
Enabled: false
@@ -202,6 +223,9 @@ Style/PercentLiteralDelimiters:
Style/RedundantSelf:
Enabled: false
+Style/SelectByRegexp:
+ Enabled: true
+
Style/SingleLineMethods:
AllowIfMethodIsEmpty: false
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
new file mode 100644
index 00000000000..7423de01eda
--- /dev/null
+++ b/.rubocop_todo.yml
@@ -0,0 +1,4994 @@
+# This configuration was generated by
+# `rubocop --auto-gen-config --auto-gen-only-exclude --exclude-limit 100000000000000`
+# on 2023-05-28 17:16:12 UTC using RuboCop version 1.22.1.
+# The point is for the user to remove these configuration records
+# one by one as the offenses are removed from the code base.
+# Note that changes in the inspected code, or installation of new
+# versions of RuboCop, may require this file to be generated again.
+
+# Offense count: 8
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, IndentationWidth.
+# SupportedStyles: outdent, indent
+Layout/AccessModifierIndentation:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/downloads_controller.rb'
+ - 'app/models/potential_match.rb'
+
+# Offense count: 133
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, IndentationWidth.
+# SupportedStyles: with_first_argument, with_fixed_indentation
+Layout/ArgumentAlignment:
+ Exclude:
+ - 'app/controllers/admin/admin_invitations_controller.rb'
+ - 'app/controllers/challenge_assignments_controller.rb'
+ - 'app/controllers/collection_items_controller.rb'
+ - 'app/controllers/comments_controller.rb'
+ - 'app/controllers/feedbacks_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/bookmarks_helper.rb'
+ - 'app/helpers/comments_helper.rb'
+ - 'app/helpers/mailer_helper.rb'
+ - 'app/helpers/tag_sets_helper.rb'
+ - 'app/models/abuse_report.rb'
+ - 'app/models/admin_post.rb'
+ - 'app/models/bookmark.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_profile.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/external_author_name.rb'
+ - 'app/models/gift.rb'
+ - 'app/models/known_issue.rb'
+ - 'app/models/prompt.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/question.rb'
+ - 'app/models/series.rb'
+ - 'app/models/skin_parent.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'app/models/work.rb'
+ - 'features/step_definitions/bookmark_steps.rb'
+ - 'features/step_definitions/work_search_steps.rb'
+ - 'lib/acts_as_commentable/comment_methods.rb'
+ - 'lib/autocomplete_source.rb'
+ - 'lib/creation_notifier.rb'
+ - 'spec/controllers/tags_controller_spec.rb'
+ - 'spec/models/search/work_search_form_spec.rb'
+
+# Offense count: 23
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, IndentationWidth.
+# SupportedStyles: with_first_element, with_fixed_indentation
+Layout/ArrayAlignment:
+ Exclude:
+ - 'app/controllers/challenge/prompt_meme_controller.rb'
+ - 'app/models/collection_participant.rb'
+ - 'app/models/potential_match_settings.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/subscription.rb'
+ - 'spec/lib/html_cleaner_spec.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: IndentationWidth.
+Layout/AssignmentIndentation:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/collection_participants_controller.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyleAlignWith, Severity.
+# SupportedStylesAlignWith: start_of_line, begin
+Layout/BeginEndAlignment:
+ Exclude:
+ - 'app/controllers/tags_controller.rb'
+
+# Offense count: 3
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyleAlignWith.
+# SupportedStylesAlignWith: either, start_of_block, start_of_line
+Layout/BlockAlignment:
+ Exclude:
+ - 'app/controllers/readings_controller.rb'
+ - 'app/models/admin_post.rb'
+ - 'app/models/challenge_signup.rb'
+
+# Offense count: 13
+# Cop supports --auto-correct.
+Layout/BlockEndNewline:
+ Exclude:
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/gift.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/work.rb'
+ - 'spec/controllers/admin_posts_controller_spec.rb'
+ - 'spec/controllers/tag_set_nominations_controller_spec.rb'
+ - 'spec/models/favorite_tag_spec.rb'
+
+# Offense count: 17
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, IndentOneStep, IndentationWidth.
+# SupportedStyles: case, end
+Layout/CaseIndentation:
+ Exclude:
+ - 'app/controllers/challenge_assignments_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/bookmarks_helper.rb'
+ - 'app/helpers/comments_helper.rb'
+ - 'app/models/inbox_comment.rb'
+
+# Offense count: 7
+# Cop supports --auto-correct.
+Layout/ClosingParenthesisIndentation:
+ Exclude:
+ - 'app/controllers/collection_participants_controller.rb'
+ - 'app/mailers/user_mailer.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/search/query_result.rb'
+ - 'app/models/work.rb'
+
+# Offense count: 9
+# Cop supports --auto-correct.
+Layout/CommentIndentation:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/user.rb'
+ - 'config/initializers/devise.rb'
+ - 'spec/models/external_work_spec.rb'
+
+# Offense count: 3
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyleAlignWith, Severity.
+# SupportedStylesAlignWith: start_of_line, def
+Layout/DefEndAlignment:
+ Exclude:
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/translation_helper.rb'
+ - 'app/models/user.rb'
+
+# Offense count: 241
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: leading, trailing
+Layout/DotPosition:
+ Exclude:
+ - 'app/controllers/archive_faqs_controller.rb'
+ - 'app/controllers/challenge_requests_controller.rb'
+ - 'app/controllers/collection_items_controller.rb'
+ - 'app/controllers/creatorships_controller.rb'
+ - 'app/controllers/fandoms_controller.rb'
+ - 'app/controllers/pseuds_controller.rb'
+ - 'app/controllers/stats_controller.rb'
+ - 'app/controllers/tag_set_associations_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/controllers/users_controller.rb'
+ - 'app/decorators/homepage.rb'
+ - 'app/models/bookmark.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_participant.rb'
+ - 'app/models/concerns/creatable.rb'
+ - 'app/models/fandom.rb'
+ - 'app/models/gift.rb'
+ - 'app/models/inbox_comment.rb'
+ - 'app/models/potential_match.rb'
+ - 'app/models/prompt.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/question.rb'
+ - 'app/models/related_work.rb'
+ - 'app/models/search/bookmark_search_form.rb'
+ - 'app/models/search_range.rb'
+ - 'app/models/series.rb'
+ - 'app/models/spam_report.rb'
+ - 'app/models/subscription.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'app/models/tagset_models/tag_set_association.rb'
+ - 'app/models/tagset_models/tag_set_nomination.rb'
+ - 'app/models/user.rb'
+ - 'app/models/work.rb'
+ - 'config/initializers/monkeypatches/textarea_convert_html_to_newlines.rb'
+ - 'features/step_definitions/work_import_steps.rb'
+ - 'lib/html_cleaner.rb'
+ - 'lib/otw_sanitize/user_class_sanitizer.rb'
+ - 'lib/tasks/creatorships.rake'
+ - 'spec/controllers/api/api_helper.rb'
+ - 'spec/controllers/tag_set_nominations_controller_spec.rb'
+ - 'spec/controllers/tags_controller_spec.rb'
+ - 'spec/helpers/tag_sets_helper_spec.rb'
+ - 'spec/models/feedback_reporters/feedback_reporter_spec.rb'
+ - 'spec/models/filter_count_spec.rb'
+ - 'spec/models/search/async_indexer_spec.rb'
+ - 'spec/models/story_parser_spec.rb'
+ - 'spec/models/work_spec.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Layout/ElseAlignment:
+ Exclude:
+ - 'app/helpers/external_authors_helper.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: AllowBorderComment, AllowMarginComment.
+Layout/EmptyComment:
+ Exclude:
+ - 'app/models/tagset_models/tag_set.rb'
+
+# Offense count: 189
+# Cop supports --auto-correct.
+Layout/EmptyLineAfterGuardClause:
+ Exclude:
+ - 'app/controllers/admin/skins_controller.rb'
+ - 'app/controllers/admin/user_creations_controller.rb'
+ - 'app/controllers/admin_posts_controller.rb'
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/controllers/bookmarks_controller.rb'
+ - 'app/controllers/challenge_assignments_controller.rb'
+ - 'app/controllers/chapters_controller.rb'
+ - 'app/controllers/comments_controller.rb'
+ - 'app/controllers/downloads_controller.rb'
+ - 'app/controllers/external_authors_controller.rb'
+ - 'app/controllers/people_controller.rb'
+ - 'app/controllers/pseuds_controller.rb'
+ - 'app/controllers/series_controller.rb'
+ - 'app/controllers/skins_controller.rb'
+ - 'app/controllers/tag_set_associations_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/controllers/tag_wranglings_controller.rb'
+ - 'app/controllers/works_controller.rb'
+ - 'app/decorators/bookmarkable_decorator.rb'
+ - 'app/decorators/homepage.rb'
+ - 'app/decorators/pseud_decorator.rb'
+ - 'app/helpers/date_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/translation_helper.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/helpers/works_helper.rb'
+ - 'app/mailers/user_mailer.rb'
+ - 'app/models/abuse_report.rb'
+ - 'app/models/admin_post_tag.rb'
+ - 'app/models/bookmark.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_claim.rb'
+ - 'app/models/challenge_signup_summary.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/common_tagging.rb'
+ - 'app/models/concerns/creatable.rb'
+ - 'app/models/download.rb'
+ - 'app/models/download_writer.rb'
+ - 'app/models/feedback.rb'
+ - 'app/models/indexing/cache_master.rb'
+ - 'app/models/indexing/index_queue.rb'
+ - 'app/models/invitation.rb'
+ - 'app/models/invite_request.rb'
+ - 'app/models/moderated_work.rb'
+ - 'app/models/potential_match_builder.rb'
+ - 'app/models/potential_matcher/potential_matcher_progress.rb'
+ - 'app/models/preference.rb'
+ - 'app/models/profile.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/search/bookmark_query.rb'
+ - 'app/models/search/bookmarkable_query.rb'
+ - 'app/models/search/index_sweeper.rb'
+ - 'app/models/search/pseud_search_form.rb'
+ - 'app/models/search/query.rb'
+ - 'app/models/search/query_cleaner.rb'
+ - 'app/models/search/query_result.rb'
+ - 'app/models/search/tag_indexer.rb'
+ - 'app/models/search/tag_query.rb'
+ - 'app/models/search/taggable_query.rb'
+ - 'app/models/search/work_query.rb'
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/serial_work.rb'
+ - 'app/models/series.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/cast_nomination.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/user.rb'
+ - 'app/models/work.rb'
+ - 'app/models/work_link.rb'
+ - 'app/models/work_skin.rb'
+ - 'app/validators/url_active_validator.rb'
+ - 'app/validators/url_format_validator.rb'
+ - 'config/initializers/monkeypatches/textarea_convert_html_to_newlines.rb'
+ - 'features/step_definitions/comment_steps.rb'
+ - 'features/step_definitions/potential_match_steps.rb'
+ - 'lib/backwards_compatible_password_decryptor.rb'
+ - 'lib/bookmarkable.rb'
+ - 'lib/collectible.rb'
+ - 'lib/creation_notifier.rb'
+ - 'lib/css_cleaner.rb'
+ - 'lib/html_cleaner.rb'
+ - 'lib/otw_sanitize/embed_sanitizer.rb'
+ - 'lib/otw_sanitize/media_sanitizer.rb'
+ - 'lib/otw_sanitize/user_class_sanitizer.rb'
+ - 'lib/redis_test_setup.rb'
+ - 'lib/responder.rb'
+ - 'lib/tasks/load_autocomplete_data.rake'
+ - 'lib/tasks/resque.rake'
+ - 'script/gift_exchange/generate_from_spec.rb'
+ - 'script/gift_exchange/load_json_challenge.rb'
+
+# Offense count: 21
+# Cop supports --auto-correct.
+Layout/EmptyLineAfterMagicComment:
+ Exclude:
+ - 'app/helpers/exports_helper.rb'
+ - 'factories/api_key.rb'
+ - 'factories/prompt.rb'
+ - 'features/step_definitions/tag_set_steps.rb'
+ - 'lib/pagination_list_link_renderer.rb'
+ - 'spec/controllers/admin/api_controller_spec.rb'
+ - 'spec/controllers/api/v2/api_works_search_spec.rb'
+ - 'spec/controllers/api/v2/api_works_spec.rb'
+ - 'spec/controllers/challenge_claims_controller_spec.rb'
+ - 'spec/controllers/collection_participants_controller_spec.rb'
+ - 'spec/controllers/external_authors_controller_spec.rb'
+ - 'spec/controllers/owned_tag_sets_controller_spec.rb'
+ - 'spec/controllers/works/default_rails_actions_spec.rb'
+ - 'spec/controllers/works/drafts_spec.rb'
+ - 'spec/controllers/works/importing_spec.rb'
+ - 'spec/controllers/works/multiple_actions_spec.rb'
+ - 'spec/helpers/exports_helper_spec.rb'
+ - 'spec/lib/html_cleaner_spec.rb'
+ - 'spec/models/filter_count_spec.rb'
+ - 'spec/models/stat_counter_spec.rb'
+ - 'spec/models/tag_spec.rb'
+
+# Offense count: 56
+# Cop supports --auto-correct.
+# Configuration parameters: EmptyLineBetweenMethodDefs, EmptyLineBetweenClassDefs, EmptyLineBetweenModuleDefs, AllowAdjacentOneLineDefs, NumberOfEmptyLines.
+Layout/EmptyLineBetweenDefs:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/controllers/bookmarks_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/chapters_controller.rb'
+ - 'app/controllers/locales_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/controllers/user_invite_requests_controller.rb'
+ - 'app/helpers/external_authors_helper.rb'
+ - 'app/models/admin_post.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_claim.rb'
+ - 'app/models/character.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_participant.rb'
+ - 'app/models/fandom.rb'
+ - 'app/models/freeform.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/relationship.rb'
+ - 'app/models/search_range.rb'
+ - 'app/models/series.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'app/models/user.rb'
+ - 'config/initializers/monkeypatches/translate_string.rb'
+ - 'lib/acts_as_commentable/comment_methods.rb'
+ - 'lib/collectible.rb'
+ - 'lib/css_cleaner.rb'
+
+# Offense count: 82
+# Cop supports --auto-correct.
+Layout/EmptyLines:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/controllers/challenge_assignments_controller.rb'
+ - 'app/controllers/challenge_claims_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/chapters_controller.rb'
+ - 'app/controllers/locales_controller.rb'
+ - 'app/controllers/owned_tag_sets_controller.rb'
+ - 'app/controllers/potential_matches_controller.rb'
+ - 'app/controllers/tag_set_associations_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/helpers/external_authors_helper.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_claim.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/fandom.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/search/bookmarkable_query.rb'
+ - 'app/models/search_range.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/fandom_nomination.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'config/deploy/production.rb'
+ - 'config/initializers/monkeypatches/override_default_form_field_sizes.rb'
+ - 'config/initializers/monkeypatches/translate_string.rb'
+ - 'features/step_definitions/challege_gift_exchange_steps.rb'
+ - 'features/step_definitions/challenge_steps.rb'
+ - 'features/step_definitions/generic_steps.rb'
+ - 'features/step_definitions/profile_steps.rb'
+ - 'lib/acts_as_commentable/comment_methods.rb'
+ - 'lib/collectible.rb'
+ - 'lib/css_cleaner.rb'
+ - 'lib/html_cleaner.rb'
+ - 'lib/tasks/cucumber.rake'
+ - 'script/get_user_data.rb'
+ - 'spec/models/external_work_spec.rb'
+ - 'spec/models/invitation_spec.rb'
+ - 'spec/models/skin_spec.rb'
+
+# Offense count: 17
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: around, only_before
+Layout/EmptyLinesAroundAccessModifier:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/archive_faqs_controller.rb'
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/languages_controller.rb'
+ - 'app/controllers/tag_set_associations_controller.rb'
+ - 'app/models/potential_match.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/user.rb'
+ - 'app/sweepers/collection_sweeper.rb'
+ - 'lib/collectible.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Layout/EmptyLinesAroundArguments:
+ Exclude:
+ - 'app/controllers/works_controller.rb'
+ - 'spec/models/story_parser_spec.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Layout/EmptyLinesAroundBeginBody:
+ Exclude:
+ - 'app/controllers/api/v2/bookmarks_controller.rb'
+
+# Offense count: 87
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: empty_lines, no_empty_lines
+Layout/EmptyLinesAroundBlockBody:
+ Exclude:
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'config/routes.rb'
+ - 'factories/subscriptions.rb'
+ - 'factories/user_invite_requests.rb'
+ - 'factories/works.rb'
+ - 'factories/wrangling_guideline.rb'
+ - 'lib/collectible.rb'
+ - 'lib/tasks/admin_tasks.rake'
+ - 'lib/tasks/deploy_tasks.rake'
+ - 'lib/tasks/memcached.rake'
+ - 'lib/tasks/resque.rake'
+ - 'lib/tasks/skin_tasks.rake'
+ - 'lib/tasks/spam_report.rake'
+ - 'spec/controllers/api/v2/api_base_controller_spec.rb'
+ - 'spec/controllers/bookmarks_controller_spec.rb'
+ - 'spec/controllers/invite_requests_controller_spec.rb'
+ - 'spec/controllers/works/default_rails_actions_spec.rb'
+ - 'spec/helpers/exports_helper_spec.rb'
+ - 'spec/helpers/home_helper_spec.rb'
+ - 'spec/helpers/inbox_helper_spec.rb'
+ - 'spec/helpers/user_invite_requests_helper_spec.rb'
+ - 'spec/lib/collectible_spec.rb'
+ - 'spec/lib/html_cleaner_spec.rb'
+ - 'spec/lib/string_cleaner_spec.rb'
+ - 'spec/lib/url_formatter_spec.rb'
+ - 'spec/lib/works_owner_spec.rb'
+ - 'spec/models/admin_blacklisted_email_spec.rb'
+ - 'spec/models/challenge_assignment_spec.rb'
+ - 'spec/models/challenge_claim_spec.rb'
+ - 'spec/models/collection_spec.rb'
+ - 'spec/models/comment_spec.rb'
+ - 'spec/models/external_author_spec.rb'
+ - 'spec/models/external_work_spec.rb'
+ - 'spec/models/gift_exchange_spec.rb'
+ - 'spec/models/gift_spec.rb'
+ - 'spec/models/invitation_spec.rb'
+ - 'spec/models/potential_match_spec.rb'
+ - 'spec/models/search/bookmark_query_spec.rb'
+ - 'spec/models/search/index_sweeper_spec.rb'
+ - 'spec/models/search/work_query_spec.rb'
+ - 'spec/models/search/work_search_form_exclusion_filters_spec.rb'
+ - 'spec/models/skin_parent_spec.rb'
+ - 'spec/models/stat_counter_spec.rb'
+ - 'spec/models/story_parser_spec.rb'
+ - 'spec/models/tag_set_nomination_spec.rb'
+ - 'spec/models/unsorted_tag_spec.rb'
+
+# Offense count: 129
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only
+Layout/EmptyLinesAroundClassBody:
+ Exclude:
+ - 'app/controllers/admin/admin_invitations_controller.rb'
+ - 'app/controllers/admin/banners_controller.rb'
+ - 'app/controllers/admin/blacklisted_emails_controller.rb'
+ - 'app/controllers/admin/skins_controller.rb'
+ - 'app/controllers/admin/spam_controller.rb'
+ - 'app/controllers/admin_posts_controller.rb'
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/archive_faqs_controller.rb'
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/controllers/challenge/gift_exchange_controller.rb'
+ - 'app/controllers/challenge/prompt_meme_controller.rb'
+ - 'app/controllers/challenge_assignments_controller.rb'
+ - 'app/controllers/challenge_claims_controller.rb'
+ - 'app/controllers/challenge_requests_controller.rb'
+ - 'app/controllers/challenges_controller.rb'
+ - 'app/controllers/collection_profile_controller.rb'
+ - 'app/controllers/collections_controller.rb'
+ - 'app/controllers/downloads_controller.rb'
+ - 'app/controllers/feedbacks_controller.rb'
+ - 'app/controllers/gifts_controller.rb'
+ - 'app/controllers/home_controller.rb'
+ - 'app/controllers/known_issues_controller.rb'
+ - 'app/controllers/languages_controller.rb'
+ - 'app/controllers/locales_controller.rb'
+ - 'app/controllers/menu_controller.rb'
+ - 'app/controllers/opendoors/external_authors_controller.rb'
+ - 'app/controllers/owned_tag_sets_controller.rb'
+ - 'app/controllers/people_controller.rb'
+ - 'app/controllers/potential_matches_controller.rb'
+ - 'app/controllers/profile_controller.rb'
+ - 'app/controllers/prompts_controller.rb'
+ - 'app/controllers/pseuds_controller.rb'
+ - 'app/controllers/readings_controller.rb'
+ - 'app/controllers/redirect_controller.rb'
+ - 'app/controllers/related_works_controller.rb'
+ - 'app/controllers/serial_works_controller.rb'
+ - 'app/controllers/skins_controller.rb'
+ - 'app/controllers/stats_controller.rb'
+ - 'app/controllers/subscriptions_controller.rb'
+ - 'app/controllers/tag_set_associations_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/controllers/unsorted_tags_controller.rb'
+ - 'app/controllers/users/sessions_controller.rb'
+ - 'app/controllers/work_links_controller.rb'
+ - 'app/controllers/works_controller.rb'
+ - 'app/controllers/wrangling_guidelines_controller.rb'
+ - 'app/decorators/homepage.rb'
+ - 'app/decorators/pseud_decorator.rb'
+ - 'app/mailers/comment_mailer.rb'
+ - 'app/mailers/user_mailer.rb'
+ - 'app/models/admin_post_tag.rb'
+ - 'app/models/banned.rb'
+ - 'app/models/category.rb'
+ - 'app/models/challenge_signup_summary.rb'
+ - 'app/models/character.rb'
+ - 'app/models/collection_profile.rb'
+ - 'app/models/external_author_name.rb'
+ - 'app/models/external_creatorship.rb'
+ - 'app/models/fandom.rb'
+ - 'app/models/freeform.rb'
+ - 'app/models/gift.rb'
+ - 'app/models/indexing/cache_master.rb'
+ - 'app/models/indexing/index_queue.rb'
+ - 'app/models/indexing/scheduled_reindex_job.rb'
+ - 'app/models/log_item.rb'
+ - 'app/models/potential_match.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/rating.rb'
+ - 'app/models/redis_mail_queue.rb'
+ - 'app/models/relationship.rb'
+ - 'app/models/search/async_indexer.rb'
+ - 'app/models/search/bookmark_indexer.rb'
+ - 'app/models/search/bookmark_search_form.rb'
+ - 'app/models/search/bookmarkable_indexer.rb'
+ - 'app/models/search/index_sweeper.rb'
+ - 'app/models/search/indexer.rb'
+ - 'app/models/search/pseud_indexer.rb'
+ - 'app/models/search/pseud_query.rb'
+ - 'app/models/search/pseud_search_form.rb'
+ - 'app/models/search/query.rb'
+ - 'app/models/search/query_cleaner.rb'
+ - 'app/models/search/query_result.rb'
+ - 'app/models/search/tag_indexer.rb'
+ - 'app/models/search/tag_query.rb'
+ - 'app/models/search/tag_search_form.rb'
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/search_range.rb'
+ - 'app/models/tagset_models/cast_nomination.rb'
+ - 'app/models/tagset_models/fandom_nomination.rb'
+ - 'app/models/tagset_models/tag_set_association.rb'
+ - 'app/models/work_link.rb'
+ - 'app/sweepers/feed_sweeper.rb'
+ - 'app/sweepers/pseud_sweeper.rb'
+ - 'app/sweepers/tag_set_sweeper.rb'
+ - 'app/validators/url_active_validator.rb'
+ - 'app/validators/url_format_validator.rb'
+ - 'config/initializers/gem-plugin_config/permit_yo_config.rb'
+ - 'config/initializers/rack_attack.rb'
+ - 'lib/pagination_list_link_renderer.rb'
+ - 'lib/word_counter.rb'
+
+# Offense count: 8
+# Cop supports --auto-correct.
+Layout/EmptyLinesAroundMethodBody:
+ Exclude:
+ - 'app/controllers/chapters_controller.rb'
+ - 'app/controllers/collection_items_controller.rb'
+ - 'app/controllers/stats_controller.rb'
+ - 'app/controllers/tag_set_associations_controller.rb'
+ - 'app/helpers/prompt_restrictions_helper.rb'
+ - 'app/helpers/tag_sets_helper.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/skin_parent.rb'
+
+# Offense count: 38
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines
+Layout/EmptyLinesAroundModuleBody:
+ Exclude:
+ - 'app/helpers/advanced_search_helper.rb'
+ - 'app/helpers/date_helper.rb'
+ - 'app/helpers/exports_helper.rb'
+ - 'app/helpers/external_authors_helper.rb'
+ - 'app/helpers/inbox_helper.rb'
+ - 'app/helpers/invitations_helper.rb'
+ - 'app/helpers/mailer_helper.rb'
+ - 'app/helpers/search_helper.rb'
+ - 'app/helpers/series_helper.rb'
+ - 'app/helpers/tag_sets_helper.rb'
+ - 'app/helpers/tag_type_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/translation_helper.rb'
+ - 'app/helpers/user_invite_requests_helper.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/helpers/works_helper.rb'
+ - 'app/models/search/taggable_query.rb'
+ - 'config/initializers/gem-plugin_config/permit_yo_config.rb'
+ - 'config/initializers/monkeypatches/accept_header.rb'
+ - 'lib/acts_as_commentable/comment_methods.rb'
+ - 'lib/acts_as_commentable/commentable_entity.rb'
+ - 'lib/bookmarkable.rb'
+ - 'lib/challenge_core.rb'
+ - 'lib/collectible.rb'
+ - 'lib/css_cleaner.rb'
+ - 'lib/redis_test_setup.rb'
+ - 'lib/searchable.rb'
+ - 'lib/string_cleaner.rb'
+ - 'lib/works_owner.rb'
+
+# Offense count: 8
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyleAlignWith, Severity.
+# SupportedStylesAlignWith: keyword, variable, start_of_line
+Layout/EndAlignment:
+ Exclude:
+ - 'app/controllers/challenge_assignments_controller.rb'
+ - 'app/controllers/user_invite_requests_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/bookmarks_helper.rb'
+ - 'app/helpers/external_authors_helper.rb'
+ - 'app/models/inbox_comment.rb'
+
+# Offense count: 34
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: native, lf, crlf
+Layout/EndOfLine:
+ Exclude:
+ - 'app/controllers/abuse_reports_controller.rb'
+ - 'app/helpers/collections_helper.rb'
+ - 'app/helpers/mailer_helper.rb'
+ - 'app/jobs/hit_count_update_job.rb'
+ - 'app/jobs/readings_job.rb'
+ - 'app/jobs/redis_hash_job.rb'
+ - 'app/jobs/redis_job_spawner.rb'
+ - 'app/jobs/redis_set_job.rb'
+ - 'app/jobs/tag_count_update_job.rb'
+ - 'app/jobs/tag_method_job.rb'
+ - 'app/mailers/kudo_mailer.rb'
+ - 'app/models/admin_setting.rb'
+ - 'app/models/redis_hit_counter.rb'
+ - 'app/policies/tag_wrangler_policy.rb'
+ - 'config/environments/test.rb'
+ - 'config/routes.rb'
+ - 'db/migrate/20230106203903_drop_open_id_tables.rb'
+ - 'features/step_definitions/collection_steps.rb'
+ - 'features/step_definitions/reading_steps.rb'
+ - 'features/step_definitions/tag_set_steps.rb'
+ - 'lib/otw_sanitize/embed_sanitizer.rb'
+ - 'lib/redis_scanning.rb'
+ - 'spec/controllers/api/v2/api_works_spec.rb'
+ - 'spec/controllers/collection_items_controller_spec.rb'
+ - 'spec/jobs/hit_count_update_job_spec.rb'
+ - 'spec/lib/html_cleaner_spec.rb'
+ - 'spec/lib/i18n/i18n_tasks_spec.rb'
+ - 'spec/mailers/user_mailer_spec.rb'
+ - 'spec/models/admin_setting_spec.rb'
+ - 'spec/models/pseud_spec.rb'
+ - 'spec/models/redis_hit_counter_spec.rb'
+ - 'spec/models/tag_spec.rb'
+ - 'spec/spec_helper.rb'
+ - 'spec/support/i18n_newlines_tasks.rb'
+
+# Offense count: 27
+# Cop supports --auto-correct.
+# Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment.
+Layout/ExtraSpacing:
+ Exclude:
+ - 'Gemfile'
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/collection_participants_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_claim.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_item.rb'
+ - 'config.ru'
+ - 'config/deploy.rb'
+ - 'config/schedule.rb'
+ - 'features/support/paths.rb'
+ - 'lib/tasks/memcached.rake'
+ - 'spec/lib/html_cleaner_spec.rb'
+ - 'spec/models/story_parser_spec.rb'
+
+# Offense count: 6
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, IndentationWidth.
+# SupportedStyles: consistent, consistent_relative_to_receiver, special_for_inner_method_call, special_for_inner_method_call_in_parentheses
+Layout/FirstArgumentIndentation:
+ Exclude:
+ - 'app/helpers/comments_helper.rb'
+ - 'app/mailers/user_mailer.rb'
+ - 'app/models/tag.rb'
+ - 'features/step_definitions/user_steps.rb'
+
+# Offense count: 6
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, IndentationWidth.
+# SupportedStyles: special_inside_parentheses, consistent, align_brackets
+Layout/FirstArrayElementIndentation:
+ Exclude:
+ - 'app/models/collection.rb'
+ - 'app/models/potential_match_settings.rb'
+ - 'app/models/skin.rb'
+
+# Offense count: 19
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, IndentationWidth.
+# SupportedStyles: special_inside_parentheses, consistent, align_braces
+Layout/FirstHashElementIndentation:
+ Exclude:
+ - 'app/controllers/comments_controller.rb'
+ - 'app/controllers/stats_controller.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/work.rb'
+ - 'spec/controllers/tag_set_nominations_controller_spec.rb'
+ - 'spec/models/story_parser_spec.rb'
+
+# Offense count: 106
+# Cop supports --auto-correct.
+# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
+# SupportedHashRocketStyles: key, separator, table
+# SupportedColonStyles: key, separator, table
+# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
+Layout/HashAlignment:
+ Exclude:
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/controllers/comments_controller.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/models/admin_post.rb'
+ - 'app/models/admin_setting.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_item.rb'
+ - 'app/models/collection_participant.rb'
+ - 'app/models/download_writer.rb'
+ - 'app/models/feedback.rb'
+ - 'app/models/known_issue.rb'
+ - 'app/models/potential_match_settings.rb'
+ - 'app/models/preference.rb'
+ - 'app/models/profile.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/question.rb'
+ - 'app/models/search/pseud_query.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'lib/creation_notifier.rb'
+ - 'lib/otw_sanitize/embed_sanitizer.rb'
+ - 'lib/otw_sanitize/media_sanitizer.rb'
+ - 'spec/controllers/api/v2/api_bookmarks_spec.rb'
+ - 'spec/controllers/tag_set_nominations_controller_spec.rb'
+ - 'spec/models/search/pseud_decorator_spec.rb'
+ - 'spec/models/skin_spec.rb'
+
+# Offense count: 285
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: normal, indented_internal_methods
+Layout/IndentationConsistency:
+ Exclude:
+ - 'app/controllers/archive_faqs_controller.rb'
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/controllers/challenge_claims_controller.rb'
+ - 'app/controllers/fandoms_controller.rb'
+ - 'app/controllers/pseuds_controller.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/translation_helper.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/skin_parent.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/user.rb'
+ - 'config/initializers/gem-plugin_config/resque.rb'
+ - 'features/step_definitions/banner_steps.rb'
+ - 'features/step_definitions/challege_gift_exchange_steps.rb'
+ - 'features/step_definitions/challenge_promptmeme_steps.rb'
+ - 'features/step_definitions/challenge_steps.rb'
+ - 'features/step_definitions/collection_steps.rb'
+ - 'features/step_definitions/skin_steps.rb'
+ - 'features/step_definitions/tag_steps.rb'
+ - 'features/step_definitions/work_related_steps.rb'
+ - 'features/step_definitions/work_search_steps.rb'
+ - 'features/step_definitions/work_steps.rb'
+ - 'lib/pagination_list_link_renderer.rb'
+ - 'lib/tasks/opendoors.rake'
+
+# Offense count: 10
+# Cop supports --auto-correct.
+# Configuration parameters: IndentationWidth, EnforcedStyle.
+# SupportedStyles: spaces, tabs
+Layout/IndentationStyle:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/challenge_claims_controller.rb'
+
+# Offense count: 62
+# Cop supports --auto-correct.
+# Configuration parameters: Width, IgnoredPatterns.
+Layout/IndentationWidth:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/controllers/challenge_claims_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/collections_controller.rb'
+ - 'app/controllers/external_works_controller.rb'
+ - 'app/controllers/fandoms_controller.rb'
+ - 'app/helpers/comments_helper.rb'
+ - 'app/helpers/external_authors_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/translation_helper.rb'
+ - 'app/models/admin_post.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/potential_match_settings.rb'
+ - 'app/models/preference.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/skin_parent.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/tagset_models/tag_set_nomination.rb'
+ - 'app/models/user.rb'
+ - 'features/step_definitions/tag_steps.rb'
+ - 'features/step_definitions/work_related_steps.rb'
+ - 'lib/pagination_list_link_renderer.rb'
+ - 'lib/tasks/cucumber.rake'
+ - 'lib/tasks/memcached.rake'
+ - 'lib/tasks/resque.rake'
+ - 'spec/controllers/works/importing_spec.rb'
+
+# Offense count: 52
+# Cop supports --auto-correct.
+# Configuration parameters: AllowDoxygenCommentStyle, AllowGemfileRubyComment.
+Layout/LeadingCommentSpace:
+ Exclude:
+ - 'Gemfile'
+ - 'app/controllers/profile_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/models/admin_setting.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/external_work.rb'
+ - 'app/models/invitation.rb'
+ - 'app/models/invite_request.rb'
+ - 'app/models/tagset_models/freeform_nomination.rb'
+ - 'app/models/user.rb'
+ - 'app/models/user_invite_request.rb'
+ - 'app/models/work.rb'
+ - 'config/deploy.rb'
+ - 'config/deploy/production.rb'
+ - 'config/deploy/staging.rb'
+ - 'config/initializers/devise.rb'
+ - 'config/initializers/gem-plugin_config/escape_utils_config.rb'
+ - 'config/initializers/monkeypatches/translate_string.rb'
+ - 'db/migrate/20171031204025_create_moderated_works.rb'
+ - 'features/step_definitions/challenge_promptmeme_steps.rb'
+ - 'features/step_definitions/icon_steps.rb'
+ - 'features/step_definitions/pickle_steps.rb'
+ - 'lib/tasks/tag_tasks.rake'
+ - 'spec/models/search/work_query_spec.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Layout/LeadingEmptyLines:
+ Exclude:
+ - 'features/step_definitions/challenge_promptmeme_steps.rb'
+ - 'features/step_definitions/challenge_yuletide_steps.rb'
+
+# Offense count: 23
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, IndentationWidth.
+# SupportedStyles: aligned, indented
+Layout/LineEndStringConcatenationIndentation:
+ Exclude:
+ - 'spec/controllers/prompts_controller_spec.rb'
+ - 'spec/controllers/tag_set_associations_controller_spec.rb'
+ - 'spec/controllers/tag_set_nominations_controller_spec.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: symmetrical, new_line, same_line
+Layout/MultilineArrayBraceLayout:
+ Exclude:
+ - 'app/controllers/challenge/prompt_meme_controller.rb'
+ - 'app/models/series.rb'
+
+# Offense count: 13
+# Cop supports --auto-correct.
+Layout/MultilineBlockLayout:
+ Exclude:
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/gift.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/work.rb'
+ - 'spec/controllers/admin_posts_controller_spec.rb'
+ - 'spec/controllers/tag_set_nominations_controller_spec.rb'
+ - 'spec/models/favorite_tag_spec.rb'
+
+# Offense count: 15
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: symmetrical, new_line, same_line
+Layout/MultilineHashBraceLayout:
+ Exclude:
+ - 'app/controllers/api/v2/works_controller.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/favorite_tag.rb'
+ - 'spec/controllers/api/v2/api_bookmarks_spec.rb'
+ - 'spec/controllers/api/v2/api_works_spec.rb'
+
+# Offense count: 13
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: symmetrical, new_line, same_line
+Layout/MultilineMethodCallBraceLayout:
+ Exclude:
+ - 'app/controllers/collection_participants_controller.rb'
+ - 'app/controllers/questions_controller.rb'
+ - 'app/helpers/comments_helper.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/search/query_result.rb'
+ - 'app/models/work.rb'
+ - 'features/step_definitions/user_steps.rb'
+ - 'spec/models/collection_item_spec.rb'
+
+# Offense count: 127
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, IndentationWidth.
+# SupportedStyles: aligned, indented, indented_relative_to_receiver
+Layout/MultilineMethodCallIndentation:
+ Exclude:
+ - 'app/controllers/api/v2/bookmarks_controller.rb'
+ - 'app/controllers/creatorships_controller.rb'
+ - 'app/controllers/fandoms_controller.rb'
+ - 'app/controllers/pseuds_controller.rb'
+ - 'app/controllers/tag_wranglers_controller.rb'
+ - 'app/controllers/works_controller.rb'
+ - 'app/decorators/homepage.rb'
+ - 'app/models/bookmark.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_claim.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_participant.rb'
+ - 'app/models/fandom.rb'
+ - 'app/models/gift.rb'
+ - 'app/models/potential_match.rb'
+ - 'app/models/prompt.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/related_work.rb'
+ - 'app/models/search/bookmark_search_form.rb'
+ - 'app/models/search_range.rb'
+ - 'app/models/series.rb'
+ - 'app/models/spam_report.rb'
+ - 'app/models/subscription.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'app/models/tagset_models/tag_set_association.rb'
+ - 'app/models/tagset_models/tag_set_nomination.rb'
+ - 'app/models/user.rb'
+ - 'app/models/work.rb'
+ - 'config/initializers/monkeypatches/textarea_convert_html_to_newlines.rb'
+ - 'lib/html_cleaner.rb'
+ - 'lib/otw_sanitize/user_class_sanitizer.rb'
+ - 'spec/models/search/index_sweeper_spec.rb'
+
+# Offense count: 21
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, IndentationWidth.
+# SupportedStyles: aligned, indented
+Layout/MultilineOperationIndentation:
+ Exclude:
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/potential_matches_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/external_authors_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/collection_item.rb'
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'features/support/paths.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Layout/RescueEnsureAlignment:
+ Exclude:
+ - 'app/controllers/tags_controller.rb'
+
+# Offense count: 294
+# Cop supports --auto-correct.
+Layout/SingleLineBlockChain:
+ Exclude:
+ - 'app/controllers/api/v2/works_controller.rb'
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/collection_items_controller.rb'
+ - 'app/controllers/collection_participants_controller.rb'
+ - 'app/controllers/stats_controller.rb'
+ - 'app/controllers/tag_wranglers_controller.rb'
+ - 'app/controllers/tags_controller.rb'
+ - 'app/controllers/works_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/collections_helper.rb'
+ - 'app/helpers/exports_helper.rb'
+ - 'app/helpers/mailer_helper.rb'
+ - 'app/helpers/mute_helper.rb'
+ - 'app/helpers/pseuds_helper.rb'
+ - 'app/helpers/tag_sets_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/helpers/works_helper.rb'
+ - 'app/mailers/admin_mailer.rb'
+ - 'app/models/admin_post.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_signup_summary.rb'
+ - 'app/models/character.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/concerns/filterable.rb'
+ - 'app/models/external_author.rb'
+ - 'app/models/freeform.rb'
+ - 'app/models/potential_match.rb'
+ - 'app/models/potential_match_settings.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/relationship.rb'
+ - 'app/models/search/index_sweeper.rb'
+ - 'app/models/series.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/spam_report.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/tagset_models/tag_set_association.rb'
+ - 'app/models/tagset_models/tag_set_nomination.rb'
+ - 'app/models/work.rb'
+ - 'lib/acts_as_commentable/commentable_entity.rb'
+ - 'lib/autocomplete_source.rb'
+ - 'lib/collectible.rb'
+ - 'lib/searchable.rb'
+ - 'lib/word_counter.rb'
+ - 'script/create_admin.rb'
+ - 'script/gift_exchange/load_json_challenge.rb'
+ - 'spec/controllers/admin/admin_users_controller_spec.rb'
+ - 'spec/controllers/admin/banners_controller_spec.rb'
+ - 'spec/controllers/admin/blacklisted_emails_controller_spec.rb'
+ - 'spec/controllers/admin/skins_controller_spec.rb'
+ - 'spec/controllers/admin/user_creations_controller_spec.rb'
+ - 'spec/controllers/admin_posts_controller_spec.rb'
+ - 'spec/controllers/archive_faqs_controller_spec.rb'
+ - 'spec/controllers/bookmarks_controller_spec.rb'
+ - 'spec/controllers/challenge_assignments_controller_spec.rb'
+ - 'spec/controllers/chapters_controller_spec.rb'
+ - 'spec/controllers/collection_items_controller_spec.rb'
+ - 'spec/controllers/comments_controller_blocking_spec.rb'
+ - 'spec/controllers/comments_controller_spec.rb'
+ - 'spec/controllers/creatorships_controller_spec.rb'
+ - 'spec/controllers/invite_requests_controller_spec.rb'
+ - 'spec/controllers/skins_controller_spec.rb'
+ - 'spec/controllers/tag_set_nominations_controller_spec.rb'
+ - 'spec/controllers/works/default_rails_actions_spec.rb'
+ - 'spec/controllers/works/importing_spec.rb'
+ - 'spec/controllers/works/multiple_actions_spec.rb'
+ - 'spec/lib/i18n/i18n_tasks_spec.rb'
+ - 'spec/lib/tasks/admin_tasks.rake_spec.rb'
+ - 'spec/lib/tasks/after_tasks.rake_spec.rb'
+ - 'spec/lib/tasks/creatorships.rake_spec.rb'
+ - 'spec/lib/tasks/opendoors.rake_spec.rb'
+ - 'spec/lib/tasks/resanitize.rake_spec.rb'
+ - 'spec/lib/tasks/skin_tasks.rake_spec.rb'
+ - 'spec/lib/tasks/tag_tasks.rake_spec.rb'
+ - 'spec/lib/tasks/work_tasks.rake_spec.rb'
+ - 'spec/models/admin_spec.rb'
+ - 'spec/models/comment_spec.rb'
+ - 'spec/models/creatorship_spec.rb'
+ - 'spec/models/search/bookmark_indexer_spec.rb'
+ - 'spec/models/search/index_sweeper_spec.rb'
+ - 'spec/models/search/pseud_indexer_spec.rb'
+ - 'spec/models/search/stat_counter_indexer_spec.rb'
+ - 'spec/models/search/work_indexer_spec.rb'
+ - 'spec/models/serial_work_spec.rb'
+ - 'spec/models/story_parser_spec.rb'
+ - 'spec/models/subscription_spec.rb'
+ - 'spec/models/work_spec.rb'
+ - 'spec/support/shared_examples/mailer_shared_examples.rb'
+
+# Offense count: 6
+# Cop supports --auto-correct.
+Layout/SpaceAfterColon:
+ Exclude:
+ - 'app/helpers/validation_helper.rb'
+ - 'app/models/search/pseud_query.rb'
+ - 'spec/controllers/admin_posts_controller_spec.rb'
+ - 'spec/models/search/bookmark_query_spec.rb'
+
+# Offense count: 33
+# Cop supports --auto-correct.
+Layout/SpaceAfterComma:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/owned_tag_sets_controller.rb'
+ - 'app/controllers/subscriptions_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/models/admin_blacklisted_email.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'app/models/tagset_models/tag_set_nomination.rb'
+ - 'app/validators/email_blacklist_validator.rb'
+ - 'app/validators/email_veracity_validator.rb'
+ - 'app/validators/url_active_validator.rb'
+ - 'app/validators/url_format_validator.rb'
+ - 'features/step_definitions/web_steps.rb'
+ - 'lib/html_cleaner.rb'
+ - 'lib/tasks/resque.rake'
+ - 'lib/url_formatter.rb'
+ - 'spec/models/collection_spec.rb'
+ - 'spec/models/moderated_work_spec.rb'
+ - 'spec/models/search/index_sweeper_spec.rb'
+ - 'spec/models/work_spec.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Layout/SpaceAfterMethodName:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'lib/url_formatter.rb'
+
+# Offense count: 68
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: space, no_space
+Layout/SpaceAroundEqualsInParameterDefault:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/bookmarks_helper.rb'
+ - 'app/helpers/comments_helper.rb'
+ - 'app/helpers/search_helper.rb'
+ - 'app/helpers/tag_sets_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/mailers/user_mailer.rb'
+ - 'app/models/admin_activity.rb'
+ - 'app/models/bookmark.rb'
+ - 'app/models/challenge_claim.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_item.rb'
+ - 'app/models/creatorship.rb'
+ - 'app/models/external_work.rb'
+ - 'app/models/invitation.rb'
+ - 'app/models/invite_request.rb'
+ - 'app/models/locale.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/search/pseud_search_form.rb'
+ - 'app/models/search/query.rb'
+ - 'app/models/search/query_result.rb'
+ - 'app/models/search/tag_search_form.rb'
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/work.rb'
+ - 'app/models/work_link.rb'
+ - 'config/initializers/monkeypatches/textarea_convert_html_to_newlines.rb'
+ - 'lib/works_owner.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Layout/SpaceAroundMethodCallOperator:
+ Exclude:
+ - 'spec/controllers/collection_participants_controller_spec.rb'
+
+# Offense count: 68
+# Cop supports --auto-correct.
+# Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator.
+# SupportedStylesForExponentOperator: space, no_space
+Layout/SpaceAroundOperators:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/archive_faqs_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/chapters_controller.rb'
+ - 'app/controllers/collection_participants_controller.rb'
+ - 'app/controllers/opendoors/external_authors_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/helpers/challenge_helper.rb'
+ - 'app/helpers/comments_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/translation_helper.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_claim.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/invite_request.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'features/step_definitions/generic_steps.rb'
+ - 'features/step_definitions/tag_set_steps.rb'
+ - 'features/step_definitions/work_steps.rb'
+ - 'features/support/vcr.rb'
+ - 'lib/acts_as_commentable/comment_methods.rb'
+ - 'lib/autocomplete_source.rb'
+ - 'lib/sortable_list.rb'
+ - 'lib/tasks/memcached.rake'
+ - 'lib/tasks/resque.rake'
+ - 'spec/lib/word_counter_spec.rb'
+ - 'spec/models/search/pseud_decorator_spec.rb'
+
+# Offense count: 35
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces.
+# SupportedStyles: space, no_space
+# SupportedStylesForEmptyBraces: space, no_space
+Layout/SpaceBeforeBlockBraces:
+ Exclude:
+ - 'app/controllers/admin/admin_users_controller.rb'
+ - 'app/controllers/application_controller.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/models/admin_post.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/indexing/scheduled_reindex_job.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/relationship.rb'
+ - 'app/models/search/bookmark_search_form.rb'
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/series.rb'
+ - 'app/models/tag.rb'
+ - 'features/step_definitions/web_steps.rb'
+ - 'lib/collectible.rb'
+ - 'spec/models/work_spec.rb'
+
+# Offense count: 3
+# Cop supports --auto-correct.
+Layout/SpaceBeforeComma:
+ Exclude:
+ - 'app/helpers/tags_helper.rb'
+ - 'app/models/external_creatorship.rb'
+ - 'app/models/work.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: AllowForAlignment.
+Layout/SpaceBeforeFirstArg:
+ Exclude:
+ - 'spec/models/story_parser_spec.rb'
+
+# Offense count: 22
+# Cop supports --auto-correct.
+Layout/SpaceBeforeSemicolon:
+ Exclude:
+ - 'app/models/collection.rb'
+ - 'app/models/collection_participant.rb'
+
+# Offense count: 5
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: require_no_space, require_space
+Layout/SpaceInLambdaLiteral:
+ Exclude:
+ - 'app/models/pseud.rb'
+ - 'app/models/user.rb'
+
+# Offense count: 53
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBrackets.
+# SupportedStyles: space, no_space, compact
+# SupportedStylesForEmptyBrackets: space, no_space
+Layout/SpaceInsideArrayLiteralBrackets:
+ Exclude:
+ - 'app/controllers/challenge/prompt_meme_controller.rb'
+ - 'app/controllers/chapters_controller.rb'
+ - 'app/controllers/comments_controller.rb'
+ - 'app/controllers/owned_tag_sets_controller.rb'
+ - 'app/controllers/series_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_participant.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/tagset_models/tag_set_association.rb'
+ - 'spec/controllers/api/v2/api_bookmarks_spec.rb'
+
+# Offense count: 386
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
+# SupportedStyles: space, no_space
+# SupportedStylesForEmptyBraces: space, no_space
+Layout/SpaceInsideBlockBraces:
+ Exclude:
+ - 'app/controllers/admin/skins_controller.rb'
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/archive_faqs_controller.rb'
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/collection_items_controller.rb'
+ - 'app/controllers/collection_participants_controller.rb'
+ - 'app/controllers/fandoms_controller.rb'
+ - 'app/controllers/owned_tag_sets_controller.rb'
+ - 'app/controllers/stats_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/controllers/unsorted_tags_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/pseuds_helper.rb'
+ - 'app/helpers/tag_sets_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/mailers/user_mailer.rb'
+ - 'app/models/bookmark.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_claim.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/character.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_item.rb'
+ - 'app/models/collection_participant.rb'
+ - 'app/models/external_author.rb'
+ - 'app/models/external_work.rb'
+ - 'app/models/fandom.rb'
+ - 'app/models/freeform.rb'
+ - 'app/models/gift.rb'
+ - 'app/models/potential_match.rb'
+ - 'app/models/potential_match_settings.rb'
+ - 'app/models/prompt.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/relationship.rb'
+ - 'app/models/series.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/subscription.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/cast_nomination.rb'
+ - 'app/models/tagset_models/fandom_nomination.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/tagset_models/tag_set_association.rb'
+ - 'app/models/tagset_models/tag_set_nomination.rb'
+ - 'app/models/work.rb'
+ - 'app/models/work_skin.rb'
+ - 'app/sweepers/tag_set_sweeper.rb'
+ - 'app/validators/url_active_validator.rb'
+ - 'features/step_definitions/challege_gift_exchange_steps.rb'
+ - 'features/step_definitions/generic_steps.rb'
+ - 'features/step_definitions/web_steps.rb'
+ - 'lib/acts_as_commentable/commentable_entity.rb'
+ - 'lib/autocomplete_source.rb'
+ - 'lib/collectible.rb'
+ - 'lib/css_cleaner.rb'
+ - 'lib/sortable_list.rb'
+ - 'lib/tasks/cucumber.rake'
+ - 'lib/tasks/resque.rake'
+ - 'lib/tasks/skin_tasks.rake'
+ - 'spec/lib/works_owner_spec.rb'
+ - 'spec/models/abuse_report_spec.rb'
+ - 'spec/models/admin_blacklisted_email_spec.rb'
+ - 'spec/models/external_work_spec.rb'
+ - 'spec/models/invitation_spec.rb'
+
+# Offense count: 195
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces.
+# SupportedStyles: space, no_space, compact
+# SupportedStylesForEmptyBraces: space, no_space
+Layout/SpaceInsideHashLiteralBraces:
+ Exclude:
+ - 'app/controllers/archive_faqs_controller.rb'
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/controllers/bookmarks_controller.rb'
+ - 'app/controllers/collections_controller.rb'
+ - 'app/controllers/comments_controller.rb'
+ - 'app/controllers/media_controller.rb'
+ - 'app/controllers/skins_controller.rb'
+ - 'app/controllers/stats_controller.rb'
+ - 'app/controllers/tag_wranglings_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/bookmarks_helper.rb'
+ - 'app/helpers/tag_sets_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/gift.rb'
+ - 'app/models/language.rb'
+ - 'app/models/locale.rb'
+ - 'app/models/prompt.rb'
+ - 'app/models/redis_mail_queue.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/skin_parent.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/tagset_models/tag_set_association.rb'
+ - 'app/models/user_invite_request.rb'
+ - 'app/models/work.rb'
+ - 'app/models/work_skin.rb'
+ - 'config/deploy.rb'
+ - 'config/deploy/production.rb'
+ - 'config/initializers/gem-plugin_config/permit_yo_config.rb'
+ - 'config/initializers/monkeypatches/override_default_form_field_sizes.rb'
+ - 'lib/autocomplete_source.rb'
+ - 'lib/pagination_list_link_renderer.rb'
+ - 'lib/sortable_list.rb'
+ - 'lib/tasks/cucumber.rake'
+ - 'spec/controllers/series_controller_spec.rb'
+ - 'spec/controllers/works/default_rails_actions_spec.rb'
+ - 'spec/models/search/bookmark_query_spec.rb'
+ - 'spec/models/search/work_query_spec.rb'
+
+# Offense count: 30
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: space, compact, no_space
+Layout/SpaceInsideParens:
+ Exclude:
+ - 'app/controllers/admin/admin_users_controller.rb'
+ - 'app/helpers/comments_helper.rb'
+ - 'app/helpers/prompt_restrictions_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_item.rb'
+ - 'app/models/kudo.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'features/step_definitions/tag_steps.rb'
+ - 'features/step_definitions/work_steps.rb'
+ - 'lib/acts_as_commentable/comment_methods.rb'
+
+# Offense count: 8
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: space, no_space
+Layout/SpaceInsideStringInterpolation:
+ Exclude:
+ - 'app/models/redis_mail_queue.rb'
+ - 'config/initializers/active_record_log_subscriber.rb'
+
+# Offense count: 22
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: final_newline, final_blank_line
+Layout/TrailingEmptyLines:
+ Exclude:
+ - 'Capfile'
+ - 'app/controllers/menu_controller.rb'
+ - 'app/models/category.rb'
+ - 'app/models/indexing/scheduled_reindex_job.rb'
+ - 'app/models/tagset_models/cast_nomination.rb'
+ - 'app/models/tagset_models/character_nomination.rb'
+ - 'app/models/tagset_models/freeform_nomination.rb'
+ - 'app/models/tagset_models/relationship_nomination.rb'
+ - 'app/views/admin_posts/index.rss.builder'
+ - 'config/initializers/gem-plugin_config/escape_utils_config.rb'
+ - 'config/initializers/gem-plugin_config/newrelic.rb'
+ - 'features/step_definitions/banner_steps.rb'
+ - 'features/step_definitions/challenge_yuletide_steps.rb'
+ - 'features/support/env.rb'
+ - 'lib/responder.rb'
+ - 'lib/sortable_list.rb'
+ - 'lib/string_cleaner.rb'
+ - 'lib/tasks/notifications.rake'
+ - 'public/dispatch.rb'
+ - 'spec/models/challenge_claim_spec.rb'
+ - 'spec/models/gift_spec.rb'
+ - 'spec/models/skin_spec.rb'
+
+# Offense count: 1
+# Configuration parameters: IgnoredMethods.
+Lint/AmbiguousBlockAssociation:
+ Exclude:
+ - 'app/controllers/autocomplete_controller.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Lint/AmbiguousOperator:
+ Exclude:
+ - 'app/models/admin_setting.rb'
+
+# Offense count: 34
+# Cop supports --auto-correct.
+Lint/AmbiguousOperatorPrecedence:
+ Exclude:
+ - 'app/controllers/api/v2/bookmarks_controller.rb'
+ - 'app/controllers/api/v2/works_controller.rb'
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/challenge_assignments_controller.rb'
+ - 'app/controllers/invitations_controller.rb'
+ - 'app/controllers/skins_controller.rb'
+ - 'app/controllers/tag_wranglers_controller.rb'
+ - 'app/controllers/works_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/collections_helper.rb'
+ - 'app/helpers/comments_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/models/potential_matcher/potential_matcher_constrained.rb'
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/work.rb'
+ - 'app/policies/skin_policy.rb'
+ - 'config/initializers/gem-plugin_config/redis.rb'
+ - 'config/initializers/gem-plugin_config/resque.rb'
+ - 'lib/autocomplete_source.rb'
+ - 'lib/pagination_list_link_renderer.rb'
+ - 'spec/models/tag_spec.rb'
+
+# Offense count: 9
+# Configuration parameters: AllowSafeAssignment.
+Lint/AssignmentInCondition:
+ Exclude:
+ - 'app/controllers/fandoms_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/models/search/query_cleaner.rb'
+ - 'app/models/search_range.rb'
+ - 'app/models/tag.rb'
+ - 'config/initializers/monkeypatches/textarea_convert_html_to_newlines.rb'
+ - 'lib/otw_sanitize/embed_sanitizer.rb'
+
+# Offense count: 11
+# Cop supports --auto-correct.
+Lint/BooleanSymbol:
+ Exclude:
+ - 'app/models/bookmark.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_item.rb'
+ - 'app/models/tagset_models/owned_set_tagging.rb'
+
+# Offense count: 10
+# Configuration parameters: AllowedMethods.
+# AllowedMethods: enums
+Lint/ConstantDefinitionInBlock:
+ Exclude:
+ - 'lib/tasks/load_autocomplete_data.rake'
+ - 'lib/tasks/opendoors.rake'
+ - 'lib/tasks/search.rake'
+ - 'spec/models/story_parser_spec.rb'
+ - 'spec/spec_helper.rb'
+
+# Offense count: 2
+# Configuration parameters: DebuggerReceivers, DebuggerMethods.
+Lint/Debugger:
+ Exclude:
+ - 'features/step_definitions/generic_steps.rb'
+ - 'features/step_definitions/web_steps.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Lint/DeprecatedClassMethods:
+ Exclude:
+ - 'app/helpers/application_helper.rb'
+ - 'lib/tasks/skin_tasks.rake'
+
+# Offense count: 20
+# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches.
+Lint/DuplicateBranch:
+ Exclude:
+ - 'app/controllers/comments_controller.rb'
+ - 'app/helpers/comments_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/models/skin.rb'
+ - 'features/step_definitions/autocomplete_steps.rb'
+ - 'features/support/paths.rb'
+ - 'script/get_user_data.rb'
+
+# Offense count: 2
+Lint/DuplicateMethods:
+ Exclude:
+ - 'app/models/feedback_reporters/feedback_reporter.rb'
+
+# Offense count: 5
+# Configuration parameters: AllowComments, AllowEmptyLambdas.
+Lint/EmptyBlock:
+ Exclude:
+ - 'factories/challenge_claims.rb'
+ - 'factories/prompt_restriction.rb'
+ - 'features/step_definitions/comment_steps.rb'
+ - 'lib/tasks/cucumber.rake'
+ - 'lib/tasks/search.rake'
+
+# Offense count: 2
+# Configuration parameters: AllowComments.
+Lint/EmptyConditionalBody:
+ Exclude:
+ - 'db/migrate/20200115232918_add_user_id_to_kudos.rb'
+
+# Offense count: 6
+Lint/ImplicitStringConcatenation:
+ Exclude:
+ - 'spec/lib/html_cleaner_spec.rb'
+
+# Offense count: 6
+Lint/IneffectiveAccessModifier:
+ Exclude:
+ - 'app/models/potential_match.rb'
+ - 'app/models/user.rb'
+ - 'app/sweepers/collection_sweeper.rb'
+
+# Offense count: 2
+Lint/MissingSuper:
+ Exclude:
+ - 'app/models/search/bookmark_query.rb'
+ - 'app/models/search/bookmarkable_query.rb'
+
+# Offense count: 1
+Lint/MixedRegexpCaptureTypes:
+ Exclude:
+ - 'app/models/story_parser.rb'
+
+# Offense count: 8
+Lint/NonLocalExitFromIterator:
+ Exclude:
+ - 'app/controllers/collection_participants_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'lib/collectible.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Lint/OrderedMagicComments:
+ Exclude:
+ - 'spec/helpers/exports_helper_spec.rb'
+
+# Offense count: 8
+# Cop supports --auto-correct.
+Lint/ParenthesesAsGroupedExpression:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/related_works_controller.rb'
+ - 'config/deploy.rb'
+ - 'features/step_definitions/admin_steps.rb'
+ - 'features/step_definitions/email_steps.rb'
+ - 'features/step_definitions/profile_steps.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: AllowedImplicitNamespaces.
+# AllowedImplicitNamespaces: Gem
+Lint/RaiseException:
+ Exclude:
+ - 'app/models/series.rb'
+ - 'app/models/work.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Lint/RedundantDirGlobSort:
+ Exclude:
+ - 'spec/spec_helper.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: AllowedMethods.
+# AllowedMethods: instance_of?, kind_of?, is_a?, eql?, respond_to?, equal?
+Lint/RedundantSafeNavigation:
+ Exclude:
+ - 'app/helpers/application_helper.rb'
+ - 'app/policies/application_policy.rb'
+
+# Offense count: 6
+# Cop supports --auto-correct.
+Lint/RedundantStringCoercion:
+ Exclude:
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'features/step_definitions/work_steps.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Lint/RedundantWithIndex:
+ Exclude:
+ - 'app/models/tagset_models/owned_tag_set.rb'
+
+# Offense count: 4
+Lint/RescueException:
+ Exclude:
+ - 'app/controllers/series_controller.rb'
+ - 'app/models/invitation.rb'
+ - 'app/validators/email_veracity_validator.rb'
+ - 'lib/tasks/resque.rake'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Lint/ScriptPermission:
+ Exclude:
+ - 'public/dispatch.fcgi'
+ - 'public/dispatch.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Lint/SendWithMixinArgument:
+ Exclude:
+ - 'config/initializers/active_record_log_subscriber.rb'
+
+# Offense count: 2
+# Configuration parameters: IgnoreImplicitReferences.
+Lint/ShadowedArgument:
+ Exclude:
+ - 'features/step_definitions/web_steps.rb'
+
+# Offense count: 3
+Lint/ShadowingOuterLocalVariable:
+ Exclude:
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/works_controller.rb'
+
+# Offense count: 2
+Lint/StructNewOverride:
+ Exclude:
+ - 'app/models/search/query_facet.rb'
+ - 'script/gift_exchange/generate_from_spec.rb'
+
+# Offense count: 3
+# Configuration parameters: AllowComments, AllowNil.
+Lint/SuppressedException:
+ Exclude:
+ - 'app/controllers/opendoors/tools_controller.rb'
+ - 'app/models/redis_mail_queue.rb'
+
+# Offense count: 4
+# Cop supports --auto-correct.
+Lint/TripleQuotes:
+ Exclude:
+ - 'spec/lib/html_cleaner_spec.rb'
+
+# Offense count: 2
+# Configuration parameters: Methods.
+Lint/UnexpectedBlockArity:
+ Exclude:
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'spec/requests/comments_n_plus_one_spec.rb'
+
+# Offense count: 25
+# Cop supports --auto-correct.
+# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
+Lint/UnusedBlockArgument:
+ Exclude:
+ - 'app/controllers/owned_tag_sets_controller.rb'
+ - 'app/controllers/unsorted_tags_controller.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/mailers/kudo_mailer.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/indexing/index_queue.rb'
+ - 'app/models/search/query.rb'
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/tagset_models/tag_set_nomination.rb'
+ - 'config.ru'
+ - 'factories/collections.rb'
+ - 'factories/wrangling_guideline.rb'
+ - 'features/step_definitions/archivist_steps.rb'
+ - 'features/step_definitions/challege_gift_exchange_steps.rb'
+ - 'features/step_definitions/challenge_promptmeme_steps.rb'
+ - 'features/step_definitions/skin_steps.rb'
+ - 'features/step_definitions/work_steps.rb'
+ - 'lib/url_formatter.rb'
+
+# Offense count: 17
+# Cop supports --auto-correct.
+# Configuration parameters: AllowUnusedKeywordArguments, IgnoreEmptyMethods, IgnoreNotImplementedMethods.
+Lint/UnusedMethodArgument:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/helpers/prompt_restrictions_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/models/bookmark.rb'
+ - 'app/models/challenge_models/prompt_meme.rb'
+ - 'app/models/external_author.rb'
+ - 'app/models/fandom.rb'
+ - 'app/models/search/bookmark_search_form.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/work.rb'
+ - 'features/step_definitions/invite_steps.rb'
+ - 'lib/challenge_core.rb'
+ - 'lib/collectible.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Lint/UriRegexp:
+ Exclude:
+ - 'app/models/collection.rb'
+
+# Offense count: 6
+# Cop supports --auto-correct.
+# Configuration parameters: ContextCreatingMethods, MethodCreatingMethods.
+Lint/UselessAccessModifier:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/chapters_controller.rb'
+ - 'app/controllers/users_controller.rb'
+ - 'app/mailers/user_mailer.rb'
+ - 'app/models/potential_match.rb'
+ - 'app/models/work.rb'
+
+# Offense count: 38
+Lint/UselessAssignment:
+ Exclude:
+ - 'app/controllers/admin/admin_users_controller.rb'
+ - 'app/controllers/users_controller.rb'
+ - 'app/controllers/works_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/challenge_helper.rb'
+ - 'app/helpers/date_helper.rb'
+ - 'app/helpers/prompt_restrictions_helper.rb'
+ - 'app/helpers/pseuds_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/works_helper.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/external_author.rb'
+ - 'app/models/feedback.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/redis_mail_queue.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/subscription.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'app/validators/email_veracity_validator.rb'
+ - 'features/step_definitions/profile_steps.rb'
+ - 'features/step_definitions/tag_steps.rb'
+ - 'features/step_definitions/user_steps.rb'
+ - 'features/support/paths.rb'
+ - 'lib/tasks/skin_tasks.rake'
+ - 'lib/tasks/work_tasks.rake'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Lint/UselessTimes:
+ Exclude:
+ - 'app/controllers/archive_faqs_controller.rb'
+
+# Offense count: 30
+# Configuration parameters: CountBlocks, Max.
+Metrics/BlockNesting:
+ Exclude:
+ - 'app/controllers/bookmarks_controller.rb'
+ - 'app/controllers/collections_controller.rb'
+ - 'app/controllers/comments_controller.rb'
+ - 'app/controllers/owned_tag_sets_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/controllers/tag_wranglers_controller.rb'
+ - 'app/controllers/works_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/models/bookmark.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/creatorship.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/work.rb'
+ - 'lib/autocomplete_source.rb'
+ - 'lib/css_cleaner.rb'
+
+# Offense count: 6
+# Configuration parameters: Max, CountKeywordArgs, MaxOptionalParameters.
+Metrics/ParameterLists:
+ Exclude:
+ - 'app/models/potential_match.rb'
+ - 'app/models/reading.rb'
+ - 'features/step_definitions/collection_steps.rb'
+ - 'features/step_definitions/work_steps.rb'
+
+# Offense count: 40
+Naming/AccessorMethodName:
+ Exclude:
+ - 'app/controllers/admin/user_creations_controller.rb'
+ - 'app/controllers/external_authors_controller.rb'
+ - 'app/controllers/owned_tag_sets_controller.rb'
+ - 'app/controllers/related_works_controller.rb'
+ - 'app/controllers/tag_set_associations_controller.rb'
+ - 'app/helpers/date_helper.rb'
+ - 'app/helpers/works_helper.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_claim.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/concerns/creatable.rb'
+ - 'app/models/download.rb'
+ - 'app/models/download_writer.rb'
+ - 'app/models/indexing/cache_master.rb'
+ - 'app/models/locale.rb'
+ - 'app/models/potential_match.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/tagset_models/cast_nomination.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'app/models/work.rb'
+ - 'lib/collectible.rb'
+ - 'lib/responder.rb'
+ - 'lib/tasks/skin_tasks.rake'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Naming/BinaryOperatorParameterName:
+ Exclude:
+ - 'app/models/tag.rb'
+ - 'app/models/work.rb'
+
+# Offense count: 3
+# Configuration parameters: ForbiddenDelimiters.
+# ForbiddenDelimiters: (?-mix:(^|\s)(EO[A-Z]{1}|END)(\s|$))
+Naming/HeredocDelimiterNaming:
+ Exclude:
+ - 'spec/helpers/validation_helper_spec.rb'
+
+# Offense count: 1
+# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
+# AllowedNames: at, by, db, id, in, io, ip, of, on, os, pp, to
+Naming/MethodParameterName:
+ Exclude:
+ - 'config/initializers/gem-plugin_config/escape_utils_config.rb'
+
+# Offense count: 34
+# Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros.
+# NamePrefix: is_, has_, have_
+# ForbiddenPrefixes: is_, has_, have_
+# AllowedMethods: is_a?
+# MethodDefinitionMacros: define_method, define_singleton_method
+Naming/PredicateName:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/collection_participants_controller.rb'
+ - 'app/helpers/users_helper.rb'
+ - 'app/models/admin_blacklisted_email.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/collection_participant.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/gift.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/user.rb'
+ - 'app/models/work.rb'
+ - 'config/initializers/gem-plugin_config/permit_yo_config.rb'
+ - 'lib/acts_as_commentable/commentable.rb'
+ - 'lib/autocomplete_source.rb'
+ - 'lib/css_cleaner.rb'
+
+# Offense count: 7
+# Cop supports --auto-correct.
+# Configuration parameters: PreferredName.
+Naming/RescuedExceptionsVariableName:
+ Exclude:
+ - 'app/controllers/api/v2/bookmarks_controller.rb'
+ - 'app/controllers/api/v2/works_controller.rb'
+ - 'app/controllers/series_controller.rb'
+ - 'app/controllers/works_controller.rb'
+ - 'app/models/invitation.rb'
+ - 'app/models/story_parser.rb'
+
+# Offense count: 52
+# Configuration parameters: EnforcedStyle, CheckMethodNames, CheckSymbols, AllowedIdentifiers.
+# SupportedStyles: snake_case, normalcase, non_integer
+# AllowedIdentifiers: capture3, iso8601, rfc1123_date, rfc822, rfc2822, rfc3339
+Naming/VariableNumber:
+ Exclude:
+ - 'app/controllers/users/registrations_controller.rb'
+ - 'app/models/user.rb'
+ - 'spec/controllers/inbox_controller_spec.rb'
+ - 'spec/controllers/invite_requests_controller_spec.rb'
+ - 'spec/controllers/tag_set_associations_controller_spec.rb'
+ - 'spec/controllers/users/registrations_controller_spec.rb'
+ - 'spec/controllers/works/default_rails_actions_spec.rb'
+ - 'spec/controllers/wrangling_guidelines_controller_spec.rb'
+ - 'spec/models/search/pseud_query_spec.rb'
+ - 'spec/models/search/pseud_search_form_spec.rb'
+ - 'spec/models/search/work_search_form_spec.rb'
+ - 'spec/models/user_spec.rb'
+
+# Offense count: 4
+# Configuration parameters: Include, CustomTransform, IgnoreMethods, SpecSuffixOnly.
+# Include: **/*_spec*rb*, **/spec/**/*
+RSpec/FilePath:
+ Exclude:
+ - 'spec/controllers/api/v2/api_base_controller_spec.rb'
+ - 'spec/controllers/gift_exchange_controller_spec.rb'
+ - 'spec/controllers/prompt_meme_controller_spec.rb'
+ - 'spec/lib/string_cleaner_spec.rb'
+
+# Offense count: 3
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: numeric, symbolic
+RSpec/Rails/HttpStatus:
+ Exclude:
+ - 'spec/controllers/prompts_controller_spec.rb'
+ - 'spec/controllers/works/drafts_spec.rb'
+
+# Offense count: 40
+# Cop supports --auto-correct.
+# Configuration parameters: Include.
+# Include: app/models/**/*.rb
+Rails/ActiveRecordCallbacksOrder:
+ Exclude:
+ - 'app/models/admin_banner.rb'
+ - 'app/models/admin_post.rb'
+ - 'app/models/bookmark.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_item.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/fandom.rb'
+ - 'app/models/favorite_tag.rb'
+ - 'app/models/kudo.rb'
+ - 'app/models/reading.rb'
+ - 'app/models/serial_work.rb'
+ - 'app/models/series.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagging.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'app/models/tagset_models/tag_set_association.rb'
+ - 'app/models/user.rb'
+ - 'app/models/work.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Rails/ApplicationRecord:
+ Exclude:
+ - 'app/models/related_work.rb'
+
+# Offense count: 36
+# Cop supports --auto-correct.
+# Configuration parameters: NilOrEmpty, NotPresent, UnlessPresent.
+Rails/Blank:
+ Exclude:
+ - 'app/controllers/api/v2/base_controller.rb'
+ - 'app/controllers/api/v2/works_controller.rb'
+ - 'app/controllers/chapters_controller.rb'
+ - 'app/controllers/people_controller.rb'
+ - 'app/controllers/questions_controller.rb'
+ - 'app/controllers/tag_set_associations_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/controllers/users_controller.rb'
+ - 'app/controllers/works_controller.rb'
+ - 'app/decorators/pseud_decorator.rb'
+ - 'app/models/moderated_work.rb'
+ - 'app/models/potential_matcher/prompt_tag_type_info.rb'
+ - 'app/models/search/bookmark_query.rb'
+ - 'app/models/search/index_sweeper.rb'
+ - 'app/models/search/pseud_search_form.rb'
+ - 'app/models/search/query_cleaner.rb'
+ - 'app/models/search/work_query.rb'
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/work.rb'
+ - 'app/sweepers/feed_sweeper.rb'
+ - 'features/step_definitions/tag_set_steps.rb'
+ - 'lib/responder.rb'
+ - 'lib/word_counter.rb'
+ - 'script/gift_exchange/generate_from_spec.rb'
+
+# Offense count: 22
+# Configuration parameters: Database, Include.
+# SupportedDatabases: mysql, postgresql
+# Include: db/migrate/*.rb
+Rails/BulkChangeTable:
+ Exclude:
+ - 'db/migrate/20170322092920_rename_taggings_count_to_taggings_count_cache.rb'
+ - 'db/migrate/20170919143944_add_indexes_to_nominations.rb'
+ - 'db/migrate/20180811160316_add_sortable_name_to_languages.rb'
+ - 'db/migrate/20180822202259_add_disable_support_form_to_admin_settings.rb'
+ - 'db/migrate/20181017032400_moves_users_to_devise.rb'
+ - 'db/migrate/20181113063726_rename_columns_that_do_not_allow_css.rb'
+ - 'db/migrate/20181222042042_add_unique_index_to_user_confirmation_tokens.rb'
+ - 'db/migrate/20181224173813_add_unique_index_to_user_emails.rb'
+ - 'db/migrate/20190213230717_rename_warnings_on_prompt_restrictions.rb'
+ - 'db/migrate/20190214054439_rename_warnings_on_potential_match_settings.rb'
+ - 'db/migrate/20190611212339_add_unique_index_to_meta_taggings.rb'
+ - 'db/migrate/20200707213354_add_unique_indices_to_admin_login_email.rb'
+ - 'db/migrate/20201210140123_add_organizational_fields_to_collections.rb'
+ - 'db/migrate/20210902040827_index_tag_set_nominations.rb'
+ - 'db/migrate/20221218033319_add_recoverable_lockable_to_admins.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Rails/ContentTag:
+ Exclude:
+ - 'lib/pagination_list_link_renderer.rb'
+
+# Offense count: 2
+# Configuration parameters: Include.
+# Include: db/migrate/*.rb
+Rails/CreateTableWithTimestamps:
+ Exclude:
+ - 'db/migrate/20141127004302_create_fannish_next_of_kins.rb'
+ - 'db/migrate/20150725141326_install_audited.rb'
+
+# Offense count: 3
+# Configuration parameters: EnforcedStyle, AllowToTime.
+# SupportedStyles: strict, flexible
+Rails/Date:
+ Exclude:
+ - 'app/models/admin_setting.rb'
+ - 'app/models/invite_request.rb'
+
+# Offense count: 7
+# Cop supports --auto-correct.
+# Configuration parameters: EnforceForPrefixed.
+Rails/Delegate:
+ Exclude:
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/search/query_result.rb'
+ - 'app/models/tagset_models/tag_set_association.rb'
+ - 'lib/otw_sanitize/embed_sanitizer.rb'
+
+# Offense count: 23
+# Cop supports --auto-correct.
+# Configuration parameters: Whitelist, AllowedMethods, AllowedReceivers.
+# Whitelist: find_by_sql
+# AllowedMethods: find_by_sql
+# AllowedReceivers: Gem::Specification
+Rails/DynamicFindBy:
+ Exclude:
+ - 'app/controllers/opendoors/tools_controller.rb'
+ - 'app/controllers/questions_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/models/external_author.rb'
+ - 'features/step_definitions/admin_steps.rb'
+ - 'features/step_definitions/external_work_steps.rb'
+ - 'features/step_definitions/work_download_steps.rb'
+ - 'features/step_definitions/work_related_steps.rb'
+ - 'features/step_definitions/work_steps.rb'
+ - 'lib/tasks/skin_tasks.rake'
+ - 'spec/controllers/wrangling_guidelines_controller_spec.rb'
+
+# Offense count: 11
+# Cop supports --auto-correct.
+Rails/EagerEvaluationLogMessage:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/user.rb'
+ - 'app/sweepers/collection_sweeper.rb'
+ - 'lib/autocomplete_source.rb'
+ - 'lib/html_cleaner.rb'
+
+# Offense count: 1
+# Configuration parameters: Include, AllowReads, AllowWrites.
+# Include: app/**/*.rb, lib/**/*.rb
+Rails/EnvironmentVariableAccess:
+ Exclude:
+ - 'app/controllers/hit_count_controller.rb'
+
+# Offense count: 1
+# Configuration parameters: Include.
+# Include: app/**/*.rb, config/**/*.rb, lib/**/*.rb
+Rails/Exit:
+ Exclude:
+ - 'config/boot.rb'
+
+# Offense count: 11
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: slashes, arguments
+Rails/FilePath:
+ Exclude:
+ - 'app/models/collection.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/skin.rb'
+ - 'config/initializers/active_record_log_subscriber.rb'
+ - 'features/step_definitions/fixtures_steps.rb'
+ - 'lib/tasks/cucumber.rake'
+ - 'lib/tasks/resque.rake'
+
+# Offense count: 9
+# Cop supports --auto-correct.
+# Configuration parameters: Include, IgnoredMethods.
+# Include: app/models/**/*.rb
+# IgnoredMethods: order, limit, select, lock
+Rails/FindEach:
+ Exclude:
+ - 'app/models/filter_count.rb'
+ - 'app/models/inherited_meta_tag_updater.rb'
+ - 'app/models/moderated_work.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/scheduled_tag_job.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'app/models/user.rb'
+
+# Offense count: 57
+# Configuration parameters: Include.
+# Include: app/models/**/*.rb
+Rails/HasManyOrHasOneDependent:
+ Exclude:
+ - 'app/models/admin.rb'
+ - 'app/models/admin_post.rb'
+ - 'app/models/admin_post_tag.rb'
+ - 'app/models/challenge_models/gift_exchange.rb'
+ - 'app/models/challenge_models/prompt_meme.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/concerns/creatable.rb'
+ - 'app/models/concerns/filterable.rb'
+ - 'app/models/external_author.rb'
+ - 'app/models/external_author_name.rb'
+ - 'app/models/external_work.rb'
+ - 'app/models/fandom.rb'
+ - 'app/models/language.rb'
+ - 'app/models/media.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/role.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/user.rb'
+ - 'app/models/work.rb'
+ - 'app/models/work_skin.rb'
+
+# Offense count: 44
+# Configuration parameters: Include.
+# Include: app/helpers/**/*.rb
+Rails/HelperInstanceVariable:
+ Exclude:
+ - 'app/helpers/advanced_search_helper.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/collections_helper.rb'
+ - 'app/helpers/pseuds_helper.rb'
+ - 'app/helpers/search_helper.rb'
+ - 'app/helpers/tag_sets_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/users_helper.rb'
+ - 'app/helpers/works_helper.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: numeric, symbolic
+Rails/HttpStatus:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/locales_controller.rb'
+
+# Offense count: 6
+# Configuration parameters: Include.
+# Include: spec/**/*.rb, test/**/*.rb
+Rails/I18nLocaleAssignment:
+ Exclude:
+ - 'spec/mailers/user_mailer_spec.rb'
+ - 'spec/models/archive_faq_spec.rb'
+ - 'spec/spec_helper.rb'
+
+# Offense count: 29
+# Configuration parameters: Include.
+# Include: app/models/**/*.rb
+Rails/InverseOf:
+ Exclude:
+ - 'app/models/admin_post.rb'
+ - 'app/models/admin_setting.rb'
+ - 'app/models/archive_faq.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_item.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/concerns/filterable.rb'
+ - 'app/models/external_creatorship.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/user.rb'
+ - 'app/models/work.rb'
+
+# Offense count: 8
+# Configuration parameters: Include.
+# Include: app/controllers/**/*.rb
+Rails/LexicallyScopedActionFilter:
+ Exclude:
+ - 'app/controllers/admin/sessions_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/external_authors_controller.rb'
+ - 'app/controllers/opendoors/external_authors_controller.rb'
+ - 'app/controllers/owned_tag_sets_controller.rb'
+ - 'app/controllers/questions_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Rails/LinkToBlank:
+ Exclude:
+ - 'app/helpers/challenge_helper.rb'
+
+# Offense count: 17
+# Cop supports --auto-correct.
+Rails/NegateInclude:
+ Exclude:
+ - 'app/controllers/chapters_controller.rb'
+ - 'app/controllers/collections_controller.rb'
+ - 'app/controllers/works_controller.rb'
+ - 'app/helpers/admin_helper.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/creatorship.rb'
+ - 'app/models/search/index_sweeper.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/skin_parent.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/tag.rb'
+ - 'config/initializers/active_record_log_subscriber.rb'
+ - 'config/locales/rails-i18n/pluralization/hr.rb'
+ - 'features/step_definitions/autocomplete_steps.rb'
+ - 'lib/collectible.rb'
+ - 'lib/css_cleaner.rb'
+
+# Offense count: 5
+# Cop supports --auto-correct.
+# Configuration parameters: Include.
+# Include: app/**/*.rb, config/**/*.rb, db/**/*.rb, lib/**/*.rb
+Rails/Output:
+ Exclude:
+ - 'app/models/search/indexer.rb'
+ - 'config/deploy.rb'
+ - 'config/initializers/monkeypatches/fix_phraseapp.rb'
+ - 'config/initializers/monkeypatches/mail_disable_starttls.rb'
+
+# Offense count: 4
+# Cop supports --auto-correct.
+Rails/Pick:
+ Exclude:
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/models/series.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+
+# Offense count: 3
+# Cop supports --auto-correct.
+Rails/Pluck:
+ Exclude:
+ - 'app/controllers/api/v2/works_controller.rb'
+ - 'app/controllers/skins_controller.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Rails/PluralizationGrammar:
+ Exclude:
+ - 'config/schedule.rb'
+
+# Offense count: 24
+# Cop supports --auto-correct.
+Rails/Presence:
+ Exclude:
+ - 'app/controllers/api/v2/bookmarks_controller.rb'
+ - 'app/controllers/api/v2/works_controller.rb'
+ - 'app/controllers/application_controller.rb'
+ - 'app/helpers/admin_post_helper.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/search/bookmark_query.rb'
+ - 'app/models/search/work_query.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/tagset_models/cast_nomination.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'app/validators/url_active_validator.rb'
+ - 'features/step_definitions/collection_steps.rb'
+ - 'features/step_definitions/work_steps.rb'
+
+# Offense count: 117
+# Cop supports --auto-correct.
+# Configuration parameters: NotNilAndNotEmpty, NotBlank, UnlessBlank.
+Rails/Present:
+ Exclude:
+ - 'app/controllers/admin/admin_invitations_controller.rb'
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/archive_faqs_controller.rb'
+ - 'app/controllers/bookmarks_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/collection_items_controller.rb'
+ - 'app/controllers/fandoms_controller.rb'
+ - 'app/controllers/invitations_controller.rb'
+ - 'app/controllers/opendoors/tools_controller.rb'
+ - 'app/controllers/owned_tag_sets_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/controllers/tag_wranglers_controller.rb'
+ - 'app/controllers/tag_wranglings_controller.rb'
+ - 'app/controllers/tags_controller.rb'
+ - 'app/controllers/unsorted_tags_controller.rb'
+ - 'app/controllers/user_invite_requests_controller.rb'
+ - 'app/controllers/users/registrations_controller.rb'
+ - 'app/controllers/users_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/challenge_helper.rb'
+ - 'app/helpers/collections_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/models/admin_post_tag.rb'
+ - 'app/models/challenge_claim.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_item.rb'
+ - 'app/models/gift.rb'
+ - 'app/models/indexing/index_queue.rb'
+ - 'app/models/invitation.rb'
+ - 'app/models/search/query_result.rb'
+ - 'app/models/search/work_query.rb'
+ - 'app/models/serial_work.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/tagset_models/cast_nomination.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/tagset_models/tag_set_association.rb'
+ - 'app/models/user.rb'
+ - 'app/models/work.rb'
+ - 'features/step_definitions/banner_steps.rb'
+ - 'features/step_definitions/collection_steps.rb'
+ - 'features/step_definitions/icon_steps.rb'
+ - 'features/step_definitions/work_import_steps.rb'
+ - 'features/step_definitions/work_steps.rb'
+ - 'lib/css_cleaner.rb'
+ - 'lib/tasks/tag_tasks.rake'
+ - 'script/get_user_data.rb'
+
+# Offense count: 4
+# Cop supports --auto-correct.
+# Configuration parameters: Include.
+# Include: app/models/**/*.rb
+Rails/ReadWriteAttribute:
+ Exclude:
+ - 'app/models/collection.rb'
+ - 'app/models/external_work.rb'
+ - 'app/models/series.rb'
+ - 'app/models/work.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: Include.
+# Include: app/models/**/*.rb
+Rails/RedundantAllowNil:
+ Exclude:
+ - 'app/models/skin.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Rails/RedundantForeignKey:
+ Exclude:
+ - 'app/models/external_creatorship.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: AutoCorrect.
+Rails/RelativeDateConstant:
+ Exclude:
+ - 'app/models/search_range.rb'
+
+# Offense count: 3
+# Configuration parameters: Include.
+# Include: db/migrate/*.rb
+Rails/ReversibleMigrationMethodDefinition:
+ Exclude:
+ - 'db/migrate/20150725141326_install_audited.rb'
+ - 'db/migrate/20160706031054_move_admins_to_devise.rb'
+ - 'db/migrate/20170414154143_add_request_uuid_to_audits.rb'
+
+# Offense count: 43
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: strict, flexible
+Rails/TimeZone:
+ Exclude:
+ - 'app/controllers/admin/admin_users_controller.rb'
+ - 'app/controllers/challenge_assignments_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/helpers/translation_helper.rb'
+ - 'app/models/admin_setting.rb'
+ - 'app/models/bookmark.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_signup_summary.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/invitation.rb'
+ - 'app/models/invite_request.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/reading.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/work.rb'
+ - 'factories/challenges.rb'
+ - 'features/step_definitions/admin_steps.rb'
+ - 'features/step_definitions/challenge_promptmeme_steps.rb'
+ - 'features/step_definitions/invite_steps.rb'
+ - 'features/support/elapsed_time.rb'
+ - 'features/support/formatter.rb'
+ - 'spec/controllers/api/v2/api_works_spec.rb'
+ - 'spec/controllers/inbox_controller_spec.rb'
+ - 'spec/models/collection_spec.rb'
+
+# Offense count: 13
+# Configuration parameters: Include.
+# Include: app/models/**/*.rb
+Rails/UniqueValidationWithoutIndex:
+ Exclude:
+ - 'app/models/admin_post_tag.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/external_author.rb'
+ - 'app/models/external_author_name.rb'
+ - 'app/models/invite_request.rb'
+ - 'app/models/language.rb'
+ - 'app/models/locale.rb'
+ - 'app/models/moderated_work.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/skin_parent.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+
+# Offense count: 174
+# Cop supports --auto-correct.
+# Configuration parameters: Include.
+# Include: app/models/**/*.rb
+Rails/Validation:
+ Exclude:
+ - 'app/models/abuse_report.rb'
+ - 'app/models/admin.rb'
+ - 'app/models/admin_activity.rb'
+ - 'app/models/admin_banner.rb'
+ - 'app/models/admin_post.rb'
+ - 'app/models/admin_post_tag.rb'
+ - 'app/models/admin_setting.rb'
+ - 'app/models/bookmark.rb'
+ - 'app/models/challenge_models/gift_exchange.rb'
+ - 'app/models/challenge_models/prompt_meme.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_item.rb'
+ - 'app/models/collection_participant.rb'
+ - 'app/models/collection_profile.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/common_tagging.rb'
+ - 'app/models/creatorship.rb'
+ - 'app/models/external_author_name.rb'
+ - 'app/models/external_work.rb'
+ - 'app/models/feedback.rb'
+ - 'app/models/filter_count.rb'
+ - 'app/models/filter_tagging.rb'
+ - 'app/models/gift.rb'
+ - 'app/models/inbox_comment.rb'
+ - 'app/models/known_issue.rb'
+ - 'app/models/language.rb'
+ - 'app/models/locale.rb'
+ - 'app/models/log_item.rb'
+ - 'app/models/meta_tagging.rb'
+ - 'app/models/potential_match_settings.rb'
+ - 'app/models/preference.rb'
+ - 'app/models/profile.rb'
+ - 'app/models/prompt.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/question.rb'
+ - 'app/models/serial_work.rb'
+ - 'app/models/series.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/skin_parent.rb'
+ - 'app/models/subscription.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagging.rb'
+ - 'app/models/tagset_models/owned_set_tagging.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'app/models/tagset_models/set_tagging.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'app/models/tagset_models/tag_set_association.rb'
+ - 'app/models/tagset_models/tag_set_nomination.rb'
+ - 'app/models/user.rb'
+ - 'app/models/user_invite_request.rb'
+ - 'app/models/work.rb'
+ - 'app/models/wrangling_assignment.rb'
+ - 'app/models/wrangling_guideline.rb'
+
+# Offense count: 100
+# Cop supports --auto-correct.
+Rails/WhereEquals:
+ Exclude:
+ - 'app/controllers/owned_tag_sets_controller.rb'
+ - 'app/controllers/stats_controller.rb'
+ - 'app/controllers/tag_set_associations_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/controllers/tag_wranglers_controller.rb'
+ - 'app/controllers/works_controller.rb'
+ - 'app/models/admin_post.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_claim.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_item.rb'
+ - 'app/models/collection_participant.rb'
+ - 'app/models/gift.rb'
+ - 'app/models/kudo.rb'
+ - 'app/models/moderated_work.rb'
+ - 'app/models/potential_match.rb'
+ - 'app/models/prompt.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/series.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'app/models/tagset_models/tag_set_nomination.rb'
+ - 'app/models/user.rb'
+ - 'app/models/work.rb'
+ - 'lib/tasks/tag_tasks.rake'
+
+# Offense count: 20
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: exists, where
+Rails/WhereExists:
+ Exclude:
+ - 'app/controllers/challenge_requests_controller.rb'
+ - 'app/helpers/works_helper.rb'
+ - 'app/models/admin_blacklisted_email.rb'
+ - 'app/models/gift.rb'
+ - 'app/models/invite_request.rb'
+ - 'app/models/meta_tagging.rb'
+ - 'app/models/prompt.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/work.rb'
+ - 'features/step_definitions/collection_steps.rb'
+ - 'features/step_definitions/series_steps.rb'
+ - 'features/step_definitions/work_steps.rb'
+
+# Offense count: 20
+# Cop supports --auto-correct.
+Rails/WhereNot:
+ Exclude:
+ - 'app/controllers/tag_set_associations_controller.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_claim.rb'
+ - 'app/models/invitation.rb'
+ - 'app/models/kudo.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'lib/tasks/skin_tasks.rake'
+
+# Offense count: 21
+Security/Eval:
+ Exclude:
+ - 'app/helpers/application_helper.rb'
+ - 'app/models/challenge_models/gift_exchange.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/prompt.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/story_parser.rb'
+ - 'features/step_definitions/pickle_steps.rb'
+ - 'lib/challenge_core.rb'
+
+# Offense count: 4
+# Cop supports --auto-correct.
+# Configuration parameters: AutoCorrect.
+Security/JSONLoad:
+ Exclude:
+ - 'script/gift_exchange/generate_from_spec.rb'
+ - 'script/gift_exchange/json_tag_frequency.rb'
+ - 'script/gift_exchange/load_json_challenge.rb'
+
+# Offense count: 1
+Security/Open:
+ Exclude:
+ - 'app/models/story_parser.rb'
+
+# Offense count: 10
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: separated, grouped
+Style/AccessorGrouping:
+ Exclude:
+ - 'app/models/potential_matcher/potential_matcher_constrained.rb'
+ - 'app/models/potential_matcher/potential_matcher_progress.rb'
+ - 'app/models/potential_matcher/prompt_batch.rb'
+ - 'app/models/user.rb'
+ - 'app/models/work.rb'
+
+# Offense count: 17
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: prefer_alias, prefer_alias_method
+Style/Alias:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/external_work.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/search/query_result.rb'
+ - 'app/models/series.rb'
+ - 'config/initializers/monkeypatches/override_default_form_field_sizes.rb'
+ - 'config/initializers/monkeypatches/translate_string.rb'
+ - 'lib/backwards_compatible_password_decryptor.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: always, conditionals
+Style/AndOr:
+ Exclude:
+ - 'app/controllers/collections_controller.rb'
+ - 'app/controllers/pseuds_controller.rb'
+
+# Offense count: 8
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: percent_q, bare_percent
+Style/BarePercentLiterals:
+ Exclude:
+ - 'features/step_definitions/invite_steps.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Style/BlockComments:
+ Exclude:
+ - 'db/migrate/20200115232918_add_user_id_to_kudos.rb'
+
+# Offense count: 60
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, ProceduralMethods, FunctionalMethods, IgnoredMethods, AllowBracesOnProceduralOneLiners, BracesRequiredMethods.
+# SupportedStyles: line_count_based, semantic, braces_for_chaining, always_braces
+# ProceduralMethods: benchmark, bm, bmbm, create, each_with_object, measure, new, realtime, tap, with_object
+# FunctionalMethods: let, let!, subject, watch
+# IgnoredMethods: lambda, proc, it
+Style/BlockDelimiters:
+ Exclude:
+ - 'app/controllers/bookmarks_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/subscriptions_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/models/admin_post.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/work.rb'
+ - 'app/validators/url_active_validator.rb'
+ - 'features/step_definitions/web_steps.rb'
+ - 'lib/acts_as_commentable/comment_methods.rb'
+ - 'spec/controllers/admin_posts_controller_spec.rb'
+ - 'spec/controllers/languages_controller_spec.rb'
+ - 'spec/controllers/related_works_controller_spec.rb'
+ - 'spec/controllers/tag_set_associations_controller_spec.rb'
+ - 'spec/controllers/tag_set_nominations_controller_spec.rb'
+ - 'spec/controllers/works/default_rails_actions_spec.rb'
+ - 'spec/controllers/works/multiple_actions_spec.rb'
+ - 'spec/models/favorite_tag_spec.rb'
+ - 'spec/models/search/index_sweeper_spec.rb'
+ - 'spec/models/story_parser_spec.rb'
+ - 'spec/models/work_spec.rb'
+
+# Offense count: 43
+# Cop supports --auto-correct.
+Style/CaseLikeIf:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/collections_controller.rb'
+ - 'app/controllers/comments_controller.rb'
+ - 'app/controllers/external_authors_controller.rb'
+ - 'app/controllers/troubleshooting_controller.rb'
+ - 'app/controllers/users_controller.rb'
+ - 'app/helpers/challenge_helper.rb'
+ - 'app/helpers/inbox_helper.rb'
+ - 'app/helpers/invitations_helper.rb'
+ - 'app/helpers/orphans_helper.rb'
+ - 'app/models/abuse_report.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/creatorship.rb'
+ - 'app/models/prompt.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/search/query_cleaner.rb'
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/story_parser.rb'
+ - 'features/step_definitions/banner_steps.rb'
+ - 'features/step_definitions/bookmark_steps.rb'
+ - 'features/step_definitions/collection_steps.rb'
+ - 'features/step_definitions/user_steps.rb'
+ - 'features/step_definitions/work_search_steps.rb'
+ - 'lib/html_cleaner.rb'
+ - 'script/get_user_data.rb'
+
+# Offense count: 4
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: is_a?, kind_of?
+Style/ClassCheck:
+ Exclude:
+ - 'app/models/comment.rb'
+ - 'app/models/external_work.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+
+# Offense count: 17
+# Cop supports --auto-correct.
+# Configuration parameters: IgnoredMethods.
+# IgnoredMethods: ==, equal?, eql?
+Style/ClassEqualityComparison:
+ Exclude:
+ - 'app/controllers/api/v2/works_controller.rb'
+ - 'app/controllers/comments_controller.rb'
+ - 'app/helpers/bookmarks_helper.rb'
+ - 'app/helpers/challenge_helper.rb'
+ - 'app/helpers/collections_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/models/meta_tagging.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/tag.rb'
+ - 'lib/url_formatter.rb'
+ - 'spec/lib/works_owner_spec.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/CollectionCompact:
+ Exclude:
+ - 'app/helpers/validation_helper.rb'
+
+# Offense count: 8
+# Cop supports --auto-correct.
+Style/ColonMethodCall:
+ Exclude:
+ - 'app/models/collection.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/validators/url_active_validator.rb'
+ - 'features/step_definitions/bookmark_steps.rb'
+ - 'features/step_definitions/generic_steps.rb'
+ - 'lib/url_formatter.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, AllowInnerBackticks.
+# SupportedStyles: backticks, percent_x, mixed
+Style/CommandLiteral:
+ Exclude:
+ - 'lib/tasks/deploy_tasks.rake'
+
+# Offense count: 13
+# Cop supports --auto-correct.
+# Configuration parameters: Keywords, RequireColon.
+# Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW, NOTE
+Style/CommentAnnotation:
+ Exclude:
+ - 'app/controllers/challenge/gift_exchange_controller.rb'
+ - 'app/models/feedback.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/work.rb'
+ - 'config/initializers/monkeypatches/translate_string.rb'
+ - 'features/step_definitions/work_steps.rb'
+ - 'lib/collectible.rb'
+ - 'spec/controllers/tags_controller_spec.rb'
+ - 'spec/controllers/works/importing_spec.rb'
+ - 'spec/models/challenge_assignment_spec.rb'
+
+# Offense count: 5
+# Cop supports --auto-correct.
+Style/CommentedKeyword:
+ Exclude:
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/mailer_helper.rb'
+ - 'lib/creation_notifier.rb'
+ - 'spec/models/collection_spec.rb'
+
+# Offense count: 66
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SingleLineConditionsOnly, IncludeTernaryExpressions.
+# SupportedStyles: assign_to_condition, assign_inside_condition
+Style/ConditionalAssignment:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/archive_faqs_controller.rb'
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/controllers/challenge_assignments_controller.rb'
+ - 'app/controllers/challenge_claims_controller.rb'
+ - 'app/controllers/collection_items_controller.rb'
+ - 'app/controllers/collection_participants_controller.rb'
+ - 'app/controllers/comments_controller.rb'
+ - 'app/controllers/fandoms_controller.rb'
+ - 'app/controllers/gifts_controller.rb'
+ - 'app/controllers/media_controller.rb'
+ - 'app/controllers/potential_matches_controller.rb'
+ - 'app/controllers/prompts_controller.rb'
+ - 'app/controllers/series_controller.rb'
+ - 'app/controllers/skins_controller.rb'
+ - 'app/controllers/stats_controller.rb'
+ - 'app/controllers/tags_controller.rb'
+ - 'app/controllers/works_controller.rb'
+ - 'app/decorators/homepage.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/collections_helper.rb'
+ - 'app/helpers/comments_helper.rb'
+ - 'app/helpers/tag_sets_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/users_helper.rb'
+ - 'app/mailers/collection_mailer.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_claim.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/search/bookmarkable_query.rb'
+ - 'app/models/search/query_cleaner.rb'
+ - 'app/models/series.rb'
+ - 'app/models/spam_report.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/tagset_models/cast_nomination.rb'
+ - 'app/models/work.rb'
+ - 'app/validators/email_format_validator.rb'
+ - 'config/initializers/phraseapp_in_context_editor.rb'
+ - 'lib/autocomplete_source.rb'
+ - 'lib/css_cleaner.rb'
+ - 'lib/html_cleaner.rb'
+ - 'lib/works_owner.rb'
+
+# Offense count: 20
+Style/DocumentDynamicEvalDefinition:
+ Exclude:
+ - 'app/helpers/application_helper.rb'
+ - 'app/models/challenge_models/gift_exchange.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/prompt.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/story_parser.rb'
+ - 'lib/challenge_core.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: allowed_in_returns, forbidden
+Style/DoubleNegation:
+ Exclude:
+ - 'app/controllers/works_controller.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/EachWithObject:
+ Exclude:
+ - 'app/models/rating.rb'
+
+# Offense count: 5
+# Cop supports --auto-correct.
+Style/EmptyCaseCondition:
+ Exclude:
+ - 'app/controllers/challenge_assignments_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/tag_sets_helper.rb'
+ - 'app/models/skin.rb'
+
+# Offense count: 3
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: empty, nil, both
+Style/EmptyElse:
+ Exclude:
+ - 'app/models/admin_setting.rb'
+ - 'app/models/skin.rb'
+ - 'app/sweepers/tag_set_sweeper.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Style/EmptyLiteral:
+ Exclude:
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/tag_set_association.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: compact, expanded
+Style/EmptyMethod:
+ Exclude:
+ - 'spec/lib/tasks/resque.rake_spec.rb'
+
+# Offense count: 9
+# Cop supports --auto-correct.
+Style/Encoding:
+ Exclude:
+ - 'features/step_definitions/banner_steps.rb'
+ - 'features/step_definitions/tag_set_steps.rb'
+ - 'lib/pagination_list_link_renderer.rb'
+ - 'spec/helpers/exports_helper_spec.rb'
+ - 'spec/lib/html_cleaner_spec.rb'
+ - 'spec/lib/string_cleaner_spec.rb'
+ - 'spec/lib/word_counter_spec.rb'
+ - 'spec/models/stat_counter_spec.rb'
+ - 'spec/models/tag_spec.rb'
+
+# Offense count: 20
+# Cop supports --auto-correct.
+Style/EvalWithLocation:
+ Exclude:
+ - 'app/helpers/application_helper.rb'
+ - 'app/models/challenge_models/gift_exchange.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/prompt.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/story_parser.rb'
+ - 'lib/challenge_core.rb'
+
+# Offense count: 4
+# Cop supports --auto-correct.
+Style/ExpandPathArguments:
+ Exclude:
+ - 'Rakefile'
+ - 'config.ru'
+ - 'config/boot.rb'
+ - 'config/environment.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Style/ExplicitBlockArgument:
+ Exclude:
+ - 'app/controllers/archive_faqs_controller.rb'
+ - 'features/step_definitions/web_steps.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: left_coerce, right_coerce, single_coerce, fdiv
+Style/FloatDivision:
+ Exclude:
+ - 'app/models/invite_request.rb'
+
+# Offense count: 9
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: each, for
+Style/For:
+ Exclude:
+ - 'app/controllers/tag_wranglers_controller.rb'
+ - 'app/helpers/series_helper.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/creatorship.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/work.rb'
+ - 'app/views/admin_posts/index.rss.builder'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: format, sprintf, percent
+Style/FormatString:
+ Exclude:
+ - 'app/helpers/validation_helper.rb'
+
+# Offense count: 37
+# Cop supports --auto-correct.
+Style/GlobalStdStream:
+ Exclude:
+ - 'config/boot.rb'
+ - 'lib/tasks/after_tasks.rake'
+ - 'lib/tasks/cucumber.rake'
+ - 'lib/tasks/opendoors.rake'
+ - 'lib/tasks/skin_tasks.rake'
+ - 'lib/tasks/tag_tasks.rake'
+ - 'lib/tasks/work_tasks.rake'
+
+# Offense count: 243
+# Configuration parameters: MinBodyLength.
+Style/GuardClause:
+ Exclude:
+ - 'app/controllers/admin/admin_invitations_controller.rb'
+ - 'app/controllers/admin/admin_users_controller.rb'
+ - 'app/controllers/admin/api_controller.rb'
+ - 'app/controllers/admin/user_creations_controller.rb'
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/archive_faqs_controller.rb'
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/controllers/bookmarks_controller.rb'
+ - 'app/controllers/challenge_assignments_controller.rb'
+ - 'app/controllers/challenge_claims_controller.rb'
+ - 'app/controllers/challenge_requests_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/chapters_controller.rb'
+ - 'app/controllers/collection_items_controller.rb'
+ - 'app/controllers/collections_controller.rb'
+ - 'app/controllers/comments_controller.rb'
+ - 'app/controllers/fandoms_controller.rb'
+ - 'app/controllers/invitations_controller.rb'
+ - 'app/controllers/owned_tag_sets_controller.rb'
+ - 'app/controllers/prompts_controller.rb'
+ - 'app/controllers/pseuds_controller.rb'
+ - 'app/controllers/questions_controller.rb'
+ - 'app/controllers/readings_controller.rb'
+ - 'app/controllers/redirect_controller.rb'
+ - 'app/controllers/related_works_controller.rb'
+ - 'app/controllers/series_controller.rb'
+ - 'app/controllers/skins_controller.rb'
+ - 'app/controllers/tag_set_associations_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/controllers/tag_wranglings_controller.rb'
+ - 'app/controllers/tags_controller.rb'
+ - 'app/controllers/unsorted_tags_controller.rb'
+ - 'app/controllers/users_controller.rb'
+ - 'app/controllers/works_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/challenge_helper.rb'
+ - 'app/helpers/collections_helper.rb'
+ - 'app/helpers/comments_helper.rb'
+ - 'app/helpers/invitations_helper.rb'
+ - 'app/helpers/series_helper.rb'
+ - 'app/helpers/skins_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/translation_helper.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/models/admin_banner.rb'
+ - 'app/models/admin_post.rb'
+ - 'app/models/admin_post_tag.rb'
+ - 'app/models/admin_setting.rb'
+ - 'app/models/archive_faq.rb'
+ - 'app/models/bookmark.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_models/gift_exchange.rb'
+ - 'app/models/challenge_models/prompt_meme.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/challenge_signup_summary.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_item.rb'
+ - 'app/models/collection_preference.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/common_tagging.rb'
+ - 'app/models/concerns/creatable.rb'
+ - 'app/models/creatorship.rb'
+ - 'app/models/external_author.rb'
+ - 'app/models/fandom.rb'
+ - 'app/models/favorite_tag.rb'
+ - 'app/models/filter_tagging.rb'
+ - 'app/models/gift.rb'
+ - 'app/models/invitation.rb'
+ - 'app/models/invite_request.rb'
+ - 'app/models/meta_tagging.rb'
+ - 'app/models/potential_match.rb'
+ - 'app/models/profile.rb'
+ - 'app/models/prompt.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/reading.rb'
+ - 'app/models/related_work.rb'
+ - 'app/models/search/bookmark_query.rb'
+ - 'app/models/search/bookmarkable_query.rb'
+ - 'app/models/search/index_sweeper.rb'
+ - 'app/models/search/indexer.rb'
+ - 'app/models/search/pseud_query.rb'
+ - 'app/models/search/work_query.rb'
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/serial_work.rb'
+ - 'app/models/series.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/skin_parent.rb'
+ - 'app/models/spam_report.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/cast_nomination.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/tagset_models/tag_set_nomination.rb'
+ - 'app/models/user.rb'
+ - 'app/models/user_invite_request.rb'
+ - 'app/models/work.rb'
+ - 'app/sweepers/collection_sweeper.rb'
+ - 'app/sweepers/feed_sweeper.rb'
+ - 'app/sweepers/pseud_sweeper.rb'
+ - 'app/sweepers/tag_set_sweeper.rb'
+ - 'app/validators/email_blacklist_validator.rb'
+ - 'app/validators/email_format_validator.rb'
+ - 'app/validators/email_veracity_validator.rb'
+ - 'app/validators/url_format_validator.rb'
+ - 'config/initializers/active_record_log_subscriber.rb'
+ - 'lib/acts_as_commentable/comment_methods.rb'
+ - 'lib/challenge_core.rb'
+ - 'lib/collectible.rb'
+ - 'lib/creation_notifier.rb'
+ - 'lib/css_cleaner.rb'
+ - 'lib/otw_sanitize/embed_sanitizer.rb'
+ - 'lib/url_formatter.rb'
+
+# Offense count: 13
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: braces, no_braces
+Style/HashAsLastArrayItem:
+ Exclude:
+ - 'app/controllers/challenge/gift_exchange_controller.rb'
+ - 'app/controllers/challenge/prompt_meme_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/chapters_controller.rb'
+ - 'app/controllers/owned_tag_sets_controller.rb'
+ - 'app/controllers/prompts_controller.rb'
+ - 'app/controllers/series_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/controllers/works_controller.rb'
+ - 'app/models/user.rb'
+ - 'app/policies/user_policy.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: AllowSplatArgument.
+Style/HashConversion:
+ Exclude:
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/spam_report.rb'
+
+# Offense count: 6
+# Cop supports --auto-correct.
+# Configuration parameters: AllowedReceivers.
+Style/HashEachMethods:
+ Exclude:
+ - 'app/controllers/bookmarks_controller.rb'
+ - 'app/controllers/works_controller.rb'
+ - 'features/step_definitions/work_steps.rb'
+ - 'lib/html_cleaner.rb'
+
+# Offense count: 1
+# Configuration parameters: MinBranchesCount.
+Style/HashLikeCase:
+ Exclude:
+ - 'features/step_definitions/autocomplete_steps.rb'
+
+# Offense count: 37
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols.
+# SupportedStyles: ruby19, hash_rockets, no_mixed_keys, ruby19_no_mixed_keys
+Style/HashSyntax:
+ Exclude:
+ - 'app/models/admin_setting.rb'
+ - 'app/views/admin_posts/index.rss.builder'
+ - 'app/views/tags/feed.atom.builder'
+ - 'db/migrate/20150725141326_install_audited.rb'
+ - 'lib/pagination_list_link_renderer.rb'
+ - 'lib/tasks/admin_tasks.rake'
+ - 'lib/tasks/deploy_tasks.rake'
+ - 'lib/tasks/memcached.rake'
+ - 'lib/tasks/notifications.rake'
+ - 'lib/tasks/resque.rake'
+ - 'lib/tasks/skin_tasks.rake'
+ - 'lib/tasks/spam_report.rake'
+ - 'lib/tasks/work_tasks.rake'
+
+# Offense count: 16
+# Cop supports --auto-correct.
+Style/IdenticalConditionalBranches:
+ Exclude:
+ - 'app/controllers/admin/admin_users_controller.rb'
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/challenge_assignments_controller.rb'
+ - 'app/controllers/related_works_controller.rb'
+ - 'app/helpers/tag_sets_helper.rb'
+ - 'features/step_definitions/series_steps.rb'
+ - 'features/step_definitions/work_steps.rb'
+
+# Offense count: 14
+# Cop supports --auto-correct.
+# Configuration parameters: AllowIfModifier.
+Style/IfInsideElse:
+ Exclude:
+ - 'app/controllers/bookmarks_controller.rb'
+ - 'app/controllers/collections_controller.rb'
+ - 'app/controllers/gifts_controller.rb'
+ - 'app/controllers/prompts_controller.rb'
+ - 'app/controllers/skins_controller.rb'
+ - 'app/controllers/users/registrations_controller.rb'
+ - 'app/helpers/tag_sets_helper.rb'
+ - 'app/models/bookmark.rb'
+ - 'app/models/collection.rb'
+ - 'app/sweepers/pseud_sweeper.rb'
+
+# Offense count: 347
+# Cop supports --auto-correct.
+Style/IfUnlessModifier:
+ Exclude:
+ - 'app/controllers/admin/admin_invitations_controller.rb'
+ - 'app/controllers/admin/api_controller.rb'
+ - 'app/controllers/admin_posts_controller.rb'
+ - 'app/controllers/api/v2/bookmarks_controller.rb'
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/archive_faqs_controller.rb'
+ - 'app/controllers/bookmarks_controller.rb'
+ - 'app/controllers/challenge_assignments_controller.rb'
+ - 'app/controllers/challenge_claims_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/chapters_controller.rb'
+ - 'app/controllers/collection_items_controller.rb'
+ - 'app/controllers/collections_controller.rb'
+ - 'app/controllers/comments_controller.rb'
+ - 'app/controllers/home_controller.rb'
+ - 'app/controllers/invitations_controller.rb'
+ - 'app/controllers/owned_tag_sets_controller.rb'
+ - 'app/controllers/prompts_controller.rb'
+ - 'app/controllers/pseuds_controller.rb'
+ - 'app/controllers/redirect_controller.rb'
+ - 'app/controllers/series_controller.rb'
+ - 'app/controllers/skins_controller.rb'
+ - 'app/controllers/stats_controller.rb'
+ - 'app/controllers/subscriptions_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/controllers/tag_wranglers_controller.rb'
+ - 'app/controllers/tag_wranglings_controller.rb'
+ - 'app/controllers/tags_controller.rb'
+ - 'app/controllers/users/sessions_controller.rb'
+ - 'app/controllers/users_controller.rb'
+ - 'app/controllers/works_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/bookmarks_helper.rb'
+ - 'app/helpers/challenge_helper.rb'
+ - 'app/helpers/collections_helper.rb'
+ - 'app/helpers/invitations_helper.rb'
+ - 'app/helpers/language_helper.rb'
+ - 'app/helpers/skins_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/helpers/works_helper.rb'
+ - 'app/mailers/user_mailer.rb'
+ - 'app/models/abuse_report.rb'
+ - 'app/models/admin_banner.rb'
+ - 'app/models/admin_post.rb'
+ - 'app/models/admin_setting.rb'
+ - 'app/models/application_record.rb'
+ - 'app/models/archive_faq.rb'
+ - 'app/models/bookmark.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_models/gift_exchange.rb'
+ - 'app/models/challenge_models/prompt_meme.rb'
+ - 'app/models/challenge_signup_summary.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_item.rb'
+ - 'app/models/collection_preference.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/common_tagging.rb'
+ - 'app/models/concerns/creatable.rb'
+ - 'app/models/creatorship.rb'
+ - 'app/models/download_writer.rb'
+ - 'app/models/fandom.rb'
+ - 'app/models/favorite_tag.rb'
+ - 'app/models/gift.rb'
+ - 'app/models/invitation.rb'
+ - 'app/models/invite_request.rb'
+ - 'app/models/kudo.rb'
+ - 'app/models/meta_tagging.rb'
+ - 'app/models/prompt.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/reading.rb'
+ - 'app/models/related_work.rb'
+ - 'app/models/relationship.rb'
+ - 'app/models/search/bookmark_query.rb'
+ - 'app/models/search/bookmark_search_form.rb'
+ - 'app/models/search/bookmarkable_query.rb'
+ - 'app/models/search/index_sweeper.rb'
+ - 'app/models/search/indexer.rb'
+ - 'app/models/search/query.rb'
+ - 'app/models/search/query_result.rb'
+ - 'app/models/search/tag_query.rb'
+ - 'app/models/search/work_query.rb'
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/serial_work.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/skin_parent.rb'
+ - 'app/models/spam_report.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/cast_nomination.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/tagset_models/tag_set_nomination.rb'
+ - 'app/models/user.rb'
+ - 'app/models/user_invite_request.rb'
+ - 'app/models/work.rb'
+ - 'app/sweepers/feed_sweeper.rb'
+ - 'app/validators/url_format_validator.rb'
+ - 'config/initializers/active_record_log_subscriber.rb'
+ - 'config/initializers/monkeypatches/mail_disable_starttls.rb'
+ - 'features/step_definitions/challenge_steps.rb'
+ - 'features/step_definitions/collection_steps.rb'
+ - 'features/step_definitions/invite_steps.rb'
+ - 'features/step_definitions/series_steps.rb'
+ - 'features/step_definitions/work_import_steps.rb'
+ - 'features/step_definitions/work_steps.rb'
+ - 'lib/autocomplete_source.rb'
+ - 'lib/challenge_core.rb'
+ - 'lib/collectible.rb'
+ - 'lib/css_cleaner.rb'
+ - 'lib/html_cleaner.rb'
+ - 'lib/sortable_list.rb'
+ - 'lib/tasks/skin_tasks.rake'
+ - 'lib/tasks/tag_tasks.rake'
+ - 'lib/tasks/work_tasks.rake'
+ - 'script/get_user_data.rb'
+ - 'script/gift_exchange/export_settings_json.rb'
+ - 'script/gift_exchange/generate_from_spec.rb'
+ - 'script/gift_exchange/requests_summary_to_json.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: AllowedMethods.
+# AllowedMethods: nonzero?
+Style/IfWithBooleanLiteralBranches:
+ Exclude:
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/prompt.rb'
+
+# Offense count: 9
+# Cop supports --auto-correct.
+# Configuration parameters: InverseMethods, InverseBlocks.
+Style/InverseMethods:
+ Exclude:
+ - 'app/controllers/pseuds_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/models/potential_match.rb'
+ - 'app/models/potential_match_settings.rb'
+ - 'app/models/search/index_sweeper.rb'
+ - 'app/models/series.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+
+# Offense count: 51
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: line_count_dependent, lambda, literal
+Style/Lambda:
+ Exclude:
+ - 'app/models/bookmark.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_claim.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/external_work.rb'
+ - 'app/models/fandom.rb'
+ - 'app/models/gift.rb'
+ - 'app/models/inbox_comment.rb'
+ - 'app/models/prompt.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/related_work.rb'
+ - 'app/models/series.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/work.rb'
+
+# Offense count: 21
+# Cop supports --auto-correct.
+Style/LineEndConcatenation:
+ Exclude:
+ - 'app/controllers/api/v2/works_controller.rb'
+ - 'app/controllers/challenge_assignments_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/mailer_helper.rb'
+ - 'features/support/paths.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: require_parentheses, require_no_parentheses, require_no_parentheses_except_multiline
+Style/MethodDefParentheses:
+ Exclude:
+ - 'app/helpers/application_helper.rb'
+
+# Offense count: 5
+Style/MixinUsage:
+ Exclude:
+ - 'Rakefile'
+ - 'config/initializers/gem-plugin_config/redis.rb'
+ - 'spec/controllers/api/v2/api_base_controller_spec.rb'
+ - 'spec/controllers/api/v2/api_works_search_spec.rb'
+ - 'spec/controllers/api/v2/api_works_spec.rb'
+
+# Offense count: 4
+# Cop supports --auto-correct.
+Style/MultilineIfModifier:
+ Exclude:
+ - 'app/controllers/users/registrations_controller.rb'
+ - 'app/models/search/pseud_query.rb'
+ - 'app/models/search/work_query.rb'
+ - 'config/boot.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/MultilineIfThen:
+ Exclude:
+ - 'config/initializers/phraseapp_in_context_editor.rb'
+
+# Offense count: 31
+# Cop supports --auto-correct.
+Style/MultilineTernaryOperator:
+ Exclude:
+ - 'app/controllers/admin/user_creations_controller.rb'
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/controllers/media_controller.rb'
+ - 'app/controllers/related_works_controller.rb'
+ - 'app/helpers/comments_helper.rb'
+ - 'app/helpers/works_helper.rb'
+ - 'app/models/download_writer.rb'
+ - 'app/models/invite_request.rb'
+ - 'app/models/kudo.rb'
+ - 'app/models/prompt.rb'
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/cast_nomination.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'lib/acts_as_commentable/comment_methods.rb'
+ - 'lib/autocomplete_source.rb'
+
+# Offense count: 6
+# Cop supports --auto-correct.
+Style/MultilineWhenThen:
+ Exclude:
+ - 'app/helpers/bookmarks_helper.rb'
+ - 'app/helpers/comments_helper.rb'
+
+# Offense count: 7
+# Cop supports --auto-correct.
+# Configuration parameters: AllowMethodComparison.
+Style/MultipleComparison:
+ Exclude:
+ - 'app/models/collection_participant.rb'
+ - 'app/models/preference.rb'
+ - 'app/models/search/query_cleaner.rb'
+ - 'features/step_definitions/tag_set_steps.rb'
+ - 'lib/tasks/load_autocomplete_data.rake'
+
+# Offense count: 68
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: literals, strict
+Style/MutableConstant:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_claim.rb'
+ - 'app/models/challenge_models/gift_exchange.rb'
+ - 'app/models/challenge_models/prompt_meme.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_item.rb'
+ - 'app/models/collection_participant.rb'
+ - 'app/models/indexing/cache_master.rb'
+ - 'app/models/potential_match_settings.rb'
+ - 'app/models/search/bookmark_search_form.rb'
+ - 'app/models/search/pseud_search_form.rb'
+ - 'app/models/search/tag_search_form.rb'
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/unsorted_tag.rb'
+ - 'features/step_definitions/external_work_steps.rb'
+ - 'features/step_definitions/skin_steps.rb'
+ - 'features/step_definitions/user_steps.rb'
+ - 'features/step_definitions/work_steps.rb'
+ - 'lib/css_cleaner.rb'
+
+# Offense count: 18
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: both, prefix, postfix
+Style/NegatedIf:
+ Exclude:
+ - 'app/controllers/chapters_controller.rb'
+ - 'app/controllers/collections_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/controllers/tag_wranglings_controller.rb'
+ - 'app/models/bookmark.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_item.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'app/models/work.rb'
+ - 'features/step_definitions/bookmark_steps.rb'
+ - 'lib/css_cleaner.rb'
+
+# Offense count: 16
+# Cop supports --auto-correct.
+Style/NegatedIfElseCondition:
+ Exclude:
+ - 'app/controllers/chapters_controller.rb'
+ - 'app/controllers/collections_controller.rb'
+ - 'app/controllers/comments_controller.rb'
+ - 'app/controllers/invitations_controller.rb'
+ - 'app/controllers/prompts_controller.rb'
+ - 'app/controllers/pseuds_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/controllers/users/registrations_controller.rb'
+ - 'app/helpers/series_helper.rb'
+ - 'app/models/challenge_claim.rb'
+ - 'app/models/collection.rb'
+ - 'features/step_definitions/admin_steps.rb'
+ - 'lib/challenge_core.rb'
+
+# Offense count: 4
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: both, prefix, postfix
+Style/NegatedUnless:
+ Exclude:
+ - 'app/controllers/challenge_assignments_controller.rb'
+ - 'app/controllers/potential_matches_controller.rb'
+ - 'app/helpers/series_helper.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/NegatedWhile:
+ Exclude:
+ - 'app/helpers/home_helper.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: AllowedMethods.
+# AllowedMethods: be, be_a, be_an, be_between, be_falsey, be_kind_of, be_instance_of, be_truthy, be_within, eq, eql, end_with, include, match, raise_error, respond_to, start_with
+Style/NestedParenthesizedCalls:
+ Exclude:
+ - 'spec/controllers/tags_controller_spec.rb'
+
+# Offense count: 24
+# Cop supports --auto-correct.
+Style/NestedTernaryOperator:
+ Exclude:
+ - 'app/controllers/series_controller.rb'
+ - 'app/controllers/tags_controller.rb'
+ - 'app/helpers/users_helper.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/kudo.rb'
+ - 'app/models/potential_match.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/cast_nomination.rb'
+ - 'lib/autocomplete_source.rb'
+
+# Offense count: 24
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, MinBodyLength.
+# SupportedStyles: skip_modifier_ifs, always
+Style/Next:
+ Exclude:
+ - 'app/controllers/bookmarks_controller.rb'
+ - 'app/controllers/collection_participants_controller.rb'
+ - 'app/controllers/owned_tag_sets_controller.rb'
+ - 'app/controllers/tag_set_associations_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/controllers/tag_wranglers_controller.rb'
+ - 'app/controllers/user_invite_requests_controller.rb'
+ - 'app/helpers/series_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/external_author.rb'
+ - 'app/models/prompt.rb'
+ - 'app/models/search/tag_indexer.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/spam_report.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/views/tags/feed.atom.builder'
+ - 'lib/autocomplete_source.rb'
+
+# Offense count: 3
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: predicate, comparison
+Style/NilComparison:
+ Exclude:
+ - 'app/models/pseud.rb'
+ - 'lib/acts_as_commentable/comment_methods.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Style/Not:
+ Exclude:
+ - 'app/controllers/collections_controller.rb'
+ - 'app/controllers/pseuds_controller.rb'
+
+# Offense count: 12
+# Cop supports --auto-correct.
+# Configuration parameters: MinDigits, Strict.
+Style/NumericLiterals:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/models/work.rb'
+ - 'lib/devise_failure_message_options.rb'
+ - 'spec/controllers/owned_tag_sets_controller_spec.rb'
+ - 'spec/lib/tasks/opendoors.rake_spec.rb'
+
+# Offense count: 83
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, IgnoredMethods.
+# SupportedStyles: predicate, comparison
+Style/NumericPredicate:
+ Exclude:
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/controllers/challenge_assignments_controller.rb'
+ - 'app/controllers/chapters_controller.rb'
+ - 'app/controllers/invitations_controller.rb'
+ - 'app/controllers/opendoors/tools_controller.rb'
+ - 'app/controllers/owned_tag_sets_controller.rb'
+ - 'app/controllers/potential_matches_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/decorators/pseud_decorator.rb'
+ - 'app/helpers/prompt_restrictions_helper.rb'
+ - 'app/helpers/series_helper.rb'
+ - 'app/helpers/tag_sets_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/translation_helper.rb'
+ - 'app/helpers/user_invite_requests_helper.rb'
+ - 'app/helpers/users_helper.rb'
+ - 'app/helpers/works_helper.rb'
+ - 'app/models/admin_setting.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/challenge_signup_summary.rb'
+ - 'app/models/invitation.rb'
+ - 'app/models/potential_match.rb'
+ - 'app/models/potential_match_settings.rb'
+ - 'app/models/prompt.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/series.rb'
+ - 'app/models/spam_report.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'app/models/tagset_models/tag_set_nomination.rb'
+ - 'app/models/user_invite_request.rb'
+ - 'features/step_definitions/admin_steps.rb'
+ - 'features/step_definitions/email_custom_steps.rb'
+ - 'features/step_definitions/potential_match_steps.rb'
+ - 'features/step_definitions/series_steps.rb'
+ - 'lib/acts_as_commentable/comment_methods.rb'
+ - 'lib/css_cleaner.rb'
+ - 'lib/html_cleaner.rb'
+ - 'lib/tasks/tag_tasks.rake'
+ - 'lib/url_helpers.rb'
+ - 'script/gift_exchange/export_settings_json.rb'
+ - 'script/gift_exchange/generate_from_spec.rb'
+ - 'script/gift_exchange/load_json_challenge.rb'
+
+# Offense count: 1
+Style/OptionalArguments:
+ Exclude:
+ - 'app/helpers/prompt_restrictions_helper.rb'
+
+# Offense count: 24
+# Configuration parameters: AllowedMethods.
+# AllowedMethods: respond_to_missing?
+Style/OptionalBooleanParameter:
+ Exclude:
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/prompt_restrictions_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/translation_helper.rb'
+ - 'app/mailers/user_mailer.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/creatorship.rb'
+ - 'app/models/potential_matcher/potential_matcher_progress.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/work.rb'
+
+# Offense count: 4
+# Cop supports --auto-correct.
+Style/OrAssignment:
+ Exclude:
+ - 'app/controllers/opendoors/external_authors_controller.rb'
+ - 'app/controllers/prompts_controller.rb'
+ - 'lib/autocomplete_source.rb'
+ - 'lib/tasks/work_tasks.rake'
+
+# Offense count: 4
+# Cop supports --auto-correct.
+Style/ParallelAssignment:
+ Exclude:
+ - 'app/controllers/tag_wranglings_controller.rb'
+ - 'app/helpers/tags_helper.rb'
+
+# Offense count: 21
+# Cop supports --auto-correct.
+# Configuration parameters: AllowSafeAssignment, AllowInMultilineConditions.
+Style/ParenthesesAroundCondition:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/challenge/gift_exchange_controller.rb'
+ - 'app/controllers/challenge/prompt_meme_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/prompts_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/mailers/user_mailer.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/tagset_models/cast_nomination.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'app/models/work.rb'
+ - 'app/validators/url_format_validator.rb'
+ - 'lib/acts_as_commentable/comment_methods.rb'
+ - 'lib/css_cleaner.rb'
+
+# Offense count: 121
+# Cop supports --auto-correct.
+# Configuration parameters: PreferredDelimiters.
+Style/PercentLiteralDelimiters:
+ Exclude:
+ - 'app/controllers/admin/admin_users_controller.rb'
+ - 'app/controllers/admin/skins_controller.rb'
+ - 'app/controllers/admin/user_creations_controller.rb'
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/challenge_assignments_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/invitations_controller.rb'
+ - 'app/controllers/owned_tag_sets_controller.rb'
+ - 'app/controllers/skins_controller.rb'
+ - 'app/controllers/stats_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/controllers/tags_controller.rb'
+ - 'app/controllers/unsorted_tags_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/works_helper.rb'
+ - 'app/models/abuse_report.rb'
+ - 'app/models/challenge_models/gift_exchange.rb'
+ - 'app/models/challenge_models/prompt_meme.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/download_writer.rb'
+ - 'app/models/feedback.rb'
+ - 'app/models/indexing/cache_master.rb'
+ - 'app/models/indexing/scheduled_reindex_job.rb'
+ - 'app/models/potential_match.rb'
+ - 'app/models/potential_match_settings.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/search/bookmark_query.rb'
+ - 'app/models/search/bookmark_search_form.rb'
+ - 'app/models/search/bookmarkable_query.rb'
+ - 'app/models/search/query.rb'
+ - 'app/models/search/query_cleaner.rb'
+ - 'app/models/search/tag_indexer.rb'
+ - 'app/models/search/taggable_query.rb'
+ - 'app/models/search/work_query.rb'
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/skin_parent.rb'
+ - 'app/models/subscription.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/tagset_models/tag_set_nomination.rb'
+ - 'app/models/work.rb'
+ - 'config/deploy.rb'
+ - 'lib/challenge_core.rb'
+ - 'lib/css_cleaner.rb'
+ - 'lib/otw_sanitize/embed_sanitizer.rb'
+ - 'lib/tasks/cucumber.rake'
+ - 'lib/tasks/deploy_tasks.rake'
+ - 'script/gift_exchange/export_settings_json.rb'
+ - 'script/gift_exchange/generate_from_spec.rb'
+ - 'script/gift_exchange/json_tag_frequency.rb'
+ - 'spec/controllers/admin/api_controller_spec.rb'
+ - 'spec/controllers/api/v2/api_authorization_spec.rb'
+ - 'spec/controllers/api/v2/api_works_search_spec.rb'
+ - 'spec/controllers/works/default_rails_actions_spec.rb'
+ - 'spec/lib/html_cleaner_spec.rb'
+ - 'spec/models/search/work_search_form_spec.rb'
+ - 'spec/models/story_parser_spec.rb'
+ - 'spec/models/unsorted_tag_spec.rb'
+
+# Offense count: 101
+# Cop supports --auto-correct.
+Style/PerlBackrefs:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/opendoors/tools_controller.rb'
+ - 'app/controllers/tag_set_associations_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/models/search/query.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/story_parser.rb'
+ - 'features/step_definitions/challege_gift_exchange_steps.rb'
+ - 'features/support/email.rb'
+ - 'features/support/paths.rb'
+ - 'lib/css_cleaner.rb'
+ - 'lib/tasks/skin_tasks.rake'
+
+# Offense count: 3
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: short, verbose
+Style/PreferredHashMethods:
+ Exclude:
+ - 'app/helpers/application_helper.rb'
+ - 'lib/html_cleaner.rb'
+ - 'lib/sortable_list.rb'
+
+# Offense count: 4
+# Cop supports --auto-correct.
+Style/Proc:
+ Exclude:
+ - 'app/helpers/validation_helper.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/tagset_models/fandom_nomination.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+
+# Offense count: 34
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: same_as_string_literals, single_quotes, double_quotes
+Style/QuotedSymbols:
+ Exclude:
+ - 'spec/controllers/tag_set_nominations_controller_spec.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, AllowedCompactTypes.
+# SupportedStyles: compact, exploded
+Style/RaiseArgs:
+ Exclude:
+ - 'app/models/series.rb'
+ - 'app/models/work.rb'
+
+# Offense count: 3
+# Cop supports --auto-correct.
+# Configuration parameters: Methods.
+Style/RedundantArgument:
+ Exclude:
+ - 'app/helpers/pseuds_helper.rb'
+ - 'app/models/search/query.rb'
+ - 'lib/otw_sanitize/user_class_sanitizer.rb'
+
+# Offense count: 4
+# Cop supports --auto-correct.
+Style/RedundantAssignment:
+ Exclude:
+ - 'app/models/download.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/tagset_models/tag_set_association.rb'
+
+# Offense count: 15
+# Cop supports --auto-correct.
+Style/RedundantBegin:
+ Exclude:
+ - 'app/controllers/readings_controller.rb'
+ - 'app/models/admin_post.rb'
+ - 'app/models/search/query.rb'
+ - 'app/models/story_parser.rb'
+ - 'config/initializers/monkeypatches/translate_string.rb'
+ - 'lib/acts_as_commentable/commentable_entity.rb'
+ - 'lib/backwards_compatible_password_decryptor.rb'
+ - 'lib/tasks/opendoors.rake'
+ - 'lib/tasks/work_tasks.rake'
+
+# Offense count: 5
+# Cop supports --auto-correct.
+Style/RedundantCondition:
+ Exclude:
+ - 'app/models/abuse_report.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/work.rb'
+ - 'features/step_definitions/invite_steps.rb'
+ - 'lib/collectible.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/RedundantConditional:
+ Exclude:
+ - 'app/models/challenge_signup.rb'
+
+# Offense count: 40
+# Cop supports --auto-correct.
+Style/RedundantInterpolation:
+ Exclude:
+ - 'app/controllers/admin/skins_controller.rb'
+ - 'app/controllers/comments_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/mailer_helper.rb'
+ - 'app/helpers/prompt_restrictions_helper.rb'
+ - 'app/helpers/tag_sets_helper.rb'
+ - 'app/mailers/user_mailer.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/potential_match.rb'
+ - 'app/models/potential_match_settings.rb'
+ - 'features/step_definitions/challege_gift_exchange_steps.rb'
+ - 'features/step_definitions/challenge_steps.rb'
+ - 'features/step_definitions/generic_steps.rb'
+ - 'features/step_definitions/invite_steps.rb'
+ - 'features/step_definitions/tag_set_steps.rb'
+ - 'features/step_definitions/work_steps.rb'
+ - 'lib/challenge_core.rb'
+ - 'lib/works_owner.rb'
+ - 'spec/controllers/tags_controller_spec.rb'
+ - 'spec/lib/html_cleaner_spec.rb'
+ - 'spec/models/potential_match_spec.rb'
+ - 'spec/models/search/pseud_decorator_spec.rb'
+
+# Offense count: 19
+# Cop supports --auto-correct.
+Style/RedundantParentheses:
+ Exclude:
+ - 'app/controllers/bookmarks_controller.rb'
+ - 'app/controllers/challenge/gift_exchange_controller.rb'
+ - 'app/controllers/challenge/prompt_meme_controller.rb'
+ - 'app/controllers/collection_items_controller.rb'
+ - 'app/controllers/comments_controller.rb'
+ - 'app/controllers/invite_requests_controller.rb'
+ - 'app/helpers/collections_helper.rb'
+ - 'app/helpers/prompt_restrictions_helper.rb'
+ - 'app/helpers/translation_helper.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/tagset_models/tag_set_nomination.rb'
+
+# Offense count: 8
+# Cop supports --auto-correct.
+Style/RedundantPercentQ:
+ Exclude:
+ - 'features/step_definitions/invite_steps.rb'
+
+# Offense count: 15
+# Cop supports --auto-correct.
+Style/RedundantRegexpCharacterClass:
+ Exclude:
+ - 'app/models/skin.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/tag_set_association.rb'
+ - 'features/step_definitions/bookmark_steps.rb'
+ - 'features/step_definitions/tag_steps.rb'
+ - 'features/step_definitions/work_search_steps.rb'
+ - 'features/step_definitions/work_steps.rb'
+ - 'features/support/paths.rb'
+
+# Offense count: 357
+# Cop supports --auto-correct.
+Style/RedundantRegexpEscape:
+ Exclude:
+ - 'app/models/abuse_report.rb'
+ - 'app/models/admin_blacklisted_email.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/download.rb'
+ - 'app/models/external_author_name.rb'
+ - 'app/models/preference.rb'
+ - 'app/models/search/query_cleaner.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/work.rb'
+ - 'features/step_definitions/admin_steps.rb'
+ - 'features/step_definitions/archivist_steps.rb'
+ - 'features/step_definitions/autocomplete_steps.rb'
+ - 'features/step_definitions/banner_steps.rb'
+ - 'features/step_definitions/bookmark_steps.rb'
+ - 'features/step_definitions/challege_gift_exchange_steps.rb'
+ - 'features/step_definitions/challenge_promptmeme_steps.rb'
+ - 'features/step_definitions/challenge_steps.rb'
+ - 'features/step_definitions/collection_steps.rb'
+ - 'features/step_definitions/comment_steps.rb'
+ - 'features/step_definitions/email_custom_steps.rb'
+ - 'features/step_definitions/external_work_steps.rb'
+ - 'features/step_definitions/fixtures_steps.rb'
+ - 'features/step_definitions/generic_steps.rb'
+ - 'features/step_definitions/invite_steps.rb'
+ - 'features/step_definitions/kudos_steps.rb'
+ - 'features/step_definitions/potential_match_steps.rb'
+ - 'features/step_definitions/pseud_steps.rb'
+ - 'features/step_definitions/request_header_steps.rb'
+ - 'features/step_definitions/series_steps.rb'
+ - 'features/step_definitions/skin_steps.rb'
+ - 'features/step_definitions/subscription_steps.rb'
+ - 'features/step_definitions/tag_set_steps.rb'
+ - 'features/step_definitions/tag_steps.rb'
+ - 'features/step_definitions/user_steps.rb'
+ - 'features/step_definitions/web_steps.rb'
+ - 'features/step_definitions/work_steps.rb'
+ - 'lib/autocomplete_source.rb'
+ - 'lib/css_cleaner.rb'
+ - 'lib/otw_sanitize/embed_sanitizer.rb'
+ - 'spec/lib/tasks/after_tasks.rake_spec.rb'
+
+# Offense count: 72
+# Cop supports --auto-correct.
+# Configuration parameters: AllowMultipleReturnValues.
+Style/RedundantReturn:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/collections_controller.rb'
+ - 'app/decorators/homepage.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/bookmarks_helper.rb'
+ - 'app/helpers/collections_helper.rb'
+ - 'app/helpers/home_helper.rb'
+ - 'app/helpers/series_helper.rb'
+ - 'app/helpers/tag_sets_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/models/admin_blacklisted_email.rb'
+ - 'app/models/bookmark.rb'
+ - 'app/models/challenge_claim.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/potential_match.rb'
+ - 'app/models/preference.rb'
+ - 'app/models/prompt.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/tagset_models/tag_set_association.rb'
+ - 'app/models/tagset_models/tag_set_nomination.rb'
+ - 'app/models/user.rb'
+ - 'app/models/work.rb'
+ - 'app/validators/email_blacklist_validator.rb'
+ - 'lib/acts_as_commentable/comment_methods.rb'
+ - 'lib/backwards_compatible_password_decryptor.rb'
+ - 'lib/collectible.rb'
+ - 'lib/css_cleaner.rb'
+ - 'lib/html_cleaner.rb'
+ - 'lib/url_formatter.rb'
+ - 'lib/works_owner.rb'
+ - 'script/gift_exchange/generate_from_spec.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Style/RedundantSort:
+ Exclude:
+ - 'app/models/series.rb'
+
+# Offense count: 52
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, AllowInnerSlashes.
+# SupportedStyles: slashes, percent_r, mixed
+Style/RegexpLiteral:
+ Exclude:
+ - 'app/controllers/opendoors/tools_controller.rb'
+ - 'app/helpers/home_helper.rb'
+ - 'app/helpers/mailer_helper.rb'
+ - 'app/models/abuse_report.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/search/query.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/work.rb'
+ - 'app/validators/url_format_validator.rb'
+ - 'config/deploy.rb'
+ - 'config/initializers/monkeypatches/textarea_convert_html_to_newlines.rb'
+ - 'features/step_definitions/web_steps.rb'
+ - 'features/step_definitions/work_search_steps.rb'
+ - 'lib/autocomplete_source.rb'
+ - 'lib/html_cleaner.rb'
+ - 'lib/tasks/skin_tasks.rake'
+ - 'lib/url_formatter.rb'
+ - 'spec/lib/html_cleaner_spec.rb'
+
+# Offense count: 28
+# Cop supports --auto-correct.
+Style/RescueModifier:
+ Exclude:
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/challenge_assignments_controller.rb'
+ - 'app/controllers/challenge_claims_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/challenges_controller.rb'
+ - 'app/controllers/potential_matches_controller.rb'
+ - 'app/controllers/prompts_controller.rb'
+ - 'app/controllers/skins_controller.rb'
+ - 'app/models/search/query_result.rb'
+ - 'app/models/story_parser.rb'
+ - 'lib/otw_sanitize/embed_sanitizer.rb'
+ - 'spec/controllers/works/importing_spec.rb'
+
+# Offense count: 24
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: implicit, explicit
+Style/RescueStandardError:
+ Exclude:
+ - 'app/controllers/api/v2/works_controller.rb'
+ - 'app/controllers/challenge_claims_controller.rb'
+ - 'app/controllers/collections_controller.rb'
+ - 'app/controllers/inbox_controller.rb'
+ - 'app/controllers/opendoors/tools_controller.rb'
+ - 'app/controllers/owned_tag_sets_controller.rb'
+ - 'app/controllers/readings_controller.rb'
+ - 'app/controllers/skins_controller.rb'
+ - 'app/controllers/works_controller.rb'
+ - 'app/helpers/date_helper.rb'
+ - 'app/models/admin_post.rb'
+ - 'app/models/redis_mail_queue.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/work_link.rb'
+ - 'app/validators/url_active_validator.rb'
+ - 'config/initializers/archive_config/locale.rb'
+ - 'features/step_definitions/challenge_steps.rb'
+ - 'lib/acts_as_commentable/commentable_entity.rb'
+
+# Offense count: 63
+# Cop supports --auto-correct.
+# Configuration parameters: ConvertCodeThatCanStartToReturnNil, AllowedMethods.
+# AllowedMethods: present?, blank?, presence, try, try!
+Style/SafeNavigation:
+ Exclude:
+ - 'app/controllers/admin/admin_users_controller.rb'
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/challenge_assignments_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/collection_items_controller.rb'
+ - 'app/controllers/collection_participants_controller.rb'
+ - 'app/controllers/external_authors_controller.rb'
+ - 'app/controllers/opendoors/tools_controller.rb'
+ - 'app/controllers/skins_controller.rb'
+ - 'app/controllers/tags_controller.rb'
+ - 'app/controllers/works_controller.rb'
+ - 'app/helpers/challenge_helper.rb'
+ - 'app/helpers/collections_helper.rb'
+ - 'app/helpers/invitations_helper.rb'
+ - 'app/helpers/pseuds_helper.rb'
+ - 'app/helpers/skins_helper.rb'
+ - 'app/helpers/users_helper.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/helpers/works_helper.rb'
+ - 'app/mailers/user_mailer.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_item.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/favorite_tag.rb'
+ - 'app/models/potential_match.rb'
+ - 'app/models/potential_matcher/potential_matcher_constrained.rb'
+ - 'app/models/search/bookmark_query.rb'
+ - 'app/models/search/pseud_query.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'app/models/user.rb'
+ - 'app/models/work.rb'
+ - 'features/step_definitions/potential_match_steps.rb'
+ - 'features/step_definitions/tag_set_steps.rb'
+ - 'features/step_definitions/work_related_steps.rb'
+ - 'lib/challenge_core.rb'
+ - 'lib/tasks/tag_tasks.rake'
+ - 'script/gift_exchange/json_tag_frequency.rb'
+ - 'spec/lib/works_owner_spec.rb'
+
+# Offense count: 3
+# Cop supports --auto-correct.
+Style/SelfAssignment:
+ Exclude:
+ - 'app/controllers/tag_wranglings_controller.rb'
+ - 'app/models/chapter.rb'
+ - 'features/step_definitions/challenge_promptmeme_steps.rb'
+
+# Offense count: 21
+# Cop supports --auto-correct.
+# Configuration parameters: AllowAsExpressionSeparator.
+Style/Semicolon:
+ Exclude:
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/helpers/external_authors_helper.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/fandom_nomination.rb'
+ - 'app/models/tagset_models/tag_set_association.rb'
+ - 'app/models/work.rb'
+ - 'lib/tasks/tag_tasks.rake'
+ - 'spec/lib/works_owner_spec.rb'
+ - 'spec/models/skin_parent_spec.rb'
+
+# Offense count: 3
+# Cop supports --auto-correct.
+Style/SingleArgumentDig:
+ Exclude:
+ - 'app/models/search/query.rb'
+ - 'spec/controllers/admin/blacklisted_emails_controller_spec.rb'
+
+# Offense count: 26
+# Cop supports --auto-correct.
+# Configuration parameters: AllowIfMethodIsEmpty.
+Style/SingleLineMethods:
+ Exclude:
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_participant.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'lib/collectible.rb'
+ - 'spec/lib/tasks/resque.rake_spec.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/SlicingWithRange:
+ Exclude:
+ - 'app/models/search/work_search_form.rb'
+
+# Offense count: 17
+# Cop supports --auto-correct.
+# Configuration parameters: AllowModifier.
+Style/SoleNestedConditional:
+ Exclude:
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/collection_participants_controller.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/mailers/user_mailer.rb'
+ - 'app/models/admin_setting.rb'
+ - 'app/models/bookmark.rb'
+ - 'app/models/collection_item.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'config/initializers/gem-plugin_config/redis.rb'
+ - 'config/initializers/monkeypatches/mail_disable_starttls.rb'
+ - 'config/initializers/monkeypatches/textarea_convert_html_to_newlines.rb'
+ - 'lib/autocomplete_source.rb'
+ - 'lib/collectible.rb'
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: RequireEnglish, EnforcedStyle.
+# SupportedStyles: use_perl_names, use_english_names
+Style/SpecialGlobalVars:
+ Exclude:
+ - 'public/dispatch.rb'
+
+# Offense count: 3
+# Cop supports --auto-correct.
+Style/StderrPuts:
+ Exclude:
+ - 'config/boot.rb'
+ - 'lib/tasks/cucumber.rake'
+
+# Offense count: 182
+# Cop supports --auto-correct.
+# Configuration parameters: Mode.
+Style/StringConcatenation:
+ Exclude:
+ - 'app/controllers/admin/api_controller.rb'
+ - 'app/controllers/admin/blacklisted_emails_controller.rb'
+ - 'app/controllers/admin/user_creations_controller.rb'
+ - 'app/controllers/api/v2/bookmarks_controller.rb'
+ - 'app/controllers/api/v2/works_controller.rb'
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/controllers/bookmarks_controller.rb'
+ - 'app/controllers/challenge_requests_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/collection_items_controller.rb'
+ - 'app/controllers/collections_controller.rb'
+ - 'app/controllers/inbox_controller.rb'
+ - 'app/controllers/opendoors/external_authors_controller.rb'
+ - 'app/controllers/owned_tag_sets_controller.rb'
+ - 'app/controllers/tag_wranglers_controller.rb'
+ - 'app/controllers/tag_wranglings_controller.rb'
+ - 'app/controllers/tags_controller.rb'
+ - 'app/controllers/works_controller.rb'
+ - 'app/decorators/pseud_decorator.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/collections_helper.rb'
+ - 'app/helpers/comments_helper.rb'
+ - 'app/helpers/mailer_helper.rb'
+ - 'app/helpers/prompt_restrictions_helper.rb'
+ - 'app/helpers/pseuds_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/users_helper.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/helpers/works_helper.rb'
+ - 'app/mailers/application_mailer.rb'
+ - 'app/mailers/user_mailer.rb'
+ - 'app/models/admin_blacklisted_email.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_claim.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/download_writer.rb'
+ - 'app/models/external_author_name.rb'
+ - 'app/models/search/bookmark_search_form.rb'
+ - 'app/models/search/query.rb'
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/search_range.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/tag_set_association.rb'
+ - 'config/initializers/gem-plugin_config/redis.rb'
+ - 'config/initializers/gem-plugin_config/resque.rb'
+ - 'config/initializers/monkeypatches/textarea_convert_html_to_newlines.rb'
+ - 'features/step_definitions/generic_steps.rb'
+ - 'features/step_definitions/work_steps.rb'
+ - 'lib/autocomplete_source.rb'
+ - 'lib/css_cleaner.rb'
+ - 'lib/html_cleaner.rb'
+ - 'lib/otw_sanitize/media_sanitizer.rb'
+ - 'lib/tasks/cucumber.rake'
+ - 'lib/tasks/skin_tasks.rake'
+ - 'lib/url_formatter.rb'
+ - 'lib/url_helpers.rb'
+ - 'public/dispatch.fcgi'
+ - 'public/dispatch.rb'
+ - 'script/gift_exchange/benchmark_assignments.rb'
+ - 'script/gift_exchange/benchmark_match.rb'
+ - 'script/gift_exchange/benchmark_regenerate.rb'
+ - 'script/gift_exchange/generate_from_spec.rb'
+ - 'script/gift_exchange/load_json_challenge.rb'
+ - 'spec/controllers/api/v2/api_works_spec.rb'
+ - 'spec/controllers/inbox_controller_spec.rb'
+ - 'spec/controllers/prompts_controller_spec.rb'
+ - 'spec/lib/html_cleaner_spec.rb'
+ - 'spec/models/story_parser_spec.rb'
+ - 'spec/models/work_spec.rb'
+
+# Offense count: 2633
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
+# SupportedStyles: single_quotes, double_quotes
+Style/StringLiterals:
+ Exclude:
+ - 'Capfile'
+ - 'Gemfile'
+ - 'Rakefile'
+ - 'app/controllers/admin/admin_invitations_controller.rb'
+ - 'app/controllers/admin/banners_controller.rb'
+ - 'app/controllers/admin/blacklisted_emails_controller.rb'
+ - 'app/controllers/admin/skins_controller.rb'
+ - 'app/controllers/admin_posts_controller.rb'
+ - 'app/controllers/api/v2/bookmarks_controller.rb'
+ - 'app/controllers/application_controller.rb'
+ - 'app/controllers/archive_faqs_controller.rb'
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/controllers/bookmarks_controller.rb'
+ - 'app/controllers/challenge/gift_exchange_controller.rb'
+ - 'app/controllers/challenge/prompt_meme_controller.rb'
+ - 'app/controllers/challenge_assignments_controller.rb'
+ - 'app/controllers/challenge_claims_controller.rb'
+ - 'app/controllers/challenge_requests_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/challenges_controller.rb'
+ - 'app/controllers/chapters_controller.rb'
+ - 'app/controllers/collection_items_controller.rb'
+ - 'app/controllers/collection_participants_controller.rb'
+ - 'app/controllers/collections_controller.rb'
+ - 'app/controllers/comments_controller.rb'
+ - 'app/controllers/downloads_controller.rb'
+ - 'app/controllers/external_authors_controller.rb'
+ - 'app/controllers/external_works_controller.rb'
+ - 'app/controllers/fandoms_controller.rb'
+ - 'app/controllers/favorite_tags_controller.rb'
+ - 'app/controllers/gifts_controller.rb'
+ - 'app/controllers/home_controller.rb'
+ - 'app/controllers/inbox_controller.rb'
+ - 'app/controllers/invitations_controller.rb'
+ - 'app/controllers/known_issues_controller.rb'
+ - 'app/controllers/languages_controller.rb'
+ - 'app/controllers/locales_controller.rb'
+ - 'app/controllers/media_controller.rb'
+ - 'app/controllers/opendoors/external_authors_controller.rb'
+ - 'app/controllers/owned_tag_sets_controller.rb'
+ - 'app/controllers/potential_matches_controller.rb'
+ - 'app/controllers/preferences_controller.rb'
+ - 'app/controllers/profile_controller.rb'
+ - 'app/controllers/prompts_controller.rb'
+ - 'app/controllers/pseuds_controller.rb'
+ - 'app/controllers/questions_controller.rb'
+ - 'app/controllers/readings_controller.rb'
+ - 'app/controllers/related_works_controller.rb'
+ - 'app/controllers/series_controller.rb'
+ - 'app/controllers/skins_controller.rb'
+ - 'app/controllers/stats_controller.rb'
+ - 'app/controllers/tag_set_associations_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/controllers/tag_wranglers_controller.rb'
+ - 'app/controllers/tag_wranglings_controller.rb'
+ - 'app/controllers/tags_controller.rb'
+ - 'app/controllers/user_invite_requests_controller.rb'
+ - 'app/controllers/users/registrations_controller.rb'
+ - 'app/controllers/users_controller.rb'
+ - 'app/controllers/works_controller.rb'
+ - 'app/controllers/wrangling_guidelines_controller.rb'
+ - 'app/decorators/pseud_decorator.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/helpers/bookmarks_helper.rb'
+ - 'app/helpers/challenge_helper.rb'
+ - 'app/helpers/collections_helper.rb'
+ - 'app/helpers/comments_helper.rb'
+ - 'app/helpers/external_authors_helper.rb'
+ - 'app/helpers/mailer_helper.rb'
+ - 'app/helpers/orphans_helper.rb'
+ - 'app/helpers/series_helper.rb'
+ - 'app/helpers/tag_sets_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/helpers/translation_helper.rb'
+ - 'app/helpers/users_helper.rb'
+ - 'app/helpers/validation_helper.rb'
+ - 'app/helpers/works_helper.rb'
+ - 'app/mailers/user_mailer.rb'
+ - 'app/models/abuse_report.rb'
+ - 'app/models/admin.rb'
+ - 'app/models/admin_blacklisted_email.rb'
+ - 'app/models/admin_setting.rb'
+ - 'app/models/archive_faq.rb'
+ - 'app/models/bookmark.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_claim.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/character.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_item.rb'
+ - 'app/models/collection_participant.rb'
+ - 'app/models/comment.rb'
+ - 'app/models/common_tagging.rb'
+ - 'app/models/download.rb'
+ - 'app/models/download_writer.rb'
+ - 'app/models/external_author.rb'
+ - 'app/models/external_author_name.rb'
+ - 'app/models/external_creatorship.rb'
+ - 'app/models/fandom.rb'
+ - 'app/models/filter_count.rb'
+ - 'app/models/freeform.rb'
+ - 'app/models/inbox_comment.rb'
+ - 'app/models/indexing/cache_master.rb'
+ - 'app/models/indexing/index_queue.rb'
+ - 'app/models/indexing/scheduled_reindex_job.rb'
+ - 'app/models/invitation.rb'
+ - 'app/models/invite_request.rb'
+ - 'app/models/language.rb'
+ - 'app/models/media.rb'
+ - 'app/models/meta_tagging.rb'
+ - 'app/models/opendoors.rb'
+ - 'app/models/preference.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/question.rb'
+ - 'app/models/redis_mail_queue.rb'
+ - 'app/models/relationship.rb'
+ - 'app/models/scheduled_tag_job.rb'
+ - 'app/models/search/async_indexer.rb'
+ - 'app/models/search/bookmark_query.rb'
+ - 'app/models/search/bookmark_search_form.rb'
+ - 'app/models/search/bookmarkable_indexer.rb'
+ - 'app/models/search/bookmarkable_query.rb'
+ - 'app/models/search/indexer.rb'
+ - 'app/models/search/pseud_query.rb'
+ - 'app/models/search/pseud_search_form.rb'
+ - 'app/models/search/query.rb'
+ - 'app/models/search/query_cleaner.rb'
+ - 'app/models/search/query_result.rb'
+ - 'app/models/search/tag_query.rb'
+ - 'app/models/search/work_query.rb'
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/series.rb'
+ - 'app/models/skin.rb'
+ - 'app/models/story_parser.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagging.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'app/models/tagset_models/tag_nomination.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/tagset_models/tag_set_nomination.rb'
+ - 'app/models/user.rb'
+ - 'app/models/work.rb'
+ - 'app/models/work_link.rb'
+ - 'app/models/work_skin.rb'
+ - 'app/models/wrangling_guideline.rb'
+ - 'app/sweepers/feed_sweeper.rb'
+ - 'app/validators/email_format_validator.rb'
+ - 'app/validators/email_veracity_validator.rb'
+ - 'app/validators/url_active_validator.rb'
+ - 'app/views/tags/feed.atom.builder'
+ - 'config.ru'
+ - 'config/boot.rb'
+ - 'config/deploy.rb'
+ - 'config/deploy/production.rb'
+ - 'config/deploy/staging.rb'
+ - 'config/environment.rb'
+ - 'config/initializers/active_record_log_subscriber.rb'
+ - 'config/initializers/archive_config/locale.rb'
+ - 'config/initializers/assets.rb'
+ - 'config/initializers/devise.rb'
+ - 'config/initializers/gem-plugin_config/redis.rb'
+ - 'config/initializers/gem-plugin_config/resque.rb'
+ - 'config/initializers/gem-plugin_config/will_paginate_config.rb'
+ - 'config/initializers/mime_types.rb'
+ - 'config/initializers/monkeypatches/accept_header.rb'
+ - 'config/initializers/monkeypatches/textarea_convert_html_to_newlines.rb'
+ - 'config/initializers/phraseapp_in_context_editor.rb'
+ - 'config/initializers/rack_attack.rb'
+ - 'config/initializers/session_store.rb'
+ - 'config/routes.rb'
+ - 'config/schedule.rb'
+ - 'config/schedule_production.rb'
+ - 'db/migrate/20150725141326_install_audited.rb'
+ - 'db/migrate/20171030201300_add_simplified_email_to_invite_requests.rb'
+ - 'factories/abuse_reports.rb'
+ - 'factories/admin.rb'
+ - 'factories/admin_blacklisted_email.rb'
+ - 'factories/admin_post.rb'
+ - 'factories/api_key.rb'
+ - 'factories/bookmarks.rb'
+ - 'factories/challenge_claims.rb'
+ - 'factories/challenges.rb'
+ - 'factories/chapters.rb'
+ - 'factories/comments.rb'
+ - 'factories/feedback.rb'
+ - 'factories/prompt.rb'
+ - 'factories/prompt_restriction.rb'
+ - 'factories/pseuds.rb'
+ - 'factories/related_works.rb'
+ - 'factories/serial_work.rb'
+ - 'factories/skins.rb'
+ - 'factories/subscriptions.rb'
+ - 'factories/tags.rb'
+ - 'factories/user_invite_requests.rb'
+ - 'factories/works.rb'
+ - 'factories/wrangling_guideline.rb'
+ - 'features/step_definitions/challege_gift_exchange_steps.rb'
+ - 'features/step_definitions/collection_steps.rb'
+ - 'features/step_definitions/comment_steps.rb'
+ - 'features/step_definitions/fixtures_steps.rb'
+ - 'features/step_definitions/generic_steps.rb'
+ - 'features/step_definitions/gift_steps.rb'
+ - 'features/step_definitions/pickle_steps.rb'
+ - 'features/step_definitions/rake_steps.rb'
+ - 'features/step_definitions/request_header_steps.rb'
+ - 'features/step_definitions/tag_set_steps.rb'
+ - 'features/step_definitions/user_steps.rb'
+ - 'features/step_definitions/web_steps.rb'
+ - 'features/step_definitions/work_download_steps.rb'
+ - 'features/step_definitions/work_import_steps.rb'
+ - 'features/step_definitions/work_search_steps.rb'
+ - 'features/step_definitions/work_steps.rb'
+ - 'features/support/capybara.rb'
+ - 'features/support/env.rb'
+ - 'features/support/paths.rb'
+ - 'features/support/pickle.rb'
+ - 'features/support/vcr.rb'
+ - 'features/support/wait_for_ajax.rb'
+ - 'lib/acts_as_commentable/commentable_entity.rb'
+ - 'lib/autocomplete_source.rb'
+ - 'lib/challenge_core.rb'
+ - 'lib/collectible.rb'
+ - 'lib/css_cleaner.rb'
+ - 'lib/html_cleaner.rb'
+ - 'lib/otw_sanitize/embed_sanitizer.rb'
+ - 'lib/otw_sanitize/media_sanitizer.rb'
+ - 'lib/otw_sanitize/user_class_sanitizer.rb'
+ - 'lib/pagination_list_link_renderer.rb'
+ - 'lib/redis_test_setup.rb'
+ - 'lib/responder.rb'
+ - 'lib/searchable.rb'
+ - 'lib/string_cleaner.rb'
+ - 'lib/tasks/cucumber.rake'
+ - 'lib/tasks/load_autocomplete_data.rake'
+ - 'lib/tasks/memcached.rake'
+ - 'lib/tasks/resque.rake'
+ - 'lib/tasks/skin_tasks.rake'
+ - 'lib/tasks/work_tasks.rake'
+ - 'lib/url_formatter.rb'
+ - 'lib/url_helpers.rb'
+ - 'lib/word_counter.rb'
+ - 'lib/works_owner.rb'
+ - 'public/dispatch.fcgi'
+ - 'script/gift_exchange/benchmark_assignments.rb'
+ - 'script/gift_exchange/benchmark_match.rb'
+ - 'script/gift_exchange/benchmark_regenerate.rb'
+ - 'script/gift_exchange/export_settings_json.rb'
+ - 'script/gift_exchange/generate_from_spec.rb'
+ - 'script/gift_exchange/json_tag_frequency.rb'
+ - 'script/gift_exchange/load_json_challenge.rb'
+ - 'script/gift_exchange/requests_summary_to_json.rb'
+ - 'spec/controllers/api/v2/api_works_spec.rb'
+ - 'spec/controllers/challenge_claims_controller_spec.rb'
+ - 'spec/controllers/challenge_signups_controller_spec.rb'
+ - 'spec/controllers/challenges_controller_spec.rb'
+ - 'spec/controllers/chapters_controller_spec.rb'
+ - 'spec/controllers/collection_items_controller_spec.rb'
+ - 'spec/controllers/collection_profile_controller_spec.rb'
+ - 'spec/controllers/creatorships_controller_spec.rb'
+ - 'spec/controllers/external_authors_controller_spec.rb'
+ - 'spec/controllers/gift_exchange_controller_spec.rb'
+ - 'spec/controllers/inbox_controller_spec.rb'
+ - 'spec/controllers/owned_tag_sets_controller_spec.rb'
+ - 'spec/controllers/profile_controller_spec.rb'
+ - 'spec/controllers/prompt_meme_controller_spec.rb'
+ - 'spec/controllers/related_works_controller_spec.rb'
+ - 'spec/controllers/serial_works_controller_spec.rb'
+ - 'spec/controllers/series_controller_spec.rb'
+ - 'spec/controllers/tag_set_nominations_controller_spec.rb'
+ - 'spec/controllers/tags_controller_spec.rb'
+ - 'spec/controllers/users/registrations_controller_spec.rb'
+ - 'spec/controllers/works/multiple_actions_spec.rb'
+ - 'spec/controllers/wrangling_guidelines_controller_spec.rb'
+ - 'spec/helpers/admin_post_helper_spec.rb'
+ - 'spec/helpers/exports_helper_spec.rb'
+ - 'spec/helpers/home_helper_spec.rb'
+ - 'spec/helpers/inbox_helper_spec.rb'
+ - 'spec/helpers/tag_sets_helper_spec.rb'
+ - 'spec/helpers/user_invite_requests_helper_spec.rb'
+ - 'spec/helpers/validation_helper_spec.rb'
+ - 'spec/helpers/works_helper_spec.rb'
+ - 'spec/lib/collectible_spec.rb'
+ - 'spec/lib/html_cleaner_spec.rb'
+ - 'spec/lib/string_cleaner_spec.rb'
+ - 'spec/lib/tasks/opendoors.rake_spec.rb'
+ - 'spec/lib/tasks/resque.rake_spec.rb'
+ - 'spec/lib/tasks/work_tasks.rake_spec.rb'
+ - 'spec/lib/url_formatter_spec.rb'
+ - 'spec/lib/word_counter_spec.rb'
+ - 'spec/lib/works_owner_spec.rb'
+ - 'spec/models/abuse_report_spec.rb'
+ - 'spec/models/admin_blacklisted_email_spec.rb'
+ - 'spec/models/admin_spec.rb'
+ - 'spec/models/challenge_assignment_spec.rb'
+ - 'spec/models/challenge_claim_spec.rb'
+ - 'spec/models/chapter_spec.rb'
+ - 'spec/models/collection_item_spec.rb'
+ - 'spec/models/collection_spec.rb'
+ - 'spec/models/concerns/filterable_spec.rb'
+ - 'spec/models/external_author_spec.rb'
+ - 'spec/models/external_work_spec.rb'
+ - 'spec/models/favorite_tag_spec.rb'
+ - 'spec/models/feedback_spec.rb'
+ - 'spec/models/filter_count_spec.rb'
+ - 'spec/models/gift_exchange_spec.rb'
+ - 'spec/models/gift_spec.rb'
+ - 'spec/models/indexing/cache_master_spec.rb'
+ - 'spec/models/indexing/index_queue_spec.rb'
+ - 'spec/models/invitation_spec.rb'
+ - 'spec/models/invite_request_spec.rb'
+ - 'spec/models/language_spec.rb'
+ - 'spec/models/potential_match_spec.rb'
+ - 'spec/models/pseud_spec.rb'
+ - 'spec/models/search/async_indexer_spec.rb'
+ - 'spec/models/search/bookmark_query_spec.rb'
+ - 'spec/models/search/bookmarkable_query_spec.rb'
+ - 'spec/models/search/index_sweeper_spec.rb'
+ - 'spec/models/search/pseud_decorator_spec.rb'
+ - 'spec/models/search/pseud_query_spec.rb'
+ - 'spec/models/search/query_cleaner_spec.rb'
+ - 'spec/models/search/query_spec.rb'
+ - 'spec/models/search/stat_counter_indexer_spec.rb'
+ - 'spec/models/search/work_query_spec.rb'
+ - 'spec/models/search/work_search_form_spec.rb'
+ - 'spec/models/series_spec.rb'
+ - 'spec/models/skin_parent_spec.rb'
+ - 'spec/models/skin_spec.rb'
+ - 'spec/models/stat_counter_spec.rb'
+ - 'spec/models/story_parser_spec.rb'
+ - 'spec/models/subscription_spec.rb'
+ - 'spec/models/tag_set_nomination_spec.rb'
+ - 'spec/models/tag_spec.rb'
+ - 'spec/models/tag_wrangling_spec.rb'
+ - 'spec/models/unsorted_tag_spec.rb'
+ - 'spec/models/work_spec.rb'
+ - 'spec/spec_helper.rb'
+ - 'spec/support/matchers/add_to_reindex_queue.rb'
+
+# Offense count: 10
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle.
+# SupportedStyles: single_quotes, double_quotes
+Style/StringLiteralsInInterpolation:
+ Exclude:
+ - 'app/controllers/admin/admin_users_controller.rb'
+ - 'app/controllers/collection_items_controller.rb'
+ - 'app/helpers/share_helper.rb'
+ - 'app/helpers/tag_sets_helper.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/search/async_indexer.rb'
+ - 'app/models/search/bookmark_search_form.rb'
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/story_parser.rb'
+ - 'lib/tasks/opendoors.rake'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Style/StructInheritance:
+ Exclude:
+ - 'app/models/challenge_signup_summary.rb'
+ - 'app/models/search/query_facet.rb'
+
+# Offense count: 65
+# Cop supports --auto-correct.
+# Configuration parameters: AllowMethodsWithArguments, IgnoredMethods.
+# IgnoredMethods: respond_to, define_method
+Style/SymbolProc:
+ Exclude:
+ - 'app/controllers/admin/skins_controller.rb'
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/collection_items_controller.rb'
+ - 'app/controllers/inbox_controller.rb'
+ - 'app/models/admin_post.rb'
+ - 'app/models/challenge_assignment.rb'
+ - 'app/models/challenge_signup.rb'
+ - 'app/models/chapter.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/collection_item.rb'
+ - 'app/models/moderated_work.rb'
+ - 'app/models/potential_match.rb'
+ - 'app/models/prompt.rb'
+ - 'app/models/prompt_restriction.rb'
+ - 'app/models/pseud.rb'
+ - 'app/models/relationship.rb'
+ - 'app/models/search/bookmark_search_form.rb'
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/series.rb'
+ - 'app/models/spam_report.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/tagset_models/owned_tag_set.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'app/models/tagset_models/tag_set_nomination.rb'
+ - 'app/models/work.rb'
+ - 'config/initializers/rack_attack.rb'
+ - 'factories/admin.rb'
+ - 'features/step_definitions/comment_steps.rb'
+ - 'lib/acts_as_commentable/commentable_entity.rb'
+ - 'lib/collectible.rb'
+ - 'lib/tasks/load_autocomplete_data.rake'
+ - 'lib/tasks/skin_tasks.rake'
+ - 'lib/tasks/tag_tasks.rake'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyleForMultiline.
+# SupportedStylesForMultiline: comma, consistent_comma, no_comma
+Style/TrailingCommaInArguments:
+ Exclude:
+ - 'app/controllers/bookmarks_controller.rb'
+ - 'lib/creation_notifier.rb'
+
+# Offense count: 13
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyleForMultiline.
+# SupportedStylesForMultiline: comma, consistent_comma, no_comma
+Style/TrailingCommaInArrayLiteral:
+ Exclude:
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/models/collection.rb'
+ - 'app/models/search/bookmark_search_form.rb'
+ - 'app/models/search/work_indexer.rb'
+ - 'app/models/skin.rb'
+ - 'spec/controllers/works/default_rails_actions_spec.rb'
+
+# Offense count: 12
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyleForMultiline.
+# SupportedStylesForMultiline: comma, consistent_comma, no_comma
+Style/TrailingCommaInHashLiteral:
+ Exclude:
+ - 'app/helpers/validation_helper.rb'
+ - 'app/models/search/indexer.rb'
+ - 'spec/controllers/tag_set_associations_controller_spec.rb'
+ - 'spec/models/skin_spec.rb'
+ - 'spec/models/story_parser_spec.rb'
+
+# Offense count: 2
+# Cop supports --auto-correct.
+# Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, IgnoreClassMethods, AllowedMethods.
+# AllowedMethods: to_ary, to_a, to_c, to_enum, to_h, to_hash, to_i, to_int, to_io, to_open, to_path, to_proc, to_r, to_regexp, to_str, to_s, to_sym
+Style/TrivialAccessors:
+ Exclude:
+ - 'app/models/search/query.rb'
+
+# Offense count: 8
+# Cop supports --auto-correct.
+Style/UnlessElse:
+ Exclude:
+ - 'app/controllers/challenge_signups_controller.rb'
+ - 'app/controllers/collections_controller.rb'
+ - 'app/controllers/opendoors/external_authors_controller.rb'
+ - 'app/controllers/tag_set_nominations_controller.rb'
+ - 'app/helpers/application_helper.rb'
+ - 'app/models/potential_match.rb'
+ - 'app/models/tagset_models/tag_set.rb'
+ - 'lib/pagination_list_link_renderer.rb'
+
+# Offense count: 50
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, MinSize, WordRegex.
+# SupportedStyles: percent, brackets
+Style/WordArray:
+ Exclude:
+ - 'app/controllers/tag_wranglers_controller.rb'
+ - 'app/helpers/collections_helper.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/models/character.rb'
+ - 'app/models/collection_participant.rb'
+ - 'app/models/fandom.rb'
+ - 'app/models/freeform.rb'
+ - 'app/models/relationship.rb'
+ - 'app/models/search/query_cleaner.rb'
+ - 'app/models/search/work_search_form.rb'
+ - 'app/models/tag.rb'
+ - 'app/models/work.rb'
+ - 'lib/tasks/cucumber.rake'
+ - 'spec/controllers/tag_wranglers_controller_spec.rb'
+ - 'spec/helpers/validation_helper_spec.rb'
+ - 'spec/lib/html_cleaner_spec.rb'
+ - 'spec/models/collection_spec.rb'
+ - 'spec/models/indexing/cache_master_spec.rb'
+ - 'spec/models/indexing/index_queue_spec.rb'
+ - 'spec/models/search/bookmark_search_form_spec.rb'
+ - 'spec/models/search/work_query_spec.rb'
+ - 'spec/models/search/work_search_form_spec.rb'
+
+# Offense count: 5
+# Cop supports --auto-correct.
+Style/ZeroLengthPredicate:
+ Exclude:
+ - 'app/controllers/autocomplete_controller.rb'
+ - 'app/helpers/tags_helper.rb'
+ - 'app/models/spam_report.rb'
+ - 'app/models/tag.rb'
+ - 'lib/url_helpers.rb'
diff --git a/.rubocop_todo_erb.yml b/.rubocop_todo_erb.yml
new file mode 100644
index 00000000000..86023c719f5
--- /dev/null
+++ b/.rubocop_todo_erb.yml
@@ -0,0 +1,1495 @@
+# This file is similar to .rubocop_todo.yml, but specifically for ERB files.
+# Since erb_lint does not have an option to make this file automatically,
+# it was semi-manually generated. The point is also similar to the regular todo file.
+Layout/ArgumentAlignment:
+ Exclude:
+ - 'app/views/prompts/_prompt_form_tag_options.html.erb'
+ - 'app/views/orphans/_orphan_series.html.erb'
+ - 'app/views/skins/_form.html.erb'
+ - 'app/views/collection_profile/show.html.erb'
+ - 'app/views/tags/_search_form.html.erb'
+ - 'app/views/collections/_sidebar.html.erb'
+ - 'app/views/tags/edit.html.erb'
+ - 'app/views/works/_collection_filters.html.erb'
+ - 'app/views/potential_matches/_no_potential_recipients.html.erb'
+ - 'app/views/works/edit_multiple.html.erb'
+ - 'app/views/challenge_assignments/confirm_purge.html.erb'
+ - 'app/views/works/_work_form_tags.html.erb'
+ - 'app/views/favorite_tags/_form.html.erb'
+ - 'app/views/known_issues/_known_issues_form.html.erb'
+ - 'app/views/comment_mailer/_comment_notification_footer_for_other.html.erb'
+ - 'app/views/potential_matches/show.html.erb'
+ - 'app/views/owned_tag_sets/_internal_tag_set_fields.html.erb'
+ - 'app/views/challenge_assignments/_maintainer_index.html.erb'
+ - 'app/views/chapters/manage.html.erb'
+ - 'app/views/external_authors/index.html.erb'
+ - 'app/views/users/sessions/_passwd_small.html.erb'
+ - 'app/views/admin_posts/show.html.erb'
+ - 'app/views/comments/_comment_form.html.erb'
+ - 'app/views/invite_requests/manage.html.erb'
+ - 'app/views/admin/passwords/edit.html.erb'
+ - 'app/views/prompts/_prompt_controls.html.erb'
+ - 'app/views/chapters/edit.html.erb'
+ - 'app/views/admin/admin_users/confirm_delete_user_creations.html.erb'
+ - 'app/views/series/show.html.erb'
+ - 'app/views/works/_standard_form.html.erb'
+ - 'app/views/challenge/shared/_challenge_form_delete.html.erb'
+ - 'app/views/challenge/shared/_challenge_form_confirm_delete.html.erb'
+ - 'app/views/collection_participants/_participant_form.html.erb'
+ - 'app/views/external_works/edit.html.erb'
+ - 'app/views/works/_work_form_associations_language.html.erb'
+ - 'app/views/admin/_admin_options.html.erb'
+ - 'app/views/prompts/_prompt_navigation.html.erb'
+ - 'app/views/home/first_login_help.html.erb'
+ - 'app/views/abuse_reports/new.html.erb'
+ - 'app/views/challenge_signups/_signup_controls.html.erb'
+ - 'app/views/bookmarks/_bookmark_blurb.html.erb'
+ - 'app/views/bookmarks/_filters.html.erb'
+ - 'app/views/tag_wranglers/index.html.erb'
+ - 'app/views/home/_intro_module.html.erb'
+ - 'app/views/comment_mailer/_comment_notification_footer_for_tag.html.erb'
+ - 'app/views/external_authors/_external_author_blurb.html.erb'
+ - 'app/views/owned_tag_sets/_navigation.html.erb'
+ - 'app/views/potential_matches/_assignment_with_request.html.erb'
+ - 'app/views/collections/_collection_blurb.html.erb'
+ - 'app/views/works/_filters.html.erb'
+ - 'app/views/users/edit.html.erb'
+ - 'app/views/users/change_password.html.erb'
+ - 'app/views/challenge_assignments/show.html.erb'
+ - 'app/views/challenge_signups/_signup_form.html.erb'
+ - 'app/views/bookmarks/_bookmarkable_blurb.html.erb'
+ - 'app/views/challenge_assignments/_assignment_blurb.html.erb'
+ - 'app/views/collections/_filters.html.erb'
+ - 'app/views/series/_series_module.html.erb'
+ - 'app/views/home/_news_module.html.erb'
+ - 'app/views/admin_posts/_admin_post_form.html.erb'
+ - 'app/views/invite_requests/_index_closed.html.erb'
+ - 'app/views/skins/_skin_actions.html.erb'
+ - 'app/views/tag_set_nominations/_tag_nominations_by_fandom.html.erb'
+ - 'app/views/users/passwords/edit.html.erb'
+ - 'app/views/admin/_admin_nav.html.erb'
+ - 'app/views/challenge_signups/summary.html.erb'
+ - 'app/views/user_mailer/abuse_report.html.erb'
+ - 'app/views/comments/_comment_actions.html.erb'
+ - 'app/views/user_mailer/signup_notification.html.erb'
+ - 'app/views/tag_set_nominations/show.html.erb'
+ - 'app/views/bookmarks/_external_work_fields.html.erb'
+ - 'app/views/users/change_username.html.erb'
+ - 'app/views/archive_faqs/_question_answer_fields.html.erb'
+ - 'app/views/invitations/manage.html.erb'
+ - 'app/views/users/registrations/_passwd.html.erb'
+ - 'app/views/bookmarks/_bookmark_form.html.erb'
+ - 'app/views/fandoms/unassigned.html.erb'
+ - 'app/views/admin/blacklisted_emails/index.html.erb'
+ - 'app/views/subscriptions/_form.html.erb'
+ - 'app/views/subscriptions/index.html.erb'
+ - 'app/views/admin/api/_api_key_form.html.erb'
+ - 'app/views/works/_work_header_navigation.html.erb'
+ - 'app/views/collections/_collection_form_delete.html.erb'
+ - 'app/views/chapters/preview.html.erb'
+ - 'app/views/owned_tag_sets/show_options.html.erb'
+ - 'app/views/collections/_header.html.erb'
+ - 'app/views/challenge/shared/_challenge_navigation_user.html.erb'
+ - 'app/views/potential_matches/_match_navigation.html.erb'
+ - 'app/views/comments/_single_comment.html.erb'
+ - 'app/views/bookmarks/_search_form.html.erb'
+ - 'app/views/potential_matches/_no_potential_givers.html.erb'
+ - 'app/views/invite_requests/_index_open.html.erb'
+ - 'app/views/user_mailer/delete_work_notification.html.erb'
+ - 'app/views/works/_search_form.html.erb'
+ - 'app/views/admin_posts/index.html.erb'
+ - 'app/views/works/new.html.erb'
+ - 'app/views/works/new_import.html.erb'
+
+Layout/BlockEndNewline:
+ Exclude:
+ - 'app/views/downloads/_download_preface.html.erb'
+ - 'app/views/downloads/_download_afterword.html.erb'
+ - 'app/views/potential_matches/_list_recipients_for_pseud.html.erb'
+
+Layout/ClosingParenthesisIndentation:
+ Exclude:
+ - 'app/views/users/edit.html.erb'
+ - 'app/views/home/_intro_module.html.erb'
+ - 'app/views/invite_requests/_index_open.html.erb'
+ - 'app/views/challenge_signups/_signup_form.html.erb'
+ - 'app/views/users/show.html.erb'
+ - 'app/views/users/change_username.html.erb'
+ - 'app/views/invite_requests/_index_closed.html.erb'
+ - 'app/views/home/first_login_help.html.erb'
+ - 'app/views/users/delete_preview.html.erb'
+
+Layout/CommentIndentation:
+ Exclude:
+ - 'app/views/challenge_requests/index.html.erb'
+ - 'app/views/skins/_skin_style_block.html.erb'
+ - 'app/views/challenge_claims/_unposted_claim_blurb.html.erb'
+ - 'app/views/skins/_skin_module.html.erb'
+ - 'app/views/inbox/_delete_form.html.erb'
+ - 'app/views/gifts/_gift_blurb.html.erb'
+ - 'app/views/prompts/_prompt_form_tag_options.html.erb'
+ - 'app/views/challenge_claims/_maintainer_index.html.erb'
+ - 'app/views/pseuds/_byline.html.erb'
+ - 'app/views/skins/_form.html.erb'
+ - 'app/views/inbox/_inbox_comment_contents.html.erb'
+ - 'app/views/layouts/application.html.erb'
+ - 'app/views/downloads/show.html.erb'
+ - 'app/views/works/_collection_filters.html.erb'
+ - 'app/views/tag_set_nominations/_review_individual_nom.html.erb'
+ - 'app/views/archive_faqs/_archive_faq_form.html.erb'
+ - 'app/views/bookmarks/index.html.erb'
+ - 'app/views/related_works/index.html.erb'
+ - 'app/views/favorite_tags/_form.html.erb'
+ - 'app/views/challenge/shared/_challenge_signups.html.erb'
+ - 'app/views/external_authors/_external_author_description.html.erb'
+ - 'app/views/owned_tag_sets/_show_tags_alpha_listbox.html.erb'
+ - 'app/views/admin/admin_users/_user_form.html.erb'
+ - 'app/views/owned_tag_sets/_internal_tag_set_fields.html.erb'
+ - 'app/views/users/sessions/_passwd_small.html.erb'
+ - 'app/views/collectibles/_collectible_form.html.erb'
+ - 'app/views/bookmarks/_bookmarks.html.erb'
+ - 'app/views/comments/_comment_form.html.erb'
+ - 'app/views/inbox/show.html.erb'
+ - 'app/views/prompts/_prompt_form.html.erb'
+ - 'app/views/admin_posts/_admin_post.html.erb'
+ - 'app/views/challenge_assignments/_maintainer_index_unfulfilled.html.erb'
+ - 'app/views/collection_items/_collection_item_form.html.erb'
+ - 'app/views/owned_tag_sets/_tag_set_associations_remove.html.erb'
+ - 'app/views/prompts/index.html.erb'
+ - 'app/views/prompts/_prompt_controls.html.erb'
+ - 'app/views/potential_matches/_list_recipients_for_pseud.html.erb'
+ - 'app/views/bookmarks/_bookmark_item_module.html.erb'
+ - 'app/views/inbox/_approve_button.html.erb'
+ - 'app/views/skins/_revert_skin_form.html.erb'
+ - 'app/views/prompts/show.html.erb'
+ - 'app/views/works/_work_abbreviated_list.html.erb'
+ - 'app/views/stats/index.html.erb'
+ - 'app/views/challenge/gift_exchange/_challenge_signups_summary.html.erb'
+ - 'app/views/works/_work_form_associations_language.html.erb'
+ - 'app/views/home/first_login_help.html.erb'
+ - 'app/views/owned_tag_sets/_show_tag_set_tags.html.erb'
+ - 'app/views/challenge_signups/_signup_controls.html.erb'
+ - 'app/views/bookmarks/_bookmark_blurb.html.erb'
+ - 'app/views/bookmarks/_filters.html.erb'
+ - 'app/views/collection_items/_item_fields.html.erb'
+ - 'app/views/external_authors/_external_author_blurb.html.erb'
+ - 'app/views/kudos/_kudos.html.erb'
+ - 'app/views/potential_matches/_assignment_with_request.html.erb'
+ - 'app/views/skins/_header.html.erb'
+ - 'app/views/challenge_signups/show.html.erb'
+ - 'app/views/inbox/_read_form.html.erb'
+ - 'app/views/tag_set_nominations/_review_fandoms.html.erb'
+ - 'app/views/skins/_skin_navigation.html.erb'
+ - 'app/views/collections/_collection_blurb.html.erb'
+ - 'app/views/works/_filters.html.erb'
+ - 'app/views/home/_tos.html.erb'
+ - 'app/views/owned_tag_sets/_tag_set_form.html.erb'
+ - 'app/views/challenge_assignments/show.html.erb'
+ - 'app/views/challenge_signups/_signup_form.html.erb'
+ - 'app/views/user_mailer/_work_info.html.erb'
+ - 'app/views/home/_inbox_module.html.erb'
+ - 'app/views/potential_matches/_assignments.html.erb'
+ - 'app/views/bookmarks/_bookmarkable_blurb.html.erb'
+ - 'app/views/works/_work_form_pseuds.html.erb'
+ - 'app/views/works/_work_module.html.erb'
+ - 'app/views/users/confirmation.html.erb'
+ - 'app/views/share/_share.html.erb'
+ - 'app/views/inbox/_reply_button.html.erb'
+ - 'app/views/invite_requests/index.html.erb'
+ - 'app/views/challenge_assignments/_assignment_blurb.html.erb'
+ - 'app/views/collections/_filters.html.erb'
+ - 'app/views/skins/_update_skin_form.html.erb'
+ - 'app/views/owned_tag_sets/_tag_set_form_management.html.erb'
+ - 'app/views/bookmarks/_bookmark_blurb_short.html.erb'
+ - 'app/views/challenge/shared/_challenge_requests.html.erb'
+ - 'app/views/series/_series_module.html.erb'
+ - 'app/views/home/_news_module.html.erb'
+ - 'app/views/skins/_skin_actions.html.erb'
+ - 'app/views/challenge_signups/index.html.erb'
+ - 'app/views/tag_set_nominations/_tag_nominations_by_fandom.html.erb'
+ - 'app/views/challenge_assignments/_maintainer_index_fulfilled.html.erb'
+ - 'app/views/challenge_signups/summary.html.erb'
+ - 'app/views/user_mailer/abuse_report.html.erb'
+ - 'app/views/comments/_comment_abbreviated_list.html.erb'
+ - 'app/views/comments/_comment_actions.html.erb'
+ - 'app/views/tag_set_nominations/show.html.erb'
+ - 'app/views/owned_tag_sets/_show_tags_in_single_list.html.erb'
+ - 'app/views/tag_set_associations/index.html.erb'
+ - 'app/views/archive_faqs/_question_answer_fields.html.erb'
+ - 'app/views/prompts/_prompt_blurb.html.erb'
+ - 'app/views/bookmarks/_bookmark_form.html.erb'
+ - 'app/views/admin/banners/_banner.html.erb'
+ - 'app/views/owned_tag_sets/_show_fandoms_by_media.html.erb'
+ - 'app/views/fandoms/unassigned.html.erb'
+ - 'app/views/challenge/prompt_meme/_challenge_sidebar.html.erb'
+ - 'app/views/subscriptions/_form.html.erb'
+ - 'app/views/collections/show.html.erb'
+ - 'app/views/works/_work_header_navigation.html.erb'
+ - 'app/views/readings/_reading_blurb.html.erb'
+ - 'app/views/external_works/_work_module.html.erb'
+ - 'app/views/layouts/_includes.html.erb'
+ - 'app/views/bookmarks/_bookmark_user_module.html.erb'
+ - 'app/views/works/index.html.erb'
+ - 'app/views/owned_tag_sets/show_options.html.erb'
+ - 'app/views/layouts/_banner.html.erb'
+ - 'app/views/tag_set_nominations/_nomination_form.html.erb'
+ - 'app/views/challenge/shared/_challenge_navigation_user.html.erb'
+ - 'app/views/archive_faqs/show.html.erb'
+ - 'app/views/comments/_single_comment.html.erb'
+ - 'app/views/potential_matches/index.html.erb'
+ - 'app/views/layouts/_javascripts.html.erb'
+ - 'app/views/users/_header_navigation.html.erb'
+ - 'app/views/owned_tag_sets/_show_tags_by_alpha.html.erb'
+ - 'app/views/works/collected.html.erb'
+ - 'app/views/user_invite_requests/index.html.erb'
+ - 'app/views/layouts/_tos_prompt.html.erb'
+ - 'app/views/layouts/_proxy_notice.html.erb'
+ - 'app/views/owned_tag_sets/wrangle.html.erb'
+ - 'app/views/works/_work_blurb.html.erb'
+ - 'app/views/tag_set_nominations/_review_cast.html.erb'
+ - 'app/views/bookmarks/_bookmark_owner_navigation.html.erb'
+ - 'app/views/challenge_assignments/_maintainer_index_defaulted.html.erb'
+
+Layout/DotPosition:
+ Exclude:
+ - 'app/views/series/_series_module.html.erb'
+ - 'app/views/owned_tag_sets/_show_tag_set_associations.html.erb'
+
+Layout/EmptyComment:
+ Exclude:
+ - 'app/views/challenge/shared/_challenge_signups.html.erb'
+
+Layout/ExtraSpacing:
+ Exclude:
+ - 'app/views/potential_matches/_no_potential_recipients.html.erb'
+ - 'app/views/potential_matches/_no_potential_givers.html.erb'
+ - 'app/views/prompts/_prompt_form_tag_options.html.erb'
+ - 'app/views/collection_profile/show.html.erb'
+ - 'app/views/comment_mailer/_comment_notification_footer_for_other.html.erb'
+ - 'app/views/works/_work_module.html.erb'
+ - 'app/views/admin/admin_invitations/index.html.erb'
+ - 'app/views/users/registrations/_legal.html.erb'
+ - 'app/views/prompt_restrictions/_prompt_restriction_form.html.erb'
+ - 'app/views/works/show_multiple.html.erb'
+
+Layout/FirstHashElementIndentation:
+ Exclude:
+ - 'app/views/comments/new.html.erb'
+ - 'app/views/admin/_admin_options.html.erb'
+ - 'app/views/prompts/_prompt_form_tag_options.html.erb'
+ - 'app/views/tag_set_nominations/_tag_nominations_by_fandom.html.erb'
+
+Layout/HashAlignment:
+ Exclude:
+ - 'app/views/home/first_login_help.html.erb'
+
+Layout/LeadingCommentSpace:
+ Exclude:
+ - 'app/views/potential_matches/_list_recipients_for_pseud.html.erb'
+ - 'app/views/archive_faqs/show.html.erb'
+
+Layout/LeadingEmptyLines:
+ Exclude:
+ - 'app/views/owned_tag_sets/_tag_set_form.html.erb'
+ - 'app/views/collection_items/_collection_item_form.html.erb'
+ - 'app/views/tags/wrangle.html.erb'
+ - 'app/views/owned_tag_sets/wrangle.html.erb'
+ - 'app/views/prompts/index.html.erb'
+ - 'app/views/kudo_mailer/batch_kudo_notification.html.erb'
+ - 'app/views/pseuds/_byline.html.erb'
+ - 'app/views/admin_posts/_admin_post_form.html.erb'
+ - 'app/views/challenge/shared/_challenge_signups.html.erb'
+ - 'app/views/owned_tag_sets/_show_tags_alpha_listbox.html.erb'
+ - 'app/views/admin/skins/index_approved.html.erb'
+ - 'app/views/bookmarks/_bookmark_form.html.erb'
+ - 'app/views/works/_standard_form.html.erb'
+ - 'app/views/owned_tag_sets/_show_tag_set_tags.html.erb'
+ - 'app/views/owned_tag_sets/_show_fandoms_by_media.html.erb'
+ - 'app/views/works/edit_multiple.html.erb'
+
+Layout/MultilineBlockLayout:
+ Exclude:
+ - 'app/views/downloads/_download_preface.html.erb'
+
+Layout/MultilineHashBraceLayout:
+ Exclude:
+ - 'app/views/collections/_filters.html.erb'
+
+Layout/MultilineMethodCallBraceLayout:
+ Exclude:
+ - 'app/views/users/edit.html.erb'
+ - 'app/views/layouts/_tos_prompt.html.erb'
+ - 'app/views/bookmarks/_filters.html.erb'
+ - 'app/views/home/_intro_module.html.erb'
+ - 'app/views/downloads/_download_preface.html.erb'
+ - 'app/views/invite_requests/_index_open.html.erb'
+ - 'app/views/challenge_signups/_signup_form.html.erb'
+ - 'app/views/users/show.html.erb'
+ - 'app/views/users/change_username.html.erb'
+ - 'app/views/invite_requests/_index_closed.html.erb'
+ - 'app/views/comment_mailer/_comment_notification_footer_for_other.html.erb'
+ - 'app/views/home/first_login_help.html.erb'
+ - 'app/views/users/delete_preview.html.erb'
+ - 'app/views/works/_filters.html.erb'
+
+Layout/MultilineMethodCallIndentation:
+ Exclude:
+ - 'app/views/series/_series_module.html.erb'
+ - 'app/views/owned_tag_sets/_show_tag_set_associations.html.erb'
+
+Layout/MultilineOperationIndentation:
+ Exclude:
+ - 'app/views/bookmarks/_filters.html.erb'
+
+Layout/SpaceAfterColon:
+ Exclude:
+ - 'app/views/home/first_login_help.html.erb'
+
+Layout/SpaceAfterComma:
+ Exclude:
+ - 'app/views/potential_match_settings/_potential_match_settings_form.html.erb'
+ - 'app/views/owned_tag_sets/_show_tag_set_tags.html.erb'
+ - 'app/views/home/site_pages.html.erb'
+
+Layout/SpaceAroundOperators:
+ Exclude:
+ - 'app/views/layouts/home.html.erb'
+ - 'app/views/comments/_comment_thread.html.erb'
+ - 'app/views/works/_work_module.html.erb'
+ - 'app/views/challenge/shared/_challenge_meta.html.erb'
+ - 'app/views/prompts/_prompt_form_tag_options.html.erb'
+ - 'app/views/collection_profile/show.html.erb'
+ - 'app/views/admin_posts/_admin_post_form.html.erb'
+ - 'app/views/layouts/application.html.erb'
+ - 'app/views/admin/admin_invitations/find.html.erb'
+ - 'app/views/tag_set_nominations/_tag_nominations_by_fandom.html.erb'
+ - 'app/views/users/_sidebar.html.erb'
+ - 'app/views/owned_tag_sets/_tag_set_association_fields.html.erb'
+ - 'app/views/home/site_map.html.erb'
+ - 'app/views/comment_mailer/_comment_notification_footer_for_other.html.erb'
+ - 'app/views/layouts/session.html.erb'
+ - 'app/views/archive_faqs/_question_answer_fields.html.erb'
+ - 'app/views/invitations/manage.html.erb'
+ - 'app/views/prompts/_prompt_blurb.html.erb'
+ - 'app/views/prompts/_prompt_form.html.erb'
+ - 'app/views/owned_tag_sets/_show_fandoms_by_media.html.erb'
+
+Layout/SpaceBeforeBlockBraces:
+ Exclude:
+ - 'app/views/fandoms/unassigned.html.erb'
+ - 'app/views/fandoms/index.html.erb'
+ - 'app/views/downloads/_download_preface.html.erb'
+ - 'app/views/series/_series_module.html.erb'
+ - 'app/views/downloads/_download_afterword.html.erb'
+ - 'app/views/user_mailer/batch_subscription_notification.html.erb'
+ - 'app/views/collection_mailer/item_added_notification.html.erb'
+ - 'app/views/user_mailer/prompter_notification.html.erb'
+ - 'app/views/user_mailer/related_work_notification.html.erb'
+ - 'app/views/prompts/_prompt_blurb.html.erb'
+ - 'app/views/works/_work_header_navigation.html.erb'
+ - 'app/views/external_works/_work_module.html.erb'
+ - 'app/views/bookmarks/_bookmark_form.html.erb'
+ - 'app/views/external_works/_blurb.html.erb'
+ - 'app/views/preferences/index.html.erb'
+ - 'app/views/works/_work_module.html.erb'
+
+Layout/SpaceBeforeComma:
+ Exclude:
+ - 'app/views/works/_work_header_navigation.html.erb'
+ - 'app/views/challenge/shared/_challenge_form_schedule.html.erb'
+ - 'app/views/prompts/_prompt_form.html.erb'
+ - 'app/views/home/tos_faq.html.erb'
+
+
+Layout/SpaceBeforeFirstArg:
+ Exclude:
+ - 'app/views/admin/admin_invitations/index.html.erb'
+
+Layout/SpaceInsideArrayLiteralBrackets:
+ Exclude:
+ - 'app/views/locales/_locale_form.html.erb'
+
+Layout/SpaceInsideBlockBraces:
+ Exclude:
+ - 'app/views/fandoms/unassigned.html.erb'
+ - 'app/views/owned_tag_sets/_tag_set_form.html.erb'
+ - 'app/views/challenge_signups/_signup_form.html.erb'
+ - 'app/views/downloads/_download_afterword.html.erb'
+ - 'app/views/user_mailer/batch_subscription_notification.html.erb'
+ - 'app/views/collection_mailer/item_added_notification.html.erb'
+ - 'app/views/works/_work_header_navigation.html.erb'
+ - 'app/views/works/_work_module.html.erb'
+ - 'app/views/prompts/_prompt_form_tag_options.html.erb'
+ - 'app/views/tag_set_nominations/_nomination_form.html.erb'
+ - 'app/views/home/site_pages.html.erb'
+ - 'app/views/collection_profile/show.html.erb'
+ - 'app/views/works/_work_abbreviated_list.html.erb'
+ - 'app/views/owned_tag_sets/_show_tag_set_associations.html.erb'
+ - 'app/views/fandoms/index.html.erb'
+ - 'app/views/owned_tag_sets/_show_tags_alpha_listbox.html.erb'
+ - 'app/views/user_mailer/related_work_notification.html.erb'
+ - 'app/views/preferences/index.html.erb'
+ - 'app/views/owned_tag_sets/_show_tag_set_tags.html.erb'
+ - 'app/views/invitations/_user_invitations.html.erb'
+ - 'app/views/downloads/_download_preface.html.erb'
+ - 'app/views/potential_match_settings/_potential_match_settings_form.html.erb'
+ - 'app/views/user_mailer/prompter_notification.html.erb'
+ - 'app/views/owned_tag_sets/_show_fandoms_by_media.html.erb'
+ - 'app/views/prompts/_prompt_blurb.html.erb'
+ - 'app/views/owned_tag_sets/_tag_set_blurb.html.erb'
+ - 'app/views/bookmarks/_bookmark_form.html.erb'
+ - 'app/views/collections/_collection_blurb.html.erb'
+ - 'app/views/owned_tag_sets/show.html.erb'
+
+Layout/SpaceInsideHashLiteralBraces:
+ Exclude:
+ - 'app/views/challenge_claims/_unposted_claim_blurb.html.erb'
+ - 'app/views/archive_faqs/_admin_index.html.erb'
+ - 'app/views/comment_mailer/comment_sent_notification.html.erb'
+ - 'app/views/orphans/index.html.erb'
+ - 'app/views/collection_profile/show.html.erb'
+ - 'app/views/tags/edit.html.erb'
+ - 'app/views/archive_faqs/_archive_faq_form.html.erb'
+ - 'app/views/potential_matches/_no_potential_recipients.html.erb'
+ - 'app/views/comments/edit.html.erb'
+ - 'app/views/collections/_challenge_collections.html.erb'
+ - 'app/views/invitations/_user_invitations.html.erb'
+ - 'app/views/admin/banners/index.html.erb'
+ - 'app/views/potential_matches/show.html.erb'
+ - 'app/views/chapters/manage.html.erb'
+ - 'app/views/comment_mailer/edited_comment_notification.html.erb'
+ - 'app/views/comments/_comment_form.html.erb'
+ - 'app/views/prompts/new.html.erb'
+ - 'app/views/users/registrations/new.html.erb'
+ - 'app/views/collection_items/_collection_item_form.html.erb'
+ - 'app/views/series/index.html.erb'
+ - 'app/views/collections/_form.html.erb'
+ - 'app/views/admin/skins/index_rejected.html.erb'
+ - 'app/views/collections/confirm_delete.html.erb'
+ - 'app/views/admin/skins/index_approved.html.erb'
+ - 'app/views/pseuds/index.html.erb'
+ - 'app/views/challenge/prompt_meme/_prompt_meme_form.html.erb'
+ - 'app/views/chapters/edit.html.erb'
+ - 'app/views/series/_series_navigation.html.erb'
+ - 'app/views/admin/skins/index.html.erb'
+ - 'app/views/stats/index.html.erb'
+ - 'app/views/users/_contents.html.erb'
+ - 'app/views/challenge_signups/confirm_delete.html.erb'
+ - 'app/views/archive_faqs/confirm_delete.html.erb'
+ - 'app/views/people/search.html.erb'
+ - 'app/views/bookmarks/confirm_delete.html.erb'
+ - 'app/views/prompts/_prompt_navigation.html.erb'
+ - 'app/views/admin_mailer/edited_comment_notification.html.erb'
+ - 'app/views/challenge_signups/_signup_controls.html.erb'
+ - 'app/views/challenge/gift_exchange/_gift_exchange_form.html.erb'
+ - 'app/views/pseuds/_pseud_blurb.html.erb'
+ - 'app/views/tag_wranglers/index.html.erb'
+ - 'app/views/owned_tag_sets/_navigation.html.erb'
+ - 'app/views/tags/show.html.erb'
+ - 'app/views/comment_mailer/comment_reply_notification.html.erb'
+ - 'app/views/collections/_collection_blurb.html.erb'
+ - 'app/views/owned_tag_sets/_tag_set_form.html.erb'
+ - 'app/views/comment_mailer/comment_notification.html.erb'
+ - 'app/views/challenge_assignments/show.html.erb'
+ - 'app/views/challenge_signups/_signup_form.html.erb'
+ - 'app/views/chapters/confirm_delete.html.erb'
+ - 'app/views/profile/show.html.erb'
+ - 'app/views/tag_set_nominations/confirm_destroy_multiple.html.erb'
+ - 'app/views/tag_wranglings/index.html.erb'
+ - 'app/views/challenge_assignments/_assignment_blurb.html.erb'
+ - 'app/views/bookmarks/_bookmark_blurb_short.html.erb'
+ - 'app/views/admin/admin_invitations/find.html.erb'
+ - 'app/views/challenge_signups/summary.html.erb'
+ - 'app/views/tag_set_nominations/confirm_delete.html.erb'
+ - 'app/views/tag_set_nominations/show.html.erb'
+ - 'app/views/owned_tag_sets/confirm_delete.html.erb'
+ - 'app/views/external_authors/_external_author_form.html.erb'
+ - 'app/views/admin_mailer/comment_notification.html.erb'
+ - 'app/views/invitations/show.html.erb'
+ - 'app/views/invitations/manage.html.erb'
+ - 'app/views/admin/banners/_navigation.html.erb'
+ - 'app/views/bookmarks/_bookmark_form.html.erb'
+ - 'app/views/fandoms/unassigned.html.erb'
+ - 'app/views/known_issues/_admin_index.html.erb'
+ - 'app/views/subscriptions/index.html.erb'
+ - 'app/views/works/_work_header_navigation.html.erb'
+ - 'app/views/collections/_collection_form_delete.html.erb'
+ - 'app/views/feedbacks/new.html.erb'
+ - 'app/views/locales/index.html.erb'
+ - 'app/views/tag_set_nominations/_nomination_form.html.erb'
+ - 'app/views/pseuds/delete_preview.html.erb'
+ - 'app/views/challenge/shared/_challenge_navigation_user.html.erb'
+ - 'app/views/prompts/edit.html.erb'
+ - 'app/views/readings/index.html.erb'
+ - 'app/views/potential_matches/_match_navigation.html.erb'
+ - 'app/views/admin/banners/confirm_delete.html.erb'
+ - 'app/views/admin_posts/_admin_index.html.erb'
+ - 'app/views/potential_matches/_no_potential_givers.html.erb'
+ - 'app/views/tags/wrangle.html.erb'
+ - 'app/views/home/site_map.html.erb'
+ - 'app/views/user_invite_requests/index.html.erb'
+ - 'app/views/works/_search_box.html.erb'
+ - 'app/views/series/confirm_delete.html.erb'
+ - 'app/views/comment_mailer/edited_comment_reply_notification.html.erb'
+ - 'app/views/works/_work_blurb.html.erb'
+ - 'app/views/owned_tag_sets/batch_load.html.erb'
+ - 'app/views/bookmarks/_bookmark_owner_navigation.html.erb'
+ - 'app/views/languages/show.html.erb'
+ - 'app/views/works/confirm_delete.html.erb'
+ - 'app/views/works/edit.html.erb'
+ - 'app/views/works/edit_multiple.html.erb'
+ - 'app/views/works/new.html.erb'
+ - 'app/views/works/search_results.html.erb'
+ - 'app/views/wrangling_guidelines/_admin_index.html.erb'
+
+Layout/SpaceInsideParens:
+ Exclude:
+ - 'app/views/layouts/_header.html.erb'
+ - 'app/views/admin/sessions/new.html.erb'
+ - 'app/views/prompts/_prompt_form_tag_options.html.erb'
+ - 'app/views/skins/_form.html.erb'
+ - 'app/views/external_authors/index.html.erb'
+ - 'app/views/tags/edit.html.erb'
+ - 'app/views/works/new.html.erb'
+ - 'app/views/works/new_import.html.erb'
+
+Layout/SpaceInsideStringInterpolation:
+ Exclude:
+ - 'app/views/works/new_import.html.erb'
+
+Layout/SingleLineBlockChain:
+ Exclude:
+ - 'app/views/user_mailer/batch_subscription_notification.html.erb'
+ - 'app/views/collection_mailer/item_added_notification.html.erb'
+ - 'app/views/kudo_mailer/batch_kudo_notification.html.erb'
+ - 'app/views/external_works/_work_module.html.erb'
+ - 'app/views/works/_work_module.html.erb'
+ - 'app/views/home/site_pages.html.erb'
+ - 'app/views/collection_profile/show.html.erb'
+ - 'app/views/works/_work_abbreviated_list.html.erb'
+ - 'app/views/external_works/_blurb.html.erb'
+ - 'app/views/share/_embed_meta.html.erb'
+ - 'app/views/owned_tag_sets/_show_tag_set_associations.html.erb'
+ - 'app/views/share/_embed_link_header.html.erb'
+ - 'app/views/user_mailer/challenge_assignment_notification.html.erb'
+ - 'app/views/owned_tag_sets/_show_tags_alpha_listbox.html.erb'
+ - 'app/views/kudos/index.html.erb'
+ - 'app/views/user_mailer/related_work_notification.html.erb'
+ - 'app/views/owned_tag_sets/_show_tag_set_tags.html.erb'
+ - 'app/views/invitations/_user_invitations.html.erb'
+ - 'app/views/downloads/_download_preface.html.erb'
+ - 'app/views/potential_match_settings/_potential_match_settings_form.html.erb'
+ - 'app/views/user_mailer/prompter_notification.html.erb'
+ - 'app/views/owned_tag_sets/_show_fandoms_by_media.html.erb'
+ - 'app/views/prompts/_prompt_blurb.html.erb'
+ - 'app/views/owned_tag_sets/_tag_set_blurb.html.erb'
+ - 'app/views/collections/_collection_blurb.html.erb'
+ - 'app/views/owned_tag_sets/show.html.erb'
+
+Lint/AmbiguousBlockAssociation:
+ Exclude:
+ - 'app/views/tag_set_nominations/_nomination_form.html.erb'
+ - 'app/views/preferences/index.html.erb'
+
+Lint/ParenthesesAsGroupedExpression:
+ Exclude:
+ - 'app/views/home/dmca.html.erb'
+ - 'app/views/downloads/_download_chapter.html.erb'
+ - 'app/views/works/confirm_delete.html.erb'
+
+Lint/RedundantStringCoercion:
+ Exclude:
+ - 'app/views/prompts/_prompt_blurb.html.erb'
+
+Lint/TopLevelReturnWithArgument:
+ Exclude:
+ - 'app/views/user_mailer/challenge_assignment_notification.html.erb'
+
+Lint/UnusedBlockArgument:
+ Exclude:
+ - 'app/views/potential_match_settings/_potential_match_settings_form.html.erb'
+
+Naming/VariableNumber:
+ Exclude:
+ - 'app/views/users/registrations/_legal.html.erb'
+
+Rails/Blank:
+ Exclude:
+ - 'app/views/user_mailer/challenge_assignment_notification.html.erb'
+ - 'app/views/challenge_signups/_signup_form.html.erb'
+
+Rails/LinkToBlank:
+ Exclude:
+ - 'app/views/prompts/_prompt_form_tag_options.html.erb'
+ - 'app/views/users/registrations/_legal.html.erb'
+
+Rails/NegateInclude:
+ Exclude:
+ - 'app/views/inbox/show.html.erb'
+
+Rails/Presence:
+ Exclude:
+ - 'app/views/comments/_single_comment.html.erb'
+ - 'app/views/prompts/_prompt_blurb.html.erb'
+ - 'app/views/prompts/_prompt_form.html.erb'
+ - 'app/views/invite_requests/manage.html.erb'
+
+Rails/Present:
+ Exclude:
+ - 'app/views/profile/show.html.erb'
+ - 'app/views/works/_notes_form.html.erb'
+
+Rails/TimeZone:
+ Exclude:
+ - 'app/views/user_mailer/collection_notification.html.erb'
+ - 'app/views/challenge/gift_exchange/_challenge_signups_summary.html.erb'
+ - 'app/views/prompts/_prompt_form_tag_options.html.erb'
+ - 'app/views/user_mailer/challenge_assignment_notification.html.erb'
+ - 'app/views/user_mailer/abuse_report.html.erb'
+ - 'app/views/user_mailer/potential_match_generation_notification.html.erb'
+ - 'app/views/user_mailer/feedback.html.erb'
+ - 'app/views/user_mailer/invalid_signup_notification.html.erb'
+
+Style/BlockDelimiters:
+ Exclude:
+ - 'app/views/downloads/_download_preface.html.erb'
+ - 'app/views/downloads/_download_afterword.html.erb'
+ - 'app/views/potential_matches/_list_recipients_for_pseud.html.erb'
+
+Style/ClassEqualityComparison:
+ Exclude:
+ - 'app/views/tags/edit.html.erb'
+
+Style/CommentAnnotation:
+ Exclude:
+ - 'app/views/prompts/_prompt_form.html.erb'
+
+Style/ConditionalAssignment:
+ Exclude:
+ - 'app/views/archive_faqs/_faq_index.html.erb'
+ - 'app/views/stats/index.html.erb'
+ - 'app/views/works/_work_header_navigation.html.erb'
+
+Style/HashAsLastArrayItem:
+ Exclude:
+ - 'app/views/works/_work_header_navigation.html.erb'
+
+Style/HashExcept:
+ Exclude:
+ - 'app/views/potential_match_settings/_potential_match_settings_form.html.erb'
+
+Style/HashSyntax:
+ Exclude:
+ - 'app/views/user_mailer/invitation_to_claim.html.erb'
+ - 'app/views/bookmarks/show.html.erb'
+ - 'app/views/challenge/shared/_challenge_form_schedule.html.erb'
+ - 'app/views/challenge_claims/_unposted_claim_blurb.html.erb'
+ - 'app/views/archive_faqs/_filters.html.erb'
+ - 'app/views/downloads/_download_afterword.html.erb'
+ - 'app/views/challenge_assignments/_user_index.html.erb'
+ - 'app/views/archive_faqs/_admin_index.html.erb'
+ - 'app/views/external_authors/_external_author_navigation.html.erb'
+ - 'app/views/comment_mailer/comment_sent_notification.html.erb'
+ - 'app/views/prompts/_prompt_form_tag_options.html.erb'
+ - 'app/views/archive_faqs/manage.html.erb'
+ - 'app/views/challenge_claims/_maintainer_index.html.erb'
+ - 'app/views/orphans/_orphan_series.html.erb'
+ - 'app/views/orphans/index.html.erb'
+ - 'app/views/collection_profile/show.html.erb'
+ - 'app/views/layouts/application.html.erb'
+ - 'app/views/skins/_form.html.erb'
+ - 'app/views/downloads/show.html.erb'
+ - 'app/views/challenge/gift_exchange/new.html.erb'
+ - 'app/views/collections/_sidebar.html.erb'
+ - 'app/views/collections/list_pm_challenges.html.erb'
+ - 'app/views/tag_set_nominations/_review_individual_nom.html.erb'
+ - 'app/views/archive_faqs/_archive_faq_form.html.erb'
+ - 'app/views/bookmarks/index.html.erb'
+ - 'app/views/potential_matches/_no_potential_recipients.html.erb'
+ - 'app/views/comments/edit.html.erb'
+ - 'app/views/related_works/index.html.erb'
+ - 'app/views/challenge_signups/edit.html.erb'
+ - 'app/views/collections/_challenge_collections.html.erb'
+ - 'app/views/works/_work_form_tags.html.erb'
+ - 'app/views/known_issues/_known_issues_form.html.erb'
+ - 'app/views/invitations/index.html.erb'
+ - 'app/views/archive_faqs/_faq_index.html.erb'
+ - 'app/views/challenge/shared/_challenge_signups.html.erb'
+ - 'app/views/comment_mailer/_comment_notification_footer_for_other.html.erb'
+ - 'app/views/locales/new.html.erb'
+ - 'app/views/invitations/_user_invitations.html.erb'
+ - 'app/views/collections/_works_module.html.erb'
+ - 'app/views/user_invite_requests/new.html.erb'
+ - 'app/views/potential_matches/show.html.erb'
+ - 'app/views/owned_tag_sets/_internal_tag_set_fields.html.erb'
+ - 'app/views/pseuds/show.html.erb'
+ - 'app/views/opendoors/external_authors/index.html.erb'
+ - 'app/views/chapters/manage.html.erb'
+ - 'app/views/comment_mailer/edited_comment_notification.html.erb'
+ - 'app/views/collectibles/_collectible_form.html.erb'
+ - 'app/views/external_authors/index.html.erb'
+ - 'app/views/gifts/_gift_search.html.erb'
+ - 'app/views/related_works/show.html.erb'
+ - 'app/views/works/_work_approved_children.html.erb'
+ - 'app/views/bookmarks/_bookmarks.html.erb'
+ - 'app/views/comments/_comment_form.html.erb'
+ - 'app/views/prompts/_prompt_form.html.erb'
+ - 'app/views/prompts/new.html.erb'
+ - 'app/views/series/_series_blurb.html.erb'
+ - 'app/views/series/edit.html.erb'
+ - 'app/views/users/registrations/new.html.erb'
+ - 'app/views/pseuds/edit.html.erb'
+ - 'app/views/challenge_assignments/_maintainer_index_unfulfilled.html.erb'
+ - 'app/views/collection_items/_collection_item_form.html.erb'
+ - 'app/views/collections/_form.html.erb'
+ - 'app/views/series/index.html.erb'
+ - 'app/views/admin/skins/index_rejected.html.erb'
+ - 'app/views/owned_tag_sets/_tag_set_associations_remove.html.erb'
+ - 'app/views/admin_posts/_filters.html.erb'
+ - 'app/views/challenge_signups/_signup_form_general_information.html.erb'
+ - 'app/views/external_works/new.html.erb'
+ - 'app/views/prompts/_prompt_controls.html.erb'
+ - 'app/views/collections/confirm_delete.html.erb'
+ - 'app/views/admin_posts/edit.html.erb'
+ - 'app/views/admin/skins/index_approved.html.erb'
+ - 'app/views/bookmarks/edit.html.erb'
+ - 'app/views/fandoms/show.html.erb'
+ - 'app/views/home/tos_faq.html.erb'
+ - 'app/views/users/registrations/_legal.html.erb'
+ - 'app/views/prompts/show.html.erb'
+ - 'app/views/locales/_navigation.html.erb'
+ - 'app/views/pseuds/index.html.erb'
+ - 'app/views/challenge/prompt_meme/_prompt_meme_form.html.erb'
+ - 'app/views/chapters/new.html.erb'
+ - 'app/views/admin/admin_users/confirm_delete_user_creations.html.erb'
+ - 'app/views/admin/skins/index.html.erb'
+ - 'app/views/layouts/_footer.html.erb'
+ - 'app/views/series/_series_navigation.html.erb'
+ - 'app/views/stats/index.html.erb'
+ - 'app/views/users/_contents.html.erb'
+ - 'app/views/challenge_signups/confirm_delete.html.erb'
+ - 'app/views/archive_faqs/confirm_delete.html.erb'
+ - 'app/views/works/_standard_form.html.erb'
+ - 'app/views/tags/search.html.erb'
+ - 'app/views/users/_sidebar.html.erb'
+ - 'app/views/challenge/gift_exchange/_challenge_signups_summary.html.erb'
+ - 'app/views/challenge/shared/_challenge_form_confirm_delete.html.erb'
+ - 'app/views/people/search.html.erb'
+ - 'app/views/bookmarks/confirm_delete.html.erb'
+ - 'app/views/prompts/_prompt_navigation.html.erb'
+ - 'app/views/owned_tag_sets/_show_tag_set_tags.html.erb'
+ - 'app/views/admin_mailer/edited_comment_notification.html.erb'
+ - 'app/views/pseuds/_pseud_blurb.html.erb'
+ - 'app/views/challenge/gift_exchange/_gift_exchange_form.html.erb'
+ - 'app/views/collections/_bookmarks_module.html.erb'
+ - 'app/views/invitations/_invitation.html.erb'
+ - 'app/views/opendoors/external_authors/show.html.erb'
+ - 'app/views/tag_wranglers/index.html.erb'
+ - 'app/views/tags/index.html.erb'
+ - 'app/views/external_authors/_external_author_blurb.html.erb'
+ - 'app/views/owned_tag_sets/_navigation.html.erb'
+ - 'app/views/potential_match_settings/_potential_match_settings_form.html.erb'
+ - 'app/views/challenge/gift_exchange/_challenge_navigation_user.html.erb'
+ - 'app/views/tags/show.html.erb'
+ - 'app/views/challenge_signups/show.html.erb'
+ - 'app/views/comment_mailer/comment_reply_notification.html.erb'
+ - 'app/views/tag_set_nominations/_review_fandoms.html.erb'
+ - 'app/views/collections/_collection_blurb.html.erb'
+ - 'app/views/owned_tag_sets/show.html.erb'
+ - 'app/views/owned_tag_sets/_tag_set_form.html.erb'
+ - 'app/views/comment_mailer/comment_notification.html.erb'
+ - 'app/views/admin/skins/_navigation.html.erb'
+ - 'app/views/archive_faqs/edit.html.erb'
+ - 'app/views/orphans/_orphan_work.html.erb'
+ - 'app/views/challenge_assignments/show.html.erb'
+ - 'app/views/challenge/shared/_challenge_form_instructions.html.erb'
+ - 'app/views/challenge_signups/_signup_form.html.erb'
+ - 'app/views/chapters/confirm_delete.html.erb'
+ - 'app/views/locales/edit.html.erb'
+ - 'app/views/pseuds/new.html.erb'
+ - 'app/views/potential_matches/_assignments.html.erb'
+ - 'app/views/known_issues/index.html.erb'
+ - 'app/views/profile/show.html.erb'
+ - 'app/views/tag_set_nominations/confirm_destroy_multiple.html.erb'
+ - 'app/views/admin/admin_invitations/index.html.erb'
+ - 'app/views/challenge/prompt_meme/_challenge_navigation_user.html.erb'
+ - 'app/views/comments/unreviewed.html.erb'
+ - 'app/views/known_issues/new.html.erb'
+ - 'app/views/tag_set_nominations/index.html.erb'
+ - 'app/views/works/_work_module.html.erb'
+ - 'app/views/owned_tag_sets/index.html.erb'
+ - 'app/views/challenge_assignments/_assignment_blurb.html.erb'
+ - 'app/views/collections/new.html.erb'
+ - 'app/views/challenge/shared/_challenge_meta.html.erb'
+ - 'app/views/owned_tag_sets/_tag_set_form_management.html.erb'
+ - 'app/views/bookmarks/_bookmark_blurb_short.html.erb'
+ - 'app/views/challenge/shared/_challenge_requests.html.erb'
+ - 'app/views/works/drafts.html.erb'
+ - 'app/views/bookmarks/new.html.erb'
+ - 'app/views/home/_news_module.html.erb'
+ - 'app/views/admin_posts/_admin_post_form.html.erb'
+ - 'app/views/chapters/_hidden_fields.html.erb'
+ - 'app/views/challenge_signups/index.html.erb'
+ - 'app/views/admin/admin_invitations/find.html.erb'
+ - 'app/views/tag_set_nominations/_tag_nominations_by_fandom.html.erb'
+ - 'app/views/challenge_assignments/_maintainer_index_fulfilled.html.erb'
+ - 'app/views/challenge_claims/_user_index.html.erb'
+ - 'app/views/opendoors/tools/_tools_navigation.html.erb'
+ - 'app/views/challenge_signups/_show_requests.html.erb'
+ - 'app/views/challenge_signups/summary.html.erb'
+ - 'app/views/collections/list_ge_challenges.html.erb'
+ - 'app/views/tag_set_nominations/confirm_delete.html.erb'
+ - 'app/views/comments/_comment_actions.html.erb'
+ - 'app/views/people/index.html.erb'
+ - 'app/views/tag_set_nominations/_show_by_tag_type.html.erb'
+ - 'app/views/tag_set_nominations/show.html.erb'
+ - 'app/views/owned_tag_sets/confirm_delete.html.erb'
+ - 'app/views/unsorted_tags/index.html.erb'
+ - 'app/views/layouts/_header.html.erb'
+ - 'app/views/challenge/gift_exchange/_challenge_sidebar.html.erb'
+ - 'app/views/external_authors/_external_author_form.html.erb'
+ - 'app/views/admin_mailer/comment_notification.html.erb'
+ - 'app/views/invitations/show.html.erb'
+ - 'app/views/tag_set_associations/index.html.erb'
+ - 'app/views/prompts/_prompt_blurb.html.erb'
+ - 'app/views/invitations/manage.html.erb'
+ - 'app/views/series/manage.html.erb'
+ - 'app/views/admin/admin_invitations/new.html.erb'
+ - 'app/views/bookmarks/_bookmark_form.html.erb'
+ - 'app/views/comments/_confirm_delete.html.erb'
+ - 'app/views/owned_tag_sets/_show_fandoms_by_media.html.erb'
+ - 'app/views/prompt_restrictions/_prompt_restriction_form.html.erb'
+ - 'app/views/fandoms/unassigned.html.erb'
+ - 'app/views/known_issues/_admin_index.html.erb'
+ - 'app/views/challenge/prompt_meme/_challenge_sidebar.html.erb'
+ - 'app/views/collection_items/new.html.erb'
+ - 'app/views/redirect/show.html.erb'
+ - 'app/views/archive_faqs/_archive_faq_order.html.erb'
+ - 'app/views/works/_hidden_fields.html.erb'
+ - 'app/views/opendoors/tools/index.html.erb'
+ - 'app/views/collections/show.html.erb'
+ - 'app/views/subscriptions/index.html.erb'
+ - 'app/views/admin/api/_api_key_form.html.erb'
+ - 'app/views/works/_work_header_navigation.html.erb'
+ - 'app/views/external_authors/_external_author_name.html.erb'
+ - 'app/views/works/confirm_delete.html.erb'
+ - 'app/views/comments/new.html.erb'
+ - 'app/views/feedbacks/new.html.erb'
+ - 'app/views/bookmarks/_bookmark_user_module.html.erb'
+ - 'app/views/locales/index.html.erb'
+ - 'app/views/tag_set_nominations/_review.html.erb'
+ - 'app/views/works/_notes_form.html.erb'
+ - 'app/views/series/_series_order.html.erb'
+ - 'app/views/archive_faqs/_archive_faq_questions_order.html.erb'
+ - 'app/views/tag_set_nominations/_nomination_form.html.erb'
+ - 'app/views/collections/edit.html.erb'
+ - 'app/views/home/about.html.erb'
+ - 'app/views/layouts/_banner.html.erb'
+ - 'app/views/challenge/shared/_challenge_navigation_user.html.erb'
+ - 'app/views/prompts/edit.html.erb'
+ - 'app/views/challenge_signups/new.html.erb'
+ - 'app/views/pseuds/delete_preview.html.erb'
+ - 'app/views/readings/index.html.erb'
+ - 'app/views/admin_posts/new.html.erb'
+ - 'app/views/owned_tag_sets/edit.html.erb'
+ - 'app/views/users/sessions/_greeting.html.erb'
+ - 'app/views/archive_faqs/show.html.erb'
+ - 'app/views/comments/_single_comment.html.erb'
+ - 'app/views/media/index.html.erb'
+ - 'app/views/potential_matches/index.html.erb'
+ - 'app/views/challenge/prompt_meme/edit.html.erb'
+ - 'app/views/external_authors/edit.html.erb'
+ - 'app/views/admin_posts/_admin_index.html.erb'
+ - 'app/views/owned_tag_sets/_tag_set_association_fields.html.erb'
+ - 'app/views/languages/new.html.erb'
+ - 'app/views/potential_matches/_no_potential_givers.html.erb'
+ - 'app/views/archive_faqs/new.html.erb'
+ - 'app/views/home/site_map.html.erb'
+ - 'app/views/users/_header_navigation.html.erb'
+ - 'app/views/owned_tag_sets/_show_tags_by_alpha.html.erb'
+ - 'app/views/known_issues/edit.html.erb'
+ - 'app/views/challenge/prompt_meme/new.html.erb'
+ - 'app/views/skins/_skin_parent_fields.html.erb'
+ - 'app/views/comments/show.html.erb'
+ - 'app/views/collections/list_challenges.html.erb'
+ - 'app/views/works/collected.html.erb'
+ - 'app/views/potential_matches/_no_match_required.html.erb'
+ - 'app/views/challenge_signups/_show_offers.html.erb'
+ - 'app/views/challenge/prompt_meme/_challenge_signups.html.erb'
+ - 'app/views/works/_search_form.html.erb'
+ - 'app/views/preferences/index.html.erb'
+ - 'app/views/user_invite_requests/index.html.erb'
+ - 'app/views/bookmarks/search_results.html.erb'
+ - 'app/views/works/_search_box.html.erb'
+ - 'app/views/series/confirm_delete.html.erb'
+ - 'app/views/comment_mailer/edited_comment_reply_notification.html.erb'
+ - 'app/views/works/_work_blurb.html.erb'
+ - 'app/views/home/dmca.html.erb'
+ - 'app/views/owned_tag_sets/batch_load.html.erb'
+ - 'app/views/challenge/gift_exchange/edit.html.erb'
+ - 'app/views/tag_set_nominations/_review_cast.html.erb'
+ - 'app/views/challenge_assignments/_maintainer_index_defaulted.html.erb'
+ - 'app/views/languages/show.html.erb'
+ - 'app/views/languages/edit.html.erb'
+ - 'app/views/locales/_locale_form.html.erb'
+ - 'app/views/works/confirm_delete_multiple.html.erb'
+ - 'app/views/works/edit.html.erb'
+ - 'app/views/works/edit_multiple.html.erb'
+ - 'app/views/works/index.html.erb'
+ - 'app/views/works/new.html.erb'
+ - 'app/views/works/new_import.html.erb'
+ - 'app/views/works/search_results.html.erb'
+ - 'app/views/works/show_multiple.html.erb'
+ - 'app/views/wrangling_guidelines/_admin_index.html.erb'
+ - 'app/views/wrangling_guidelines/_wrangling_guideline_form.html.erb'
+ - 'app/views/wrangling_guidelines/_wrangling_guideline_order.html.erb'
+
+Style/IfUnlessModifier:
+ Exclude:
+ - 'app/views/pseuds/_byline.html.erb'
+
+Style/InverseMethods:
+ Exclude:
+ - 'app/views/downloads/_download_preface.html.erb'
+ - 'app/views/potential_match_settings/_potential_match_settings_form.html.erb'
+
+Style/LineEndConcatenation:
+ Exclude:
+ - 'app/views/admin/admin_users/bulk_search.html.erb'
+
+Style/MultilineTernaryOperator:
+ Exclude:
+ - 'app/views/downloads/_download_preface.html.erb'
+ - 'app/views/works/_work_form_tags.html.erb'
+ - 'app/views/user_mailer/batch_subscription_notification.html.erb'
+ - 'app/views/comment_mailer/edited_comment_reply_notification.html.erb'
+ - 'app/views/subscriptions/index.html.erb'
+ - 'app/views/user_mailer/delete_work_notification.html.erb'
+ - 'app/views/potential_matches/_list_recipients_for_pseud.html.erb'
+ - 'app/views/admin_posts/index.html.erb'
+ - 'app/views/comment_mailer/comment_reply_notification.html.erb'
+ - 'app/views/home/first_login_help.html.erb'
+
+Style/NegatedUnless:
+ Exclude:
+ - 'app/views/challenge_signups/_signup_form.html.erb'
+
+Style/NestedParenthesizedCalls:
+ Exclude:
+ - 'app/views/user_mailer/collection_notification.html.erb'
+ - 'app/views/comment_mailer/comment_notification.html.erb'
+ - 'app/views/user_mailer/invitation_to_claim.html.erb'
+ - 'app/views/comment_mailer/comment_sent_notification.html.erb'
+ - 'app/views/layouts/_javascripts.html.erb'
+ - 'app/views/admin_mailer/comment_notification.html.erb'
+ - 'app/views/user_mailer/admin_hidden_work_notification.html.erb'
+ - 'app/views/user_mailer/abuse_report.html.erb'
+ - 'app/views/user_mailer/batch_subscription_notification.html.erb'
+ - 'app/views/user_mailer/prompter_notification.html.erb'
+ - 'app/views/comment_mailer/edited_comment_notification.html.erb'
+ - 'app/views/comment_mailer/edited_comment_reply_notification.html.erb'
+ - 'app/views/user_mailer/_work_info.html.erb'
+ - 'app/views/user_mailer/delete_work_notification.html.erb'
+ - 'app/views/user_mailer/signup_notification.html.erb'
+ - 'app/views/comment_mailer/comment_reply_notification.html.erb'
+ - 'app/views/user_mailer/admin_deleted_work_notification.html.erb'
+ - 'app/views/admin_mailer/edited_comment_notification.html.erb'
+
+Style/NestedTernaryOperator:
+ Exclude:
+ - 'app/views/layouts/_header.html.erb'
+ - 'app/views/user_mailer/challenge_assignment_notification.html.erb'
+ - 'app/views/comment_mailer/edited_comment_reply_notification.html.erb'
+ - 'app/views/collection_mailer/item_added_notification.html.erb'
+ - 'app/views/potential_matches/_list_recipients_for_pseud.html.erb'
+ - 'app/views/comment_mailer/comment_reply_notification.html.erb'
+ - 'app/views/tags/edit.html.erb'
+
+Style/NumericLiterals:
+ Exclude:
+ - 'app/views/archive_faqs/show.html.erb'
+
+Style/NumericPredicate:
+ Exclude:
+ - 'app/views/prompts/_prompt_form_tag_options.html.erb'
+ - 'app/views/challenge/gift_exchange/_challenge_signups_summary.html.erb'
+ - 'app/views/collection_mailer/item_added_notification.html.erb'
+
+Style/ParenthesesAroundCondition:
+ Exclude:
+ - 'app/views/collectibles/_collectible_form.html.erb'
+
+Style/PercentLiteralDelimiters:
+ Exclude:
+ - 'app/views/bookmarks/_filters.html.erb'
+ - 'app/views/tags/index.html.erb'
+ - 'app/views/owned_tag_sets/_internal_tag_set_fields.html.erb'
+ - 'app/views/tag_set_nominations/_nomination_form.html.erb'
+ - 'app/views/tags/show.html.erb'
+ - 'app/views/works/_filters.html.erb'
+ - 'app/views/owned_tag_sets/_tag_set_blurb.html.erb'
+ - 'app/views/admin/skins/index_approved.html.erb'
+ - 'app/views/inbox/show.html.erb'
+ - 'app/views/tag_set_nominations/_tag_nominations_by_fandom.html.erb'
+ - 'app/views/tags/edit.html.erb'
+ - 'app/views/tag_set_nominations/_review.html.erb'
+
+Style/RedundantCondition:
+ Exclude:
+ - 'app/views/layouts/_header.html.erb'
+ - 'app/views/series/index.html.erb'
+ - 'app/views/admin/admin_users/_user_history.html.erb'
+
+Style/RedundantInterpolation:
+ Exclude:
+ - 'app/views/works/_hidden_fields.html.erb'
+ - 'app/views/challenge_signups/_signup_form.html.erb'
+ - 'app/views/potential_matches/_list_recipients_for_pseud.html.erb'
+ - 'app/views/tag_wranglings/index.html.erb'
+ - 'app/views/challenge/shared/_challenge_meta.html.erb'
+ - 'app/views/prompts/_prompt_form_tag_options.html.erb'
+ - 'app/views/archive_faqs/show.html.erb'
+ - 'app/views/chapters/_hidden_fields.html.erb'
+ - 'app/views/tags/edit.html.erb'
+ - 'app/views/bookmarks/index.html.erb'
+ - 'app/views/tags/wrangle.html.erb'
+ - 'app/views/user_mailer/challenge_assignment_notification.html.erb'
+ - 'app/views/menu/_menu_fandoms.html.erb'
+ - 'app/views/layouts/_header.html.erb'
+ - 'app/views/bookmarks/_bookmark_blurb.html.erb'
+ - 'app/views/owned_tag_sets/_internal_tag_set_fields.html.erb'
+ - 'app/views/works/_search_box.html.erb'
+ - 'app/views/bookmarks/_bookmark_owner_navigation.html.erb'
+ - 'app/views/comments/_comment_form.html.erb'
+ - 'app/views/bookmarks/_bookmark_form.html.erb'
+ - 'app/views/works/preview_tags.html.erb'
+
+Style/RedundantParentheses:
+ Exclude:
+ - 'app/views/users/_sidebar.html.erb'
+ - 'app/views/gifts/index.html.erb'
+ - 'app/views/collection_items/_collection_item_form.html.erb'
+ - 'app/views/archive_faqs/_question_answer_fields.html.erb'
+ - 'app/views/inbox/show.html.erb'
+ - 'app/views/prompts/_prompt_form.html.erb'
+ - 'app/views/tags/edit.html.erb'
+
+Style/RedundantSort:
+ Exclude:
+ - 'app/views/owned_tag_sets/show_options.html.erb'
+
+Style/SafeNavigation:
+ Exclude:
+ - 'app/views/admin/admin_users/_user_history.html.erb'
+
+Style/Semicolon:
+ Exclude:
+ - 'app/views/owned_tag_sets/_show_fandoms_by_media.html.erb'
+
+Style/StringConcatenation:
+ Exclude:
+ - 'app/views/user_mailer/invitation_to_claim.html.erb'
+ - 'app/views/collections/_form.html.erb'
+ - 'app/views/admin_posts/_filters.html.erb'
+ - 'app/views/archive_faqs/_filters.html.erb'
+ - 'app/views/collection_mailer/item_added_notification.html.erb'
+ - 'app/views/admin/api/_api_key_form.html.erb'
+ - 'app/views/works/_work_header_navigation.html.erb'
+ - 'app/views/home/tos_faq.html.erb'
+ - 'app/views/user_mailer/feedback.html.erb'
+ - 'app/views/chapters/_chapter_form.html.erb'
+ - 'app/views/skins/_form.html.erb'
+ - 'app/views/external_works/_blurb.html.erb'
+ - 'app/views/works/_standard_form.html.erb'
+ - 'app/views/admin/banners/_banner_form.html.erb'
+ - 'app/views/tags/edit.html.erb'
+ - 'app/views/archive_faqs/_archive_faq_form.html.erb'
+ - 'app/views/related_works/index.html.erb'
+ - 'app/views/questions/manage.html.erb'
+ - 'app/views/collection_participants/_participant_form.html.erb'
+ - 'app/views/languages/_form.html.erb'
+ - 'app/views/external_works/edit.html.erb'
+ - 'app/views/known_issues/_known_issues_form.html.erb'
+ - 'app/views/invitations/index.html.erb'
+ - 'app/views/works/_work_form_associations_language.html.erb'
+ - 'app/views/comment_mailer/_comment_notification_footer_for_other.html.erb'
+ - 'app/views/user_mailer/invalid_signup_notification.html.erb'
+ - 'app/views/home/first_login_help.html.erb'
+ - 'app/views/layouts/_header.html.erb'
+ - 'app/views/tag_wranglers/index.html.erb'
+ - 'app/views/user_invite_requests/new.html.erb'
+ - 'app/views/comment_mailer/_comment_notification_footer_for_tag.html.erb'
+ - 'app/views/chapters/manage.html.erb'
+ - 'app/views/bookmarks/_external_work_fields.html.erb'
+ - 'app/views/archive_faqs/_question_answer_fields.html.erb'
+ - 'app/views/invitations/manage.html.erb'
+ - 'app/views/bookmarks/_bookmark_form.html.erb'
+ - 'app/views/locales/_locale_form.html.erb'
+ - 'app/views/prompts/_prompt_form.html.erb'
+ - 'app/views/layouts/session.html.erb'
+ - 'app/views/prompt_restrictions/_prompt_restriction_form.html.erb'
+ - 'app/views/works/new_import.html.erb'
+ - 'app/views/wrangling_guidelines/_wrangling_guideline_form.html.erb'
+
+Style/StringLiterals:
+ Exclude:
+ - 'app/views/user_mailer/invitation_to_claim.html.erb'
+ - 'app/views/layouts/home.html.erb'
+ - 'app/views/bookmarks/show.html.erb'
+ - 'app/views/challenge/shared/_challenge_form_schedule.html.erb'
+ - 'app/views/challenge_claims/_unposted_claim_blurb.html.erb'
+ - 'app/views/archive_faqs/_filters.html.erb'
+ - 'app/views/downloads/_download_afterword.html.erb'
+ - 'app/views/comments/_comment_thread.html.erb'
+ - 'app/views/skins/_skin_module.html.erb'
+ - 'app/views/challenge_assignments/_user_index.html.erb'
+ - 'app/views/users/delete_confirmation.html.erb'
+ - 'app/views/archive_faqs/_admin_index.html.erb'
+ - 'app/views/menu/search.html.erb'
+ - 'app/views/inbox/_delete_form.html.erb'
+ - 'app/views/comment_mailer/comment_sent_notification.html.erb'
+ - 'app/views/prompts/_prompt_form_tag_options.html.erb'
+ - 'app/views/archive_faqs/manage.html.erb'
+ - 'app/views/orphans/_orphan_series.html.erb'
+ - 'app/views/orphans/index.html.erb'
+ - 'app/views/collection_profile/show.html.erb'
+ - 'app/views/inbox/_inbox_comment_contents.html.erb'
+ - 'app/views/layouts/application.html.erb'
+ - 'app/views/skins/_form.html.erb'
+ - 'app/views/downloads/_download_chapter.html.erb'
+ - 'app/views/downloads/show.html.erb'
+ - 'app/views/home/donate.html.erb'
+ - 'app/views/collections/_sidebar.html.erb'
+ - 'app/views/tags/edit.html.erb'
+ - 'app/views/works/_collection_filters.html.erb'
+ - 'app/views/tag_set_nominations/_review_individual_nom.html.erb'
+ - 'app/views/bookmarks/index.html.erb'
+ - 'app/views/potential_matches/_no_potential_recipients.html.erb'
+ - 'app/views/works/edit_multiple.html.erb'
+ - 'app/views/comments/edit.html.erb'
+ - 'app/views/menu/_menu_about.html.erb'
+ - 'app/views/questions/manage.html.erb'
+ - 'app/views/related_works/index.html.erb'
+ - 'app/views/works/_work_form_tags.html.erb'
+ - 'app/views/favorite_tags/_form.html.erb'
+ - 'app/views/known_issues/_known_issues_form.html.erb'
+ - 'app/views/invitations/index.html.erb'
+ - 'app/views/archive_faqs/_faq_index.html.erb'
+ - 'app/views/works/_work_series_links.html.erb'
+ - 'app/views/locales/new.html.erb'
+ - 'app/views/user_mailer/admin_deleted_work_notification.html.erb'
+ - 'app/views/invitations/_user_invitations.html.erb'
+ - 'app/views/admin/banners/index.html.erb'
+ - 'app/views/admin/banners/edit.html.erb'
+ - 'app/views/collections/_works_module.html.erb'
+ - 'app/views/user_invite_requests/new.html.erb'
+ - 'app/views/tag_set_nominations/new.html.erb'
+ - 'app/views/pseuds/show.html.erb'
+ - 'app/views/related_works/_approve.html.erb'
+ - 'app/views/tag_set_nominations/edit.html.erb'
+ - 'app/views/chapters/manage.html.erb'
+ - 'app/views/downloads/_download_preface.html.erb'
+ - 'app/views/comment_mailer/edited_comment_notification.html.erb'
+ - 'app/views/external_authors/index.html.erb'
+ - 'app/views/gifts/_gift_search.html.erb'
+ - 'app/views/related_works/show.html.erb'
+ - 'app/views/skins/_skin_type_navigation.html.erb'
+ - 'app/views/related_works/_remove.html.erb'
+ - 'app/views/bookmarks/_bookmarks.html.erb'
+ - 'app/views/comments/_comment_form.html.erb'
+ - 'app/views/works/_adult.html.erb'
+ - 'app/views/works/_work_approved_children.html.erb'
+ - 'app/views/inbox/show.html.erb'
+ - 'app/views/prompts/_prompt_form.html.erb'
+ - 'app/views/series/_series_blurb.html.erb'
+ - 'app/views/users/registrations/new.html.erb'
+ - 'app/views/series/edit.html.erb'
+ - 'app/views/pseuds/edit.html.erb'
+ - 'app/views/menu/browse.html.erb'
+ - 'app/views/collection_items/_collection_item_form.html.erb'
+ - 'app/views/series/index.html.erb'
+ - 'app/views/collections/_form.html.erb'
+ - 'app/views/admin/skins/index_rejected.html.erb'
+ - 'app/views/owned_tag_sets/_tag_set_associations_remove.html.erb'
+ - 'app/views/admin_posts/_filters.html.erb'
+ - 'app/views/external_works/new.html.erb'
+ - 'app/views/home/index.html.erb'
+ - 'app/views/bookmarks/_bookmark_item_module.html.erb'
+ - 'app/views/admin_posts/edit.html.erb'
+ - 'app/views/admin/skins/index_approved.html.erb'
+ - 'app/views/bookmarks/edit.html.erb'
+ - 'app/views/fandoms/show.html.erb'
+ - 'app/views/inbox/_approve_button.html.erb'
+ - 'app/views/users/registrations/_legal.html.erb'
+ - 'app/views/locales/_navigation.html.erb'
+ - 'app/views/bookmarks/share.html.erb'
+ - 'app/views/pseuds/index.html.erb'
+ - 'app/views/series/_series_navigation.html.erb'
+ - 'app/views/chapters/new.html.erb'
+ - 'app/views/admin/admin_users/confirm_delete_user_creations.html.erb'
+ - 'app/views/admin/skins/index.html.erb'
+ - 'app/views/chapters/edit.html.erb'
+ - 'app/views/admin/banners/show.html.erb'
+ - 'app/views/layouts/_footer.html.erb'
+ - 'app/views/works/_work_abbreviated_list.html.erb'
+ - 'app/views/stats/index.html.erb'
+ - 'app/views/users/_contents.html.erb'
+ - 'app/views/menu/_menu_browse.html.erb'
+ - 'app/views/admin/banners/_banner_form.html.erb'
+ - 'app/views/owned_tag_sets/_show_tag_set_associations.html.erb'
+ - 'app/views/tags/search.html.erb'
+ - 'app/views/challenge/gift_exchange/_challenge_signups_summary.html.erb'
+ - 'app/views/people/search.html.erb'
+ - 'app/views/bookmarks/confirm_delete.html.erb'
+ - 'app/views/menu/about.html.erb'
+ - 'app/views/collection_participants/_participant_form.html.erb'
+ - 'app/views/external_works/edit.html.erb'
+ - 'app/views/menu/_menu_fandoms.html.erb'
+ - 'app/views/admin/activities/show.html.erb'
+ - 'app/views/prompts/_prompt_navigation.html.erb'
+ - 'app/views/home/first_login_help.html.erb'
+ - 'app/views/owned_tag_sets/_show_tag_set_tags.html.erb'
+ - 'app/views/admin_mailer/edited_comment_notification.html.erb'
+ - 'app/views/challenge_signups/_signup_controls.html.erb'
+ - 'app/views/challenge/gift_exchange/_gift_exchange_form.html.erb'
+ - 'app/views/collections/_bookmarks_module.html.erb'
+ - 'app/views/pseuds/_pseud_blurb.html.erb'
+ - 'app/views/tag_wranglers/index.html.erb'
+ - 'app/views/home/lost_cookie.html.erb'
+ - 'app/views/tags/index.html.erb'
+ - 'app/views/collection_items/_item_fields.html.erb'
+ - 'app/views/external_authors/_external_author_blurb.html.erb'
+ - 'app/views/owned_tag_sets/_navigation.html.erb'
+ - 'app/views/tags/show.html.erb'
+ - 'app/views/skins/_header.html.erb'
+ - 'app/views/users/change_email.html.erb'
+ - 'app/views/owned_tag_sets/_tag_set_blurb.html.erb'
+ - 'app/views/comment_mailer/comment_reply_notification.html.erb'
+ - 'app/views/inbox/_read_form.html.erb'
+ - 'app/views/skins/new_wizard.html.erb'
+ - 'app/views/orphans/new.html.erb'
+ - 'app/views/skins/_skin_navigation.html.erb'
+ - 'app/views/collections/_collection_blurb.html.erb'
+ - 'app/views/external_works/show.html.erb'
+ - 'app/views/owned_tag_sets/_tag_set_form.html.erb'
+ - 'app/views/comment_mailer/comment_notification.html.erb'
+ - 'app/views/gifts/index.html.erb'
+ - 'app/views/users/change_password.html.erb'
+ - 'app/views/users/edit.html.erb'
+ - 'app/views/archive_faqs/edit.html.erb'
+ - 'app/views/orphans/_orphan_work.html.erb'
+ - 'app/views/challenge_signups/_signup_form.html.erb'
+ - 'app/views/locales/edit.html.erb'
+ - 'app/views/pseuds/new.html.erb'
+ - 'app/views/home/_inbox_module.html.erb'
+ - 'app/views/orphans/_choose_pseud.html.erb'
+ - 'app/views/profile/show.html.erb'
+ - 'app/views/admin/admin_invitations/index.html.erb'
+ - 'app/views/layouts/mailer.html.erb'
+ - 'app/views/comments/unreviewed.html.erb'
+ - 'app/views/known_issues/new.html.erb'
+ - 'app/views/tag_wranglings/index.html.erb'
+ - 'app/views/users/confirmation.html.erb'
+ - 'app/views/admin/spam/index.html.erb'
+ - 'app/views/inbox/_reply_button.html.erb'
+ - 'app/views/owned_tag_sets/index.html.erb'
+ - 'app/views/share/_share.html.erb'
+ - 'app/views/works/_work_module.html.erb'
+ - 'app/views/collections/new.html.erb'
+ - 'app/views/owned_tag_sets/_tag_set_form_management.html.erb'
+ - 'app/views/bookmarks/_bookmark_blurb_short.html.erb'
+ - 'app/views/series/_series_module.html.erb'
+ - 'app/views/works/drafts.html.erb'
+ - 'app/views/bookmarks/new.html.erb'
+ - 'app/views/home/_news_module.html.erb'
+ - 'app/views/admin_posts/_admin_post_form.html.erb'
+ - 'app/views/skins/_skin_actions.html.erb'
+ - 'app/views/admin/admin_invitations/find.html.erb'
+ - 'app/views/challenge_claims/_user_index.html.erb'
+ - 'app/views/tag_wranglings/_wrangler_dashboard.html.erb'
+ - 'app/views/challenge_signups/summary.html.erb'
+ - 'app/views/languages/_form.html.erb'
+ - 'app/views/menu/fandoms.html.erb'
+ - 'app/views/comments/_comment_abbreviated_list.html.erb'
+ - 'app/views/comments/_comment_actions.html.erb'
+ - 'app/views/user_mailer/signup_notification.html.erb'
+ - 'app/views/people/index.html.erb'
+ - 'app/views/tag_set_nominations/show.html.erb'
+ - 'app/views/skins/new.html.erb'
+ - 'app/views/layouts/_header.html.erb'
+ - 'app/views/external_authors/_external_author_form.html.erb'
+ - 'app/views/user_mailer/change_email.html.erb'
+ - 'app/views/admin_mailer/comment_notification.html.erb'
+ - 'app/views/invitations/show.html.erb'
+ - 'app/views/tag_set_associations/index.html.erb'
+ - 'app/views/errors/500.html.erb'
+ - 'app/views/archive_faqs/_question_answer_fields.html.erb'
+ - 'app/views/invitations/manage.html.erb'
+ - 'app/views/prompts/_prompt_blurb.html.erb'
+ - 'app/views/series/manage.html.erb'
+ - 'app/views/users/registrations/_passwd.html.erb'
+ - 'app/views/admin/banners/_navigation.html.erb'
+ - 'app/views/admin/admin_invitations/new.html.erb'
+ - 'app/views/bookmarks/_bookmark_form.html.erb'
+ - 'app/views/comments/_confirm_delete.html.erb'
+ - 'app/views/series/new.html.erb'
+ - 'app/views/layouts/session.html.erb'
+ - 'app/views/prompt_restrictions/_prompt_restriction_form.html.erb'
+ - 'app/views/fandoms/unassigned.html.erb'
+ - 'app/views/known_issues/_admin_index.html.erb'
+ - 'app/views/redirect/show.html.erb'
+ - 'app/views/collection_items/new.html.erb'
+ - 'app/views/archive_faqs/_archive_faq_order.html.erb'
+ - 'app/views/skins/edit.html.erb'
+ - 'app/views/opendoors/tools/index.html.erb'
+ - 'app/views/subscriptions/_form.html.erb'
+ - 'app/views/collections/show.html.erb'
+ - 'app/views/menu/_menu_search.html.erb'
+ - 'app/views/subscriptions/index.html.erb'
+ - 'app/views/works/_work_header_navigation.html.erb'
+ - 'app/views/collections/_collection_form_delete.html.erb'
+ - 'app/views/external_authors/_external_author_name.html.erb'
+ - 'app/views/readings/_reading_blurb.html.erb'
+ - 'app/views/comments/new.html.erb'
+ - 'app/views/layouts/_includes.html.erb'
+ - 'app/views/bookmarks/_bookmark_user_module.html.erb'
+ - 'app/views/locales/index.html.erb'
+ - 'app/views/tag_set_nominations/_review.html.erb'
+ - 'app/views/works/_notes_form.html.erb'
+ - 'app/views/series/_series_order.html.erb'
+ - 'app/views/archive_faqs/_archive_faq_questions_order.html.erb'
+ - 'app/views/skins/index.html.erb'
+ - 'app/views/tag_set_nominations/_nomination_form.html.erb'
+ - 'app/views/collections/_header.html.erb'
+ - 'app/views/collections/edit.html.erb'
+ - 'app/views/home/about.html.erb'
+ - 'app/views/challenge/shared/_challenge_navigation_user.html.erb'
+ - 'app/views/layouts/_banner.html.erb'
+ - 'app/views/pseuds/delete_preview.html.erb'
+ - 'app/views/readings/index.html.erb'
+ - 'app/views/skins/show.html.erb'
+ - 'app/views/admin_posts/new.html.erb'
+ - 'app/views/users/sessions/_greeting.html.erb'
+ - 'app/views/admin_mailer/send_spam_alert.html.erb'
+ - 'app/views/admin_sessions/new.html.erb'
+ - 'app/views/archive_faqs/show.html.erb'
+ - 'app/views/challenge/gift_exchange/_challenge_signups.html.erb'
+ - 'app/views/comments/_single_comment.html.erb'
+ - 'app/views/admin/banners/confirm_delete.html.erb'
+ - 'app/views/external_works/_blurb.html.erb'
+ - 'app/views/media/index.html.erb'
+ - 'app/views/potential_matches/index.html.erb'
+ - 'app/views/external_authors/edit.html.erb'
+ - 'app/views/pseuds/_pseuds_form.html.erb'
+ - 'app/views/admin_posts/_admin_index.html.erb'
+ - 'app/views/owned_tag_sets/_tag_set_association_fields.html.erb'
+ - 'app/views/admin/banners/new.html.erb'
+ - 'app/views/languages/new.html.erb'
+ - 'app/views/potential_matches/_no_potential_givers.html.erb'
+ - 'app/views/archive_faqs/new.html.erb'
+ - 'app/views/tags/wrangle.html.erb'
+ - 'app/views/home/site_map.html.erb'
+ - 'app/views/users/_header_navigation.html.erb'
+ - 'app/views/known_issues/edit.html.erb'
+ - 'app/views/skins/_skin_parent_fields.html.erb'
+ - 'app/views/tags/new.html.erb'
+ - 'app/views/comments/show.html.erb'
+ - 'app/views/user_mailer/delete_work_notification.html.erb'
+ - 'app/views/works/collected.html.erb'
+ - 'app/views/potential_matches/_no_match_required.html.erb'
+ - 'app/views/languages/index.html.erb'
+ - 'app/views/preferences/index.html.erb'
+ - 'app/views/user_invite_requests/index.html.erb'
+ - 'app/views/tag_set_nominations/_tag_nominations.html.erb'
+ - 'app/views/user_mailer/claim_notification.html.erb'
+ - 'app/views/bookmarks/search.html.erb'
+ - 'app/views/bookmarks/search_results.html.erb'
+ - 'app/views/works/_search_box.html.erb'
+ - 'app/views/comment_mailer/edited_comment_reply_notification.html.erb'
+ - 'app/views/works/_work_blurb.html.erb'
+ - 'app/views/home/dmca.html.erb'
+ - 'app/views/admin_posts/index.html.erb'
+ - 'app/views/owned_tag_sets/batch_load.html.erb'
+ - 'app/views/bookmarks/_bookmark_owner_navigation.html.erb'
+ - 'app/views/challenge_assignments/_maintainer_index_defaulted.html.erb'
+ - 'app/views/languages/show.html.erb'
+ - 'app/views/languages/edit.html.erb'
+ - 'app/views/locales/_locale_form.html.erb'
+ - 'app/views/works/edit.html.erb'
+ - 'app/views/works/edit_tags.html.erb'
+ - 'app/views/works/index.html.erb'
+ - 'app/views/works/new.html.erb'
+ - 'app/views/works/new_import.html.erb'
+ - 'app/views/works/preview_tags.html.erb'
+ - 'app/views/works/search.html.erb'
+ - 'app/views/works/search_results.html.erb'
+ - 'app/views/works/share.html.erb'
+ - 'app/views/works/show_multiple.html.erb'
+ - 'app/views/wrangling_guidelines/_admin_index.html.erb'
+ - 'app/views/wrangling_guidelines/_wrangling_guideline_form.html.erb'
+ - 'app/views/wrangling_guidelines/_wrangling_guideline_order.html.erb'
+ - 'app/views/wrangling_guidelines/edit.html.erb'
+ - 'app/views/wrangling_guidelines/index.html.erb'
+ - 'app/views/wrangling_guidelines/manage.html.erb'
+ - 'app/views/wrangling_guidelines/new.html.erb'
+ - 'app/views/wrangling_guidelines/show.html.erb'
+
+Style/StringLiteralsInInterpolation:
+ Exclude:
+ - 'app/views/owned_tag_sets/_internal_tag_set_fields.html.erb'
+ - 'app/views/invitations/_user_invitations.html.erb'
+ - 'app/views/challenge/shared/_challenge_requests.html.erb'
+ - 'app/views/user_mailer/challenge_assignment_notification.html.erb'
+ - 'app/views/admin/admin_users/bulk_search.html.erb'
+ - 'app/views/pseuds/_pseud_blurb.html.erb'
+ - 'app/views/works/new_import.html.erb'
+
+Style/SymbolProc:
+ Exclude:
+ - 'app/views/fandoms/unassigned.html.erb'
+ - 'app/views/owned_tag_sets/_tag_set_form.html.erb'
+ - 'app/views/prompts/_prompt_form_tag_options.html.erb'
+ - 'app/views/fandoms/index.html.erb'
+ - 'app/views/potential_match_settings/_potential_match_settings_form.html.erb'
+
+Style/TernaryParentheses:
+ Exclude:
+ - 'app/views/challenge_assignments/_maintainer_index_unfulfilled.html.erb'
+ - 'app/views/layouts/home.html.erb'
+ - 'app/views/admin/skins/index_rejected.html.erb'
+ - 'app/views/collection_mailer/item_added_notification.html.erb'
+ - 'app/views/works/_work_header_navigation.html.erb'
+ - 'app/views/admin/skins/index_approved.html.erb'
+ - 'app/views/tag_set_nominations/_review.html.erb'
+ - 'app/views/works/_notes_form.html.erb'
+ - 'app/views/works/_work_module.html.erb'
+ - 'app/views/challenge_assignments/_assignment_blurb.html.erb'
+ - 'app/views/challenge/shared/_challenge_meta.html.erb'
+ - 'app/views/prompts/_prompt_form_tag_options.html.erb'
+ - 'app/views/admin/skins/index.html.erb'
+ - 'app/views/skins/_form.html.erb'
+ - 'app/views/collection_profile/show.html.erb'
+ - 'app/views/layouts/application.html.erb'
+ - 'app/views/stats/index.html.erb'
+ - 'app/views/tags/edit.html.erb'
+ - 'app/views/users/_sidebar.html.erb'
+ - 'app/views/challenge/gift_exchange/_challenge_signups_summary.html.erb'
+ - 'app/views/works/_filters.html.erb'
+ - 'app/views/bookmarks/_filters.html.erb'
+ - 'app/views/admin_posts/index.html.erb'
+ - 'app/views/skins/_skin_navigation.html.erb'
+ - 'app/views/layouts/session.html.erb'
+ - 'app/views/prompt_restrictions/_prompt_restriction_form.html.erb'
+
+Style/TrailingCommaInArguments:
+ Exclude:
+ - 'app/views/home/first_login_help.html.erb'
+
+Style/WordArray:
+ Exclude:
+ - 'app/views/works/_search_form.html.erb'
+ - 'app/views/tags/_search_form.html.erb'
+
+Style/ZeroLengthPredicate:
+ Exclude:
+ - 'app/views/collection_mailer/item_added_notification.html.erb'
diff --git a/Gemfile b/Gemfile
index 05c6ca1dd32..0ff372463ec 100644
--- a/Gemfile
+++ b/Gemfile
@@ -154,10 +154,10 @@ group :development do
end
group :linters do
- gem "erb_lint", "0.0.29"
- gem "rubocop", "0.83.0"
- gem "rubocop-rails", "2.6.0"
- gem "rubocop-rspec", "1.41.0"
+ gem "erb_lint", "0.4.0"
+ gem "rubocop", "1.22.1"
+ gem "rubocop-rails", "2.12.4"
+ gem "rubocop-rspec", "2.6.0"
end
group :test, :development, :staging do
diff --git a/Gemfile.lock b/Gemfile.lock
index 15da0f6b766..88d9c8bffe7 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -120,12 +120,11 @@ GEM
aws-eventstream (~> 1, >= 1.0.2)
backports (3.23.0)
bcrypt (3.1.16)
- better_html (1.0.16)
- actionview (>= 4.0)
- activesupport (>= 4.0)
+ better_html (2.0.1)
+ actionview (>= 6.0)
+ activesupport (>= 6.0)
ast (~> 2.0)
erubi (~> 1.4)
- html_tokenizer (~> 0.0.6)
parser (>= 2.4)
smart_properties
brakeman (5.2.1)
@@ -229,12 +228,12 @@ GEM
email_spec (1.6.0)
launchy (~> 2.1)
mail (~> 2.2)
- erb_lint (0.0.29)
+ erb_lint (0.4.0)
activesupport
- better_html (~> 1.0.7)
- html_tokenizer
+ better_html (>= 2.0.1)
+ parser (>= 2.7.1.4)
rainbow
- rubocop (~> 0.51)
+ rubocop
smart_properties
erubi (1.12.0)
escape_utils (1.2.1)
@@ -280,7 +279,6 @@ GEM
god (0.13.7)
hashdiff (1.0.1)
highline (2.0.3)
- html_tokenizer (0.0.7)
htmlentities (4.3.4)
http-accept (1.7.0)
http-cookie (1.0.5)
@@ -371,7 +369,7 @@ GEM
mini_portile2 (~> 2.8.0)
racc (~> 1.4)
orm_adapter (0.5.0)
- parallel (1.21.0)
+ parallel (1.23.0)
parser (3.1.0.0)
ast (~> 2.4.1)
permit_yo (2.1.3)
@@ -485,20 +483,24 @@ GEM
rspec-mocks (~> 3.10)
rspec-support (~> 3.10)
rspec-support (3.10.3)
- rubocop (0.83.0)
+ rubocop (1.22.1)
parallel (~> 1.10)
- parser (>= 2.7.0.1)
+ parser (>= 3.0.0.0)
rainbow (>= 2.2.2, < 4.0)
+ regexp_parser (>= 1.8, < 3.0)
rexml
+ rubocop-ast (>= 1.12.0, < 2.0)
ruby-progressbar (~> 1.7)
- unicode-display_width (>= 1.4.0, < 2.0)
- rubocop-rails (2.6.0)
+ unicode-display_width (>= 1.4.0, < 3.0)
+ rubocop-ast (1.15.2)
+ parser (>= 3.0.1.1)
+ rubocop-rails (2.12.4)
activesupport (>= 4.2.0)
rack (>= 1.1)
- rubocop (>= 0.82.0)
- rubocop-rspec (1.41.0)
- rubocop (>= 0.68.1)
- ruby-progressbar (1.11.0)
+ rubocop (>= 1.7.0, < 2.0)
+ rubocop-rspec (2.6.0)
+ rubocop (~> 1.19)
+ ruby-progressbar (1.13.0)
ruby2_keywords (0.0.5)
rubyntlm (0.6.3)
rubyzip (2.3.2)
@@ -629,7 +631,7 @@ DEPENDENCIES
devise-async
elasticsearch (= 7.17.1)
email_spec (= 1.6.0)
- erb_lint (= 0.0.29)
+ erb_lint (= 0.4.0)
escape_utils (= 1.2.1)
factory_bot
factory_bot_rails
@@ -671,9 +673,9 @@ DEPENDENCIES
rest-client (~> 2.1.0)
rollout
rspec-rails (~> 4.0.1)
- rubocop (= 0.83.0)
- rubocop-rails (= 2.6.0)
- rubocop-rspec (= 1.41.0)
+ rubocop (= 1.22.1)
+ rubocop-rails (= 2.12.4)
+ rubocop-rspec (= 2.6.0)
rvm-capistrano
sanitize (>= 4.6.5)
selenium-webdriver
From f430b6afe017461397f886ff34c1624c6a0aa7f5 Mon Sep 17 00:00:00 2001
From: tickinginstant
Date: Mon, 10 Jul 2023 18:50:27 -0400
Subject: [PATCH 003/208] AO3-6557 Delete old monkeypatch. (#4572)
---
.../monkeypatches/accept_header.rb | 24 -------------------
1 file changed, 24 deletions(-)
delete mode 100644 config/initializers/monkeypatches/accept_header.rb
diff --git a/config/initializers/monkeypatches/accept_header.rb b/config/initializers/monkeypatches/accept_header.rb
deleted file mode 100644
index 0a665a391ef..00000000000
--- a/config/initializers/monkeypatches/accept_header.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# https://rails.lighthouseapp.com/projects/8994/tickets/6022-content-negotiation-fails-for-some-headers-regression#ticket-6022-13
-# https://rails.lighthouseapp.com/projects/8994/tickets/5833
-# specific reason: user agent "Mozilla/4.0 (PSP (PlayStation Portable); 2.00)" sets http accept header to "*/*;q=0.01" and rails gives it a 500
-
-module ActionDispatch
- module Http
- module MimeNegotiation
-
- # Patched to always accept at least HTML
- def accepts
- @env["action_dispatch.request.accepts"] ||= begin
- header = @env['HTTP_ACCEPT'].to_s.strip
-
- if header.empty?
- [content_mime_type]
- else
- Mime::Type.parse(header) << Mime[:html]
- end
- end
- end
-
- end
- end
-end
From a41dcfb23e598c75367035d03e32cc4d60ca4909 Mon Sep 17 00:00:00 2001
From: tickinginstant
Date: Mon, 10 Jul 2023 19:56:27 -0400
Subject: [PATCH 004/208] AO3-6558 Remove fetch_admin_settings hook. (#4573)
---
app/controllers/application_controller.rb | 12 +++---------
app/controllers/autocomplete_controller.rb | 1 -
app/controllers/downloads_controller.rb | 2 +-
app/controllers/invite_requests_controller.rb | 3 +--
app/controllers/users/registrations_controller.rb | 6 +++---
app/views/comments/_commentable.html.erb | 2 +-
app/views/home/_intro_module.html.erb | 8 ++++----
app/views/invite_requests/_index_open.html.erb | 2 +-
app/views/invite_requests/_invite_request.html.erb | 2 +-
app/views/invite_requests/index.html.erb | 2 +-
app/views/invite_requests/status.html.erb | 4 ++--
app/views/users/sessions/_passwd_small.html.erb | 4 ++--
app/views/users/sessions/new.html.erb | 12 ++++++------
app/views/works/_work_header_navigation.html.erb | 2 +-
spec/controllers/invite_requests_controller_spec.rb | 1 -
15 files changed, 27 insertions(+), 36 deletions(-)
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 27148b57516..94d0a643a93 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -175,11 +175,6 @@ def process_title(string)
public
- before_action :fetch_admin_settings
- def fetch_admin_settings
- @admin_settings = AdminSetting.current
- end
-
before_action :load_admin_banner
def load_admin_banner
if Rails.env.development?
@@ -415,7 +410,7 @@ def see_adult?
end
def use_caching?
- %w(staging production test).include?(Rails.env) && @admin_settings.enable_test_caching?
+ %w(staging production test).include?(Rails.env) && AdminSetting.current.enable_test_caching?
end
protected
@@ -465,7 +460,7 @@ def check_visibility
# Make sure user is allowed to access tag wrangling pages
def check_permission_to_wrangle
- if @admin_settings.tag_wrangling_off? && !logged_in_as_admin?
+ if AdminSetting.current.tag_wrangling_off? && !logged_in_as_admin?
flash[:error] = "Wrangling is disabled at the moment. Please check back later."
redirect_to root_path
else
@@ -521,8 +516,7 @@ def flash_search_warnings(result)
end
# Don't get unnecessary data for json requests
- skip_before_action :fetch_admin_settings,
- :load_admin_banner,
+ skip_before_action :load_admin_banner,
:set_redirects,
:store_location,
if: proc { %w(js json).include?(request.format) }
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 5e2a4e102e3..2172fe2eb41 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -3,7 +3,6 @@ class AutocompleteController < ApplicationController
skip_before_action :store_location
skip_before_action :set_current_user, except: [:collection_parent_name, :owned_tag_sets, :site_skins]
- skip_before_action :fetch_admin_settings
skip_before_action :set_redirects
skip_before_action :sanitize_ac_params # can we dare!
diff --git a/app/controllers/downloads_controller.rb b/app/controllers/downloads_controller.rb
index 0b7f813816b..4fe5aa9ea75 100644
--- a/app/controllers/downloads_controller.rb
+++ b/app/controllers/downloads_controller.rb
@@ -34,7 +34,7 @@ def show
# It can't contain unposted chapters, nor unrevealed creators, even
# if the creator is the one requesting the download.
def load_work
- unless @admin_settings.downloads_enabled?
+ unless AdminSetting.current.downloads_enabled?
flash[:error] = ts("Sorry, downloads are currently disabled.")
redirect_back_or_default works_path
return
diff --git a/app/controllers/invite_requests_controller.rb b/app/controllers/invite_requests_controller.rb
index c202902dd8e..ba1c69166f3 100644
--- a/app/controllers/invite_requests_controller.rb
+++ b/app/controllers/invite_requests_controller.rb
@@ -8,7 +8,6 @@ def index
# GET /invite_requests/1
def show
- fetch_admin_settings # we normally skip this for js requests
@invite_request = InviteRequest.find_by(email: params[:email])
@position_in_queue = @invite_request.position if @invite_request.present?
unless (request.xml_http_request?) || @invite_request
@@ -23,7 +22,7 @@ def show
# POST /invite_requests
def create
- unless @admin_settings.invite_from_queue_enabled?
+ unless AdminSetting.current.invite_from_queue_enabled?
flash[:error] = ts("New invitation requests are currently closed. For more information, please check the %{news}.",
news: view_context.link_to("\"Invitations\" tag on AO3 News", admin_posts_path(tag: 143))).html_safe
redirect_to invite_requests_path
diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb
index 001b07ee956..3ff2a21e428 100644
--- a/app/controllers/users/registrations_controller.rb
+++ b/app/controllers/users/registrations_controller.rb
@@ -66,11 +66,11 @@ def check_account_creation_status
token = params[:invitation_token]
- if !@admin_settings.account_creation_enabled?
+ if !AdminSetting.current.account_creation_enabled?
flash[:error] = ts('Account creation is suspended at the moment. Please check back with us later.')
redirect_to root_path and return
else
- check_account_creation_invite(token) if @admin_settings.creation_requires_invite?
+ check_account_creation_invite(token) if AdminSetting.current.creation_requires_invite?
end
end
@@ -89,7 +89,7 @@ def check_account_creation_invite(token)
return
end
- if !@admin_settings.invite_from_queue_enabled?
+ if !AdminSetting.current.invite_from_queue_enabled?
flash[:error] = ts('Account creation currently requires an invitation. We are unable to give out additional invitations at present, but existing invitations can still be used to create an account.')
redirect_to root_path
else
diff --git a/app/views/comments/_commentable.html.erb b/app/views/comments/_commentable.html.erb
index aa2a9dbe3b4..971098077bc 100644
--- a/app/views/comments/_commentable.html.erb
+++ b/app/views/comments/_commentable.html.erb
@@ -79,7 +79,7 @@
<%= flash_div :comment_error, :comment_notice %>
<% commentable_parent = find_parent(commentable) %>
- <% if @admin_settings.guest_comments_off? && guest? %>
+ <% if AdminSetting.current.guest_comments_off? && guest? %>
<%= t(".guest_comments_disabled") %>
diff --git a/app/views/home/_intro_module.html.erb b/app/views/home/_intro_module.html.erb
index be2592caa33..c031d316c10 100644
--- a/app/views/home/_intro_module.html.erb
+++ b/app/views/home/_intro_module.html.erb
@@ -5,7 +5,7 @@
- <% if !@admin_settings.invite_from_queue_enabled? && @admin_settings.creation_requires_invite? %>
+ <% if !AdminSetting.current.invite_from_queue_enabled? && AdminSetting.current.creation_requires_invite? %>
<%= ts("Joining the Archive currently requires an invitation; however, we
are not accepting new invitation requests at this time. Please check
@@ -27,13 +27,13 @@
<%= ts("Keep track of works you've visited and works you want to check out later") %>
- <% if @admin_settings.invite_from_queue_enabled? && @admin_settings.creation_requires_invite? && @admin_settings.request_invite_enabled? %>
+ <% if AdminSetting.current.invite_from_queue_enabled? && AdminSetting.current.creation_requires_invite? && AdminSetting.current.request_invite_enabled? %>
<%= ts("You can join by getting an invitation from another user or from our automated invite queue. All fans and fanworks are welcome!") %>
<%= link_to ts("Get Invited!"), invite_requests_path %>
- <% elsif @admin_settings.invite_from_queue_enabled? && @admin_settings.creation_requires_invite? %>
+ <% elsif AdminSetting.current.invite_from_queue_enabled? && AdminSetting.current.creation_requires_invite? %>
<%= ts("You can join by getting an invitation from our automated invite queue. All fans and fanworks are welcome!") %>
<%= link_to ts("Get Invited!"), invite_requests_path %>
- <% elsif @admin_settings.account_creation_enabled? && !@admin_settings.creation_requires_invite? %>
+ <% elsif AdminSetting.current.account_creation_enabled? && !AdminSetting.current.creation_requires_invite? %>
<%= link_to ts("Create an Account!"), signup_path %>
<% end %>
diff --git a/app/views/invite_requests/_index_open.html.erb b/app/views/invite_requests/_index_open.html.erb
index 05eda51b305..5726e4c7a9b 100644
--- a/app/views/invite_requests/_index_open.html.erb
+++ b/app/views/invite_requests/_index_open.html.erb
@@ -38,6 +38,6 @@
status: link_to("check your position on the waiting list",
status_invite_requests_path),
count: InviteRequest.count,
- invites: @admin_settings.invite_from_queue_number).html_safe %>
+ invites: AdminSetting.current.invite_from_queue_number).html_safe %>
diff --git a/app/views/invite_requests/_invite_request.html.erb b/app/views/invite_requests/_invite_request.html.erb
index ce1ae3914d6..8ff1584fc07 100644
--- a/app/views/invite_requests/_invite_request.html.erb
+++ b/app/views/invite_requests/_invite_request.html.erb
@@ -7,7 +7,7 @@
<%= t(".position_html", position: tag.strong(@position_in_queue)) %>
- <% if @admin_settings.invite_from_queue_enabled? %>
+ <% if AdminSetting.current.invite_from_queue_enabled? %>
<%= t(".date", date: l(invite_request.proposed_fill_date, format: :long)) %>
<% end %>
diff --git a/app/views/invite_requests/index.html.erb b/app/views/invite_requests/index.html.erb
index 8caffc59b23..fbb4d62abe4 100644
--- a/app/views/invite_requests/index.html.erb
+++ b/app/views/invite_requests/index.html.erb
@@ -1,5 +1,5 @@
<% # Page changes depending on whether queue is enabled %>
-<% if @admin_settings.invite_from_queue_enabled? %>
+<% if AdminSetting.current.invite_from_queue_enabled? %>
<%= render "index_open" %>
<% else %>
<%= render "index_closed" %>
diff --git a/app/views/invite_requests/status.html.erb b/app/views/invite_requests/status.html.erb
index 7c36a3f19f8..a08867da496 100644
--- a/app/views/invite_requests/status.html.erb
+++ b/app/views/invite_requests/status.html.erb
@@ -5,8 +5,8 @@
<%= ts("There are currently %{count} people on the waiting list.", count: InviteRequest.count) %>
- <% if @admin_settings.invite_from_queue_enabled? %>
- <%= ts("We are sending out %{invites} invitations per day.", invites: @admin_settings.invite_from_queue_number) %>
+ <% if AdminSetting.current.invite_from_queue_enabled? %>
+ <%= ts("We are sending out %{invites} invitations per day.", invites: AdminSetting.current.invite_from_queue_number) %>
<% end %>
diff --git a/app/views/users/sessions/_passwd_small.html.erb b/app/views/users/sessions/_passwd_small.html.erb
index 8ad6ead1331..1d49f0b7704 100644
--- a/app/views/users/sessions/_passwd_small.html.erb
+++ b/app/views/users/sessions/_passwd_small.html.erb
@@ -20,11 +20,11 @@
-
+
+<%= will_paginate @pseuds %>
+
diff --git a/features/other_a/pseuds.feature b/features/other_a/pseuds.feature
index ce6fe1cc767..c8e40515e0d 100644
--- a/features/other_a/pseuds.feature
+++ b/features/other_a/pseuds.feature
@@ -161,6 +161,15 @@ Scenario: Many pseuds
Then I should see "Slartibartfast" within "li.pseud > a"
And I should not see "Slartibartfast" within "ul.expandable"
+ When I go to my pseuds page
+ Then I should not see "Zaphod (Zaphod)" within "ul.pseud.index"
+ But I should see "Agrajag (Zaphod)" within "ul.pseud.index"
+ And I should see "Betelgeuse (Zaphod)" within "ul.pseud.index"
+ And I should see "Slartibartfast (Zaphod)" within "ul.pseud.index"
+ And I should see "Next" within ".pagination"
+ When I follow "Next" within ".pagination"
+ Then I should see "Zaphod (Zaphod)" within "ul.pseud.index"
+
When there are 10 pseuds per page
And I view my profile
Then I should see "Zaphod, Agrajag, Betelgeuse, and Slartibartfast" within "dl.meta"
From ff9f334bb93d69f2f1c38d39fe95cf97a0afec57 Mon Sep 17 00:00:00 2001
From: potpotkettle <40988246+potpotkettle@users.noreply.github.com>
Date: Tue, 11 Jul 2023 16:08:47 +0900
Subject: [PATCH 006/208] AO3-3997 Fix for email address validation #4390
(#4565)
* AO3-3997 Catch email validation failure in import
* AO3-3997 Add missing error message in invitation page
* AO3-3997 more fixes for invite
* AO3-3997 more fixes for import
* AO3-3997 more fixes for import (2)
---------
Co-authored-by: tickinginstant
---
app/models/story_parser.rb | 32 ++++++------
app/views/invitations/_invitation.html.erb | 3 +-
config/locales/views/en.yml | 3 ++
features/other_a/invite_request.feature | 10 ++++
spec/models/story_parser_spec.rb | 58 +++++++++++++++++++---
5 files changed, 84 insertions(+), 22 deletions(-)
diff --git a/app/models/story_parser.rb b/app/models/story_parser.rb
index 2206b2166f6..65364dc4725 100644
--- a/app/models/story_parser.rb
+++ b/app/models/story_parser.rb
@@ -416,23 +416,25 @@ def parse_author_from_unknown(_location)
end
def parse_author_common(email, name)
- if name.present? && email.present?
- # convert to ASCII and strip out invalid characters (everything except alphanumeric characters, _, @ and -)
- name = name.to_ascii.gsub(/[^\w[ \-@.]]/u, "")
+ errors = []
+
+ errors << "No author name specified" if name.blank?
+
+ if email.present?
external_author = ExternalAuthor.find_or_create_by(email: email)
- external_author_name = external_author.default_name
- unless name.blank?
- external_author_name = ExternalAuthorName.where(name: name, external_author_id: external_author.id).first ||
- ExternalAuthorName.new(name: name)
- external_author.external_author_names << external_author_name
- external_author.save
- end
- external_author_name
+ errors += external_author.errors.full_messages
+ else
+ errors << "No author email specified"
+ end
+
+ raise Error, errors.join("\n") if errors.present?
+
+ # convert to ASCII and strip out invalid characters (everything except alphanumeric characters, _, @ and -)
+ redacted_name = name.to_ascii.gsub(/[^\w[ \-@.]]/u, "")
+ if redacted_name.present?
+ external_author.names.find_or_create_by(name: redacted_name)
else
- messages = []
- messages << "No author name specified" if name.blank?
- messages << "No author email specified" if email.blank?
- raise Error, messages.join("\n")
+ external_author.default_name
end
end
diff --git a/app/views/invitations/_invitation.html.erb b/app/views/invitations/_invitation.html.erb
index 680dacf6a49..f2cefdfb5c9 100644
--- a/app/views/invitations/_invitation.html.erb
+++ b/app/views/invitations/_invitation.html.erb
@@ -11,7 +11,8 @@
<% else %>
<%= form_for(invitation) do |f| %>
- Enter an email address: <%= f.text_field :invitee_email %>
+ <%= error_messages_for invitation %>
+ <%= f.label :invitee_email, t(".email_address_label") %> <%= f.text_field :invitee_email %>
<%= hidden_field_tag :user_id, @user.try(:login) %>
<%= f.submit %>
<% end %>
diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml
index 507c541b3d8..e0becc48b3e 100644
--- a/config/locales/views/en.yml
+++ b/config/locales/views/en.yml
@@ -428,6 +428,9 @@ en:
home:
donate:
page_title: Donate or Volunteer
+ invitations:
+ invitation:
+ email_address_label: Enter an email address
invite_requests:
invite_request:
date: 'At our current rate, you should receive an invitation on or around: %{date}.'
diff --git a/features/other_a/invite_request.feature b/features/other_a/invite_request.feature
index d7d9fb4bada..d6f02ac12f1 100644
--- a/features/other_a/invite_request.feature
+++ b/features/other_a/invite_request.feature
@@ -62,6 +62,16 @@ Feature: Invite requests
And I should not see "Sorry, you have no unsent invitations right now."
And I should see "You have 2 open invitations and 0 that have been sent but not yet used."
+ Scenario: User can see an error after trying to invite an invalid email address
+
+ Given I am logged in as "user1"
+ And "user1" has "1" invitation
+ And I am on user1's manage invitations page
+ When I follow the link for "user1" first invite
+ And I fill in "Enter an email address" with "test@"
+ And I press "Update Invitation"
+ Then I should see "Invitee email should look like an email address"
+
Scenario: User can send out invites they have been granted, and the recipient can sign up
Given invitations are required
diff --git a/spec/models/story_parser_spec.rb b/spec/models/story_parser_spec.rb
index ec23ee1ea2f..15ddfbe03e1 100644
--- a/spec/models/story_parser_spec.rb
+++ b/spec/models/story_parser_spec.rb
@@ -193,21 +193,67 @@ class StoryParser
end
it "raises an exception when the external author name is not provided" do
- expect {
+ expect do
@sp.parse_author("", nil, "author@example.com")
- }.to raise_exception(StoryParser::Error) { |e| expect(e.message).to eq("No author name specified") }
+ end.to raise_exception(StoryParser::Error, "No author name specified")
end
it "raises an exception when the external author email is not provided" do
- expect {
+ expect do
@sp.parse_author("", "Author Name", nil)
- }.to raise_exception(StoryParser::Error) { |e| expect(e.message).to eq("No author email specified") }
+ end.to raise_exception(StoryParser::Error, "No author email specified")
end
it "raises an exception when neither the external author name nor email is provided" do
- expect {
+ expect do
@sp.parse_author("", nil, nil)
- }.to raise_exception(StoryParser::Error) { |e| expect(e.message).to eq("No author name specified\nNo author email specified") }
+ end.to raise_exception(StoryParser::Error, "No author name specified\nNo author email specified")
+ end
+
+ it "gives the same external author object for the same email" do
+ res1 = @sp.parse_author("", "Author Name", "author@example.com")
+ res2 = @sp.parse_author("", "Author Name Second", "author@example.com")
+ res3 = @sp.parse_author("", "Author!! Name!!", "author@example.com")
+ expect(res2.external_author.id).to eq(res1.external_author.id)
+ expect(res3.external_author.id).to eq(res1.external_author.id)
+ expect(res1.name).to eq("Author Name")
+ expect(res2.name).to eq("Author Name Second")
+ end
+
+ it "ignores the external author name when it is invalid" do
+ results = @sp.parse_author("", "!!!!", "author@example.com")
+ expect(results.name).to eq("author@example.com")
+ expect(results.external_author.email).to eq("author@example.com")
+ end
+
+ it "ignores invalid letters in the external author name" do
+ results = @sp.parse_author("", "Author!! Name!!", "author@example.com")
+ expect(results.name).to eq("Author Name")
+ expect(results.external_author.email).to eq("author@example.com")
+ end
+
+ it "raises an exception when the external author email is invalid" do
+ expect do
+ @sp.parse_author("", "Author Name", "not_email")
+ end.to raise_exception(StoryParser::Error, "Email should look like an email address.")
+ end
+
+ it "raises an exception when the external author name and email are invalid" do
+ expect do
+ @sp.parse_author("", "!!!!", "not_email")
+ end.to raise_exception(StoryParser::Error, "Email should look like an email address.")
+ end
+
+ it "raises an exception when the external author name is blank and email is invalid" do
+ expect do
+ @sp.parse_author("", "", "not_email")
+ end.to raise_exception(StoryParser::Error, "No author name specified\nEmail should look like an email address.")
+ end
+
+ it "raises an exception when the external author name is invalid and email is blank" do
+ expect do
+ @sp.parse_author("", "!!!!", "")
+ end.to raise_exception(StoryParser::Error, "No author email specified")
end
end
From d99747574ed348866cdeed6cda507b7636612582 Mon Sep 17 00:00:00 2001
From: neuroalien <105230050+neuroalien@users.noreply.github.com>
Date: Mon, 17 Jul 2023 00:43:08 +0100
Subject: [PATCH 007/208] AO3-5901 Pseud switcher text on the New Pseud page
(#4579)
* AO3-5901 Pseud switcher text on the New Pseud page
Provide fallback text for the edge case of the New Pseud page, where `@pseud` is defined, but doesn't have a `name` yet.
* Update app/views/users/_sidebar.html.erb
Co-authored-by: sarken
* AO3-5901 Ensure pseud switcher shows current pseud
When we started limiting the number of pseuds in the sidebar, the way to guarantee that the current pseud would show up in the sidebar was to show it at the top of the pseud switcher.
However, [AO3-6249](https://otwarchive.atlassian.net/browse/AO3-6249) decided that the pseud switcher should always say "Pseuds" at the top.
We broke this behaviour in https://github.com/otwcode/otwarchive/pull/4554.
Fixing it in c3e4ab9c04e0299d20799f90a224b7d24b2f7a6b resulted in the current pseud not showing in the sidebar in some conditions.
Move or add the current pseud at the beginning of the list, whether or not it is present in the abbreviated list.
We are aware that this could make the abbreviated list in the pseud switcher show `ITEMS_PER_PAGE` + 1 pseuds in some cases, discuss with @sarken if you disagree.
* AO3-5901 Don't use instance vars in helpers
The Hound has arisen from its slumber as soon as I touched some ancient scrolls, and so I've tried to appease it with the following sacrifices:
- add spaces after and before curly bois (`{` and `}`)
- IN-TER-PO-LATE! IN-TER-PO-LATE! (AKA prefer interpolation to string concatenation)
- stop using instance vars in helper method
The latter change has morally forced my hand to:
- separate the method that determines which pseuds to show in the sidebar selector from the method that outputs the HTML
- remove the `print_` prefix from the method that I touched, as [per precedent](50f7b7e2c7c78aad7b2e5651f379cf52c28061cf)
* AO3-5901 Remove code unused since 2013
This method has had no callers since https://github.com/otwcode/otwarchive/commit/c10ef8ace7c2b9c015620684b0da97d2595b8c0d#diff-638a702b43ccba6b5dcb42edd2c6f8bb4ba56c492cb2ef7ccf56815663743e07.
Tacking it onto this issue because its presence confused me when deciding whether the `pseuds_for_sidebar` method belonged in the user helper or the pseud helper.
---------
Co-authored-by: sarken
---
app/helpers/pseuds_helper.rb | 12 +++++++++---
app/helpers/users_helper.rb | 11 -----------
app/views/users/_sidebar.html.erb | 5 ++---
features/other_a/pseuds.feature | 4 ++--
4 files changed, 13 insertions(+), 19 deletions(-)
diff --git a/app/helpers/pseuds_helper.rb b/app/helpers/pseuds_helper.rb
index cc3b1234193..997023d01a4 100644
--- a/app/helpers/pseuds_helper.rb
+++ b/app/helpers/pseuds_helper.rb
@@ -37,9 +37,15 @@ def print_pseud_list(user, pseuds, first: true)
end
end
+ def pseuds_for_sidebar(user, pseud)
+ pseuds = user.pseuds.abbreviated_list - [pseud]
+ pseuds = pseuds.sort
+ pseuds = [pseud] + pseuds if pseud && !pseud.new_record?
+ pseuds
+ end
+
# used in the sidebar
- def print_pseud_selector(pseuds)
- pseuds -= [@pseud] if @pseud && @pseud.new_record?
- list = pseuds.sort.collect {|pseud| "" + span_if_current(pseud.name, [pseud.user, pseud]) + " "}.join("").html_safe
+ def pseud_selector(pseuds)
+ pseuds.collect { |pseud| "#{span_if_current(pseud.name, [pseud.user, pseud])} " }.join.html_safe
end
end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index c4f56e26113..b6a6b173a2b 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -15,17 +15,6 @@ def is_maintainer?
current_user.is_a?(User) ? current_user.maintained_collections.present? : false
end
- def sidebar_pseud_link_text(user, pseud)
- text = if current_page?(user)
- ts('Pseuds')
- elsif pseud.present? && !pseud.new_record?
- pseud.name
- else
- user.login
- end
- (text + ' ↓').html_safe
- end
-
# Prints user pseuds with links to anchors for each pseud on the page and the description as the title
def print_pseuds(user)
user.pseuds.collect(&:name).join(', ')
diff --git a/app/views/users/_sidebar.html.erb b/app/views/users/_sidebar.html.erb
index cbd8264ba54..429c8ebcc95 100644
--- a/app/views/users/_sidebar.html.erb
+++ b/app/views/users/_sidebar.html.erb
@@ -5,10 +5,9 @@
<%= span_if_current ts("Profile"), user_profile_path(@user) %>
<% if @user.pseuds.size > 1 %>
- <% pseud_link_text = current_page?(@user) ? ts("Pseuds") : (@pseud ? @pseud.name : @user.login) %>
- "><%= pseud_link_text %>
+ "><%= ts("Pseuds") %>
- <%= print_pseud_selector(@user.pseuds.abbreviated_list) %>
+ <%= pseud_selector(pseuds_for_sidebar(@user, @pseud)) %>
<%= span_if_current ts("All Pseuds (%{pseud_number})", :pseud_number => @user.pseuds.count), user_pseuds_path(@user) %>
diff --git a/features/other_a/pseuds.feature b/features/other_a/pseuds.feature
index c8e40515e0d..b35f9414982 100644
--- a/features/other_a/pseuds.feature
+++ b/features/other_a/pseuds.feature
@@ -158,8 +158,8 @@ Scenario: Many pseuds
And I should see "All Pseuds (4)" within "ul.expandable"
When I go to my "Slartibartfast" pseud page
- Then I should see "Slartibartfast" within "li.pseud > a"
- And I should not see "Slartibartfast" within "ul.expandable"
+ Then I should see "Pseuds" within "li.pseud > a"
+ And I should see "Slartibartfast" within "ul.expandable"
When I go to my pseuds page
Then I should not see "Zaphod (Zaphod)" within "ul.pseud.index"
From f09c964c3c2326fb17dd0b908b670e5df18c1593 Mon Sep 17 00:00:00 2001
From: Skawt
Date: Wed, 19 Jul 2023 00:24:45 -0700
Subject: [PATCH 008/208] AO3-6560 Add Tumblr links to error pages (#4583)
* AO3-6560 Add hyperlinks to Tumblr and updated test support form references
* AO3-6560 Fix typo in tumblr link
* AO3-6560 Removing unnecessary test text changes
* Delete .gitkeep
* AO3-6560 Run i18n tasks to clear up test failures
---
app/views/feedbacks/new.html.erb | 2 +-
config/locales/views/en.yml | 5 +++--
public/502.html | 2 +-
public/503.html | 2 +-
public/507.html | 2 +-
public/nomaintenance.html | 2 +-
public/status/index.html | 2 +-
7 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/app/views/feedbacks/new.html.erb b/app/views/feedbacks/new.html.erb
index d2dde42cdb5..aecfa301a66 100644
--- a/app/views/feedbacks/new.html.erb
+++ b/app/views/feedbacks/new.html.erb
@@ -21,7 +21,7 @@
<% else %>
<%= t(".heading.instructions") %>
-
<%= t(".status.current", twitter_link: link_to(t(".status.twitter"), "https://twitter.com/AO3_Status")).html_safe %>
+
<%= t(".status.current", twitter_link: link_to(t(".status.twitter"), "https://twitter.com/AO3_Status"), tumblr_link: link_to(t(".status.tumblr"), "https://ao3org.tumblr.com")).html_safe %>
<%= t(".abuse.reports", contact_link: link_to(t(".abuse.contact"), new_abuse_report_path)).html_safe %>
<%= t(".reportable.intro") %>
diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml
index e0becc48b3e..8b3d5cdd91f 100644
--- a/config/locales/views/en.yml
+++ b/config/locales/views/en.yml
@@ -423,8 +423,9 @@ en:
tag_changes: Requests to canonize or change tags
work_problems: Works labeled with the wrong language or duplicate works
status:
- current: 'For current updates on Archive performance or downtime, please check out our Twitter feed: %{twitter_link}.'
- twitter: "@AO3_Status"
+ current: For current updates on Archive performance or downtime, please check the %{twitter_link} or %{tumblr_link}.
+ tumblr: ao3org Tumblr
+ twitter: "@AO3_Status Twitter feed"
home:
donate:
page_title: Donate or Volunteer
diff --git a/public/502.html b/public/502.html
index 3dd6e076d13..7288583a395 100644
--- a/public/502.html
+++ b/public/502.html
@@ -102,7 +102,7 @@ Error 502
The page was responding too slowly.
We're experiencing heavy load. The problem should be temporary; try refreshing the page.
- Follow @AO3_Status on Twitter for updates if this keeps happening.
+ Follow @AO3_Status on Twitter or ao3org on Tumblr for updates if this keeps happening.
diff --git a/public/503.html b/public/503.html
index 1a6112a70fd..d37bbaa37ef 100644
--- a/public/503.html
+++ b/public/503.html
@@ -102,7 +102,7 @@ Site Navigation
Error 503
The page was responding too slowly.
- Follow @AO3_Status on Twitter for updates if this keeps happening.
+ Follow @AO3_Status on Twitter or ao3org on Tumblr for updates if this keeps happening.
diff --git a/public/507.html b/public/507.html
index d4750e5a68f..4d6542e966f 100644
--- a/public/507.html
+++ b/public/507.html
@@ -102,7 +102,7 @@ Site Navigation
Error 507
Exceeded maximum posting rate.
To combat bots, we are currently banning IP addresses that post too many works in a short time period. If you see this page repeatedly please pause a while between posting works. If you are banned, you will be unable to access the Archive. Access will be restored 24 hours after the ban started.
- Follow @AO3_Status on Twitter for updates.
+ Follow @AO3_Status on Twitter or ao3org on Tumblr for updates.
diff --git a/public/nomaintenance.html b/public/nomaintenance.html
index 4a8dbf3b576..28d0d8ea51e 100644
--- a/public/nomaintenance.html
+++ b/public/nomaintenance.html
@@ -100,7 +100,7 @@ Site Navigation
Error 503 - Service unavailable
The Archive is down for maintenance.
-
If @AO3_Status says the site is up, but you still see this page, try clearing your browser cache and refreshing the page.
+
If @AO3_Status and ao3org say the site is up, but you still see this page, try clearing your browser cache and refreshing the page.
diff --git a/public/status/index.html b/public/status/index.html
index 1958b3d5983..f2abe4e72a4 100644
--- a/public/status/index.html
+++ b/public/status/index.html
@@ -162,7 +162,7 @@ Development
$j("#tabs").tabs({
beforeLoad: function(event, ui){
ui.jqXHR.error(function(){
- ui.panel.html("We are having issues loading this page." + 'Follow @AO3_Status on Twitter for updates.');
+ ui.panel.html("We are having issues loading this page." + 'Follow @AO3_Status on Twitter or ao3org on Tumblr for updates.');
});
}
});
From 702394ddc76edd81c901f81710c135db1365657f Mon Sep 17 00:00:00 2001
From: neuroalien <105230050+neuroalien@users.noreply.github.com>
Date: Wed, 26 Jul 2023 23:16:37 +0100
Subject: [PATCH 009/208] AO3-6568 Remove webdrivers gem (#4588)
Co-authored-by: Sarken
---
Gemfile | 1 -
Gemfile.lock | 5 -----
2 files changed, 6 deletions(-)
diff --git a/Gemfile b/Gemfile
index 0ff372463ec..c156ca636db 100644
--- a/Gemfile
+++ b/Gemfile
@@ -121,7 +121,6 @@ group :test do
gem "cucumber"
gem 'database_cleaner'
gem "selenium-webdriver"
- gem "webdrivers"
gem 'capybara-screenshot'
gem 'cucumber-rails', require: false
gem 'launchy' # So you can do Then show me the page
diff --git a/Gemfile.lock b/Gemfile.lock
index 88d9c8bffe7..4be0139ca98 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -572,10 +572,6 @@ GEM
rack (>= 1.0.0)
warden (1.2.9)
rack (>= 2.0.9)
- webdrivers (5.2.0)
- nokogiri (~> 1.6)
- rubyzip (>= 1.3.0)
- selenium-webdriver (~> 4.0)
webmock (3.18.1)
addressable (>= 2.8.0)
crack (>= 0.3.2)
@@ -692,7 +688,6 @@ DEPENDENCIES
unicorn (~> 5.5)
unidecoder
vcr (~> 3.0, >= 3.0.1)
- webdrivers
webmock
whenever (~> 0.6.2)
whiny_validation
From ebd7fbd1c3e2660aaf84a74e73b3db87c0c418c1 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 31 Jul 2023 22:17:26 -0400
Subject: [PATCH 010/208] AO3-6559 Bump sanitize from 6.0.1 to 6.0.2 (#4566)
Bump sanitize from 6.0.1 to 6.0.2
Bumps [sanitize](https://github.com/rgrove/sanitize) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/rgrove/sanitize/releases)
- [Changelog](https://github.com/rgrove/sanitize/blob/main/HISTORY.md)
- [Commits](https://github.com/rgrove/sanitize/compare/v6.0.1...v6.0.2)
---
updated-dependencies:
- dependency-name: sanitize
dependency-type: direct:production
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Gemfile.lock | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/Gemfile.lock b/Gemfile.lock
index 4be0139ca98..7b99e789a8b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -341,7 +341,7 @@ GEM
nokogiri (~> 1)
rake
mini_mime (1.1.2)
- mini_portile2 (2.8.1)
+ mini_portile2 (2.8.2)
minitest (5.17.0)
mono_logger (1.1.1)
multi_json (1.15.0)
@@ -393,7 +393,7 @@ GEM
pundit (2.1.1)
activesupport (>= 3.0.0)
raabro (1.4.0)
- racc (1.6.2)
+ racc (1.7.1)
rack (2.2.6.4)
rack-attack (6.6.0)
rack (>= 1.0, < 3)
@@ -508,7 +508,7 @@ GEM
fugit (~> 1.1, >= 1.1.6)
rvm-capistrano (1.5.6)
capistrano (~> 2.15.4)
- sanitize (6.0.1)
+ sanitize (6.0.2)
crass (~> 1.0.2)
nokogiri (>= 1.12.0)
selenium-webdriver (4.8.1)
From 7af7953bdf5e5a9902e1a79fb866538281e893e7 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Mon, 31 Jul 2023 22:20:25 -0400
Subject: [PATCH 011/208] AO3-6562 Bump audited from 4.10.0 to 5.3.3 (#4507)
Bump audited from 4.10.0 to 5.3.3
Bumps [audited](https://github.com/collectiveidea/audited) from 4.10.0 to 5.3.3.
- [Release notes](https://github.com/collectiveidea/audited/releases)
- [Changelog](https://github.com/collectiveidea/audited/blob/main/CHANGELOG.md)
- [Commits](https://github.com/collectiveidea/audited/compare/v4.10.0...v5.3.3)
---
updated-dependencies:
- dependency-name: audited
dependency-type: direct:production
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Gemfile | 2 +-
Gemfile.lock | 15 ++++++++-------
2 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/Gemfile b/Gemfile
index c156ca636db..4a5b053ba05 100644
--- a/Gemfile
+++ b/Gemfile
@@ -100,7 +100,7 @@ gem 'phraseapp-in-context-editor-ruby', '>=1.0.6'
# For URL mangling
gem 'addressable'
-gem 'audited', '~> 4.4'
+gem 'audited', '~> 5.3'
# For controlling application behavour dynamically
gem 'rollout'
diff --git a/Gemfile.lock b/Gemfile.lock
index 7b99e789a8b..567490619f6 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -99,8 +99,9 @@ GEM
activesupport
akismetor (1.0.0)
ast (2.4.2)
- audited (4.10.0)
- activerecord (>= 4.2, < 6.2)
+ audited (5.3.3)
+ activerecord (>= 5.0, < 7.1)
+ request_store (~> 1.2)
awesome_print (1.9.2)
aws-eventstream (1.2.0)
aws-partitions (1.553.0)
@@ -162,7 +163,7 @@ GEM
chronic (0.10.2)
climate_control (0.2.0)
coderay (1.1.3)
- concurrent-ruby (1.2.0)
+ concurrent-ruby (1.2.2)
connection_pool (2.2.5)
crack (0.4.5)
rexml
@@ -286,7 +287,7 @@ GEM
httparty (0.21.0)
mini_mime (>= 1.0.0)
multi_xml (>= 0.5.2)
- i18n (1.12.0)
+ i18n (1.13.0)
concurrent-ruby (~> 1.0)
i18n-tasks (1.0.12)
activesupport (>= 4.0.2)
@@ -554,7 +555,7 @@ GEM
tilt (2.0.11)
timecop (0.9.4)
timeliness (0.4.4)
- tzinfo (2.0.5)
+ tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unf (0.1.4)
unf_ext
@@ -591,7 +592,7 @@ GEM
will_paginate (3.3.1)
xpath (3.2.0)
nokogiri (~> 1.8)
- zeitwerk (2.6.6)
+ zeitwerk (2.6.8)
PLATFORMS
ruby
@@ -604,7 +605,7 @@ DEPENDENCIES
addressable
after_commit_everywhere
akismetor
- audited (~> 4.4)
+ audited (~> 5.3)
awesome_print
aws-sdk-s3
bcrypt
From 23c2df681a4e6862e1f927cd75e01aa5aa20c529 Mon Sep 17 00:00:00 2001
From: tickinginstant
Date: Fri, 4 Aug 2023 18:18:25 -0400
Subject: [PATCH 012/208] AO3-6567 Use % for translate_string instead of
I18n.translate. (#4586)
* AO3-6567 Use % for translate_string.
* AO3-6567 Fix ts call with missing interpolation.
* AO3-6567 Fix syntax on line 6.
---
.../_tag_nominations_by_fandom.html.erb | 6 +++---
config/initializers/monkeypatches/translate_string.rb | 10 ++++------
2 files changed, 7 insertions(+), 9 deletions(-)
diff --git a/app/views/tag_set_nominations/_tag_nominations_by_fandom.html.erb b/app/views/tag_set_nominations/_tag_nominations_by_fandom.html.erb
index 3ea2735cf7d..5268ce28580 100644
--- a/app/views/tag_set_nominations/_tag_nominations_by_fandom.html.erb
+++ b/app/views/tag_set_nominations/_tag_nominations_by_fandom.html.erb
@@ -1,14 +1,14 @@
<% @tag_set_nomination.fandom_nominations.each_with_index do |nom, index| %>
<%= f.fields_for :fandom_nominations, nom do |nom_form| %>
- <%= ts("Fandom %{index}", :index => index+1) %>
+ <%= ts("Fandom %{index}", index: index + 1) %>
- <%= nom_form.label :tagname, ts("Fandom %{index}", :index => index+1) %>
+ <%= nom_form.label :tagname, ts("Fandom %{index}", index: index + 1) %>
<% if nom.approved || nom.rejected %>
<%= nom.tagname %> <%= nomination_status(nom) %>
<% else %>
- "><%= nom_form.text_field :tagname, autocomplete_options("fandom", data: { autocomplete_token_limit: 1 }, class: "autocomplete") %>
+ "><%= nom_form.text_field :tagname, autocomplete_options("fandom", data: { autocomplete_token_limit: 1 }, class: "autocomplete") %>
<% end %>
diff --git a/config/initializers/monkeypatches/translate_string.rb b/config/initializers/monkeypatches/translate_string.rb
index 7786903a1e0..fbd81a6d3c5 100644
--- a/config/initializers/monkeypatches/translate_string.rb
+++ b/config/initializers/monkeypatches/translate_string.rb
@@ -1,11 +1,11 @@
module I18n
class << self
- # A shorthand for translation that takes a string as its first argument, which
- # will be the default string.
+ # Formats a string. Used to mark strings that should eventually be
+ # translated with I18n, but aren't at the moment.
#
# Deprecated.
def translate_string(str, **options)
- translate(str, **options.merge(default: str))
+ str % options
end
alias :ts :translate_string
@@ -65,13 +65,11 @@ def translate_string(str, **options)
end
end
-# Note: we define this separately for ActionView so that we get the controller/action name
-# in the key, and use the added scoping for translate in TranslationHelper.
module ActionView
module Helpers
module TranslationHelper
def translate_string(str, **options)
- translate(str, **options.merge(default: str))
+ str % options
end
alias :ts :translate_string
From e763f328b57554dd19ae9ae689f2b2cac6897fb3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=89ric=20B?=
Date: Sat, 5 Aug 2023 00:18:52 +0200
Subject: [PATCH 013/208] AO3-6545 Default FAQ to last valid locale instead of
always English (#4577)
* Default FAQ to last valid locale
https://otwarchive.atlassian.net/browse/AO3-6545
* Hound
* Improve logged in tests
* Better test name?
* Handle feature flag on and off
* More detailed context
---
app/controllers/archive_faqs_controller.rb | 11 +-
.../archive_faqs_controller_spec.rb | 106 +++++++++++++-----
2 files changed, 86 insertions(+), 31 deletions(-)
diff --git a/app/controllers/archive_faqs_controller.rb b/app/controllers/archive_faqs_controller.rb
index 7739cab5097..11a7935be81 100644
--- a/app/controllers/archive_faqs_controller.rb
+++ b/app/controllers/archive_faqs_controller.rb
@@ -127,26 +127,25 @@ def default_url_options
# Set the locale as an instance variable first
def set_locale
- session[:language_id] = params[:language_id].presence if session[:language_id] != params[:language_id].presence
+ session[:language_id] = params[:language_id] if Locale.exists?(iso: params[:language_id])
if current_user.present? && $rollout.active?(:set_locale_preference,
current_user)
- @i18n_locale = session[:language_id] || Locale.find(current_user.
- preference.preferred_locale).iso
+ @i18n_locale = session[:language_id].presence || Locale.find(current_user.preference.preferred_locale).iso
else
- @i18n_locale = session[:language_id] || I18n.default_locale
+ @i18n_locale = session[:language_id].presence || I18n.default_locale
end
end
def validate_locale
- return if Locale.exists?(iso: @i18n_locale)
+ return if params[:language_id].blank? || Locale.exists?(iso: params[:language_id])
flash[:error] = "The specified locale does not exist."
redirect_to url_for(request.query_parameters.merge(language_id: I18n.default_locale))
end
def require_language_id
- return if params[:language_id].present?
+ return if params[:language_id].present? && Locale.exists?(iso: params[:language_id])
redirect_to url_for(request.query_parameters.merge(language_id: @i18n_locale.to_s))
end
diff --git a/spec/controllers/archive_faqs_controller_spec.rb b/spec/controllers/archive_faqs_controller_spec.rb
index b2696101578..d08ddea1032 100644
--- a/spec/controllers/archive_faqs_controller_spec.rb
+++ b/spec/controllers/archive_faqs_controller_spec.rb
@@ -6,38 +6,56 @@
include LoginMacros
include RedirectExpectationHelper
- describe "GET #index" do
- it "renders the index page anyway when the locale param is invalid" do
- expect(I18n).to receive(:with_locale).with("eldritch").and_call_original
- get :index, params: { language_id: "eldritch" }
- expect(response).to render_template(:index)
- end
+ let(:non_standard_locale) { create(:locale) }
+ let(:user_locale) { create(:locale) }
+ let(:user) do
+ user = create(:user)
+ user.preference.update!(preferred_locale: user_locale.id)
+ user
+ end
- it "redirects to the default locale when the locale param is empty" do
- expect(I18n).not_to receive(:with_locale)
- get :index, params: { language_id: "" }
- it_redirects_to(archive_faqs_path(language_id: I18n.default_locale))
- end
+ describe "GET #index" do
+ context "when there's no locale in session" do
+ it "redirects to the default locale when the locale param is invalid" do
+ expect(I18n).not_to receive(:with_locale)
+ get :index, params: { language_id: "eldritch" }
+ it_redirects_to(archive_faqs_path(language_id: I18n.default_locale))
+ end
- it "redirects to the default locale when the locale param is empty and the session locale is not" do
- expect(I18n).not_to receive(:with_locale)
- get :index, params: { language_id: "" }, session: { language_id: "eldritch" }
- it_redirects_to(archive_faqs_path(language_id: "en"))
- end
+ it "redirects to the default locale when the locale param is empty" do
+ expect(I18n).not_to receive(:with_locale)
+ get :index, params: { language_id: "" }
+ it_redirects_to(archive_faqs_path(language_id: I18n.default_locale))
+ end
- it "redirects to the default locale when the locale param and the session locale are empty" do
- expect(I18n).not_to receive(:with_locale)
- get :index, params: { language_id: "" }, session: { language_id: "" }
- it_redirects_to(archive_faqs_path(language_id: "en"))
+ it "redirects to the default locale when the locale param and the session locale are _explicty_ empty (legacy session behavior)" do
+ expect(I18n).not_to receive(:with_locale)
+ get :index, params: { language_id: "" }, session: { language_id: "" }
+ it_redirects_to(archive_faqs_path(language_id: "en"))
+ end
end
context "when logged in as a regular user" do
- before { fake_login }
+ before { fake_login_known_user(user) }
- it "renders the index page anyway when the locale param is invalid" do
- expect(I18n).to receive(:with_locale).with("eldritch").and_call_original
- get :index, params: { language_id: "eldritch" }
- expect(response).to render_template(:index)
+ context "when the set locale preference feature flag is off" do
+ before { $rollout.deactivate_user(:set_locale_preference, user) }
+
+ it "redirects to the default locale when the locale param is invalid" do
+ expect(I18n).not_to receive(:with_locale)
+ get :index, params: { language_id: "eldritch" }
+ it_redirects_to(archive_faqs_path(language_id: I18n.default_locale))
+ end
+ end
+
+ context "when the set locale preference feature flag is on" do
+ before { $rollout.activate_user(:set_locale_preference, user) }
+
+ it "redirects to the user preferred locale when the locale param is invalid" do
+ expect(I18n).not_to receive(:with_locale)
+ get :index, params: { language_id: "eldritch" }
+ it_redirects_to(archive_faqs_path(language_id: user_locale.iso))
+ end
end
end
@@ -51,6 +69,44 @@
"The specified locale does not exist.")
end
end
+
+ context "when there's a locale in session" do
+ before do
+ get :index, params: { language_id: non_standard_locale.iso }
+ expect(response).to render_template(:index)
+ expect(session[:language_id]).to eq(non_standard_locale.iso)
+ end
+
+ it "redirects to the previous locale when the locale param is empty" do
+ get :index
+ it_redirects_to(archive_faqs_path(language_id: non_standard_locale.iso))
+ end
+
+ it "redirects to the previous locale when the locale param is invalid" do
+ get :index, params: { language_id: "eldritch" }
+ it_redirects_to(archive_faqs_path(language_id: non_standard_locale.iso))
+ end
+
+ context "when logged in as a regular user" do
+ before do
+ fake_login_known_user(user)
+ end
+
+ it "still redirects to the previous locale when the locale param is invalid" do
+ get :index, params: { language_id: "eldritch" }
+ it_redirects_to(archive_faqs_path(language_id: non_standard_locale.iso))
+ end
+
+ context "with set_locale_preference" do
+ before { $rollout.activate_user(:set_locale_preference, user) }
+
+ it "still redirects to the previous locale when the locale param is invalid" do
+ get :index, params: { language_id: "eldritch" }
+ it_redirects_to(archive_faqs_path(language_id: non_standard_locale.iso))
+ end
+ end
+ end
+ end
end
describe "GET #show" do
From c33817cfc577cd3a874c24d22419d9efff5e58ef Mon Sep 17 00:00:00 2001
From: weeklies <80141759+weeklies@users.noreply.github.com>
Date: Sat, 5 Aug 2023 06:19:28 +0800
Subject: [PATCH 014/208] AO3-6488 Optimize Challenge queries to make orphaned
works editable (#4561)
* AO3-6488 Use optimized find over find_by for loading ChallengeClaim
* Make query of ChallengeAssignment more efficient
* AO3-6488 Good to see you, Hound
* Filter nil values within challenge assignments
* AO3-6488 Try to sidestep loading all orphaned pseuds
* AO3-6488 Return empty array for orphan_account assignments
* AO3-6488 Give optimization a valiant try
* AO3-6488 Remove orphan_account special case
* AO3-6488 Second try
* AO3-6488 Feedback
---
app/helpers/works_helper.rb | 7 +++++++
app/models/work.rb | 6 ++++--
app/views/works/_standard_form.html.erb | 3 ++-
3 files changed, 13 insertions(+), 3 deletions(-)
diff --git a/app/helpers/works_helper.rb b/app/helpers/works_helper.rb
index 2cb03bab5f1..f6965a54c7e 100644
--- a/app/helpers/works_helper.rb
+++ b/app/helpers/works_helper.rb
@@ -191,4 +191,11 @@ def chapter_total_display_with_link(work)
chapter_total_display(work)
end
end
+
+ def get_open_assignments(user)
+ offer_signups = user.offer_assignments.undefaulted.unstarted.sent
+ pinch_hits = user.pinch_hit_assignments.undefaulted.unstarted.sent
+
+ (offer_signups + pinch_hits)
+ end
end
diff --git a/app/models/work.rb b/app/models/work.rb
index eb15f4d9695..206ea254d8d 100755
--- a/app/models/work.rb
+++ b/app/models/work.rb
@@ -454,9 +454,11 @@ def challenge_claim_ids
# Only allow a work to fulfill an assignment assigned to one of this work's authors
def challenge_assignment_ids=(ids)
+ valid_users = (self.users + [User.current_user]).compact
+
self.challenge_assignments =
- ids.map { |id| id.blank? ? nil : ChallengeAssignment.find(id) }.compact.
- select { |assign| (self.users + [User.current_user]).compact.include?(assign.offering_user) }
+ ChallengeAssignment.where(id: ids)
+ .select { |assign| valid_users.include?(assign.offering_user) }
end
def recipients=(recipient_names)
diff --git a/app/views/works/_standard_form.html.erb b/app/views/works/_standard_form.html.erb
index 6e97a8b341c..2dbcdf2286c 100644
--- a/app/views/works/_standard_form.html.erb
+++ b/app/views/works/_standard_form.html.erb
@@ -46,7 +46,8 @@
<%= ts("Associations") %>
<%= ts("Associations") %>
- <% if !(@assignments = ChallengeAssignment.by_offering_user(current_user).undefaulted.unstarted.sent).empty? || !@work.challenge_assignments.empty? %>
+ <% @assignments = get_open_assignments(current_user) %>
+ <% if @assignments.any? || @work.challenge_assignments.any? %>
<%= f.label "challenge_assignment_ids[]", ts("Does this fulfill a challenge assignment") %>
From 596cf3676a2128968161c745828f1c4abcead331 Mon Sep 17 00:00:00 2001
From: Mitch Stark <61703668+MitchStark10@users.noreply.github.com>
Date: Fri, 4 Aug 2023 17:19:45 -0500
Subject: [PATCH 015/208] AO3-6530 Display validation messages on top of footer
(#4571)
add z-index to validation message to ensure it is visible above footer
---
public/stylesheets/site/2.0/07-interactions.css | 1 +
1 file changed, 1 insertion(+)
diff --git a/public/stylesheets/site/2.0/07-interactions.css b/public/stylesheets/site/2.0/07-interactions.css
index 77af02ef2dc..66cae9fc64c 100644
--- a/public/stylesheets/site/2.0/07-interactions.css
+++ b/public/stylesheets/site/2.0/07-interactions.css
@@ -180,6 +180,7 @@ We only use error messages for LiveValidation. Style spoofs the system error mes
position: absolute;
margin-top: 0.643em;
margin-right: 15em;
+ z-index: 1;
}
.LV_invalid {
From 279521279e510d9da9dac5e96262a9be4c5488e6 Mon Sep 17 00:00:00 2001
From: tickinginstant
Date: Fri, 4 Aug 2023 18:20:09 -0400
Subject: [PATCH 016/208] AO3-4867 & AO3-4868 Prevent pseud ambiguity issues.
(#4570)
* AO3-4867 & AO3-4868 Prevent pseud ambiguity issues.
* AO3-4867 & AO3-4868 Try to fix errors.
* AO3-4867 & AO3-4868 Fix tests & add new tests.
* AO3-4867 & AO3-4868 More OwnedTagSet tests.
---
.../challenge_assignments_controller.rb | 2 +-
.../collection_participants_controller.rb | 2 +-
app/controllers/gifts_controller.rb | 2 +-
app/models/block.rb | 4 +-
app/models/challenge_assignment.rb | 28 ++--
app/models/collection_item.rb | 2 +-
app/models/concerns/creatable.rb | 12 +-
app/models/creatorship.rb | 2 +-
app/models/gift.rb | 10 +-
app/models/mute.rb | 4 +-
app/models/pseud.rb | 80 ++++++-----
lib/creation_notifier.rb | 2 +-
spec/models/challenge_assignment_spec.rb | 81 +++++++++++
spec/models/owned_tag_set_spec.rb | 128 ++++++++++++++++++
14 files changed, 281 insertions(+), 78 deletions(-)
create mode 100644 spec/models/owned_tag_set_spec.rb
diff --git a/app/controllers/challenge_assignments_controller.rb b/app/controllers/challenge_assignments_controller.rb
index 43d8905e6f5..9cbe0b8790d 100644
--- a/app/controllers/challenge_assignments_controller.rb
+++ b/app/controllers/challenge_assignments_controller.rb
@@ -191,7 +191,7 @@ def update_multiple
when "cover"
# cover_[assignment_id] = pinch hitter pseud
next if val.blank? || assignment.pinch_hitter.try(:byline) == val
- pseud = Pseud.parse_byline(val).first
+ pseud = Pseud.parse_byline(val)
if pseud.nil?
@errors << ts("We couldn't find the user #{val} to assign that to.")
else
diff --git a/app/controllers/collection_participants_controller.rb b/app/controllers/collection_participants_controller.rb
index 8b90af0cad9..5c3f4ac0a73 100644
--- a/app/controllers/collection_participants_controller.rb
+++ b/app/controllers/collection_participants_controller.rb
@@ -89,7 +89,7 @@ def destroy
def add
@participants_added = []
@participants_invited = []
- pseud_results = Pseud.parse_bylines(params[:participants_to_invite], assume_matching_login: true)
+ pseud_results = Pseud.parse_bylines(params[:participants_to_invite])
pseud_results[:pseuds].each do |pseud|
if @collection.participants.include?(pseud)
participant = CollectionParticipant.where(collection_id: @collection.id, pseud_id: pseud.id).first
diff --git a/app/controllers/gifts_controller.rb b/app/controllers/gifts_controller.rb
index 8d1c0f43f2b..87a11a9ecc0 100644
--- a/app/controllers/gifts_controller.rb
+++ b/app/controllers/gifts_controller.rb
@@ -21,7 +21,7 @@ def index
end
end
else
- pseud = Pseud.parse_byline(@recipient_name, assume_matching_login: true).first
+ pseud = Pseud.parse_byline(@recipient_name)
if pseud
if current_user.nil?
@works = pseud.gift_works.visible_to_all
diff --git a/app/models/block.rb b/app/models/block.rb
index 1cb61af74d6..f7937ee7ffd 100644
--- a/app/models/block.rb
+++ b/app/models/block.rb
@@ -21,7 +21,7 @@ def check_block_limit
end
def blocked_byline=(byline)
- pseuds = Pseud.parse_byline(byline, assume_matching_login: true)
- self.blocked = pseuds.first.user unless pseuds.empty?
+ pseud = Pseud.parse_byline(byline)
+ self.blocked = pseud.user unless pseud.nil?
end
end
diff --git a/app/models/challenge_assignment.rb b/app/models/challenge_assignment.rb
index 91bc25f999f..93986f1eb15 100755
--- a/app/models/challenge_assignment.rb
+++ b/app/models/challenge_assignment.rb
@@ -169,12 +169,8 @@ def offer_signup_pseud=(pseud_byline)
if pseud_byline.blank?
self.offer_signup = nil
else
- pseuds = Pseud.parse_byline(pseud_byline)
- if pseuds.size == 1
- pseud = pseuds.first
- signup = ChallengeSignup.in_collection(self.collection).where(pseud_id: pseud.id).first
- self.offer_signup = signup if signup
- end
+ signup = signup_for_byline(pseud_byline)
+ self.offer_signup = signup if signup
end
end
@@ -186,13 +182,8 @@ def request_signup_pseud=(pseud_byline)
if pseud_byline.blank?
self.request_signup = nil
else
- pseuds = Pseud.parse_byline(pseud_byline)
- if pseuds.size == 1
- pseud = pseuds.first
- signup = ChallengeSignup.in_collection(self.collection).where(pseud_id: pseud.id).first
- # If there's an existing assignment then this is a pinch recipient
- self.request_signup = signup if signup
- end
+ signup = signup_for_byline(pseud_byline)
+ self.request_signup = signup if signup
end
end
@@ -200,6 +191,11 @@ def request_signup_pseud
self.request_signup.try(:pseud).try(:byline) || ""
end
+ def signup_for_byline(byline)
+ pseud = Pseud.parse_byline(byline)
+ collection.signups.find_by(pseud: pseud)
+ end
+
def title
"#{self.collection.title} (#{self.request_byline})"
end
@@ -230,7 +226,7 @@ def pinch_hitter_byline
end
def pinch_hitter_byline=(byline)
- self.pinch_hitter = Pseud.by_byline(byline).first
+ self.pinch_hitter = Pseud.parse_byline(byline)
end
def pinch_request_byline
@@ -238,8 +234,8 @@ def pinch_request_byline
end
def pinch_request_byline=(byline)
- pinch_pseud = Pseud.by_byline(byline).first
- self.pinch_request_signup = ChallengeSignup.in_collection(self.collection).by_pseud(pinch_pseud).first if pinch_pseud
+ signup = signup_for_byline(byline)
+ self.pinch_request_signup = signup if signup
end
def default
diff --git a/app/models/collection_item.rb b/app/models/collection_item.rb
index 8347e596346..9db2757fc57 100644
--- a/app/models/collection_item.rb
+++ b/app/models/collection_item.rb
@@ -218,7 +218,7 @@ def posted?
def notify_of_reveal
unless self.unrevealed? || !self.posted?
- recipient_pseuds = Pseud.parse_bylines(self.recipients, assume_matching_login: true)[:pseuds]
+ recipient_pseuds = Pseud.parse_bylines(self.recipients)[:pseuds]
recipient_pseuds.each do |pseud|
unless pseud.user.preference.recipient_emails_off
UserMailer.recipient_notification(pseud.user.id, self.item.id, self.collection.id).deliver_after_commit
diff --git a/app/models/concerns/creatable.rb b/app/models/concerns/creatable.rb
index 333f3c81d08..b407bfa90ef 100644
--- a/app/models/concerns/creatable.rb
+++ b/app/models/concerns/creatable.rb
@@ -132,13 +132,13 @@ def pseuds_to_add=(pseud_names)
names = pseud_names.split(",").reject(&:blank?).map(&:strip)
names.each do |name|
- possible_pseuds = Pseud.parse_byline(name)
+ possible_pseuds = Pseud.parse_byline_ambiguous(name)
- if possible_pseuds.size > 1
- possible_pseuds = Pseud.parse_byline(name, assume_matching_login: true)
- end
-
- pseud = possible_pseuds.first
+ pseud = if possible_pseuds.size > 1
+ Pseud.parse_byline(name)
+ else
+ possible_pseuds.first
+ end
if pseud
creatorship = creatorships.find_or_initialize_by(pseud: pseud)
diff --git a/app/models/creatorship.rb b/app/models/creatorship.rb
index a1167afe15d..c9db5842ae7 100644
--- a/app/models/creatorship.rb
+++ b/app/models/creatorship.rb
@@ -206,7 +206,7 @@ def expire_caches
# ambiguous/missing pseuds. By storing the desired name in the @byline
# variable, we can generate nicely formatted messages.
def byline=(byline)
- pseuds = Pseud.parse_byline(byline).to_a
+ pseuds = Pseud.parse_byline_ambiguous(byline).to_a
if pseuds.size == 1
self.pseud = pseuds.first
diff --git a/app/models/gift.rb b/app/models/gift.rb
index 2c7ec5f0962..3d06a317609 100644
--- a/app/models/gift.rb
+++ b/app/models/gift.rb
@@ -43,9 +43,11 @@ def has_not_given_to_user
scope :for_recipient_name, lambda {|name| where("recipient_name = ?", name)}
- scope :for_name_or_byline, lambda {|name| where("recipient_name = ? OR pseud_id = ?",
- name,
- Pseud.parse_byline(name, assume_matching_login: true).first)}
+ scope :for_name_or_byline, lambda { |name|
+ where("recipient_name = ? OR pseud_id = ?",
+ name,
+ Pseud.parse_byline(name))
+ }
scope :in_collection, lambda {|collection|
select("DISTINCT gifts.*").
@@ -62,7 +64,7 @@ def has_not_given_to_user
scope :are_rejected, -> { where(rejected: true) }
def recipient=(new_recipient_name)
- self.pseud = Pseud.parse_byline(new_recipient_name, assume_matching_login: true).first
+ self.pseud = Pseud.parse_byline(new_recipient_name)
self.recipient_name = pseud ? nil : new_recipient_name
end
diff --git a/app/models/mute.rb b/app/models/mute.rb
index f9ff8d60fd9..3bf83870e9e 100644
--- a/app/models/mute.rb
+++ b/app/models/mute.rb
@@ -29,7 +29,7 @@ def update_cache
end
def muted_byline=(byline)
- pseuds = Pseud.parse_byline(byline, assume_matching_login: true)
- self.muted = pseuds.first.user unless pseuds.empty?
+ pseud = Pseud.parse_byline(byline)
+ self.muted = pseud.user unless pseud.nil?
end
end
diff --git a/app/models/pseud.rb b/app/models/pseud.rb
index 374675e2496..e2b53d872d5 100644
--- a/app/models/pseud.rb
+++ b/app/models/pseud.rb
@@ -160,16 +160,6 @@ def unposted_works
@unposted_works = self.works.where(posted: false).order(created_at: :desc)
end
-
- # look up by byline
- scope :by_byline, -> (byline) {
- joins(:user)
- .where('users.login = ? AND pseuds.name = ?',
- (byline.include?('(') ? byline.split('(', 2)[1].strip.chop : byline),
- (byline.include?('(') ? byline.split('(', 2)[0].strip : byline)
- )
- }
-
# Produces a byline that indicates the user's name if pseud is not unique
def byline
(name != user_name) ? "#{name} (#{user_name})" : name
@@ -183,52 +173,58 @@ def byline_was
(past_name != past_user_name) ? "#{past_name} (#{past_user_name})" : past_name
end
- # Parse a string of the "pseud.name (user.login)" format into a pseud
- def self.parse_byline(byline, options = {})
- pseud_name = ""
- login = ""
- if byline.include?("(")
- pseud_name, login = byline.split('(', 2)
- pseud_name = pseud_name.strip
- login = login.strip.chop
- conditions = ['users.login = ? AND pseuds.name = ?', login, pseud_name]
+ # Parse a string of the "pseud.name (user.login)" format into an array
+ # [pseud.name, user.login]. If there is no parenthesized login after the
+ # pseud name, returns [pseud.name, nil].
+ def self.split_byline(byline)
+ pseud_name, login = byline.split("(", 2)
+ [pseud_name&.strip, login&.strip&.delete_suffix(")")]
+ end
+
+ # Parse a string of the "pseud.name (user.login)" format into a pseud. If the
+ # form is just "pseud.name" with no parenthesized login, assumes that
+ # pseud.name = user.login and goes from there.
+ def self.parse_byline(byline)
+ pseud_name, login = split_byline(byline)
+ login ||= pseud_name
+
+ Pseud.joins(:user).find_by(pseuds: { name: pseud_name }, users: { login: login })
+ end
+
+ # Parse a string of the "pseud.name (user.login)" format into a list of
+ # pseuds. Usually this will be just one pseud, but if the byline is of the
+ # form "pseud.name" with no parenthesized user name, it'll look for any pseud
+ # with that name.
+ def self.parse_byline_ambiguous(byline)
+ pseud_name, login = split_byline(byline)
+
+ if login
+ Pseud.joins(:user).where(pseuds: { name: pseud_name }, users: { login: login })
else
- pseud_name = byline.strip
- if options[:assume_matching_login]
- conditions = ['users.login = ? AND pseuds.name = ?', pseud_name, pseud_name]
- else
- conditions = ['pseuds.name = ?', pseud_name]
- end
+ Pseud.where(name: pseud_name)
end
- Pseud.joins(:user).where(conditions)
end
# Takes a comma-separated list of bylines
# Returns a hash containing an array of pseuds and an array of bylines that couldn't be found
- def self.parse_bylines(list, options = {})
+ def self.parse_bylines(bylines)
valid_pseuds = []
- ambiguous_pseuds = {}
failures = []
banned_pseuds = []
- bylines = list.split ","
- for byline in bylines
- pseuds = Pseud.parse_byline(byline, options)
- if pseuds.length == 1
- pseud = pseuds.first
- if pseud.user.banned? || pseud.user.suspended?
- banned_pseuds << pseud
- else
- valid_pseuds << pseud
- end
- elsif pseuds.length > 1
- ambiguous_pseuds[pseuds.first.name] = pseuds
- else
+
+ bylines.split(",").each do |byline|
+ pseud = parse_byline(byline)
+ if pseud.nil?
failures << byline.strip
+ elsif pseud.user.banned? || pseud.user.suspended?
+ banned_pseuds << pseud
+ else
+ valid_pseuds << pseud
end
end
+
{
pseuds: valid_pseuds.flatten.uniq,
- ambiguous_pseuds: ambiguous_pseuds,
invalid_pseuds: failures,
banned_pseuds: banned_pseuds.flatten.uniq.map(&:byline)
}
diff --git a/lib/creation_notifier.rb b/lib/creation_notifier.rb
index 03b195ce242..3b7eac2bf8b 100644
--- a/lib/creation_notifier.rb
+++ b/lib/creation_notifier.rb
@@ -31,7 +31,7 @@ def do_notify
def notify_recipients
return unless self.posted && self.new_gifts.present? && !self.unrevealed?
- recipient_pseuds = Pseud.parse_bylines(self.new_gifts.collect(&:recipient).join(","), assume_matching_login: true)[:pseuds]
+ recipient_pseuds = Pseud.parse_bylines(self.new_gifts.collect(&:recipient).join(","))[:pseuds]
# check user prefs to see which recipients want to get gift notifications
# (since each user has only one preference item, this removes duplicates)
recip_ids = Preference.where(user_id: recipient_pseuds.map(&:user_id),
diff --git a/spec/models/challenge_assignment_spec.rb b/spec/models/challenge_assignment_spec.rb
index f4b1b2766cb..94b1278fe10 100644
--- a/spec/models/challenge_assignment_spec.rb
+++ b/spec/models/challenge_assignment_spec.rb
@@ -83,4 +83,85 @@
end
end
+ describe "request_signup_pseud=" do
+ let!(:collection) { create(:collection, challenge: create(:gift_exchange)) }
+
+ let(:assignment) { collection.assignments.build }
+
+ context "when a user has signed up with a pseud matching their login" do
+ let(:user) { create(:user) }
+
+ let!(:signup) do
+ create(:challenge_signup,
+ collection: collection,
+ pseud: user.default_pseud)
+ end
+
+ it "assigns the user's signup when entering the user's login" do
+ assignment.request_signup_pseud = user.login
+ expect(assignment.request_signup).to eq(signup)
+ end
+
+ context "when another user has signed up with the same pseud name" do
+ let(:ambiguous) { create(:pseud, name: user.login) }
+
+ let!(:ambiguous_signup) do
+ create(:challenge_signup,
+ collection: collection,
+ pseud: ambiguous)
+ end
+
+ it "assigns the first user's signup when entering the first user's login" do
+ assignment.request_signup_pseud = user.login
+ expect(assignment.request_signup).to eq(signup)
+ end
+
+ it "assigns the second user's signup when entering the full byline for the other user's pseud" do
+ assignment.request_signup_pseud = "#{ambiguous.name} (#{ambiguous.user.login})"
+ expect(assignment.request_signup).to eq(ambiguous_signup)
+ end
+ end
+ end
+ end
+
+ describe "offer_signup_pseud=" do
+ let!(:collection) { create(:collection, challenge: create(:gift_exchange)) }
+
+ let(:assignment) { collection.assignments.build }
+
+ context "when a user has signed up with a pseud matching their login" do
+ let(:user) { create(:user) }
+
+ let!(:signup) do
+ create(:challenge_signup,
+ collection: collection,
+ pseud: user.default_pseud)
+ end
+
+ it "assigns the user's signup when entering the user's login" do
+ assignment.offer_signup_pseud = user.login
+ expect(assignment.offer_signup).to eq(signup)
+ end
+
+ context "when another user has signed up with the same pseud name" do
+ let(:ambiguous) { create(:pseud, name: user.login) }
+
+ let!(:ambiguous_signup) do
+ create(:challenge_signup,
+ collection: collection,
+ pseud: ambiguous)
+ end
+
+ it "assigns the first user's signup when entering the first user's login" do
+ assignment.offer_signup_pseud = user.login
+ expect(assignment.offer_signup).to eq(signup)
+ end
+
+ it "assigns the second user's signup when entering the full byline for the other user's pseud" do
+ assignment.offer_signup_pseud = "#{ambiguous.name} (#{ambiguous.user.login})"
+ expect(assignment.offer_signup).to eq(ambiguous_signup)
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/owned_tag_set_spec.rb b/spec/models/owned_tag_set_spec.rb
new file mode 100644
index 00000000000..baeac9e399d
--- /dev/null
+++ b/spec/models/owned_tag_set_spec.rb
@@ -0,0 +1,128 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+describe OwnedTagSet do
+ let(:owned_tag_set) { create(:owned_tag_set) }
+ let(:user) { create(:user) }
+
+ describe "#owner_changes=" do
+ context "given a user that is not an owner" do
+ it "makes the user an owner when assigning their login" do
+ owned_tag_set.update(owner_changes: user.login)
+ expect(owned_tag_set.owners.reload).to include(user.default_pseud)
+ end
+
+ context "when there is another user with a pseud of the same name" do
+ let!(:other_pseud) { create(:pseud, name: user.login) }
+
+ it "makes the first user an owner when assigning their login" do
+ owned_tag_set.update(owner_changes: user.login)
+ expect(owned_tag_set.owners.reload).to include(user.default_pseud)
+ expect(owned_tag_set.owners.reload).not_to include(other_pseud)
+ end
+
+ it "makes the second user an owner when assigning the full byline for the other user's pseud" do
+ owned_tag_set.update(owner_changes: "#{other_pseud.name} (#{other_pseud.user.login})")
+ expect(owned_tag_set.owners.reload).not_to include(user.default_pseud)
+ expect(owned_tag_set.owners.reload).to include(other_pseud)
+ end
+ end
+ end
+
+ context "given a user that is a co-owner" do
+ before do
+ owned_tag_set.add_owner(user.default_pseud)
+ owned_tag_set.save
+ owned_tag_set.reload
+ end
+
+ it "removes the user as an owner when assigning their login" do
+ owned_tag_set.update(owner_changes: user.login)
+ expect(owned_tag_set.owners.reload).not_to include(user.default_pseud)
+ end
+
+ context "when there is another user with a pseud of the same name who is also a co-owner" do
+ let!(:other_pseud) { create(:pseud, name: user.login) }
+
+ before do
+ owned_tag_set.add_owner(other_pseud)
+ owned_tag_set.save
+ owned_tag_set.reload
+ end
+
+ it "removes the first user as an owner when assigning their login" do
+ owned_tag_set.update(owner_changes: user.login)
+ expect(owned_tag_set.owners.reload).not_to include(user.default_pseud)
+ expect(owned_tag_set.owners.reload).to include(other_pseud)
+ end
+
+ it "removes the second user an owner when assigning the full byline for the other user's pseud" do
+ owned_tag_set.update(owner_changes: "#{other_pseud.name} (#{other_pseud.user.login})")
+ expect(owned_tag_set.owners.reload).to include(user.default_pseud)
+ expect(owned_tag_set.owners.reload).not_to include(other_pseud)
+ end
+ end
+ end
+ end
+
+ describe "#moderator_changes=" do
+ context "given a user that is not a moderator" do
+ it "makes the user a moderator when assigning their login" do
+ owned_tag_set.update(moderator_changes: user.login)
+ expect(owned_tag_set.moderators.reload).to include(user.default_pseud)
+ end
+
+ context "when there is another user with a pseud of the same name" do
+ let!(:other_pseud) { create(:pseud, name: user.login) }
+
+ it "makes the first user a moderator when assigning their login" do
+ owned_tag_set.update(moderator_changes: user.login)
+ expect(owned_tag_set.moderators.reload).to include(user.default_pseud)
+ expect(owned_tag_set.moderators.reload).not_to include(other_pseud)
+ end
+
+ it "makes the second user a moderator when assigning the full byline for the other user's pseud" do
+ owned_tag_set.update(moderator_changes: "#{other_pseud.name} (#{other_pseud.user.login})")
+ expect(owned_tag_set.moderators.reload).not_to include(user.default_pseud)
+ expect(owned_tag_set.moderators.reload).to include(other_pseud)
+ end
+ end
+ end
+
+ context "given a user that is a moderator" do
+ before do
+ owned_tag_set.add_moderator(user.default_pseud)
+ owned_tag_set.save
+ owned_tag_set.reload
+ end
+
+ it "removes the user as a moderator when assigning their login" do
+ owned_tag_set.update(moderator_changes: user.login)
+ expect(owned_tag_set.moderators.reload).not_to include(user.default_pseud)
+ end
+
+ context "when there is another user with a pseud of the same name who is also a moderator" do
+ let!(:other_pseud) { create(:pseud, name: user.login) }
+
+ before do
+ owned_tag_set.add_moderator(other_pseud)
+ owned_tag_set.save
+ owned_tag_set.reload
+ end
+
+ it "removes the first user as a moderator when assigning their login" do
+ owned_tag_set.update(moderator_changes: user.login)
+ expect(owned_tag_set.moderators.reload).not_to include(user.default_pseud)
+ expect(owned_tag_set.moderators.reload).to include(other_pseud)
+ end
+
+ it "removes the second user a moderator when assigning the full byline for the other user's pseud" do
+ owned_tag_set.update(moderator_changes: "#{other_pseud.name} (#{other_pseud.user.login})")
+ expect(owned_tag_set.moderators.reload).to include(user.default_pseud)
+ expect(owned_tag_set.moderators.reload).not_to include(other_pseud)
+ end
+ end
+ end
+ end
+end
From 4955b7f679183025284780553c63f39ba8e125d2 Mon Sep 17 00:00:00 2001
From: EchoEkhi
Date: Sat, 5 Aug 2023 06:20:29 +0800
Subject: [PATCH 017/208] AO3-6536 Add elections admin role (#4541)
* AO3-6536 Add elections admin role
* AO3-6536 Fix test
* AO3-6536 Test shouldn't fail now
* AO3-6536 Refactor tests
* AO3-6536 Fix missed tests
---
app/models/admin.rb | 2 +-
app/policies/comment_policy.rb | 12 +++++--
config/locales/models/en.yml | 1 +
.../comments_and_kudos/admin_info.feature | 1 +
.../admin/activities_controller_spec.rb | 4 +--
.../admin/banners_controller_spec.rb | 2 +-
.../blacklisted_emails_controller_spec.rb | 2 +-
.../admin/skins_controller_spec.rb | 8 ++---
.../controllers/admin/spam_controller_spec.rb | 4 +--
spec/controllers/comments_controller_spec.rb | 34 ++++++++++++++++---
.../invite_requests_controller_spec.rb | 4 +--
spec/controllers/languages_controller_spec.rb | 12 +++----
spec/controllers/locales_controller_spec.rb | 10 +++---
spec/controllers/skins_controller_spec.rb | 8 ++---
14 files changed, 69 insertions(+), 35 deletions(-)
diff --git a/app/models/admin.rb b/app/models/admin.rb
index 1115ea6e882..e91b31a0493 100644
--- a/app/models/admin.rb
+++ b/app/models/admin.rb
@@ -1,5 +1,5 @@
class Admin < ApplicationRecord
- VALID_ROLES = %w[superadmin board communications translation tag_wrangling docs support policy_and_abuse open_doors].freeze
+ VALID_ROLES = %w[superadmin board communications elections translation tag_wrangling docs support policy_and_abuse open_doors].freeze
serialize :roles, Array
diff --git a/app/policies/comment_policy.rb b/app/policies/comment_policy.rb
index b73e9b2cf72..c6c1fdbb840 100644
--- a/app/policies/comment_policy.rb
+++ b/app/policies/comment_policy.rb
@@ -1,13 +1,19 @@
class CommentPolicy < ApplicationPolicy
- DESTROY_ROLES = %w[superadmin board policy_and_abuse communications support].freeze
+ DESTROY_COMMENT_ROLES = %w[superadmin board policy_and_abuse support].freeze
+ DESTROY_ADMIN_POST_COMMENT_ROLES = %w[superadmin board communications elections policy_and_abuse support].freeze
FREEZE_TAG_COMMENT_ROLES = %w[superadmin tag_wrangling].freeze
FREEZE_WORK_COMMENT_ROLES = %w[superadmin policy_and_abuse].freeze
HIDE_TAG_COMMENT_ROLES = %w[superadmin tag_wrangling].freeze
HIDE_WORK_COMMENT_ROLES = %w[superadmin policy_and_abuse].freeze
- SPAM_ROLES = %w[superadmin board policy_and_abuse communications support].freeze
+ SPAM_ROLES = %w[superadmin board communications elections policy_and_abuse support].freeze
def can_destroy_comment?
- user_has_roles?(DESTROY_ROLES)
+ case record.ultimate_parent
+ when AdminPost
+ user_has_roles?(DESTROY_ADMIN_POST_COMMENT_ROLES)
+ else
+ user_has_roles?(DESTROY_COMMENT_ROLES)
+ end
end
def can_freeze_comment?
diff --git a/config/locales/models/en.yml b/config/locales/models/en.yml
index 6a74e4dd05b..fc8aa2470d6 100644
--- a/config/locales/models/en.yml
+++ b/config/locales/models/en.yml
@@ -6,6 +6,7 @@ en:
board: Board
communications: Communications
docs: AO3 Docs
+ elections: Elections
open_doors: Open Doors
policy_and_abuse: Policy & Abuse
superadmin: Super admin
diff --git a/features/comments_and_kudos/admin_info.feature b/features/comments_and_kudos/admin_info.feature
index 75313c13d56..89e63d71b9f 100644
--- a/features/comments_and_kudos/admin_info.feature
+++ b/features/comments_and_kudos/admin_info.feature
@@ -34,6 +34,7 @@ Feature: Some admins can see IP addresses and emails for comments
| board | should not | should not |
| communications | should not | should not |
| docs | should not | should not |
+ | elections | should not | should not |
| open_doors | should not | should not |
| tag_wrangling | should not | should not |
| translation | should not | should not |
diff --git a/spec/controllers/admin/activities_controller_spec.rb b/spec/controllers/admin/activities_controller_spec.rb
index 3e9df7c15be..51c1a9ea6dd 100644
--- a/spec/controllers/admin/activities_controller_spec.rb
+++ b/spec/controllers/admin/activities_controller_spec.rb
@@ -22,7 +22,7 @@
it_behaves_like "unauthorized"
end
- %w[board communications translation tag_wrangling docs support open_doors].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin policy_and_abuse]).each do |role|
context "when logged in as an admin with #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
@@ -49,7 +49,7 @@
it_behaves_like "unauthorized"
end
- %w[board communications translation tag_wrangling docs support open_doors].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin policy_and_abuse]).each do |role|
context "when logged in as an admin with #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
diff --git a/spec/controllers/admin/banners_controller_spec.rb b/spec/controllers/admin/banners_controller_spec.rb
index 83702c809b1..53e06a14ef3 100644
--- a/spec/controllers/admin/banners_controller_spec.rb
+++ b/spec/controllers/admin/banners_controller_spec.rb
@@ -16,7 +16,7 @@
end
end
- %w[translation tag_wrangling docs policy_and_abuse open_doors].each do |role|
+ (Admin::VALID_ROLES - %w[support communications superadmin board]).each do |role|
it "displays an error to #{role} admins" do
fake_login_admin(create(:admin, roles: [role]))
subject
diff --git a/spec/controllers/admin/blacklisted_emails_controller_spec.rb b/spec/controllers/admin/blacklisted_emails_controller_spec.rb
index 7ced0a97a9f..6eba89b5a32 100644
--- a/spec/controllers/admin/blacklisted_emails_controller_spec.rb
+++ b/spec/controllers/admin/blacklisted_emails_controller_spec.rb
@@ -16,7 +16,7 @@
end
end
- %w[board communications docs open_doors tag_wrangling translation].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin policy_and_abuse support]).each do |role|
it "redirects to root with error for #{role} admins" do
fake_login_admin(create(:admin, roles: [role]))
subject
diff --git a/spec/controllers/admin/skins_controller_spec.rb b/spec/controllers/admin/skins_controller_spec.rb
index 725c8637d6d..3abd8d9691d 100644
--- a/spec/controllers/admin/skins_controller_spec.rb
+++ b/spec/controllers/admin/skins_controller_spec.rb
@@ -20,7 +20,7 @@
end
end
- %w[board communications docs open_doors policy_and_abuse tag_wrangling translation].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin support]).each do |role|
context "when admin has #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
@@ -54,7 +54,7 @@
it_redirects_to_with_error(root_path, "Sorry, only an authorized admin can access the page you were trying to reach.")
end
- %w[board communications docs open_doors policy_and_abuse tag_wrangling translation].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin support]).each do |role|
context "when admin has #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
@@ -88,7 +88,7 @@
it_redirects_to_with_error(root_path, "Sorry, only an authorized admin can access the page you were trying to reach.")
end
- %w[board communications docs open_doors policy_and_abuse tag_wrangling translation].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin support]).each do |role|
context "when admin has #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
@@ -210,7 +210,7 @@
it_behaves_like "unauthorized admin cannot update work skin"
end
- %w[board communications docs open_doors policy_and_abuse tag_wrangling translation].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin support]).each do |role|
context "when admin has #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
diff --git a/spec/controllers/admin/spam_controller_spec.rb b/spec/controllers/admin/spam_controller_spec.rb
index 630ca5f2423..30c4061494f 100644
--- a/spec/controllers/admin/spam_controller_spec.rb
+++ b/spec/controllers/admin/spam_controller_spec.rb
@@ -16,7 +16,7 @@
end
end
- %w[board communications translation tag_wrangling docs support open_doors].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin policy_and_abuse]).each do |role|
context "when logged in as an admin with #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
@@ -55,7 +55,7 @@
end
end
- %w[board communications translation tag_wrangling docs support open_doors].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin policy_and_abuse]).each do |role|
context "when logged in as an admin with #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
diff --git a/spec/controllers/comments_controller_spec.rb b/spec/controllers/comments_controller_spec.rb
index 410dab6a512..9ce7ed462b9 100644
--- a/spec/controllers/comments_controller_spec.rb
+++ b/spec/controllers/comments_controller_spec.rb
@@ -2746,7 +2746,7 @@
end
end
- %w[superadmin board policy_and_abuse communications support].each do |admin_role|
+ %w[superadmin board communications elections policy_and_abuse support].each do |admin_role|
context "with role #{admin_role}" do
it "destroys comment and redirects with success message" do
admin.update(roles: [admin_role])
@@ -2811,7 +2811,20 @@
end
end
- %w[superadmin board policy_and_abuse communications support].each do |admin_role|
+ (Admin::VALID_ROLES - %w[superadmin board policy_and_abuse support]).each do |admin_role|
+ context "with role #{admin_role}" do
+ it "doesn't destroy comment and redirects with error" do
+ admin.update(roles: [admin_role])
+ fake_login_admin(admin)
+ delete :destroy, params: { id: comment.id }
+
+ it_redirects_to_with_error(root_path, "Sorry, only an authorized admin can access the page you were trying to reach.")
+ expect { comment.reload }.not_to raise_exception
+ end
+ end
+ end
+
+ %w[superadmin board policy_and_abuse support].each do |admin_role|
context "with the #{admin_role} role" do
it "destroys comment and redirects with success message" do
admin.update(roles: [admin_role])
@@ -2916,7 +2929,20 @@
end
end
- %w[superadmin policy_and_abuse].each do |admin_role|
+ (Admin::VALID_ROLES - %w[superadmin board policy_and_abuse support]).each do |admin_role|
+ context "with role #{admin_role}" do
+ it "doesn't destroy comment and redirects with error" do
+ admin.update(roles: [admin_role])
+ fake_login_admin(admin)
+ delete :destroy, params: { id: comment.id }
+
+ it_redirects_to_with_error(root_path, "Sorry, only an authorized admin can access the page you were trying to reach.")
+ expect { comment.reload }.not_to raise_exception
+ end
+ end
+ end
+
+ %w[superadmin board policy_and_abuse support].each do |admin_role|
context "with the #{admin_role} role" do
it "destroys comment and redirects with success message" do
admin.update(roles: [admin_role])
@@ -3276,7 +3302,7 @@
it_redirects_to_with_error(root_url, "Sorry, only an authorized admin can access the page you were trying to reach.")
end
- %w[superadmin board communications policy_and_abuse support].each do |admin_role|
+ %w[superadmin board support policy_and_abuse].each do |admin_role|
it "successfully deletes the comment when admin has #{admin_role} role" do
admin.update(roles: [admin_role])
fake_login_admin(admin)
diff --git a/spec/controllers/invite_requests_controller_spec.rb b/spec/controllers/invite_requests_controller_spec.rb
index cf61d031b9a..c7d42457936 100644
--- a/spec/controllers/invite_requests_controller_spec.rb
+++ b/spec/controllers/invite_requests_controller_spec.rb
@@ -180,7 +180,7 @@
end
end
- %w[board communications docs open_doors tag_wrangling translation].each do |admin_role|
+ (Admin::VALID_ROLES - %w[superadmin policy_and_abuse support]).each do |admin_role|
context "with #{admin_role} role" do
before do
admin.update(roles: [admin_role])
@@ -282,7 +282,7 @@
end
end
- %w[board communications docs open_doors tag_wrangling translation].each do |admin_role|
+ (Admin::VALID_ROLES - %w[superadmin policy_and_abuse support]).each do |admin_role|
context "with #{admin_role} role" do
before do
admin.update(roles: [admin_role])
diff --git a/spec/controllers/languages_controller_spec.rb b/spec/controllers/languages_controller_spec.rb
index 6938855aa65..17d8f2383c9 100644
--- a/spec/controllers/languages_controller_spec.rb
+++ b/spec/controllers/languages_controller_spec.rb
@@ -12,7 +12,7 @@
end
end
- %w[board communications policy_and_abuse tag_wrangling docs support open_doors translation superadmin].each do |role|
+ Admin::VALID_ROLES.each do |role|
context "when logged in as an admin with #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
@@ -33,7 +33,7 @@
end
end
- %w[board communications policy_and_abuse tag_wrangling docs support open_doors translation superadmin].each do |role|
+ Admin::VALID_ROLES.each do |role|
context "when logged in as an admin with #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
@@ -53,7 +53,7 @@
end
end
- %w[board communications policy_and_abuse tag_wrangling docs support open_doors].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin translation]).each do |role|
context "when logged in as an admin with #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
@@ -99,7 +99,7 @@
end
end
- %w[board communications policy_and_abuse tag_wrangling docs support open_doors].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin translation]).each do |role|
context "when logged in as an admin with #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
@@ -146,7 +146,7 @@
end
end
- %w[board communications policy_and_abuse tag_wrangling docs support open_doors].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin translation]).each do |role|
context "when logged in as an admin with #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
@@ -195,7 +195,7 @@
end
end
- %w[board communications policy_and_abuse tag_wrangling docs support open_doors].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin translation]).each do |role|
context "when logged in as an admin with #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
diff --git a/spec/controllers/locales_controller_spec.rb b/spec/controllers/locales_controller_spec.rb
index 306ba08a30a..54eb7f39025 100644
--- a/spec/controllers/locales_controller_spec.rb
+++ b/spec/controllers/locales_controller_spec.rb
@@ -22,7 +22,7 @@
end
end
- %w[board communications policy_and_abuse tag_wrangling docs support open_doors].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin translation]).each do |role|
context "when logged in as an admin with #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
@@ -67,7 +67,7 @@
end
end
- %w[board communications policy_and_abuse tag_wrangling docs support open_doors].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin translation]).each do |role|
context "when logged in as an admin with #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
@@ -116,7 +116,7 @@
end
end
- %w[board communications policy_and_abuse tag_wrangling docs support open_doors].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin translation]).each do |role|
context "when logged in as an admin with #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
@@ -164,7 +164,7 @@
end
end
- %w[board communications policy_and_abuse tag_wrangling docs support open_doors].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin translation]).each do |role|
context "when logged in as an admin with #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
@@ -225,7 +225,7 @@
end
end
- %w[board communications policy_and_abuse tag_wrangling docs support open_doors].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin translation]).each do |role|
context "when logged in as an admin with #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
diff --git a/spec/controllers/skins_controller_spec.rb b/spec/controllers/skins_controller_spec.rb
index 462e6c9288b..f9890543a29 100644
--- a/spec/controllers/skins_controller_spec.rb
+++ b/spec/controllers/skins_controller_spec.rb
@@ -32,7 +32,7 @@
it_behaves_like "unauthorized admin cannot edit"
end
- %w[board communications docs open_doors policy_and_abuse support tag_wrangling translation].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin]).each do |role|
context "when admin has #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
@@ -54,7 +54,7 @@
it_behaves_like "unauthorized admin cannot edit"
end
- %w[board communications docs open_doors policy_and_abuse tag_wrangling translation].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin support]).each do |role|
context "when admin has #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
@@ -104,7 +104,7 @@
it_behaves_like "unauthorized admin cannot update"
end
- %w[board communications docs open_doors policy_and_abuse support tag_wrangling translation].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin]).each do |role|
context "when admin has #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
@@ -126,7 +126,7 @@
it_behaves_like "unauthorized admin cannot update"
end
- %w[board communications docs open_doors policy_and_abuse tag_wrangling translation].each do |role|
+ (Admin::VALID_ROLES - %w[superadmin support]).each do |role|
context "when admin has #{role} role" do
let(:admin) { create(:admin, roles: [role]) }
From 8986e20fce98351eaaedd4aad87e6da6b4ddf241 Mon Sep 17 00:00:00 2001
From: Brian Austin <13002992+brianjaustin@users.noreply.github.com>
Date: Fri, 4 Aug 2023 15:21:52 -0700
Subject: [PATCH 018/208] AO3-6192 Disable admin post comments after 2 weeks
(#4574)
* AO3-6192 Disable admin post comments after 2 weeks
* style
* Clearer end condition
---
app/models/admin_post.rb | 17 ++++++++++++
config/config.yml | 5 ++++
config/resque_schedule.yml | 9 +++++++
spec/models/admin_post_spec.rb | 49 ++++++++++++++++++++++++++++++++++
4 files changed, 80 insertions(+)
create mode 100644 spec/models/admin_post_spec.rb
diff --git a/app/models/admin_post.rb b/app/models/admin_post.rb
index 625a4c8be0f..740a7a5e8f9 100644
--- a/app/models/admin_post.rb
+++ b/app/models/admin_post.rb
@@ -79,6 +79,23 @@ def translated_post_language_must_differ
errors.add(:translated_post_id, "cannot be same language as original post")
end
+ ####################
+ # DELAYED JOBS
+ ####################
+
+ include AsyncWithResque
+ @queue = :utilities
+
+ # Turns off comments for all posts that are older than the configured time period.
+ # If the configured period is nil or less than 1 day, no action is taken.
+ def self.disable_old_post_comments
+ return unless ArchiveConfig.ADMIN_POST_COMMENTING_EXPIRATION_DAYS&.positive?
+
+ where.not(comment_permissions: :disable_all)
+ .where(created_at: ..ArchiveConfig.ADMIN_POST_COMMENTING_EXPIRATION_DAYS.days.ago)
+ .update_all(comment_permissions: :disable_all)
+ end
+
private
def expire_cached_home_admin_posts
diff --git a/config/config.yml b/config/config.yml
index 2eccdffcf89..6625b10062e 100644
--- a/config/config.yml
+++ b/config/config.yml
@@ -513,3 +513,8 @@ ORIGINAL_CREATOR_TTL_HOURS: 72
# The maximum number of tags to provide in a tag wrangling report.
WRANGLING_REPORT_LIMIT: 1000
+
+# The number of days after which an Admin Post should allow comments.
+# After this window, all comments are disabled. Setting this value to
+# something below 1 -- or commenting it out -- will turn off comment disabling.
+ADMIN_POST_COMMENTING_EXPIRATION_DAYS: 14
diff --git a/config/resque_schedule.yml b/config/resque_schedule.yml
index 12a0f6e0c78..0d9fba22d29 100644
--- a/config/resque_schedule.yml
+++ b/config/resque_schedule.yml
@@ -106,3 +106,12 @@ cleanup_work_original_creators:
description: >-
Remove original_creators for works orphaned/moved more than
ORIGINAL_CREATOR_TTL_HOURS hours ago.
+
+disable_admin_post_comments:
+ every: 1d
+ class: "AdminPost"
+ queue: utilities
+ args: disable_old_post_comments
+ description: >-
+ Disables all comments on admin (news) posts older than the
+ configured window.
diff --git a/spec/models/admin_post_spec.rb b/spec/models/admin_post_spec.rb
new file mode 100644
index 00000000000..64eaf26c292
--- /dev/null
+++ b/spec/models/admin_post_spec.rb
@@ -0,0 +1,49 @@
+require "spec_helper"
+
+describe AdminPost do
+ describe ".disable_old_post_comments" do
+ context "when the configured threshold is nil" do
+ before do
+ allow(ArchiveConfig)
+ .to receive(:ADMIN_POST_COMMENTING_EXPIRATION_DAYS)
+ .and_return(nil)
+ end
+
+ it "does not error" do
+ expect { AdminPost.disable_old_post_comments }
+ .not_to raise_error
+ end
+ end
+
+ context "when the configured threshold is non-positive" do
+ before do
+ allow(ArchiveConfig)
+ .to receive(:ADMIN_POST_COMMENTING_EXPIRATION_DAYS)
+ .and_return(0)
+ end
+
+ it "does not update any posts" do
+ post = create(:admin_post)
+
+ AdminPost.disable_old_post_comments
+ expect(post.reload.disable_all_comments?).to be(false)
+ end
+ end
+
+ it "disables comments on a post outside the window" do
+ old_post = travel_to(ArchiveConfig.ADMIN_POST_COMMENTING_EXPIRATION_DAYS.days.ago) do
+ create(:admin_post)
+ end
+
+ AdminPost.disable_old_post_comments
+ expect(old_post.reload.disable_all_comments?).to be(true)
+ end
+
+ it "does not disable comments on a post inside the window" do
+ new_post = create(:admin_post)
+
+ AdminPost.disable_old_post_comments
+ expect(new_post.reload.disable_all_comments?).to be(false)
+ end
+ end
+end
From a8ba9e363e7fb80b3f5d98b7ae8d8f0a1722cad9 Mon Sep 17 00:00:00 2001
From: Bilka
Date: Sat, 5 Aug 2023 00:22:00 +0200
Subject: [PATCH 019/208] AO3-6556 Update text to refer to account activation
rather than confirmation (#4585)
* AO3-6556 Update text to refer to activation rather than confirmation
* AO3-6556 Normalize locale file
* AO3-6556 Fix test
* AO3-6556 Fix test (for real this time)
* AO3-6556 Minor fixes
* AO3-6556 Move HTML out of locale values
* AO3-6556 Better html safe version of strong tags
* Fix parenthesis placement
Co-authored-by: Brian Austin <13002992+brianjaustin@users.noreply.github.com>
---------
Co-authored-by: Brian Austin <13002992+brianjaustin@users.noreply.github.com>
---
app/views/users/confirmation.html.erb | 13 +++++++------
config/locales/devise/en.yml | 2 +-
config/locales/views/en.yml | 10 ++++++++++
features/other_a/invite_request.feature | 2 +-
features/step_definitions/invite_steps.rb | 2 +-
5 files changed, 20 insertions(+), 9 deletions(-)
diff --git a/app/views/users/confirmation.html.erb b/app/views/users/confirmation.html.erb
index 1245c0e3adf..d7cc574b52b 100644
--- a/app/views/users/confirmation.html.erb
+++ b/app/views/users/confirmation.html.erb
@@ -1,20 +1,21 @@
-<%= ts("Almost Done!") %>
+<%= t(".page_heading") %>
- <%= ts("You should soon receive a confirmation email at the address you gave us. This will have the link you need to follow in order to activate your account and complete the account creation process. The confirmation email will come from %{return_address} -- you might want to add this to your address book to make sure you get the email.", return_address: ArchiveConfig.RETURN_ADDRESS).html_safe %>
+ <%= t(".receive_email_html", return_address: tag.strong(ArchiveConfig.RETURN_ADDRESS)) %>
- <%= ts("If you haven't received this email within 24 hours, and you don't find our email in your spam filter or Social folder, please contact Support for help.", support_path: new_feedback_report_path).html_safe %>
+ <%= t(".no_email_html", contact_support_link: link_to(t(".contact_support"), new_feedback_report_path)) %>
- <% # We do days_to_purge_unactivated * 7 because it's actually weeks %>
- <%= ts("Important! You must confirm your email address within %{days_before_purge} days, or your account will expire. If this happens, you can sign up again using the same invitation.", days_before_purge: AdminSetting.current.days_to_purge_unactivated? ? (AdminSetting.current.days_to_purge_unactivated * 7) : ArchiveConfig.DAYS_TO_PURGE_UNACTIVATED).html_safe %>
+ <%# We do days_to_purge_unactivated * 7 because it's actually weeks %>
+ <%= t(".important")%>
+ <%= t(".must_activate", count: AdminSetting.current.days_to_purge_unactivated? ? (AdminSetting.current.days_to_purge_unactivated * 7) : ArchiveConfig.DAYS_TO_PURGE_UNACTIVATED) %>
- <%= link_to ts('Return to Archive front page'), root_path %>
+ <%= link_to t(".go_back"), root_path %>
diff --git a/config/locales/devise/en.yml b/config/locales/devise/en.yml
index 71da9f82b74..e6839d9f580 100644
--- a/config/locales/devise/en.yml
+++ b/config/locales/devise/en.yml
@@ -25,7 +25,7 @@ en:
not_found_in_database: The password or user name you entered doesn't match our records. Please try again or reset your password . If you still can't log in, please visit Problems When Logging In for help.
timeout: Your session expired. Please sign in again to continue.
unauthenticated: You need to sign in or sign up before continuing.
- unconfirmed: You have to confirm your email before continuing. Please check your email for the confirmation link.
+ unconfirmed: You have to activate your account before continuing. Please check your email for the activation link.
mailer:
confirmation_instructions:
subject: Confirmation instructions
diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml
index 8b3d5cdd91f..5e04081cb93 100644
--- a/config/locales/views/en.yml
+++ b/config/locales/views/en.yml
@@ -585,6 +585,16 @@ en:
contact_support: contact Support
last_renamed: You last changed your user name on %{renamed_at}.
more_info: For information on how changing your user name will affect your account, please check out the %{account_faq_link}. Note that changes to your user name may take several days or longer to appear. If you are still seeing your old user name on your works, bookmarks, series, or collections after a week, please %{contact_support_link}.
+ confirmation:
+ contact_support: contact Support
+ go_back: Return to Archive front page
+ important: Important!
+ must_activate:
+ one: You must activate your account within %{count} day, or your account will expire. If this happens, you can sign up again using the same invitation.
+ other: You must activate your account within %{count} days, or your account will expire. If this happens, you can sign up again using the same invitation.
+ no_email_html: If you haven't received this email within 24 hours, and you don't find our email in your spam filter or Social folder, please %{contact_support_link} for help.
+ page_heading: Almost Done!
+ receive_email_html: You should soon receive an activation email at the address you gave us. This will have the link you need to follow in order to activate your account and complete the account creation process. The activation email will come from %{return_address} -- you might want to add this to your address book to make sure you get the email.
delete_preview:
cancel: Cancel
co_creations:
diff --git a/features/other_a/invite_request.feature b/features/other_a/invite_request.feature
index d6f02ac12f1..8d8a4f5725d 100644
--- a/features/other_a/invite_request.feature
+++ b/features/other_a/invite_request.feature
@@ -95,7 +95,7 @@ Feature: Invite requests
| user_registration_password | password1 |
| user_registration_password_confirmation | password1 |
And I press "Create Account"
- Then I should see "You should soon receive a confirmation email at the address you gave us"
+ Then I should see "You should soon receive an activation email at the address you gave us"
And I should see how long I have to activate my account
And I should see "If you haven't received this email within 24 hours"
diff --git a/features/step_definitions/invite_steps.rb b/features/step_definitions/invite_steps.rb
index 57a2f6cba01..3011381e863 100644
--- a/features/step_definitions/invite_steps.rb
+++ b/features/step_definitions/invite_steps.rb
@@ -150,7 +150,7 @@ def invite(attributes = {})
Then /^I should see how long I have to activate my account$/ do
days_to_activate = AdminSetting.first.days_to_purge_unactivated? ? (AdminSetting.first.days_to_purge_unactivated * 7) : ArchiveConfig.DAYS_TO_PURGE_UNACTIVATED
- step %{I should see "You must confirm your email address within #{days_to_activate} days"}
+ step %{I should see "You must activate your account within #{days_to_activate} days"}
end
Then /^"([^"]*)" should have "([^"]*)" invitations$/ do |login, invitation_count|
From 38f7d81af0ef83363f62d77d5919edf0c66deb39 Mon Sep 17 00:00:00 2001
From: neuroalien <105230050+neuroalien@users.noreply.github.com>
Date: Fri, 4 Aug 2023 23:22:58 +0100
Subject: [PATCH 020/208] AO3-6552 Login link needs ARIA role (#4580)
* AO3-6552 Add menuitem ARIA role to login link
The containing paragraph has a `menu` role. This requires[1] its direct
children to have one of a few allowed roles, the most appropriate for
this case being `menuitem`.
[1]https://www.w3.org/TR/wai-aria-1.1/#menu
* AO3-6552 Add i18n for the login link
Separate commit so it can be easily removed if we don't want to i18n
partially. (I'd prefer not to i18n the entire file in this PR.)
---
app/views/layouts/_header.html.erb | 2 +-
config/locales/views/en.yml | 2 ++
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/app/views/layouts/_header.html.erb b/app/views/layouts/_header.html.erb
index 1e313adc9da..52a2772f383 100644
--- a/app/views/layouts/_header.html.erb
+++ b/app/views/layouts/_header.html.erb
@@ -31,7 +31,7 @@
<% else %>
- <%= link_to ts('Log In', key: 'header'), new_user_session_path, id: "login-dropdown" %>
+ <%= link_to t(".login"), new_user_session_path, id: "login-dropdown", role: "menuitem" %>
<%= render 'users/sessions/login' %>
diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml
index 5e04081cb93..911a4a53a15 100644
--- a/config/locales/views/en.yml
+++ b/config/locales/views/en.yml
@@ -446,6 +446,8 @@ en:
one: "%{count} more user"
other: "%{count} more users"
layouts:
+ header:
+ login: Log In
proxy_notice:
button: Dismiss Notice
faux_heading: 'Important message:'
From 280200632a1248bc008e4c9586a6f8304ec6c97b Mon Sep 17 00:00:00 2001
From: neuroalien <105230050+neuroalien@users.noreply.github.com>
Date: Thu, 10 Aug 2023 00:16:27 +0100
Subject: [PATCH 021/208] AO3-6577 Upgrade Rails to 6.1.7.4 (#4592)
Upgrade rails to 6.1.7.4
This is a patch level upgrade, addressing a couple of vulnerabilities.
---
Gemfile | 2 +-
Gemfile.lock | 108 +++++++++++++++++++++++++--------------------------
2 files changed, 55 insertions(+), 55 deletions(-)
diff --git a/Gemfile b/Gemfile
index 4a5b053ba05..9236d99478e 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,7 +6,7 @@ gem 'test-unit', '~> 3.2'
gem 'bundler'
-gem "rails", "~> 6.1.0"
+gem "rails", "~> 6.1.7"
gem "rails-i18n"
gem "rack", "~> 2.2"
gem "sprockets", "< 4"
diff --git a/Gemfile.lock b/Gemfile.lock
index 567490619f6..420e80799d3 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -24,67 +24,67 @@ GEM
remote: https://rubygems.org/
specs:
aaronh-chronic (0.3.9)
- actioncable (6.1.7.2)
- actionpack (= 6.1.7.2)
- activesupport (= 6.1.7.2)
+ actioncable (6.1.7.4)
+ actionpack (= 6.1.7.4)
+ activesupport (= 6.1.7.4)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
- actionmailbox (6.1.7.2)
- actionpack (= 6.1.7.2)
- activejob (= 6.1.7.2)
- activerecord (= 6.1.7.2)
- activestorage (= 6.1.7.2)
- activesupport (= 6.1.7.2)
+ actionmailbox (6.1.7.4)
+ actionpack (= 6.1.7.4)
+ activejob (= 6.1.7.4)
+ activerecord (= 6.1.7.4)
+ activestorage (= 6.1.7.4)
+ activesupport (= 6.1.7.4)
mail (>= 2.7.1)
- actionmailer (6.1.7.2)
- actionpack (= 6.1.7.2)
- actionview (= 6.1.7.2)
- activejob (= 6.1.7.2)
- activesupport (= 6.1.7.2)
+ actionmailer (6.1.7.4)
+ actionpack (= 6.1.7.4)
+ actionview (= 6.1.7.4)
+ activejob (= 6.1.7.4)
+ activesupport (= 6.1.7.4)
mail (~> 2.5, >= 2.5.4)
rails-dom-testing (~> 2.0)
- actionpack (6.1.7.2)
- actionview (= 6.1.7.2)
- activesupport (= 6.1.7.2)
+ actionpack (6.1.7.4)
+ actionview (= 6.1.7.4)
+ activesupport (= 6.1.7.4)
rack (~> 2.0, >= 2.0.9)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.0, >= 1.2.0)
actionpack-page_caching (1.2.4)
actionpack (>= 4.0.0)
- actiontext (6.1.7.2)
- actionpack (= 6.1.7.2)
- activerecord (= 6.1.7.2)
- activestorage (= 6.1.7.2)
- activesupport (= 6.1.7.2)
+ actiontext (6.1.7.4)
+ actionpack (= 6.1.7.4)
+ activerecord (= 6.1.7.4)
+ activestorage (= 6.1.7.4)
+ activesupport (= 6.1.7.4)
nokogiri (>= 1.8.5)
- actionview (6.1.7.2)
- activesupport (= 6.1.7.2)
+ actionview (6.1.7.4)
+ activesupport (= 6.1.7.4)
builder (~> 3.1)
erubi (~> 1.4)
rails-dom-testing (~> 2.0)
rails-html-sanitizer (~> 1.1, >= 1.2.0)
active_record_query_trace (1.8)
- activejob (6.1.7.2)
- activesupport (= 6.1.7.2)
+ activejob (6.1.7.4)
+ activesupport (= 6.1.7.4)
globalid (>= 0.3.6)
- activemodel (6.1.7.2)
- activesupport (= 6.1.7.2)
+ activemodel (6.1.7.4)
+ activesupport (= 6.1.7.4)
activemodel-serializers-xml (1.0.2)
activemodel (> 5.x)
activesupport (> 5.x)
builder (~> 3.1)
- activerecord (6.1.7.2)
- activemodel (= 6.1.7.2)
- activesupport (= 6.1.7.2)
- activestorage (6.1.7.2)
- actionpack (= 6.1.7.2)
- activejob (= 6.1.7.2)
- activerecord (= 6.1.7.2)
- activesupport (= 6.1.7.2)
+ activerecord (6.1.7.4)
+ activemodel (= 6.1.7.4)
+ activesupport (= 6.1.7.4)
+ activestorage (6.1.7.4)
+ actionpack (= 6.1.7.4)
+ activejob (= 6.1.7.4)
+ activerecord (= 6.1.7.4)
+ activesupport (= 6.1.7.4)
marcel (~> 1.0)
mini_mime (>= 1.1.0)
- activesupport (6.1.7.2)
+ activesupport (6.1.7.4)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@@ -404,20 +404,20 @@ GEM
rack
rack-test (2.0.2)
rack (>= 1.3)
- rails (6.1.7.2)
- actioncable (= 6.1.7.2)
- actionmailbox (= 6.1.7.2)
- actionmailer (= 6.1.7.2)
- actionpack (= 6.1.7.2)
- actiontext (= 6.1.7.2)
- actionview (= 6.1.7.2)
- activejob (= 6.1.7.2)
- activemodel (= 6.1.7.2)
- activerecord (= 6.1.7.2)
- activestorage (= 6.1.7.2)
- activesupport (= 6.1.7.2)
+ rails (6.1.7.4)
+ actioncable (= 6.1.7.4)
+ actionmailbox (= 6.1.7.4)
+ actionmailer (= 6.1.7.4)
+ actionpack (= 6.1.7.4)
+ actiontext (= 6.1.7.4)
+ actionview (= 6.1.7.4)
+ activejob (= 6.1.7.4)
+ activemodel (= 6.1.7.4)
+ activerecord (= 6.1.7.4)
+ activestorage (= 6.1.7.4)
+ activesupport (= 6.1.7.4)
bundler (>= 1.15.0)
- railties (= 6.1.7.2)
+ railties (= 6.1.7.4)
sprockets-rails (>= 2.0.0)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
@@ -431,9 +431,9 @@ GEM
rails-i18n (7.0.3)
i18n (>= 0.7, < 2)
railties (>= 6.0.0, < 8)
- railties (6.1.7.2)
- actionpack (= 6.1.7.2)
- activesupport (= 6.1.7.2)
+ railties (6.1.7.4)
+ actionpack (= 6.1.7.4)
+ activesupport (= 6.1.7.4)
method_source
rake (>= 12.2)
thor (~> 1.0)
@@ -659,7 +659,7 @@ DEPENDENCIES
rack (~> 2.2)
rack-attack
rack-dev-mark (>= 0.7.8)
- rails (~> 6.1.0)
+ rails (~> 6.1.7)
rails-controller-testing
rails-i18n
rails-observers!
From 0f30207d6dfe9050d38398522339f1a0d754c58c Mon Sep 17 00:00:00 2001
From: tickinginstant
Date: Fri, 11 Aug 2023 13:04:49 -0400
Subject: [PATCH 022/208] AO3-6589 Don't call ts() on user-generated text.
(#4597)
* AO3-6589 Don't call ts() on user-generated text.
* AO3-6589 Woof.
---
app/controllers/admin/blacklisted_emails_controller.rb | 4 ++--
app/controllers/bookmarks_controller.rb | 6 +++---
app/controllers/challenge_assignments_controller.rb | 8 ++++----
app/controllers/opendoors/external_authors_controller.rb | 6 +++---
app/controllers/opendoors/tools_controller.rb | 2 +-
app/controllers/redirect_controller.rb | 2 +-
app/controllers/tag_set_nominations_controller.rb | 4 ++--
app/helpers/series_helper.rb | 2 +-
app/models/work.rb | 2 +-
app/models/work_skin.rb | 2 +-
app/views/archive_faqs/show.html.erb | 4 ++--
app/views/bookmarks/_bookmark_user_module.html.erb | 2 +-
app/views/menu/_menu_fandoms.html.erb | 2 +-
app/views/works/_search_box.html.erb | 2 +-
lib/css_cleaner.rb | 8 ++++----
15 files changed, 28 insertions(+), 28 deletions(-)
diff --git a/app/controllers/admin/blacklisted_emails_controller.rb b/app/controllers/admin/blacklisted_emails_controller.rb
index 48080f6e4d9..13489050459 100644
--- a/app/controllers/admin/blacklisted_emails_controller.rb
+++ b/app/controllers/admin/blacklisted_emails_controller.rb
@@ -15,7 +15,7 @@ def create
@page_subtitle = t(".browser_title")
if @admin_blacklisted_email.save
- flash[:notice] = ts("Email address #{@admin_blacklisted_email.email} banned.")
+ flash[:notice] = ts("Email address %{email} banned.", email: @admin_blacklisted_email.email)
redirect_to admin_blacklisted_emails_path
else
render action: "index"
@@ -26,7 +26,7 @@ def destroy
@admin_blacklisted_email = authorize AdminBlacklistedEmail.find(params[:id])
@admin_blacklisted_email.destroy
- flash[:notice] = ts("Email address #{@admin_blacklisted_email.email} removed from banned emails list.")
+ flash[:notice] = ts("Email address %{email} removed from banned emails list.", email: @admin_blacklisted_email.email)
redirect_to admin_blacklisted_emails_path
end
diff --git a/app/controllers/bookmarks_controller.rb b/app/controllers/bookmarks_controller.rb
index 8d36973cbb5..f51722cf50f 100644
--- a/app/controllers/bookmarks_controller.rb
+++ b/app/controllers/bookmarks_controller.rb
@@ -203,12 +203,12 @@ def update
bookmark_params[:collection_names]&.split(",")&.map(&:strip)&.uniq&.each do |collection_name|
collection = Collection.find_by(name: collection_name)
if collection.nil?
- errors << ts("#{collection_name} does not exist.")
+ errors << ts("%{name} does not exist.", name: collection_name)
else
if @bookmark.collections.include?(collection)
next
elsif collection.closed? && !collection.user_is_maintainer?(User.current_user)
- errors << ts("#{collection.title} is closed to new submissions.")
+ errors << ts("%{title} is closed to new submissions.", title: collection.title)
elsif @bookmark.add_to_collection(collection) && @bookmark.save
if @bookmark.approved_collections.include?(collection)
new_collections << collection
@@ -216,7 +216,7 @@ def update
unapproved_collections << collection
end
else
- errors << ts("Something went wrong trying to add collection #{collection.title}, sorry!")
+ errors << ts("Something went wrong trying to add collection %{title}, sorry!", title: collection.title)
end
end
end
diff --git a/app/controllers/challenge_assignments_controller.rb b/app/controllers/challenge_assignments_controller.rb
index 9cbe0b8790d..f8e18062b60 100644
--- a/app/controllers/challenge_assignments_controller.rb
+++ b/app/controllers/challenge_assignments_controller.rb
@@ -181,11 +181,11 @@ def update_multiple
case action
when "default"
# default_assignment_id = y/n
- assignment.default || @errors << ts("We couldn't default the assignment for #{assignment.offer_byline}")
+ assignment.default || (@errors << ts("We couldn't default the assignment for %{offer}", offer: assignment.offer_byline))
when "undefault"
# undefault_[assignment_id] = y/n - if set, undefault
assignment.defaulted_at = nil
- assignment.save || @errors << ts("We couldn't undefault the assignment covering #{assignment.request_byline}.")
+ assignment.save || (@errors << ts("We couldn't undefault the assignment covering %{request}.", request: assignment.request_byline))
when "approve"
assignment.get_collection_item.approve_by_collection if assignment.get_collection_item
when "cover"
@@ -193,9 +193,9 @@ def update_multiple
next if val.blank? || assignment.pinch_hitter.try(:byline) == val
pseud = Pseud.parse_byline(val)
if pseud.nil?
- @errors << ts("We couldn't find the user #{val} to assign that to.")
+ @errors << ts("We couldn't find the user %{val} to assign that to.", val: val)
else
- assignment.cover(pseud) || @errors << ts("We couldn't assign #{val} to cover #{assignment.request_byline}.")
+ assignment.cover(pseud) || (@errors << ts("We couldn't assign %{val} to cover %{request}.", val: val, request: assignment.request_byline))
end
end
end
diff --git a/app/controllers/opendoors/external_authors_controller.rb b/app/controllers/opendoors/external_authors_controller.rb
index 3483009b4d6..dd05ee2a02b 100644
--- a/app/controllers/opendoors/external_authors_controller.rb
+++ b/app/controllers/opendoors/external_authors_controller.rb
@@ -33,7 +33,7 @@ def create
unless @external_author.save
flash[:error] = ts("We couldn't save that address.")
else
- flash[:notice] = ts("We have saved and blocked the email address #{@external_author.email}")
+ flash[:notice] = ts("We have saved and blocked the email address %{email}", email: @external_author.email)
end
redirect_to opendoors_tools_path
@@ -58,9 +58,9 @@ def forward
@invitation.invitee_email = @email
@invitation.creator = User.find_by(login: "open_doors") || current_user
if @invitation.save
- flash[:notice] = ts("Claim invitation for #{@external_author.email} has been forwarded to #{@invitation.invitee_email}!")
+ flash[:notice] = ts("Claim invitation for %{author_email} has been forwarded to %{invitee_email}!", author_email: @external_author.email, invitee_email: @invitation.invitee_email)
else
- flash[:error] = ts("We couldn't forward the claim for #{@external_author.email} to that email address.") + @invitation.errors.full_messages.join(", ")
+ flash[:error] = ts("We couldn't forward the claim for %{author_email} to that email address.", author_email: @external_author.email) + @invitation.errors.full_messages.join(", ")
end
# redirect to external author listing for that user
diff --git a/app/controllers/opendoors/tools_controller.rb b/app/controllers/opendoors/tools_controller.rb
index d12e1d07811..1f3cb805a64 100644
--- a/app/controllers/opendoors/tools_controller.rb
+++ b/app/controllers/opendoors/tools_controller.rb
@@ -48,7 +48,7 @@ def url_update
# check for any other works
works = Work.where(imported_from_url: @imported_from_url)
if works.count > 0
- flash[:error] = ts("There is already a work imported from the url #{@imported_from_url}.")
+ flash[:error] = ts("There is already a work imported from the url %{url}.", url: @imported_from_url)
else
# ok let's try to update
@work.update_attribute(:imported_from_url, @imported_from_url)
diff --git a/app/controllers/redirect_controller.rb b/app/controllers/redirect_controller.rb
index 3ea68dee6ea..320cc5c96b0 100644
--- a/app/controllers/redirect_controller.rb
+++ b/app/controllers/redirect_controller.rb
@@ -11,7 +11,7 @@ def do_redirect
else
@work = Work.find_by_url(url)
if @work
- flash[:notice] = ts("You have been redirected here from #{url}. Please update the original link if possible!")
+ flash[:notice] = ts("You have been redirected here from %{url}. Please update the original link if possible!", url: url)
redirect_to work_path(@work) and return
else
flash[:error] = ts("We could not find a work imported from that url in the Archive of Our Own, sorry! Try another url?")
diff --git a/app/controllers/tag_set_nominations_controller.rb b/app/controllers/tag_set_nominations_controller.rb
index f4b4991a9fc..8b554768d54 100644
--- a/app/controllers/tag_set_nominations_controller.rb
+++ b/app/controllers/tag_set_nominations_controller.rb
@@ -312,10 +312,10 @@ def collect_update_multiple_results
# this is the tricky one: make sure we can do this name change
tagnom = TagNomination.for_tag_set(@tag_set).where(type: "#{type.classify}Nomination", tagname: name).first
if !tagnom
- @errors << ts("Couldn't find a #{type} nomination for #{name}")
+ @errors << ts("Couldn't find a #{type} nomination for %{name}", name: name)
@force_expand[type] = true
elsif !tagnom.change_tagname?(val)
- @errors << ts("Invalid name change for #{name} to #{val}: %{msg}", msg: tagnom.errors.full_messages.join(', '))
+ @errors << ts("Invalid name change for %{name} to %{val}: %{msg}", name: name, val: val, msg: tagnom.errors.full_messages.join(", "))
@force_expand[type] = true
elsif action == "synonym"
@synonym[type] << val
diff --git a/app/helpers/series_helper.rb b/app/helpers/series_helper.rb
index e19f8f84fe3..af77855ed61 100644
--- a/app/helpers/series_helper.rb
+++ b/app/helpers/series_helper.rb
@@ -55,7 +55,7 @@ def series_data_for_work(work)
def work_series_description(work, series)
serial = SerialWork.where(work_id: work.id, series_id: series.id).first
- ts("Part #{serial.position} of #{link_to(series.title, series)}").html_safe
+ ts("Part %{position} of %{title}".html_safe, position: serial.position, title: link_to(series.title, series))
end
def series_list_for_feeds(work)
diff --git a/app/models/work.rb b/app/models/work.rb
index 206ea254d8d..c4e3edcae86 100755
--- a/app/models/work.rb
+++ b/app/models/work.rb
@@ -179,7 +179,7 @@ def new_recipients_allow_gifts
next if self.challenge_assignments.map(&:requesting_pseud).include?(gift.pseud)
next if self.challenge_claims.reject { |c| c.request_prompt.anonymous? }.map(&:requesting_pseud).include?(gift.pseud)
- self.errors.add(:base, ts("#{gift.pseud.byline} does not accept gifts."))
+ self.errors.add(:base, ts("%{byline} does not accept gifts.", byline: gift.pseud.byline))
end
end
diff --git a/app/models/work_skin.rb b/app/models/work_skin.rb
index 9724f3e6030..cb9dc0b3f4c 100644
--- a/app/models/work_skin.rb
+++ b/app/models/work_skin.rb
@@ -9,7 +9,7 @@ def clean_css
return if self.css.blank?
check = lambda {|ruleset, property, value|
if property == "position" && value == "fixed"
- errors.add(:base, ts("The #{property} property in #{ruleset.selectors.join(', ')} cannot have the value #{value} in Work skins, sorry!"))
+ errors.add(:base, ts("The %{property} property in %{selectors} cannot have the value %{value} in Work skins, sorry!", property: property, selectors: ruleset.selectors.join(", "), value: value))
return false
end
return true
diff --git a/app/views/archive_faqs/show.html.erb b/app/views/archive_faqs/show.html.erb
index 76be2314fa9..e91dfa09324 100644
--- a/app/views/archive_faqs/show.html.erb
+++ b/app/views/archive_faqs/show.html.erb
@@ -28,7 +28,7 @@
<% #TODO: Change this back to 'Questions in the NAME Category' once Translation system in place %>
- <%= ts("#{@archive_faq.title}") %>
+ <%= @archive_faq.title %>
<% for q in @questions %>
@@ -53,4 +53,4 @@
<% end %>
-
\ No newline at end of file
+
diff --git a/app/views/bookmarks/_bookmark_user_module.html.erb b/app/views/bookmarks/_bookmark_user_module.html.erb
index 4b982bc088f..45faac2fbca 100644
--- a/app/views/bookmarks/_bookmark_user_module.html.erb
+++ b/app/views/bookmarks/_bookmark_user_module.html.erb
@@ -23,7 +23,7 @@
<% unless bookmark.collections.blank? %>
<% bookmark.collections.each do |modded| %>
<% if modded.moderated? && !bookmark.approved_collections.include?(modded) && is_author_of?(bookmark) %>
- <%= ts("The collection #{modded.title} is currently moderated. Your bookmark must be approved by the collection maintainers before being listed there.") %>
+ <%= ts("The collection %{title} is currently moderated. Your bookmark must be approved by the collection maintainers before being listed there.", title: modded.title) %>
<% end %>
<% end %>
<% unless bookmark.approved_collections.empty? && !is_author_of?(bookmark) %>
diff --git a/app/views/menu/_menu_fandoms.html.erb b/app/views/menu/_menu_fandoms.html.erb
index c863d0c481c..2a631f06afb 100644
--- a/app/views/menu/_menu_fandoms.html.erb
+++ b/app/views/menu/_menu_fandoms.html.erb
@@ -3,7 +3,7 @@
<% cache "menu-fandoms-version4", skip_digest: true do %>
<% Media.for_menu.each do |medium| %>
<% unless medium.id.nil? %>
- <%= link_to ts("#{medium.name}", key: 'header.fandom'), medium_fandoms_path(medium) %>
+ <%= link_to medium.name, medium_fandoms_path(medium) %>
<% end %>
<% end %>
<% end %>
diff --git a/app/views/works/_search_box.html.erb b/app/views/works/_search_box.html.erb
index f3e8e50988b..6b10347802a 100644
--- a/app/views/works/_search_box.html.erb
+++ b/app/views/works/_search_box.html.erb
@@ -4,7 +4,7 @@
<%= label_tag :site_search, ts('Work Search:', key: 'header'), :class => 'landmark' %>
<%= f.text_field :query, :class => 'text', :id => 'site_search', :"aria-describedby" => 'site_search_tooltip' %>
- <%= ts('tip:', key: 'header') %> <%= ts("#{random_search_tip}", key: 'searchtip') %>
+ <%= ts('tip:', key: 'header') %> <%= random_search_tip %>
<%= submit_tag ts('Search', key: 'header'), :class => 'button', :name => nil %>
diff --git a/lib/css_cleaner.rb b/lib/css_cleaner.rb
index 9616666b596..32ebc2c6799 100644
--- a/lib/css_cleaner.rb
+++ b/lib/css_cleaner.rb
@@ -63,17 +63,17 @@ def clean_css_code(css_code, options = {})
clean_declarations = ""
rs.each_declaration do |property, value, is_important|
if property.blank? || value.blank?
- errors.add(:base, ts("The code for #{rs.selectors.join(',')} doesn't seem to be a valid CSS rule."))
+ errors.add(:base, ts("The code for %{selectors} doesn't seem to be a valid CSS rule.", selectors: rs.selectors.join(",")))
elsif sanitize_css_property(property).blank?
- errors.add(:base, ts("We don't currently allow the CSS property #{property} -- please notify support if you think this is an error."))
+ errors.add(:base, ts("We don't currently allow the CSS property %{property} -- please notify support if you think this is an error.", property: property))
elsif (cleanval = sanitize_css_declaration_value(property, value)).blank?
- errors.add(:base, ts("The #{property} property in #{rs.selectors.join(', ')} cannot have the value #{value}, sorry!"))
+ errors.add(:base, ts("The %{property} property in %{selectors} cannot have the value %{value}, sorry!", property: property, selectors: rs.selectors.join(", "), value: value))
elsif (!caller_check || caller_check.call(rs, property, value))
clean_declarations += " #{property}: #{cleanval}#{is_important ? ' !important' : ''};\n"
end
end
if clean_declarations.blank?
- errors.add(:base, ts("There don't seem to be any rules for #{rs.selectors.join(',')}"))
+ errors.add(:base, ts("There don't seem to be any rules for %{selectors}", selectors: rs.selectors.join(",")))
else
# everything looks ok, add it to the css
clean_css += "#{selectors.join(",\n")} {\n"
From f34ff6cf26a153d8724f9d6e12c4263c5e04da84 Mon Sep 17 00:00:00 2001
From: Jonathan S
Date: Sat, 12 Aug 2023 23:38:19 -0400
Subject: [PATCH 023/208] AO3-6583 Updated the robots.txt to block chatGPT.
(#4594)
* AO3-6583 Updated the robots.txt to block chatGPT.
* Adjusted robots per request of reviewer.
---
public/robots.public.txt | 9 ++++-----
1 file changed, 4 insertions(+), 5 deletions(-)
diff --git a/public/robots.public.txt b/public/robots.public.txt
index f4ac6386c46..5ff24d7ec7e 100644
--- a/public/robots.public.txt
+++ b/public/robots.public.txt
@@ -1,5 +1,4 @@
# See https://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file
-#
User-agent: *
Disallow: /works? # cruel but efficient
@@ -12,7 +11,7 @@ Disallow: /people/search?
Disallow: /tags/search?
Disallow: /works/search?
-User-agent: Googlebot
+User-agent: Googlebot
Disallow: /autocomplete/
Disallow: /downloads/
Disallow: /external_works/
@@ -22,17 +21,17 @@ Disallow: /*search?
Disallow: /*?*query=
Disallow: /*?*sort_
Disallow: /*?*selected_tags
-Disallow: /*?*selected_pseuds
-Disallow: /*?*use_openid
Disallow: /*?*view_adult
Disallow: /*?*tag_id
Disallow: /*?*pseud_id
Disallow: /*?*user_id
Disallow: /*?*pseud=
-Disallow: /people?*show=
User-agent: CCBot
Disallow: /
+User-agent: GPTBot
+Disallow: /
+
User-agent: Slurp
Crawl-delay: 30
From 6504b0b1dbf54f3d1279fb72b73bd1b0f9e2cda4 Mon Sep 17 00:00:00 2001
From: tickinginstant
Date: Sat, 12 Aug 2023 23:39:18 -0400
Subject: [PATCH 024/208] AO3-6212 AO3-6573 Add indices to
tag_set_associations, and add the departure gem to make future migrations
easier. (#4528)
* AO3-6212 Add indices to tag_set_associations.
Also introduces the departure gem, to make future migrations easier.
* AO3-6212 Use the right environment check.
* AO3-6212 Indentation.
* AO3-6212 - Add no-check-unique-key-change to default options.
---
Gemfile | 3 +++
Gemfile.lock | 5 +++++
config/config.yml | 11 +++++++++++
config/initializers/departure.rb | 10 ++++++++++
...10162442_add_indices_to_tag_set_associations.rb | 14 ++++++++++++++
5 files changed, 43 insertions(+)
create mode 100644 config/initializers/departure.rb
create mode 100644 db/migrate/20230610162442_add_indices_to_tag_set_associations.rb
diff --git a/Gemfile b/Gemfile
index 9236d99478e..4cc989816b0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -113,6 +113,9 @@ gem 'kgio', '2.10.0'
# TODO: AO3-6297 Update the download code so we can remove mimemagic.
gem "mimemagic", "0.3.10"
+# Library for helping run pt-online-schema-change commands:
+gem "departure", "~> 6.5"
+
group :test do
gem "rspec-rails", "~> 4.0.1"
gem 'pickle'
diff --git a/Gemfile.lock b/Gemfile.lock
index 420e80799d3..4959418eebd 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -205,6 +205,10 @@ GEM
database_cleaner-core (2.0.1)
delorean (2.1.0)
chronic
+ departure (6.5.0)
+ activerecord (>= 5.2.0, < 7.1, != 7.0.0)
+ mysql2 (>= 0.4.0, <= 0.5.5)
+ railties (>= 5.2.0, < 7.1, != 7.0.0)
devise (4.8.1)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
@@ -624,6 +628,7 @@ DEPENDENCIES
dalli
database_cleaner
delorean
+ departure (~> 6.5)
devise
devise-async
elasticsearch (= 7.17.1)
diff --git a/config/config.yml b/config/config.yml
index 6625b10062e..e52da538648 100644
--- a/config/config.yml
+++ b/config/config.yml
@@ -518,3 +518,14 @@ WRANGLING_REPORT_LIMIT: 1000
# After this window, all comments are disabled. Setting this value to
# something below 1 -- or commenting it out -- will turn off comment disabling.
ADMIN_POST_COMMENTING_EXPIRATION_DAYS: 14
+
+# The arguments to pass to pt-online-schema-change:
+PERCONA_ARGS: >
+ --chunk-size=5k
+ --max-flow-ctl 0
+ --pause-file /tmp/pauseme
+ --max-load Threads_running=15
+ --critical-load Threads_running=100
+ --set-vars innodb_lock_wait_timeout=2
+ --alter-foreign-keys-method=auto
+ --no-check-unique-key-change
diff --git a/config/initializers/departure.rb b/config/initializers/departure.rb
new file mode 100644
index 00000000000..f201a5630d4
--- /dev/null
+++ b/config/initializers/departure.rb
@@ -0,0 +1,10 @@
+Departure.configure do |config|
+ # Disable departure by default. To use pt-online-schema-change for a
+ # migration, call
+ # uses_departure! if Rails.env.staging? || Rails.env.production?
+ # in the migration file.
+ config.enabled_by_default = false
+
+ # Set the arguments based on the config file:
+ config.global_percona_args = ArchiveConfig.PERCONA_ARGS.squish
+end
diff --git a/db/migrate/20230610162442_add_indices_to_tag_set_associations.rb b/db/migrate/20230610162442_add_indices_to_tag_set_associations.rb
new file mode 100644
index 00000000000..be9ff0658a9
--- /dev/null
+++ b/db/migrate/20230610162442_add_indices_to_tag_set_associations.rb
@@ -0,0 +1,14 @@
+class AddIndicesToTagSetAssociations < ActiveRecord::Migration[6.1]
+ uses_departure! if Rails.env.staging? || Rails.env.production?
+
+ def change
+ change_table :tag_set_associations do |t|
+ t.index :tag_id
+ t.index :parent_tag_id
+
+ t.index [:owned_tag_set_id, :parent_tag_id, :tag_id],
+ name: :index_tag_set_associations_on_tag_set_and_parent_and_tag,
+ unique: true
+ end
+ end
+end
From c472b7ac8da991ac33e69462a5c5ff5cf313b988 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=89ric=20B?=
Date: Sun, 13 Aug 2023 05:40:00 +0200
Subject: [PATCH 025/208] AO3-6561 Repair confirm dialog on comment "Spam"
button (#4578)
* Repair confirm dialog on "Spam" button
https://otwarchive.atlassian.net/browse/AO3-6561
* Add test that isn't testing anything
* Hound
* Hound (bis)
* Actually test popup
* Hound
* Hound (always two or more)
* Update features/comments_and_kudos/spam_comments.feature
Co-authored-by: sarken
* Update features/comments_and_kudos/spam_comments.feature
Co-authored-by: sarken
* Update features/step_definitions/comment_steps.rb
Co-authored-by: sarken
* Explicit popup confirm in test
Otherwise
What is being tested is indeed quite mysterious
---------
Co-authored-by: sarken
---
app/helpers/comments_helper.rb | 2 +-
.../comments_and_kudos/spam_comments.feature | 28 +++++++++++++++++++
features/step_definitions/comment_steps.rb | 8 ++++++
3 files changed, 37 insertions(+), 1 deletion(-)
diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb
index 377e50c6e3d..9a4266dc4b6 100644
--- a/app/helpers/comments_helper.rb
+++ b/app/helpers/comments_helper.rb
@@ -302,7 +302,7 @@ def cancel_delete_comment_link(comment)
# return html link to mark/unmark comment as spam
def tag_comment_as_spam_link(comment)
if comment.approved
- link_to(ts("Spam"), reject_comment_path(comment), method: :put, confirm: "Are you sure you want to mark this as spam?" )
+ link_to(ts("Spam"), reject_comment_path(comment), method: :put, data: { confirm: "Are you sure you want to mark this as spam?" })
else
link_to(ts("Not Spam"), approve_comment_path(comment), method: :put)
end
diff --git a/features/comments_and_kudos/spam_comments.feature b/features/comments_and_kudos/spam_comments.feature
index 7a959d3b12a..ef87225955a 100644
--- a/features/comments_and_kudos/spam_comments.feature
+++ b/features/comments_and_kudos/spam_comments.feature
@@ -46,3 +46,31 @@ Feature: Marking comments as spam
When I follow "Default Admin Post"
Then I should see "Comments (1)"
+
+ Scenario: Author can mark comments as spam
+ Given I am logged in as "author"
+ And I post the work "Popular Fic"
+ And I log out
+ When I view the work "Popular Fic" with comments
+ And I post a spam comment
+ And I post a guest comment
+ And I am logged in as "author"
+ And I view the work "Popular Fic" with comments
+ Then I should see "Comments (2)"
+ And I should see "Buy my product"
+ When I mark the comment as spam
+ Then I should see "Comments (1)"
+ And I should not see "Buy my product"
+
+ @javascript
+ Scenario: If Javascript is enabled, there's a confirmation popup before marking a comment as spam
+ Given the work "Popular Fic" by "author"
+ And a guest comment on the work "Popular Fic"
+ And a guest comment on the work "Popular Fic"
+ When I am logged in as "author"
+ And I view the work "Popular Fic" with comments
+ Then I should see "Comments (2)"
+ When I mark the comment as spam
+ And I confirm I want to mark the comment as spam
+ And I view the work "Popular Fic" with comments
+ Then I should see "Comments (1)"
diff --git a/features/step_definitions/comment_steps.rb b/features/step_definitions/comment_steps.rb
index c434b7ea7c3..9167278d888 100644
--- a/features/step_definitions/comment_steps.rb
+++ b/features/step_definitions/comment_steps.rb
@@ -302,3 +302,11 @@
click_link("Yes, delete!") # TODO: Fix along with comment deletion.
end
end
+
+When "I mark the comment as spam" do
+ click_link("Spam")
+end
+
+When "I confirm I want to mark the comment as spam" do
+ expect(page.accept_alert).to eq("Are you sure you want to mark this as spam?") if @javascript
+end
From 53395fd54b7050047a94a58a11aa88c4c6b73a3c Mon Sep 17 00:00:00 2001
From: neuroalien <105230050+neuroalien@users.noreply.github.com>
Date: Sun, 13 Aug 2023 04:40:48 +0100
Subject: [PATCH 026/208] AO3-6553 Better semantics and a11y for the media list
on the homepage (#4581)
AO3-6553 Turn homepage media list into navigation
Semantically, the list of media (plus All Fandoms) on the homepage is a
navigation element, not a menu.
The list of media used in the main site menu needs to stay a `menu`, so
we're unDRYing the partial and creating a new partial just for use on
the homepage.
The preferred element to use in modern web development is `nav`. We
don't use this anywhere else (yet), so this will be our test case.
(Per https://www.w3.org/TR/html-aria/#docconformance, `ul`s are not
allowed to have the role `navigation`.)
---
app/views/home/_fandoms.html.erb | 10 ++++++++++
app/views/home/index.html.erb | 12 ++++++------
config/locales/views/en.yml | 8 ++++++++
public/stylesheets/site/2.0/26-media-narrow.css | 2 +-
4 files changed, 25 insertions(+), 7 deletions(-)
create mode 100644 app/views/home/_fandoms.html.erb
diff --git a/app/views/home/_fandoms.html.erb b/app/views/home/_fandoms.html.erb
new file mode 100644
index 00000000000..f5cd74ef2e7
--- /dev/null
+++ b/app/views/home/_fandoms.html.erb
@@ -0,0 +1,10 @@
+
+ <%= link_to t(".all_fandoms"), media_path %>
+ <% cache "homepage-fandoms-version1", skip_digest: true do %>
+ <% Media.for_menu.each do |medium| %>
+ <% unless medium.id.nil? %>
+ <%= link_to ts("#{medium.name}"), medium_fandoms_path(medium) %>
+ <% end %>
+ <% end %>
+ <% end %>
+
diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb
index 5cd39fbb77c..0bafaa432d0 100644
--- a/app/views/home/index.html.erb
+++ b/app/views/home/index.html.erb
@@ -5,7 +5,7 @@
<% if logged_in? && @homepage.favorite_tags.present? %>
-
<%= ts('Find your favorites') %>
+
<%= t(".find_your_favorites") %>
<% @homepage.favorite_tags.each do |favorite_tag| %>
@@ -15,13 +15,13 @@
<% else %>
-
-
<%= ts('Find your favorites') %>
+
" class="browse module">
+ <%= t(".find_your_favorites") %>
<% if logged_in? %>
- <%= ts('Browse fandoms by media or favorite up to %{maximum} tags to have them listed here!', maximum: ArchiveConfig.MAX_FAVORITE_TAGS) %>
+ <%= t(".browse_or_favorite", count: ArchiveConfig.MAX_FAVORITE_TAGS) %>
<% end %>
- <%= render 'menu/menu_fandoms' %>
-
+ <%= render 'fandoms' %>
+
<% end %>
<% if @homepage.admin_posts.present? %>
diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml
index 911a4a53a15..9b0d68cc4c4 100644
--- a/config/locales/views/en.yml
+++ b/config/locales/views/en.yml
@@ -429,6 +429,14 @@ en:
home:
donate:
page_title: Donate or Volunteer
+ fandoms:
+ all_fandoms: All Fandoms
+ index:
+ browse_or_favorite:
+ one: Browse fandoms by media or favorite up to %{count} tag to have it listed here!
+ other: Browse fandoms by media or favorite up to %{count} tags to have them listed here!
+ find_your_favorites: Find your favorites
+ media_navigation_label: Media
invitations:
invitation:
email_address_label: Enter an email address
diff --git a/public/stylesheets/site/2.0/26-media-narrow.css b/public/stylesheets/site/2.0/26-media-narrow.css
index aee2a1722be..0fb07fc582e 100644
--- a/public/stylesheets/site/2.0/26-media-narrow.css
+++ b/public/stylesheets/site/2.0/26-media-narrow.css
@@ -182,7 +182,7 @@ body .narrow-shown {
padding: 0;
}
-.splash div.module, .logged-in .splash div.module {
+.splash div.module, .splash nav.module, .logged-in .splash div.module, .logged-in .splash nav.module {
clear: both;
margin-left: 0;
margin-right: 0;
From 98b0c2e4bbcabc2d3b50fa778d20845d3f043835 Mon Sep 17 00:00:00 2001
From: Brian Austin <13002992+brianjaustin@users.noreply.github.com>
Date: Sat, 12 Aug 2023 23:41:21 -0400
Subject: [PATCH 027/208] AO3-6549 Remove ignore of auto collection invite
column (#4568)
* Remove ignore column
* Fix style and add some tests
* Use better anchors
* Simplify spec structure
---
app/models/preference.rb | 19 ++++++++-------
spec/models/preference_spec.rb | 43 ++++++++++++++++++++++++++++++++++
2 files changed, 53 insertions(+), 9 deletions(-)
create mode 100644 spec/models/preference_spec.rb
diff --git a/app/models/preference.rb b/app/models/preference.rb
index 6960e06e1de..9ba6786718d 100644
--- a/app/models/preference.rb
+++ b/app/models/preference.rb
@@ -1,12 +1,12 @@
class Preference < ApplicationRecord
- self.ignored_columns = [:automatically_approve_collections]
-
belongs_to :user
belongs_to :skin
- validates_format_of :work_title_format, with: /^[a-zA-Z0-9_\-,\. ]+$/,
- message: ts("can only contain letters, numbers, spaces, and some limited punctuation (comma, period, dash, underscore)."),
- multiline: true
+ validates :work_title_format,
+ format: {
+ with: /\A[a-zA-Z0-9_\-,\. ]+\z/,
+ message: ts("can only contain letters, numbers, spaces, and some limited punctuation (comma, period, dash, underscore).")
+ }
validate :can_use_skin, if: :skin_id_changed?
@@ -16,10 +16,11 @@ def set_default_skin
end
def self.disable_work_skin?(param)
- return false if param == 'creator'
- return true if param == 'light' || param == 'disable'
- return false unless User.current_user.is_a? User
- return User.current_user.try(:preference).try(:disable_work_skins)
+ return false if param == "creator"
+ return true if %w[light disable].include?(param)
+ return false unless User.current_user.is_a?(User)
+
+ User.current_user.try(:preference).try(:disable_work_skins)
end
def can_use_skin
diff --git a/spec/models/preference_spec.rb b/spec/models/preference_spec.rb
new file mode 100644
index 00000000000..546d4940c9e
--- /dev/null
+++ b/spec/models/preference_spec.rb
@@ -0,0 +1,43 @@
+require "spec_helper"
+
+describe Preference do
+ it { is_expected.to allow_value("Test_Title-1 .,").for(:work_title_format) }
+ it { is_expected.not_to allow_value("@; Test").for(:work_title_format) }
+ it { is_expected.not_to allow_value("Sneaky\n\\").for(:work_title_format) }
+
+ describe ".disable_work_skin?" do
+ it "returns false for creator" do
+ expect(Preference.disable_work_skin?("creator")).to be(false)
+ end
+
+ %w[light disable].each do |param|
+ it "returns true for #{param}" do
+ expect(Preference.disable_work_skin?(param)).to be(true)
+ end
+ end
+
+ context "when the current user is a guest" do
+ it "returns false" do
+ expect(Preference.disable_work_skin?("foo")).to be(false)
+ end
+ end
+
+ context "when the current user is registered" do
+ let(:user) { create(:user) }
+
+ before do
+ User.current_user = user
+ end
+
+ it "returns false when the user's preference has skins enabled" do
+ user.preference.update!(disable_work_skins: false)
+ expect(Preference.disable_work_skin?("foo")).to be(false)
+ end
+
+ it "returns true when the user's preference has skins disabled" do
+ user.preference.update!(disable_work_skins: true)
+ expect(Preference.disable_work_skin?("foo")).to be(true)
+ end
+ end
+ end
+end
From 8aff781ebf82e46598f613a3be326d1094f8b701 Mon Sep 17 00:00:00 2001
From: weeklies <80141759+weeklies@users.noreply.github.com>
Date: Sun, 13 Aug 2023 11:41:56 +0800
Subject: [PATCH 028/208] AO3-6546 Fix account creation form becoming stuck
(#4587)
* AO3-6546 Do not use JS to prevent double submission
* AO3-6546 Fix minor TOS and age validation message bugs
* AO3-6546 Fix missed i18n
Co-authored-by: neuroalien <105230050+neuroalien@users.noreply.github.com>
* Disable submit button until LV error is fixed
---------
Co-authored-by: neuroalien <105230050+neuroalien@users.noreply.github.com>
---
.../users/registrations_controller.rb | 24 ++++++++-----------
app/models/user.rb | 4 ++--
app/views/users/registrations/new.html.erb | 20 +++++-----------
config/locales/views/en.yml | 9 +++++++
public/javascripts/application.js | 10 --------
5 files changed, 27 insertions(+), 40 deletions(-)
diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb
index 3ff2a21e428..13f48b394b5 100644
--- a/app/controllers/users/registrations_controller.rb
+++ b/app/controllers/users/registrations_controller.rb
@@ -16,20 +16,16 @@ def new
def create
@hide_dashboard = true
- if params[:cancel_create_account]
- redirect_to root_path
- else
- build_resource(sign_up_params)
-
- resource.transaction do
- # skip sending the Devise confirmation notification
- resource.skip_confirmation_notification!
- resource.invitation_token = params[:invitation_token]
- if resource.save
- notify_and_show_confirmation_screen
- else
- render action: 'new'
- end
+ build_resource(sign_up_params)
+
+ resource.transaction do
+ # skip sending the Devise confirmation notification
+ resource.skip_confirmation_notification!
+ resource.invitation_token = params[:invitation_token]
+ if resource.save
+ notify_and_show_confirmation_screen
+ else
+ render action: "new"
end
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 56228b20d74..a8370ac93ac 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -219,12 +219,12 @@ def unread_inbox_comments_count
validates_acceptance_of :terms_of_service,
allow_nil: false,
- message: ts("Sorry, you need to accept the Terms of Service in order to sign up."),
+ message: ts("^Sorry, you need to accept the Terms of Service in order to sign up."),
if: :first_save?
validates_acceptance_of :age_over_13,
allow_nil: false,
- message: ts("Sorry, you have to be over 13!"),
+ message: ts("^Sorry, you have to be over 13!"),
if: :first_save?
def to_param
diff --git a/app/views/users/registrations/new.html.erb b/app/views/users/registrations/new.html.erb
index adc86681c7a..56184ce310d 100644
--- a/app/views/users/registrations/new.html.erb
+++ b/app/views/users/registrations/new.html.erb
@@ -1,5 +1,5 @@
-<%= ts("Create Account") %>
+<%= t(".heading") %>
<%= error_messages_for :user %>
@@ -9,29 +9,21 @@
<%= hidden_field_tag :invitation_token, resource.invitation_token %>
- <%= ts("User Details") %>
+ <%= t(".legend.user") %>
<%= render :partial => "passwd", :locals => {:f => f} %>
- <%= ts("Legal Agreements") %>
+ <%= t(".legend.legal") %>
<%= render :partial => "legal", :locals => {:f => f} %>
- <%= ts("Submit") %>
+ <%= t(".submit") %>
-
- <%= submit_tag ts('Create Account') %>
+ <%= link_to t(".cancel"), root_path %>
+ <%= submit_tag t(".submit"), data: { disable_with: t(".wait") } %>
<% end %>
-
-<%= content_for :footer_js do %>
- <%= javascript_tag do %>
- $j(document).ready(function(){
- $j('#user_registration_form').preventDoubleSubmit();
- })
- <% end %>
-<% end %>
diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml
index 9b0d68cc4c4..bd30557f484 100644
--- a/config/locales/views/en.yml
+++ b/config/locales/views/en.yml
@@ -626,6 +626,15 @@ en:
options_info: You can delete them, but please consider %{orphaning_link} them instead!
works_summary: 'You have %{work_count} work(s) under the following pseuds: %{pseuds}.'
submit: Save
+ registrations:
+ new:
+ cancel: Cancel
+ heading: Create Account
+ legend:
+ legal: Legal Agreements
+ user: User Details
+ submit: Create Account
+ wait: Please wait...
sessions:
new:
beta_reminder:
diff --git a/public/javascripts/application.js b/public/javascripts/application.js
index 9a095578a1e..24817a345fc 100644
--- a/public/javascripts/application.js
+++ b/public/javascripts/application.js
@@ -352,16 +352,6 @@ function attachCharacterCounters() {
$j('.observe_textlength').each(countFn);
}
-// prevent double submission for JS enabled
-jQuery.fn.preventDoubleSubmit = function() {
- jQuery(this).submit(function() {
- if (this.beenSubmitted)
- return false;
- else
- this.beenSubmitted = true;
- });
-};
-
// add attributes that are only needed in the primary menus and when JavaScript is enabled
function setupDropdown(){
$j('#header').find('.dropdown').attr("aria-haspopup", true);
From 5758166563101ffebfe28135b79ecb089f6848a4 Mon Sep 17 00:00:00 2001
From: EchoEkhi
Date: Sun, 13 Aug 2023 11:43:03 +0800
Subject: [PATCH 029/208] AO3-6540 Allow banned users to delete works, chapters
and remove themselves as co-creators (#4552)
* AO3-6540 Edit controller permissions and add chapters spec
* AO3-6540 Fix test
* Add tests for work actions
* AO3-6540 Pleasing the hound
* AO3-6540 Disallow suspended users to remove stuff
* AO3-6540 Pleasing the hound
* AO3-6540 Add tests for orphaning
* AO3-6540 Add multiple_actions spec tests
* AO3-6540 Pleasing the hound
* AO3-6540 Pleasing the hound
* AO3-6540 Pleasing the hound
* AO3-6540 Refactor multiple_actions_spec
* AO3-6540 Pleasing the hound
* AO3-6540 Correct typos in tests
* AO3-6540 Correct typos in tests
* AO3-6540 Revert _html keys in controllers
* AO3-6540 Restore i18n-tasks keys as well
---
app/controllers/application_controller.rb | 12 +-
app/controllers/chapters_controller.rb | 3 +-
app/controllers/orphans_controller.rb | 1 +
app/controllers/works_controller.rb | 3 +-
spec/controllers/chapters_controller_spec.rb | 116 ++++++++-
spec/controllers/orphans_controller_spec.rb | 219 ++++++++++++++++-
.../works/default_rails_actions_spec.rb | 68 ++++++
.../works/multiple_actions_spec.rb | 221 +++++++++++++++---
8 files changed, 597 insertions(+), 46 deletions(-)
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 94d0a643a93..9d52b0aed7f 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -419,14 +419,22 @@ def use_caching?
def check_user_status
if current_user.is_a?(User) && (current_user.suspended? || current_user.banned?)
if current_user.suspended?
- flash[:error] = t('suspension_notice', default: "Your account has been suspended until %{suspended_until}. You may not add or edit content until your suspension has been resolved. Please contact Abuse for more information.", suspended_until: localize(current_user.suspended_until)).html_safe
+ flash[:error] = t("suspension_notice", default: "Your account has been suspended until %{suspended_until}. You may not add or edit content until your suspension has been resolved. Please contact Abuse for more information.", suspended_until: localize(current_user.suspended_until)).html_safe
else
- flash[:error] = t('ban_notice', default: "Your account has been banned. You are not permitted to add or edit archive content. Please contact Abuse for more information.").html_safe
+ flash[:error] = t("ban_notice", default: "Your account has been banned. You are not permitted to add or edit archive content. Please contact Abuse for more information.").html_safe
end
redirect_to current_user
end
end
+ # Prevents temporarily suspended users from deleting content
+ def check_user_not_suspended
+ return unless current_user.is_a?(User) && current_user.suspended?
+
+ flash[:error] = t("suspension_notice", default: "Your account has been suspended until %{suspended_until}. You may not add or edit content until your suspension has been resolved. Please contact Abuse for more information.", suspended_until: localize(current_user.suspended_until)).html_safe
+ redirect_to current_user
+ end
+
# Does the current user own a specific object?
def current_user_owns?(item)
!item.nil? && current_user.is_a?(User) && (item.is_a?(User) ? current_user == item : current_user.is_author_of?(item))
diff --git a/app/controllers/chapters_controller.rb b/app/controllers/chapters_controller.rb
index 66d70cc8ab5..f6cad1bcbde 100644
--- a/app/controllers/chapters_controller.rb
+++ b/app/controllers/chapters_controller.rb
@@ -1,7 +1,8 @@
class ChaptersController < ApplicationController
# only registered users and NOT admin should be able to create new chapters
before_action :users_only, except: [ :index, :show, :destroy, :confirm_delete ]
- before_action :check_user_status, only: [:new, :create, :edit, :update]
+ before_action :check_user_status, only: [:new, :create, :update]
+ before_action :check_user_not_suspended, only: [:edit, :confirm_delete, :destroy]
before_action :load_work
# only authors of a work should be able to edit its chapters
before_action :check_ownership, except: [:index, :show]
diff --git a/app/controllers/orphans_controller.rb b/app/controllers/orphans_controller.rb
index 31ba2dd0086..3e3fd82374a 100644
--- a/app/controllers/orphans_controller.rb
+++ b/app/controllers/orphans_controller.rb
@@ -2,6 +2,7 @@ class OrphansController < ApplicationController
# You must be logged in to orphan works - relies on current_user data
before_action :users_only, except: [:index]
+ before_action :check_user_not_suspended, except: [:index]
before_action :load_pseuds, only: [:create]
before_action :load_orphans, only: [:create]
diff --git a/app/controllers/works_controller.rb b/app/controllers/works_controller.rb
index 578af45eb77..3d6fd0f4607 100755
--- a/app/controllers/works_controller.rb
+++ b/app/controllers/works_controller.rb
@@ -5,7 +5,8 @@ class WorksController < ApplicationController
before_action :load_collection
before_action :load_owner, only: [:index]
before_action :users_only, except: [:index, :show, :navigate, :search, :collected, :edit_tags, :update_tags, :drafts, :share]
- before_action :check_user_status, except: [:index, :show, :navigate, :search, :collected, :share]
+ before_action :check_user_status, except: [:index, :edit, :edit_multiple, :confirm_delete_multiple, :delete_multiple, :confirm_delete, :destroy, :show, :show_multiple, :navigate, :search, :collected, :share]
+ before_action :check_user_not_suspended, only: [:edit, :confirm_delete, :destroy, :show_multiple, :edit_multiple, :confirm_delete_multiple, :delete_multiple]
before_action :load_work, except: [:new, :create, :import, :index, :show_multiple, :edit_multiple, :update_multiple, :delete_multiple, :search, :drafts, :collected]
# this only works to check ownership of a SINGLE item and only if load_work has happened beforehand
before_action :check_ownership, except: [:index, :show, :navigate, :new, :create, :import, :show_multiple, :edit_multiple, :edit_tags, :update_tags, :update_multiple, :delete_multiple, :search, :mark_for_later, :mark_as_read, :drafts, :collected, :share]
diff --git a/spec/controllers/chapters_controller_spec.rb b/spec/controllers/chapters_controller_spec.rb
index 01ba9a1e98b..cdca361e7b4 100644
--- a/spec/controllers/chapters_controller_spec.rb
+++ b/spec/controllers/chapters_controller_spec.rb
@@ -8,11 +8,34 @@
let!(:work) { create(:work, authors: [user.pseuds.first]) }
let(:unposted_work) { create(:draft, authors: [user.pseuds.first]) }
- let(:banned_users_work) { create(:work) }
- let(:banned_user) do
- user = banned_users_work.users.first
- user.update(banned: true)
- user
+ let(:co_creator) { create(:user) }
+
+ let(:banned_user) { create(:user, banned: true) }
+ let(:banned_users_work) do
+ banned_user.update!(banned: false)
+ work = create(:work, authors: [banned_user.pseuds.first, co_creator.pseuds.first])
+ banned_user.update!(banned: true)
+ work
+ end
+ let(:banned_users_work_chapter2) do
+ banned_user.update!(banned: false)
+ chapter = create(:chapter, work: banned_users_work, position: 2, authors: [banned_user.pseuds.first, co_creator.pseuds.first])
+ banned_user.update!(banned: true)
+ chapter
+ end
+
+ let(:suspended_user) { create(:user, suspended: true, suspended_until: 1.week.from_now) }
+ let(:suspended_users_work) do
+ suspended_user.update!(suspended: false, suspended_until: nil)
+ work = create(:work, authors: [suspended_user.pseuds.first, co_creator.pseuds.first])
+ suspended_user.update!(suspended: true, suspended_until: 1.week.from_now)
+ work
+ end
+ let(:suspended_users_work_chapter2) do
+ suspended_user.update!(suspended: false, suspended_until: nil)
+ chapter = create(:chapter, work: suspended_users_work, position: 2, authors: [suspended_user.pseuds.first, co_creator.pseuds.first])
+ suspended_user.update!(suspended: true, suspended_until: 1.week.from_now)
+ chapter
end
describe "index" do
@@ -280,6 +303,13 @@
expect(response).to render_template(:new)
end
+ it "errors and redirects to user page when user is suspended" do
+ fake_login_known_user(suspended_user)
+ get :new, params: { work_id: suspended_users_work.id }
+ it_redirects_to_simple(user_path(suspended_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+
it "errors and redirects to user page when user is banned" do
fake_login_known_user(banned_user)
get :new, params: { work_id: banned_users_work.id }
@@ -318,11 +348,17 @@
expect(response).to render_template(:edit)
end
- it "errors and redirects to user page when user is banned" do
+ it "errors and redirects to user page when user is suspended" do
+ fake_login_known_user(suspended_user)
+ get :edit, params: { work_id: suspended_users_work.id, id: suspended_users_work.chapters.first.id }
+ it_redirects_to_simple(user_path(suspended_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+
+ it "renders edit template when user is banned" do
fake_login_known_user(banned_user)
get :edit, params: { work_id: banned_users_work.id, id: banned_users_work.chapters.first.id }
- it_redirects_to_simple(user_path(banned_user))
- expect(flash[:error]).to include("Your account has been banned.")
+ expect(response).to render_template(:edit)
end
end
@@ -339,7 +375,6 @@
context "with valid remove params" do
context "when work is multichaptered and co-created" do
- let(:co_creator) { create(:user) }
let!(:co_created_chapter) { create(:chapter, work: work, authors: [user.pseuds.first, co_creator.pseuds.first]) }
context "when logged in user also owns other chapters" do
@@ -371,6 +406,33 @@
it_redirects_to(edit_work_path(work, remove: "me"))
end
end
+
+ context "when the logged in user is suspended" do
+ before do
+ fake_login_known_user(suspended_user)
+ end
+
+ it "errors and redirects to user page" do
+ get :edit, params: { work_id: suspended_users_work.id, id: suspended_users_work_chapter2.id, remove: "me" }
+
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+ end
+
+ context "when the logged in user is banned" do
+ before do
+ fake_login_known_user(banned_user)
+ end
+
+ it "removes user from chapter, gives notice, and redirects to work" do
+ get :edit, params: { work_id: banned_users_work.id, id: banned_users_work_chapter2.id, remove: "me" }
+
+ expect(banned_users_work_chapter2.reload.pseuds).to eq [co_creator.pseuds.first]
+ expect(banned_users_work.reload.pseuds).to eq [co_creator.pseuds.first, banned_user.pseuds.first]
+
+ it_redirects_to_with_notice(work_path(banned_users_work), "You have been removed as a creator from the chapter.")
+ end
+ end
end
end
end
@@ -391,6 +453,13 @@
chapter_attributes[:author_attributes] = { ids: [user.pseuds.first.id] }
end
+ it "errors and redirects to user page when user is suspended" do
+ fake_login_known_user(suspended_user)
+ post :create, params: { work_id: suspended_users_work.id, chapter: chapter_attributes }
+ it_redirects_to_simple(user_path(suspended_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+
it "errors and redirects to user page when user is banned" do
fake_login_known_user(banned_user)
post :create, params: { work_id: banned_users_work.id, chapter: chapter_attributes }
@@ -576,6 +645,13 @@
fake_login_known_user(user)
end
+ it "errors and redirects to user page when user is suspended" do
+ fake_login_known_user(suspended_user)
+ put :update, params: { work_id: suspended_users_work.id, id: suspended_users_work.chapters.first.id, chapter: chapter_attributes }
+ it_redirects_to_simple(user_path(suspended_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+
it "errors and redirects to user page when user is banned" do
fake_login_known_user(banned_user)
put :update, params: { work_id: banned_users_work.id, id: banned_users_work.chapters.first.id, chapter: chapter_attributes }
@@ -1076,6 +1152,28 @@
it_redirects_to_with_error(edit_work_path(work), "You can't delete the only chapter in your work. If you want to delete the work, choose \"Delete Work\".")
end
end
+
+ context "when the logged in user is suspended" do
+ before do
+ fake_login_known_user(suspended_user)
+ end
+
+ it "errors and redirects to user page" do
+ delete :destroy, params: { work_id: suspended_users_work.id, id: suspended_users_work_chapter2.id }
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+ end
+
+ context "when the logged in user is banned" do
+ before do
+ fake_login_known_user(banned_user)
+ end
+
+ it "gives a notice that the chapter was deleted and redirects to work" do
+ delete :destroy, params: { work_id: banned_users_work.id, id: banned_users_work_chapter2.id }
+ it_redirects_to_with_notice(banned_users_work, "The chapter was successfully deleted.")
+ end
+ end
end
context "when other user is logged in" do
diff --git a/spec/controllers/orphans_controller_spec.rb b/spec/controllers/orphans_controller_spec.rb
index 6947d5b3c93..cf450116863 100644
--- a/spec/controllers/orphans_controller_spec.rb
+++ b/spec/controllers/orphans_controller_spec.rb
@@ -8,12 +8,55 @@
before { create(:user, login: "orphan_account") }
let!(:user) { create(:user) }
-
let!(:pseud) { create(:pseud, user: user) }
let!(:work) { create(:work, authors: [pseud]) }
let(:second_work) { create(:work, authors: user.pseuds) }
let(:series) { create(:series, works: [work], authors: [pseud]) }
+ let!(:suspended_user) { create(:user, suspended: true, suspended_until: 1.week.from_now) }
+ let!(:suspended_pseud) { create(:pseud, user: suspended_user) }
+ let!(:suspended_second_pseud) { create(:pseud, user: suspended_user) }
+ let!(:suspended_users_work) do
+ suspended_user.update!(suspended: false, suspended_until: nil)
+ work = create(:work, authors: [suspended_pseud])
+ suspended_user.update!(suspended: true, suspended_until: 1.week.from_now)
+ work
+ end
+ let!(:suspended_users_second_work) do
+ suspended_user.update!(suspended: false, suspended_until: nil)
+ work = create(:work, authors: suspended_user.pseuds)
+ suspended_user.update!(suspended: true, suspended_until: 1.week.from_now)
+ work
+ end
+ let(:suspended_users_series) do
+ suspended_user.update!(suspended: false, suspended_until: nil)
+ series = create(:series, works: [work], authors: [suspended_pseud])
+ suspended_user.update!(suspended: true, suspended_until: 1.week.from_now)
+ series
+ end
+
+ let!(:banned_user) { create(:user, banned: true) }
+ let!(:banned_pseud) { create(:pseud, user: banned_user) }
+ let!(:banned_second_pseud) { create(:pseud, user: banned_user) }
+ let!(:banned_users_work) do
+ banned_user.update!(banned: false)
+ work = create(:work, authors: [banned_pseud])
+ banned_user.update!(banned: true)
+ work
+ end
+ let!(:banned_users_second_work) do
+ banned_user.update!(banned: false)
+ work = create(:work, authors: banned_user.pseuds)
+ banned_user.update!(banned: true)
+ work
+ end
+ let(:banned_users_series) do
+ banned_user.update!(banned: false)
+ series = create(:series, works: [work], authors: [banned_pseud])
+ banned_user.update!(banned: true)
+ series
+ end
+
let(:other_user) { create(:user) }
let(:other_work) { create(:work, authors: [other_user.default_pseud]) }
@@ -49,6 +92,69 @@
end
end
+ context "when logged in as a suspended user" do
+ before { fake_login_known_user(suspended_user.reload) }
+
+ it "errors and redirects to user page" do
+ get :new, params: { work_id: suspended_users_work }
+ it_redirects_to_simple(user_path(suspended_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+
+ it "errors and redirects to user page" do
+ get :new, params: { work_ids: [suspended_users_work, suspended_users_second_work] }
+ it_redirects_to_simple(user_path(suspended_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+
+ it "errors and redirects to user page" do
+ get :new, params: { series_id: suspended_users_series }
+ it_redirects_to_simple(user_path(suspended_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+
+ it "errors and redirects to user page" do
+ get :new, params: { pseud_id: suspended_user.pseuds.first }
+ it_redirects_to_simple(user_path(suspended_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+
+ it "errors and redirects to user page" do
+ get :new, params: {}
+ it_redirects_to_simple(user_path(suspended_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+ end
+
+ context "when logged in as a banned user" do
+ before { fake_login_known_user(banned_user.reload) }
+
+ it "shows the form for orphaning a work" do
+ get :new, params: { work_id: banned_users_work }
+ expect(response).to render_template(partial: "orphans/_orphan_work")
+ end
+
+ it "shows the form for orphaning multiple works" do
+ get :new, params: { work_ids: [banned_users_work, banned_users_second_work] }
+ expect(response).to render_template(partial: "orphans/_orphan_work")
+ end
+
+ it "shows the form for orphaning a series" do
+ get :new, params: { series_id: banned_users_series }
+ expect(response).to render_template(partial: "orphans/_orphan_series")
+ end
+
+ it "shows the form for orphaning a pseud" do
+ get :new, params: { pseud_id: banned_pseud.id }
+ expect(response).to render_template(partial: "orphans/_orphan_pseud")
+ end
+
+ it "shows the form for orphaning all your works" do
+ get :new, params: {}
+ expect(response).to render_template(partial: "orphans/_orphan_user")
+ end
+ end
+
context "when logged in as another user" do
before { fake_login_known_user(other_user.reload) }
@@ -132,6 +238,117 @@
end
end
+ context "when logged in as a suspended user" do
+ before { fake_login_known_user(suspended_user.reload) }
+
+ it "errors and redirects to user page" do
+ post :create, params: { work_ids: [suspended_users_work], use_default: "true" }
+ expect(suspended_users_work.reload.users).to include(suspended_user)
+ it_redirects_to_simple(user_path(suspended_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+
+ it "errors and redirects to user page" do
+ post :create, params: { work_ids: [suspended_users_work, suspended_users_second_work], use_default: "true" }
+ expect(suspended_users_work.reload.users).to include(suspended_user)
+ expect(suspended_users_second_work.reload.users).to include(suspended_user)
+ it_redirects_to_simple(user_path(suspended_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+
+ context "when a work has multiple pseuds for the same user" do
+ let(:second_pseud) { create(:pseud, user: suspended_user) }
+ let(:work) do
+ suspended_user.update(suspended: false, suspended_until: nil)
+ work = create(:work, authors: [suspended_pseud, suspended_second_pseud])
+ suspended_user.update(suspended: true, suspended_until: 1.week.from_now)
+ work
+ end
+
+ it "errors and redirects to user page" do
+ post :create, params: { work_ids: [suspended_users_work], use_default: "true" }
+ it_redirects_to_simple(user_path(suspended_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+ end
+
+ it "errors and redirects to user page" do
+ post :create, params: { series_id: suspended_users_series, use_default: "true" }
+ it_redirects_to_simple(user_path(suspended_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+
+ it "errors and redirects to user page" do
+ post :create, params: { work_ids: suspended_pseud.works.pluck(:id),
+ pseud_id: suspended_pseud.id, use_default: "true" }
+ it_redirects_to_simple(user_path(suspended_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+
+ it "errors and redirects to user page" do
+ post :create, params: { pseud_id: suspended_pseud.id, use_default: "true" }
+ it_redirects_to_simple(user_path(suspended_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+ end
+
+ context "when logged in as a banned user" do
+ before { fake_login_known_user(banned_user.reload) }
+
+ it "successfully orphans a single work and redirects" do
+ post :create, params: { work_ids: [banned_users_work], use_default: "true" }
+ expect(banned_users_work.reload.users).not_to include(banned_user)
+ it_redirects_to_with_notice(user_path(banned_user), "Orphaning was successful.")
+
+ expect(banned_users_work.original_creators.map(&:user_id)).to contain_exactly(banned_user.id)
+ end
+
+ it "successfully orphans multiple works and redirects" do
+ post :create, params: { work_ids: [banned_users_work, banned_users_second_work], use_default: "true" }
+ expect(banned_users_work.reload.users).not_to include(banned_user)
+ expect(banned_users_second_work.reload.users).not_to include(banned_user)
+ it_redirects_to_with_notice(user_path(banned_user), "Orphaning was successful.")
+
+ expect(banned_users_work.original_creators.map(&:user_id)).to contain_exactly(banned_user.id)
+ expect(banned_users_second_work.original_creators.map(&:user_id)).to contain_exactly(banned_user.id)
+ end
+
+ context "when a work has multiple pseuds for the same user" do
+ let(:second_pseud) { create(:pseud, user: banned_user) }
+ let(:work) do
+ banned_user.update(banned: false)
+ work = create(:work, authors: [banned_pseud, banned_second_pseud])
+ banned_user.update(banned: true)
+ work
+ end
+
+ it "only saves the original creator once" do
+ post :create, params: { work_ids: [banned_users_work], use_default: "true" }
+ expect(banned_users_work.reload.users).not_to include(banned_user)
+
+ expect(banned_users_work.original_creators.map(&:user_id)).to contain_exactly(banned_user.id)
+ end
+ end
+
+ it "successfully orphans a series and redirects" do
+ post :create, params: { series_id: banned_users_series, use_default: "true" }
+ expect(banned_users_series.reload.users).not_to include(banned_user)
+ it_redirects_to_with_notice(user_path(banned_user), "Orphaning was successful.")
+ end
+
+ it "successfully orphans a pseud and redirects" do
+ post :create, params: { work_ids: banned_pseud.works.pluck(:id),
+ pseud_id: banned_pseud.id, use_default: "true" }
+ expect(banned_users_work.reload.users).not_to include(banned_user)
+ it_redirects_to_with_notice(user_path(banned_user), "Orphaning was successful.")
+ end
+
+ it "errors and redirects if you don't specify any works or series" do
+ post :create, params: { pseud_id: banned_pseud.id, use_default: "true" }
+ it_redirects_to_with_error(user_path(banned_user), "What did you want to orphan?")
+ end
+ end
+
context "when logged in as another user" do
before { fake_login_known_user(other_user.reload) }
diff --git a/spec/controllers/works/default_rails_actions_spec.rb b/spec/controllers/works/default_rails_actions_spec.rb
index 7d902175415..17c7f548e02 100644
--- a/spec/controllers/works/default_rails_actions_spec.rb
+++ b/spec/controllers/works/default_rails_actions_spec.rb
@@ -5,6 +5,22 @@
include LoginMacros
include RedirectExpectationHelper
+ let(:banned_user) { create(:user, banned: true) }
+ let(:banned_users_work) do
+ banned_user.update!(banned: false)
+ work = create(:work, authors: [banned_user.pseuds.first])
+ banned_user.update!(banned: true)
+ work
+ end
+
+ let(:suspended_user) { create(:user, suspended: true, suspended_until: 1.week.from_now) }
+ let(:suspended_users_work) do
+ suspended_user.update!(suspended: false, suspended_until: nil)
+ work = create(:work, authors: [suspended_user.pseuds.first])
+ suspended_user.update!(suspended: true, suspended_until: 1.week.from_now)
+ work
+ end
+
describe "before_action #clean_work_search_params" do
let(:params) { {} }
@@ -174,6 +190,13 @@ def call_with_params(params)
get :new
expect(response).to render_template("new")
end
+
+ it "errors and redirects to user page when user is banned" do
+ fake_login_known_user(banned_user)
+ get :new
+ it_redirects_to_simple(user_path(banned_user))
+ expect(flash[:error]).to include("Your account has been banned.")
+ end
end
describe "create" do
@@ -267,6 +290,15 @@ def call_with_params(params)
expect(user.last_wrangling_activity).to be_nil
end
end
+
+ it "errors and redirects to user page when user is banned" do
+ fake_login_known_user(banned_user)
+ tag = create(:unsorted_tag)
+ work_attributes = attributes_for(:work).except(:posted, :freeform_string).merge(freeform_string: tag.name)
+ post :create, params: { work: work_attributes }
+ it_redirects_to_simple(user_path(banned_user))
+ expect(flash[:error]).to include("Your account has been banned.")
+ end
end
describe "show" do
@@ -604,6 +636,14 @@ def call_with_params(params)
end
end
end
+
+ it "errors and redirects to user page when user is banned" do
+ fake_login_known_user(banned_user)
+ attrs = { title: "New Work Title" }
+ put :update, params: { id: banned_users_work.id, work: attrs }
+ it_redirects_to_simple(user_path(banned_user))
+ expect(flash[:error]).to include("Your account has been banned.")
+ end
end
describe "collected" do
@@ -792,5 +832,33 @@ def call_with_params(params)
expect(Comment.count).to eq(0)
end
end
+
+ context "when a logged in user is suspended" do
+ before do
+ fake_login_known_user(suspended_user)
+ end
+
+ it "errors and redirects to user page" do
+ fake_login_known_user(suspended_user)
+ delete :destroy, params: { id: suspended_users_work.id }
+
+ it_redirects_to_simple(user_path(suspended_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+ end
+
+ context "when a logged in user is banned" do
+ before do
+ fake_login_known_user(banned_user)
+ end
+
+ it "deletes the work and redirects to the user's works with a notice" do
+ delete :destroy, params: { id: banned_users_work.id }
+
+ it_redirects_to_with_notice(user_works_path(controller.current_user), "Your work #{banned_users_work.title} was deleted.")
+ expect { banned_users_work.reload }
+ .to raise_exception(ActiveRecord::RecordNotFound)
+ end
+ end
end
end
diff --git a/spec/controllers/works/multiple_actions_spec.rb b/spec/controllers/works/multiple_actions_spec.rb
index 97a3cf8eb72..1ca93868e6b 100644
--- a/spec/controllers/works/multiple_actions_spec.rb
+++ b/spec/controllers/works/multiple_actions_spec.rb
@@ -5,76 +5,195 @@
include LoginMacros
include RedirectExpectationHelper
- let!(:multiple_works_user) { create(:user) }
+ let(:multiple_works_user) { create(:user) }
+ let(:banned_user) { create(:user, banned: true) }
+ let(:suspended_user) { create(:user, suspended: true, suspended_until: 1.week.from_now) }
+
+ describe "show_multiple" do
+ context "when logged in as a user" do
+ it "shows Edit Multiple Works page" do
+ fake_login_known_user(multiple_works_user)
+ get :show_multiple, params: { user_id: multiple_works_user.id }
+ expect(response).to render_template("show_multiple")
+ end
+ end
+
+ context "when logged in as a banned user" do
+ it "shows Edit Multiple Works page" do
+ fake_login_known_user(banned_user)
+ get :show_multiple, params: { user_id: banned_user.id }
+ expect(response).to render_template("show_multiple")
+ end
+ end
+
+ context "when logged in as a suspended user" do
+ it "errors and redirects to user page" do
+ fake_login_known_user(suspended_user)
+ get :show_multiple, params: { user_id: suspended_user.id }
+ it_redirects_to_simple(user_path(suspended_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+ end
+ end
describe "edit_multiple" do
- it "redirects to the orphan path when the Orphan button was clicked" do
- work1 = create(:work, authors: [multiple_works_user.default_pseud])
- work2 = create(:work, authors: [multiple_works_user.default_pseud])
- work_ids = [work1.id, work2.id]
+ let!(:work1) { create(:work, authors: [multiple_works_user.default_pseud]) }
+ let!(:work2) { create(:work, authors: [multiple_works_user.default_pseud]) }
+ let(:work_ids) { [work1.id, work2.id] }
+
+ before do
fake_login_known_user(multiple_works_user)
- post :edit_multiple, params: { id: work1.id, work_ids: work_ids, commit: "Orphan" }
- it_redirects_to new_orphan_path(work_ids: work_ids)
+ end
+
+ context "when logged in as a user" do
+ it "redirects to the orphan path when the Orphan button was clicked" do
+ post :edit_multiple, params: { id: work1.id, work_ids: work_ids, commit: "Orphan" }
+
+ it_redirects_to new_orphan_path(work_ids: work_ids)
+ end
+ end
+
+ context "when logged in as a banned user" do
+ it "redirects to the orphan path when the Orphan button was clicked" do
+ multiple_works_user.update(banned: true)
+ post :edit_multiple, params: { id: work1.id, work_ids: work_ids, commit: "Orphan" }
+
+ it_redirects_to new_orphan_path(work_ids: work_ids)
+ end
+ end
+
+ context "when logged in as a suspended user" do
+ it "errors and redirects to user page" do
+ multiple_works_user.update(suspended: true, suspended_until: 1.week.from_now)
+ post :edit_multiple, params: { id: work1.id, work_ids: work_ids, commit: "Orphan" }
+
+ it_redirects_to_simple(user_path(multiple_works_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
end
end
describe "confirm_delete_multiple" do
- it "returns the works specified in the work_ids parameters" do
- work1 = create(:work, authors: [multiple_works_user.default_pseud])
- work2 = create(:work, authors: [multiple_works_user.default_pseud])
+ let!(:work1) { create(:work, authors: [multiple_works_user.default_pseud]) }
+ let!(:work2) { create(:work, authors: [multiple_works_user.default_pseud]) }
+ let(:params) { { commit: "Orphan", id: work1.id, work_ids: [work1.id, work2.id] } }
+
+ before do
fake_login_known_user(multiple_works_user)
- params = { commit: "Orphan", id: work1.id, work_ids: [work1.id, work2.id] }
- post :confirm_delete_multiple, params: params
- expect(assigns(:works)).to include(work1)
- expect(assigns(:works)).to include(work2)
+ end
+
+ context "when logged in as a user" do
+ it "returns the works specified in the work_ids parameters" do
+ post :confirm_delete_multiple, params: params
+
+ expect(assigns(:works)).to include(work1)
+ expect(assigns(:works)).to include(work2)
+ end
+ end
+
+ context "when logged in as a banned user" do
+ it "returns the works specified in the work_ids parameters" do
+ multiple_works_user.update(banned: true)
+ post :confirm_delete_multiple, params: params
+
+ expect(assigns(:works)).to include(work1)
+ expect(assigns(:works)).to include(work2)
+ end
+ end
+
+ context "when logged in as a suspended user" do
+ it "errors and redirects to user page" do
+ multiple_works_user.update(suspended: true, suspended_until: 1.week.from_now)
+
+ post :confirm_delete_multiple, params: params
+ it_redirects_to_simple(user_path(multiple_works_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
end
end
describe "delete_multiple" do
- let(:multiple_work1) {
+ let!(:multiple_work1) do
create(:work,
authors: [multiple_works_user.default_pseud],
title: "Work 1")
- }
- let(:multiple_work2) {
+ end
+ let!(:multiple_work2) do
create(:work,
authors: [multiple_works_user.default_pseud],
title: "Work 2")
- }
+ end
before do
fake_login_known_user(multiple_works_user)
- post :delete_multiple, params: { id: multiple_work1.id, work_ids: [multiple_work1.id, multiple_work2.id] }
end
- # already covered - just for completeness
- it "deletes all the works" do
- expect { Work.find(multiple_work1.id) }.to raise_exception(ActiveRecord::RecordNotFound)
- expect { Work.find(multiple_work2.id) }.to raise_exception(ActiveRecord::RecordNotFound)
+ context "when logged in as a user" do
+ before do
+ post :delete_multiple, params: { id: multiple_work1.id, work_ids: [multiple_work1.id, multiple_work2.id] }
+ end
+
+ # already covered - just for completeness
+ it "deletes all the works" do
+ expect { Work.find(multiple_work1.id) }
+ .to raise_exception(ActiveRecord::RecordNotFound)
+ expect { Work.find(multiple_work2.id) }
+ .to raise_exception(ActiveRecord::RecordNotFound)
+ end
+
+ it "displays a notice" do
+ expect(flash[:notice]).to eq "Your works Work 1, Work 2 were deleted."
+ end
end
- it "displays a notice" do
- expect(flash[:notice]).to eq "Your works Work 1, Work 2 were deleted."
+ context "when logged in as a banned user" do
+ before do
+ multiple_works_user.update(banned: true)
+ post :delete_multiple, params: { id: multiple_work1.id, work_ids: [multiple_work1.id, multiple_work2.id] }
+ end
+
+ it "deletes all the works" do
+ expect { Work.find(multiple_work1.id) }
+ .to raise_exception(ActiveRecord::RecordNotFound)
+ expect { Work.find(multiple_work2.id) }
+ .to raise_exception(ActiveRecord::RecordNotFound)
+ end
+
+ it "displays a notice" do
+ expect(flash[:notice]).to eq "Your works Work 1, Work 2 were deleted."
+ end
+ end
+
+ context "when logged in as a suspended user" do
+ before do
+ multiple_works_user.update(suspended: true, suspended_until: 1.week.from_now)
+ post :delete_multiple, params: { id: multiple_work1.id, work_ids: [multiple_work1.id, multiple_work2.id] }
+ end
+
+ it "errors and redirects to user page" do
+ it_redirects_to_simple(user_path(multiple_works_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
end
end
describe "update_multiple" do
let(:multiple_works_user) { create(:user) }
- let(:multiple_work1) {
+ let!(:multiple_work1) do
create(:work,
authors: [multiple_works_user.default_pseud],
title: "Work 1",
comment_permissions: :disable_anon,
moderated_commenting_enabled: true)
- }
- let(:multiple_work2) {
+ end
+ let!(:multiple_work2) do
create(:work,
authors: [multiple_works_user.default_pseud],
title: "Work 2",
comment_permissions: :disable_all,
moderated_commenting_enabled: true)
- }
- let(:params) {
+ end
+ let(:params) do
{
work_ids: [multiple_work1.id, multiple_work2.id],
work: {
@@ -93,7 +212,7 @@
moderated_commenting_enabled: ""
}
}.merge(work_params)
- }
+ end
before do
fake_login_known_user(multiple_works_user)
@@ -270,6 +389,44 @@
end
end
end
- end
+ end
+
+ context "when user is banned" do
+ let(:work_params) do
+ {
+ work: {
+ comment_permissions: "enable_all",
+ moderated_commenting_enabled: "0"
+ }
+ }
+ end
+
+ it "errors and redirects to user page" do
+ multiple_works_user.update(banned: true)
+ put :update_multiple, params: params
+
+ it_redirects_to_simple(user_path(multiple_works_user))
+ expect(flash[:error]).to include("Your account has been banned")
+ end
+ end
+
+ context "when user is suspended" do
+ let(:work_params) do
+ {
+ work: {
+ comment_permissions: "enable_all",
+ moderated_commenting_enabled: "0"
+ }
+ }
+ end
+
+ it "errors and redirects to user page" do
+ multiple_works_user.update(suspended: true, suspended_until: 1.week.from_now)
+ put :update_multiple, params: params
+
+ it_redirects_to_simple(user_path(multiple_works_user))
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+ end
end
end
From c73caf23fb46a54561d2a26cca899ff9f42efb97 Mon Sep 17 00:00:00 2001
From: tickinginstant
Date: Sat, 12 Aug 2023 23:45:58 -0400
Subject: [PATCH 030/208] AO3-6506 Use parent aggregations for bookmark
listings. (#4559)
* AO3-6506 Parent aggregations for bookmarks.
* AO3-6506 Hound.
* AO3-6506 Avoid "should" in test names.
* AO3-6506 Add tests for the match_all query.
* AO3-6506 Use build_stubbed.
* AO3-6506 Extra aggregation tests.
* AO3-6506 Facet tests.
* AO3-6506 For Hound.
---
app/models/search/bookmark_query.rb | 162 ++++-----
app/models/search/bookmarkable_query.rb | 180 +++++-----
app/models/search/query.rb | 29 +-
app/models/search/query_result.rb | 27 +-
spec/models/search/bookmark_query_spec.rb | 158 ++++----
.../search/bookmark_search_form_spec.rb | 123 +++++++
spec/models/search/bookmarkable_query_spec.rb | 73 +++-
spec/models/search/pseud_query_spec.rb | 149 ++++----
spec/models/search/tag_query_spec.rb | 339 +++++++++---------
9 files changed, 720 insertions(+), 520 deletions(-)
diff --git a/app/models/search/bookmark_query.rb b/app/models/search/bookmark_query.rb
index d4149f87a5b..0ef43e3fdca 100644
--- a/app/models/search/bookmark_query.rb
+++ b/app/models/search/bookmark_query.rb
@@ -21,41 +21,51 @@ def initialize(options = {})
self.bookmarkable_query = BookmarkableQuery.new(self)
end
- # After the initial search, run an additional query to get work/series tag filters
- # Elasticsearch doesn't support parent aggregations, and doing the main query on the parents
- # limits searching and sorting on the bookmarks themselves
- # Hopefully someday they'll fix this and we can get the data from a single query
- def search_results
- response = search
- if response['aggregations']
- response['aggregations'].merge!(bookmarkable_query.aggregation_results)
- end
- QueryResult.new(klass, response, options.slice(:page, :per_page))
- end
-
- # Combine the query on the bookmark with the query on the bookmarkable.
+ # Combine the filters and queries for both the bookmark and the bookmarkable.
+ def filtered_query
+ make_bool(
+ # Score is based on our query + the bookmarkable query:
+ must: make_list(queries, bookmarkable_queries_and_filters),
+ filter: filters,
+ must_not: make_list(exclusion_filters, bookmarkable_exclusion_filters)
+ )
+ end
+
+ # Queries that apply only to the bookmark. Bookmarkable queries are handled
+ # in filtered_query, and should not be included here.
def queries
- @queries ||= [
- bookmark_query_or_filter,
- parent_bookmarkable_query_or_filter
- ].flatten.compact
+ @queries ||= make_list(
+ general_query
+ )
end
- # Combine the filters on the bookmark with the filters on the bookmarkable.
+ # Filters that apply only to the bookmark. Bookmarkable filters are handled
+ # in filtered_query, and should not be included here.
def filters
- @filters ||= [
- bookmark_filters,
- bookmarkable_filter
- ].flatten.compact
+ @filters ||= make_list(
+ privacy_filter,
+ hidden_filter,
+ bookmarks_only_filter,
+ pseud_filter,
+ user_filter,
+ rec_filter,
+ notes_filter,
+ tags_filter,
+ named_tag_inclusion_filter,
+ collections_filter,
+ type_filter,
+ date_filter
+ )
end
- # Combine the exclusion filters on the bookmark with the exclusion filters on
- # the bookmarkable.
+ # Exclusion filters that apply only to the bookmark. Exclusion filters for
+ # the bookmarkable are handled in filtered_query, and should not be included
+ # here.
def exclusion_filters
- @exclusion_filters ||= [
- bookmark_exclusion_filters,
- bookmarkable_exclusion_filter
- ].flatten.compact
+ @exclusion_filters ||= make_list(
+ tag_exclusion_filter,
+ named_tag_exclusion_filter
+ )
end
def add_owner
@@ -82,20 +92,10 @@ def add_owner
# QUERIES
####################
- def bookmark_query_or_filter
+ def general_query
return nil if bookmark_query_text.blank?
- { query_string: { query: bookmark_query_text, default_operator: "AND" } }
- end
- def parent_bookmarkable_query_or_filter
- return nil if bookmarkable_query.bookmarkable_query_or_filter.blank?
- {
- has_parent: {
- parent_type: "bookmarkable",
- score: true, # include the score from the bookmarkable
- query: bookmarkable_query.bookmarkable_query_or_filter
- }
- }
+ { query_string: { query: bookmark_query_text, default_operator: "AND" } }
end
def bookmark_query_text
@@ -133,8 +133,10 @@ def sort
[sort_hash, { id: { order: sort_direction } }]
end
- def aggregations
+ # The aggregations for just the bookmarks:
+ def bookmark_aggregations
aggs = {}
+
if facet_collections?
aggs[:collections] = { terms: { field: 'collection_ids' } }
end
@@ -143,55 +145,45 @@ def aggregations
aggs[:tag] = { terms: { field: "tag_ids" } }
end
- { aggs: aggs }
+ aggs
end
- ####################
- # GROUPS OF FILTERS
- ####################
+ # Combine the bookmark aggregations with the bookmarkable aggregations from
+ # the bookmarkable query.
+ def aggregations
+ aggs = bookmark_aggregations
- # Filters that apply only to the bookmark. These are must/and filters,
- # meaning that all of them are required to occur in all bookmarks.
- def bookmark_filters
- @bookmark_filters ||= [
- privacy_filter,
- hidden_filter,
- bookmarks_only_filter,
- pseud_filter,
- user_filter,
- rec_filter,
- notes_filter,
- tags_filter,
- named_tag_inclusion_filter,
- collections_filter,
- type_filter,
- date_filter
- ].flatten.compact
- end
+ bookmarkable_aggregations = bookmarkable_query.bookmarkable_aggregations
+ if bookmarkable_aggregations.present?
+ aggs[:bookmarkable] = {
+ parent: { type: "bookmark" },
+ aggs: bookmarkable_aggregations
+ }
+ end
- # Exclusion filters that apply only to the bookmark. These are must_not/not
- # filters, meaning that none of them are allowed to occur in any search
- # results. DO NOT INCLUDE FILTERS ON THE BOOKMARKABLE HERE. If you do, this
- # may cause an infinite loop.
- def bookmark_exclusion_filters
- @bookmark_exclusion_filters ||= [
- tag_exclusion_filter,
- named_tag_exclusion_filter
- ].flatten.compact
+ { aggs: aggs } if aggs.present?
end
- # Wrap all of the must/and filters on the bookmarkable into a single
- # has_parent query. (The more has_parent queries we have, the slower our
- # search will be.)
- def bookmarkable_filter
- return if bookmarkable_query.bookmarkable_filters.blank?
+ ####################
+ # BOOKMARKABLE
+ ####################
+
+ # Wrap both the queries and the filters from the bookmarkable query into a
+ # single has_parent query. (The fewer has_parent queries we have, the faster
+ # the query will be.)
+ def bookmarkable_queries_and_filters
+ bool = make_bool(
+ must: bookmarkable_query.queries,
+ filter: bookmarkable_query.filters
+ )
+
+ return if bool.nil?
- @bookmarkable_filter ||= {
+ {
has_parent: {
parent_type: "bookmarkable",
- query: make_bool(
- must: bookmarkable_query.bookmarkable_filters
- )
+ score: true, # include the score from the bookmarkable
+ query: bool
}
}
end
@@ -200,14 +192,14 @@ def bookmarkable_filter
# has_parent query. Note that we wrap them in a should/or query because if
# any of the parent queries return true, we want to return false. (De
# Morgan's Law.)
- def bookmarkable_exclusion_filter
- return if bookmarkable_query.bookmarkable_exclusion_filters.blank?
+ def bookmarkable_exclusion_filters
+ return if bookmarkable_query.exclusion_filters.blank?
- @bookmarkable_exclusion_filter ||= {
+ {
has_parent: {
parent_type: "bookmarkable",
query: make_bool(
- should: bookmarkable_query.bookmarkable_exclusion_filters
+ should: bookmarkable_query.exclusion_filters
)
}
}
diff --git a/app/models/search/bookmarkable_query.rb b/app/models/search/bookmarkable_query.rb
index 0eff0833c5c..19105a32f2b 100644
--- a/app/models/search/bookmarkable_query.rb
+++ b/app/models/search/bookmarkable_query.rb
@@ -32,54 +32,61 @@ def initialize(bookmark_query)
@options = bookmark_query.options
end
- # Do a regular search and return only the aggregations
- # Note that we make a few modifications to the query before sending it. We
- # don't want to return the nested bookmark aggregations, since this is called
- # in the BookmarkQuery class (which has its own aggregations). And we don't
- # want to return any results.
- def aggregation_results
- # Override the values for "from" and "size":
- modified_query = generated_query.merge(from: 0, size: 0)
-
- # Delete the bookmark aggregations.
- modified_query[:aggs].delete(:bookmarks)
-
- $elasticsearch.search(
- index: index_name,
- body: modified_query
- )["aggregations"]
+ # Combine the filters and queries for both the bookmark and the bookmarkable.
+ def filtered_query
+ make_bool(
+ # All queries/filters/exclusion filters for the bookmark are wrapped in a
+ # single has_child query by the bookmark_filter function:
+ must: bookmark_filter,
+ # We never sort by score, so we can always ignore the score on our
+ # queries, grouping them together with our filters. (Note, however, that
+ # the bookmark search can incorporate our score, so there is a
+ # distinction between queries and filters -- just not in this function.)
+ filter: make_list(queries, filters),
+ must_not: exclusion_filters
+ )
end
- # Because we want to calculate our score based on the bookmark's search results,
- # we use bookmark_filter as our "query" (because it goes in the "must"
- # section of the query, meaning that its score isn't discarded).
+ # Queries that apply only to the bookmarkable. Bookmark queries are handled
+ # in filtered_query, and should not be included here.
def queries
- bookmark_filter
+ @queries ||= make_list(
+ general_query
+ )
end
- # Because we want to calculate the score from our bookmarks, we only use the
- # bookmarkable filters here.
+ # Filters that apply only to the bookmarkable. Bookmark filters are handled
+ # in filtered_query, and should not be included here.
def filters
- @filters ||= [
- bookmarkable_query_or_filter, # acts as a filter
- bookmarkable_filters
- ].flatten.compact
+ @filters ||= make_list(
+ complete_filter,
+ language_filter,
+ filter_id_filter,
+ named_tag_inclusion_filter,
+ date_filter
+ )
end
- # Because we want to calculate the score from our bookmarks, we only use the
- # bookmarkable exclusion filters here.
+ # Exclusion filters that apply only to the bookmarkable. Exclusion filters
+ # for the bookmark are handled in filtered_query, and should not be included
+ # here.
def exclusion_filters
- @exclusion_filters ||= [
- bookmarkable_exclusion_filters
- ].flatten.compact
+ @exclusion_filters ||= make_list(
+ unposted_filter,
+ hidden_filter,
+ restricted_filter,
+ tag_exclusion_filter,
+ named_tag_exclusion_filter
+ )
end
####################
# QUERIES
####################
- def bookmarkable_query_or_filter
+ def general_query
return nil if bookmarkable_query_text.blank?
+
{ query_string: { query: bookmarkable_query_text, default_operator: "AND" } }
end
@@ -106,79 +113,67 @@ def sort
[sort_hash, { sort_id: { order: sort_direction } }]
end
- # Define the aggregations for the search
- # In this case, the various tag fields
- def aggregations
+ # Define the aggregations for just the bookmarkable. This is combined with
+ # the bookmark's aggregations below.
+ def bookmarkable_aggregations
aggs = {}
- %w(rating archive_warning category fandom character relationship freeform).each do |facet_type|
- aggs[facet_type] = {
- terms: {
- field: "#{facet_type}_ids"
+ if bookmark_query.facet_tags?
+ %w[rating archive_warning category fandom character relationship freeform].each do |facet_type|
+ aggs[facet_type] = {
+ terms: {
+ field: "#{facet_type}_ids"
+ }
}
- }
+ end
end
- if bookmark_query.facet_tags? || bookmark_query.facet_collections?
+ aggs
+ end
+
+ # Combine the bookmarkable aggregations with the bookmark aggregations from
+ # the bookmark query.
+ def aggregations
+ aggs = bookmarkable_aggregations
+
+ bookmark_aggregations = bookmark_query.bookmark_aggregations
+ if bookmark_aggregations.present?
aggs[:bookmarks] = {
# Aggregate on our child bookmarks.
children: { type: "bookmark" },
aggs: {
filtered_bookmarks: {
- # Only include bookmarks that satisfy the bookmark_query's filters.
- filter: make_bool(
- must: bookmark_query.bookmark_query_or_filter, # acts as a query
- filter: bookmark_query.bookmark_filters,
- must_not: bookmark_query.bookmark_exclusion_filters
- )
- }.merge(bookmark_query.aggregations) # Use bookmark aggregations.
+ filter: bookmark_bool,
+ aggs: bookmark_aggregations
+ }
}
}
end
- { aggs: aggs }
+ { aggs: aggs } if aggs.present?
end
-
####################
- # GROUPS OF FILTERS
+ # BOOKMARKS
####################
- # Filters that apply only to the bookmarkable.
- def bookmarkable_filters
- @bookmarkable_filters ||= [
- complete_filter,
- language_filter,
- filter_id_filter,
- named_tag_inclusion_filter,
- date_filter
- ].flatten.compact
- end
-
- # Exclusion filters that apply only to the bookmarkable.
- # Note that in order to include bookmarks of deleted works/series/external
- # works in some search results, we set up all of the visibility filters
- # (unposted/hidden/restricted) as *exclusion* filters.
- def bookmarkable_exclusion_filters
- @bookmarkable_exclusion_filters ||= [
- unposted_filter,
- hidden_filter,
- restricted_filter,
- tag_exclusion_filter,
- named_tag_exclusion_filter
- ].flatten.compact
- end
-
# Create a single has_child query with ALL of the child's queries and filters
# included. In order to avoid issues with multiple bookmarks combining to
# create an (incorrect) bookmarkable match, there MUST be exactly one
# has_child query. (Plus, it probably makes it faster.)
def bookmark_filter
- @bookmark_filter ||= {
+ bool = bookmark_bool
+
+ # If we're sorting by created_at, we actually need to fetch the bookmarks'
+ # created_at as the score of this query, so that we can sort by score (and
+ # therefore by the bookmarks' created_at).
+ bool = field_value_score("created_at", bool) if sort_column == "created_at"
+
+ {
has_child: {
type: "bookmark",
score_mode: "max",
- query: bookmark_bool,
+ query: bool,
inner_hits: {
size: inner_hits_size,
sort: { created_at: { order: "desc", unmapped_type: "date" } }
@@ -187,28 +182,15 @@ def bookmark_filter
}
end
- # The bool used in the has_child query.
+ # The bool used in the has_child query and to filter the bookmark
+ # aggregations. Contains all of the constraints on bookmarks, and no
+ # constraints on bookmarkables.
def bookmark_bool
- if sort_column == "created_at"
- # In this case, we need to take the max of the creation dates of our
- # children in order to calculate the correct order.
- make_bool(
- must: field_value_score("created_at"), # score = bookmark's created_at
- filter: [
- bookmark_query.bookmark_query_or_filter, # acts as a filter
- bookmark_query.bookmark_filters
- ].flatten.compact,
- must_not: bookmark_query.bookmark_exclusion_filters
- )
- else
- # In this case, we can fall back on the default behavior and use the
- # bookmark query to score the bookmarks.
- make_bool(
- must: bookmark_query.bookmark_query_or_filter, # acts as a query
- filter: bookmark_query.bookmark_filters,
- must_not: bookmark_query.bookmark_exclusion_filters
- )
- end
+ make_bool(
+ must: bookmark_query.queries,
+ filter: bookmark_query.filters,
+ must_not: bookmark_query.exclusion_filters
+ )
end
####################
diff --git a/app/models/search/query.rb b/app/models/search/query.rb
index aa2ed217bdf..240f54b1161 100644
--- a/app/models/search/query.rb
+++ b/app/models/search/query.rb
@@ -113,19 +113,20 @@ def generated_query
from: pagination_offset,
sort: sort
}
- if aggregations.present?
- q.merge!(aggregations)
+ if (aggs = aggregations).present?
+ q.merge!(aggs)
end
q
end
- # Combine the filters and queries
+ # Combine the filters and queries, with a fallback in case there are no
+ # filters or queries:
def filtered_query
make_bool(
must: queries, # required, score calculated
filter: filters, # required, score ignored
must_not: exclusion_filters # disallowed, score ignored
- )
+ ) || { match_all: {} }
end
# Define specifics in subclasses
@@ -150,16 +151,18 @@ def match_filter(field, value, options = {})
{ match: { field => { query: value, operator: "and" }.merge(options) } }
end
- # Set the score equal to the value of a field. The optional value "missing"
- # determines what score value should be used if the specified field is
- # missing from a document.
- def field_value_score(field, missing: 0)
+ # Replaces the existing scores for a query with the value of a field. The
+ # optional value "missing" determines what score value should be used if the
+ # specified field is missing from a document.
+ def field_value_score(field, query, missing: 0)
{
function_score: {
+ query: query,
field_value_factor: {
field: field,
missing: missing
- }
+ },
+ boost_mode: :replace
}
}
end
@@ -248,7 +251,9 @@ def make_bool(query)
query.reject! { |_, value| value.blank? }
query[:minimum_should_match] = 1 if query[:should].present?
- if query.values.flatten.size == 1 && (query[:must] || query[:should])
+ if query.empty?
+ nil
+ elsif query.values.flatten.size == 1 && (query[:must] || query[:should])
# There's only one clause in our boolean, so we might as well skip the
# bool and just require it.
query.values.flatten.first
@@ -256,4 +261,8 @@ def make_bool(query)
{ bool: query }
end
end
+
+ def make_list(*args)
+ args.flatten.compact
+ end
end
diff --git a/app/models/search/query_result.rb b/app/models/search/query_result.rb
index a4368c552c8..1dc7c871a0e 100644
--- a/app/models/search/query_result.rb
+++ b/app/models/search/query_result.rb
@@ -77,21 +77,28 @@ def load_collection_facets(info)
end
end
+ def load_facets(aggregations)
+ aggregations.each_pair do |term, results|
+ if Tag::TYPES.include?(term.classify) || term == "tag"
+ load_tag_facets(term, results)
+ elsif term == "collections"
+ load_collection_facets(results)
+ elsif term == "bookmarks"
+ load_facets(results["filtered_bookmarks"])
+ elsif term == "bookmarkable"
+ load_facets(results)
+ end
+ end
+ end
+
def facets
- return if response['aggregations'].nil?
+ return if response["aggregations"].nil?
if @facets.nil?
@facets = {}
- response['aggregations'].each_pair do |term, results|
- if Tag::TYPES.include?(term.classify) || term == 'tag'
- load_tag_facets(term, results)
- elsif term == 'collections'
- load_collection_facets(results)
- elsif term == 'bookmarks'
- load_tag_facets("tag", results["filtered_bookmarks"]["tag"])
- end
- end
+ load_facets(response["aggregations"])
end
+
@facets
end
diff --git a/spec/models/search/bookmark_query_spec.rb b/spec/models/search/bookmark_query_spec.rb
index 9e39de54e7e..e27b30235f4 100644
--- a/spec/models/search/bookmark_query_spec.rb
+++ b/spec/models/search/bookmark_query_spec.rb
@@ -1,8 +1,16 @@
require 'spec_helper'
describe BookmarkQuery do
+ let(:collection) { build_stubbed(:collection) }
+ let(:pseud) { build_stubbed(:pseud, user: user) }
+ let(:tag) { build_stubbed(:tag) }
+ let(:user) { build_stubbed(:user) }
- it "should allow you to perform a simple search" do
+ def find_parent_filter(query_list)
+ query_list.find { |query| query.key? :has_parent }
+ end
+
+ it "allows you to perform a simple search" do
q = BookmarkQuery.new(bookmarkable_query: "space", bookmark_query: "unicorns")
search_body = q.generated_query
query = { query_string: { query: "unicorns", default_operator: "AND" } }
@@ -16,121 +24,129 @@
)
end
- it "should not return private bookmarks by default" do
+ it "excludes private bookmarks by default" do
q = BookmarkQuery.new
- expect(q.filters).to include({term: { private: 'false'} })
+ expect(q.generated_query.dig(:query, :bool, :filter)).to include({ term: { private: "false" } })
end
- it "should not return private bookmarks by default when a user is logged in" do
- user = User.new
- user.id = 5
+ it "excludes private bookmarks by default when a user is logged in" do
User.current_user = user
q = BookmarkQuery.new
- expect(q.filters).to include({term: { private: 'false'} })
+ expect(q.generated_query.dig(:query, :bool, :filter)).to include({ term: { private: "false" } })
end
- it "should return private bookmarks when a user is logged in and looking at their own page" do
- user = User.new
- user.id = 5
+ it "includes private bookmarks when a user is logged in and looking at their own page" do
User.current_user = user
q = BookmarkQuery.new(parent: user)
- expect(q.filters).not_to include({term: { private: 'false'} })
+ expect(q.generated_query.dig(:query, :bool, :filter)).not_to include({ term: { private: "false" } })
end
- it "should never return hidden bookmarks" do
+ it "excludes hidden bookmarks" do
q = BookmarkQuery.new
- expect(q.filters).to include({term: { hidden_by_admin: 'false'} })
+ expect(q.generated_query.dig(:query, :bool, :filter)).to include({ term: { hidden_by_admin: "false" } })
end
- context "default bookmarkable filters" do
+ context "with empty search terms" do
let(:query) { BookmarkQuery.new }
- let(:parent_filter) do
- query.exclusion_filters.first { |f| f.key? :has_parent }
+
+ let(:excluded_parent_filter) do
+ find_parent_filter(query.generated_query.dig(:query, :bool, :must_not))
end
- it "should not return bookmarks of hidden objects" do
- expect(parent_filter.dig(:has_parent, :query, :bool, :should)).to include(term: { hidden_by_admin: 'true' })
+ it "excludes bookmarks of hidden objects" do
+ expect(excluded_parent_filter.dig(:has_parent, :query, :bool, :should)).to \
+ include({ term: { hidden_by_admin: "true" } })
end
- it "should not return bookmarks of drafts" do
- expect(parent_filter.dig(:has_parent, :query, :bool, :should)).to include(term: { posted: 'false' })
+ it "excludes bookmarks of drafts" do
+ expect(excluded_parent_filter.dig(:has_parent, :query, :bool, :should)).to \
+ include({ term: { posted: "false" } })
end
- it "should not return restricted bookmarked works when logged out" do
+ it "excludes restricted works when logged out" do
User.current_user = nil
- expect(parent_filter.dig(:has_parent, :query, :bool, :should)).to include(term: { restricted: 'true' })
+ expect(excluded_parent_filter.dig(:has_parent, :query, :bool, :should)).to \
+ include({ term: { restricted: "true" } })
end
- it "should return restricted bookmarked works when a user is logged in" do
- User.current_user = User.new
- expect(parent_filter.dig(:has_parent, :query, :bool, :should)).not_to include(term: { restricted: 'true' })
+ it "includes restricted works when logged in" do
+ User.current_user = user
+ expect(excluded_parent_filter.dig(:has_parent, :query, :bool, :should)).not_to \
+ include({ term: { restricted: "true" } })
end
end
- it "should allow you to filter for recs" do
+ it "allows you to filter for recs" do
q = BookmarkQuery.new(rec: true)
- expect(q.filters).to include({term: { rec: 'true'} })
+ expect(q.generated_query.dig(:query, :bool, :filter)).to include({ term: { rec: "true" } })
end
- it "should allow you to filter for bookmarks with notes" do
+ it "allows you to filter for bookmarks with notes" do
q = BookmarkQuery.new(with_notes: true)
- expect(q.filters).to include({term: { with_notes: 'true'} })
- end
-
- it "should allow you to filter for complete works" do
- q = BookmarkQuery.new(complete: true)
- expect(q.filters).to include({has_parent:{parent_type: 'bookmarkable', query:{term: {complete: 'true'}}}})
+ expect(q.generated_query.dig(:query, :bool, :filter)).to include({ term: { with_notes: "true" } })
end
- it "should allow you to filter for bookmarks by pseud" do
- pseud = Pseud.new
- pseud.id = 42
+ it "allows you to filter for bookmarks by pseud" do
q = BookmarkQuery.new(parent: pseud)
- expect(q.filters).to include(terms: { pseud_id: [42] })
+ expect(q.generated_query.dig(:query, :bool, :filter)).to include(terms: { pseud_id: [pseud.id] })
end
- it "should allow you to filter for bookmarks by user" do
- user = User.new
- user.id = 2
+ it "allows you to filter for bookmarks by user" do
q = BookmarkQuery.new(parent: user)
- expect(q.filters).to include({term: { user_id: 2 }})
+ expect(q.generated_query.dig(:query, :bool, :filter)).to include({ term: { user_id: user.id } })
end
- it "should allow you to filter for bookmarks by bookmarkable tags" do
- tag = Tag.new
- tag.id = 1
- q = BookmarkQuery.new(parent: tag)
- expected_filter = {
- has_parent: {
- parent_type: 'bookmarkable',
- query: {
- term: {
- filter_ids: 1
- }
- }
- }
- }
+ it "allows you to filter for bookmarks by bookmark tags" do
+ q = BookmarkQuery.new(tag_ids: [tag.id])
- expect(q.filters).to include(expected_filter)
+ expect(q.generated_query.dig(:query, :bool, :filter)).to include({ term: { tag_ids: tag.id } })
end
- it "should allow you to filter for bookmarks by bookmark tags" do
- tag = Tag.new
- tag.id = 1
- q = BookmarkQuery.new(tag_ids: [1])
-
- expect(q.filters).to include({term: { tag_ids: 1}})
+ it "allows you to filter for bookmarks by collection" do
+ q = BookmarkQuery.new(parent: collection)
+ expect(q.generated_query.dig(:query, :bool, :filter)).to include({ terms: { collection_ids: [collection.id] } })
end
- it "should allow you to filter for bookmarks by collection" do
- collection = Collection.new
- collection.id = 5
- q = BookmarkQuery.new(parent: collection)
- expect(q.filters).to include({terms: { collection_ids: [5]} })
+ context "when filtering on properties of the bookmarkable" do
+ it "allows you to filter for complete works" do
+ q = BookmarkQuery.new(complete: true)
+ parent = find_parent_filter(q.generated_query.dig(:query, :bool, :must))
+ expect(parent.dig(:has_parent, :query, :bool, :filter)).to \
+ include({ term: { complete: "true" } })
+ end
+
+ it "allows you to filter by bookmarkable tags" do
+ q = BookmarkQuery.new(parent: tag)
+ parent = find_parent_filter(q.generated_query.dig(:query, :bool, :must))
+ expect(parent.dig(:has_parent, :query, :bool, :filter)).to \
+ include({ term: { filter_ids: tag.id } })
+ end
+
+ it "allows you to filter by bookmarkable language" do
+ q = BookmarkQuery.new(language_id: "ig")
+ parent = find_parent_filter(q.generated_query.dig(:query, :bool, :must))
+ expect(parent.dig(:has_parent, :query, :bool, :filter)).to \
+ include({ term: { "language_id.keyword": "ig" } })
+ end
end
- it "should allow you to filter for bookmarks by language" do
- q = BookmarkQuery.new(language_id: "ig")
- expect(q.filters).to include(has_parent: { parent_type: "bookmarkable", query: { term: { "language_id.keyword": "ig" } } })
+ describe "a faceted query" do
+ let(:bookmark_query) { BookmarkQuery.new(faceted: true) }
+ let(:aggregations) { bookmark_query.generated_query[:aggs] }
+
+ it "includes aggregations for the bookmark tags" do
+ expect(aggregations[:tag]).to \
+ include({ terms: { field: "tag_ids" } })
+ end
+
+ Tag::FILTERS.each do |type|
+ it "includes #{type.underscore.humanize.downcase} aggregations for the bookmarkable" do
+ expect(aggregations[:bookmarkable]).to \
+ include({ parent: { type: "bookmark" } })
+
+ expect(aggregations.dig(:bookmarkable, :aggs, type.underscore)).to \
+ include({ terms: { field: "#{type.underscore}_ids" } })
+ end
+ end
end
end
diff --git a/spec/models/search/bookmark_search_form_spec.rb b/spec/models/search/bookmark_search_form_spec.rb
index 7db53ddbd1e..cc78b960ae8 100644
--- a/spec/models/search/bookmark_search_form_spec.rb
+++ b/spec/models/search/bookmark_search_form_spec.rb
@@ -384,4 +384,127 @@
expect(searcher.options[:archive_warning_ids]).to eq([13])
end
end
+
+ describe "facets" do
+ let(:author) { create(:user).default_pseud }
+ let(:bookmarker) { create(:user).default_pseud }
+
+ let(:fandom1) { create(:canonical_fandom) }
+ let(:fandom2) { create(:canonical_fandom) }
+ let(:fandom3) { create(:fandom, merger: fandom1) }
+
+ let(:character1) { create(:canonical_character) }
+ let(:character2) { create(:canonical_character) }
+ let(:character3) { create(:canonical_character) }
+
+ let(:freeform1) { create(:canonical_freeform) }
+ let(:freeform2) { create(:freeform, merger: freeform1) }
+
+ let(:work1) do
+ create(:work,
+ fandom_string: fandom1.name,
+ character_string: [character1.name, character2.name].join(","),
+ authors: [author])
+ end
+
+ let(:work2) do
+ create(:work,
+ fandom_string: fandom2.name,
+ character_string: [character1.name, character2.name].join(","),
+ authors: [author])
+ end
+
+ let(:work3) do
+ create(:work,
+ fandom_string: fandom3.name,
+ character_string: [character1.name, character3.name].join(","),
+ authors: [author])
+ end
+
+ let!(:bookmark1) { create(:bookmark, pseud: bookmarker, bookmarkable: work1, tag_string: freeform1.name) }
+ let!(:bookmark2) { create(:bookmark, pseud: bookmarker, bookmarkable: work2, tag_string: freeform1.name) }
+ let!(:bookmark3) { create(:bookmark, pseud: bookmarker, bookmarkable: work3, tag_string: freeform2.name) }
+
+ before { run_all_indexing_jobs }
+
+ let(:form) { BookmarkSearchForm.new(faceted: true) }
+ let(:facets) { results.facets }
+
+ def facet_hash(facets)
+ facets.to_h { |facet| [facet.name, facet.count] }
+ end
+
+ shared_examples "it calculates the correct counts" do
+ it "lists the canonical fandoms with their counts" do
+ expect(facet_hash(facets["fandom"])).to eq(
+ {
+ fandom1.name => 2,
+ fandom2.name => 1
+ }
+ )
+ end
+
+ it "lists the canonical characters with their counts" do
+ expect(facet_hash(facets["character"])).to eq(
+ {
+ character1.name => 3,
+ character2.name => 2,
+ character3.name => 1
+ }
+ )
+ end
+
+ it "lists all bookmark tags with their counts" do
+ expect(facet_hash(facets["tag"])).to eq(
+ {
+ freeform1.name => 2,
+ freeform2.name => 1
+ }
+ )
+ end
+
+ context "when excluding tags" do
+ let(:form) { BookmarkSearchForm.new(faceted: true, excluded_tag_ids: [fandom2.id]) }
+
+ it "lists the canonical fandoms with their counts" do
+ expect(facet_hash(facets["fandom"])).to eq(
+ {
+ fandom1.name => 2
+ }
+ )
+ end
+
+ it "lists the canonical characters with their counts" do
+ expect(facet_hash(facets["character"])).to eq(
+ {
+ character1.name => 2,
+ character2.name => 1,
+ character3.name => 1
+ }
+ )
+ end
+
+ it "lists all bookmark tags with their counts" do
+ expect(facet_hash(facets["tag"])).to eq(
+ {
+ freeform1.name => 1,
+ freeform2.name => 1
+ }
+ )
+ end
+ end
+ end
+
+ context "for search_results" do
+ let(:results) { form.search_results }
+
+ it_behaves_like "it calculates the correct counts"
+ end
+
+ context "for bookmarkable_search_results" do
+ let(:results) { form.bookmarkable_search_results }
+
+ it_behaves_like "it calculates the correct counts"
+ end
+ end
end
diff --git a/spec/models/search/bookmarkable_query_spec.rb b/spec/models/search/bookmarkable_query_spec.rb
index 3ab7b4ae356..6ef20c46e9f 100644
--- a/spec/models/search/bookmarkable_query_spec.rb
+++ b/spec/models/search/bookmarkable_query_spec.rb
@@ -1,20 +1,73 @@
require 'spec_helper'
describe BookmarkableQuery do
- describe "#add_bookmark_filters" do
- it "should take a default has_parent query and flip it around" do
- bookmark_query = BookmarkQuery.new
- q = bookmark_query.bookmarkable_query
- excluded = q.generated_query.dig(:query, :bool, :must_not)
- expect(excluded).to include(term: { restricted: "true" })
- expect(excluded).to include(term: { hidden_by_admin: "true" })
- expect(excluded).to include(term: { posted: "false" })
+ describe "#generated_query" do
+ describe "a blank query" do
+ let(:bookmark_query) { BookmarkQuery.new }
+ let(:bookmarkable_query) { bookmark_query.bookmarkable_query }
+
+ it "excludes hidden, draft, and restricted bookmarkables when logged out" do
+ User.current_user = nil
+
+ excluded = bookmarkable_query.generated_query.dig(:query, :bool, :must_not)
+ expect(excluded).to include(term: { hidden_by_admin: "true" })
+ expect(excluded).to include(term: { posted: "false" })
+ expect(excluded).to include(term: { restricted: "true" })
+ end
+
+ it "excludes hidden and draft bookmarkables, but not restricted when logged in" do
+ User.current_user = build_stubbed(:user)
+
+ excluded = bookmarkable_query.generated_query.dig(:query, :bool, :must_not)
+ expect(excluded).to include(term: { hidden_by_admin: "true" })
+ expect(excluded).to include(term: { posted: "false" })
+ expect(excluded).not_to include(term: { restricted: "true" })
+ end
+
+ it "excludes private and hidden bookmarks" do
+ child_filter = bookmarkable_query.generated_query.dig(:query, :bool, :must)
+
+ expect(child_filter.dig(:has_child, :query, :bool, :filter)).to include(term: { hidden_by_admin: "false" })
+ expect(child_filter.dig(:has_child, :query, :bool, :filter)).to include(term: { private: "false" })
+ end
+
+ it "doesn't include aggregations" do
+ aggregations = bookmarkable_query.generated_query[:aggs]
+ expect(aggregations).to be_blank
+ end
+ end
+
+ describe "a faceted query" do
+ let(:bookmark_query) { BookmarkQuery.new(faceted: true) }
+ let(:bookmarkable_query) { bookmark_query.bookmarkable_query }
+ let(:aggregations) { bookmarkable_query.generated_query[:aggs] }
+
+ Tag::FILTERS.each do |type|
+ it "includes #{type.underscore.humanize.downcase} aggregations" do
+ expect(aggregations[type.underscore]).to \
+ include({ terms: { field: "#{type.underscore}_ids" } })
+ end
+ end
+
+ it "includes aggregations for the bookmark tags" do
+ # Top-level aggregation to get all children:
+ expect(aggregations[:bookmarks]).to \
+ include({ children: { type: "bookmark" } })
+
+ # Nested aggregation to filter the children:
+ expect(aggregations.dig(:bookmarks, :aggs, :filtered_bookmarks)).to \
+ include({ filter: bookmarkable_query.bookmark_bool })
+
+ # Nest even further to get the tags of the children:
+ expect(aggregations.dig(:bookmarks, :aggs, :filtered_bookmarks, :aggs, :tag)).to \
+ include({ terms: { field: "tag_ids" } })
+ end
end
- it "should take bookmark filters and combine them into one child query" do
+ it "combines all bookmark filters (positive and negative) in a single has_child query" do
bookmark_query = BookmarkQuery.new(user_ids: [5], excluded_bookmark_tag_ids: [666])
q = bookmark_query.bookmarkable_query
- child_filter = q.bookmark_filter
+ child_filter = q.generated_query.dig(:query, :bool, :must)
expect(child_filter.dig(:has_child, :query, :bool, :filter)).to include(term: { private: "false" })
expect(child_filter.dig(:has_child, :query, :bool, :filter)).to include(term: { user_id: 5 })
expect(child_filter.dig(:has_child, :query, :bool, :must_not)).to include(terms: { tag_ids: [666] })
diff --git a/spec/models/search/pseud_query_spec.rb b/spec/models/search/pseud_query_spec.rb
index 8ff16a10066..b18fb4e7a3d 100644
--- a/spec/models/search/pseud_query_spec.rb
+++ b/spec/models/search/pseud_query_spec.rb
@@ -1,83 +1,92 @@
require 'spec_helper'
-describe PseudQuery, pseud_search: true do
- let!(:pseuds) do
- users = {
- user_abc: create(:user, login: "abc"),
- user_abc_d: create(:user, login: "abc_d"),
- user_abc_num: create(:user, login: "abc123"),
- user_foo: create(:user, login: "foo"),
- user_bar: create(:user, login: "bar"),
- user_aisha: create(:user, login: "aisha")
- }
- pseuds = {
- pseud_abc: users[:user_abc].default_pseud,
- pseud_abc_d: users[:user_abc_d].default_pseud,
- pseud_abc_d_2: create(:pseud, user: users[:user_abc_d], name: "Abc_ D"),
- pseud_abc_num: users[:user_abc_num].default_pseud,
- pseud_abc_num_2: create(:pseud, user: users[:user_abc_num], name: "Abc 123 Pseud"),
- pseud_foo: users[:user_foo].default_pseud,
- pseud_foo_2: create(:pseud, user: users[:user_foo], name: "bar"),
- pseud_bar: users[:user_bar].default_pseud,
- pseud_bar_2: create(:pseud, user: users[:user_bar], name: "foo"),
- pseud_aisha: create(:pseud, user: users[:user_aisha], name: "عيشة")
- }
- run_all_indexing_jobs
- pseuds
- end
-
- context "Search all fields" do
- it "performs a case-insensitive search ('AbC' matches 'abc' first, then variations including abc)" do
- pseud_query = PseudQuery.new(query: "AbC")
- results = pseud_query.search_results
- expect(results[0]).to eq(pseuds[:pseud_abc])
- expect(results[1]).to eq(pseuds[:pseud_abc_num_2])
- expect(results[2]).to eq(pseuds[:pseud_abc_num])
- # these two have the same score
- expect(results).to include(pseuds[:pseud_abc_d_2])
- expect(results).to include(pseuds[:pseud_abc_d])
+describe PseudQuery do
+ describe "#search_results", pseud_search: true do
+ let!(:pseuds) do
+ users = {
+ user_abc: create(:user, login: "abc"),
+ user_abc_d: create(:user, login: "abc_d"),
+ user_abc_num: create(:user, login: "abc123"),
+ user_foo: create(:user, login: "foo"),
+ user_bar: create(:user, login: "bar"),
+ user_aisha: create(:user, login: "aisha")
+ }
+ pseuds = {
+ pseud_abc: users[:user_abc].default_pseud,
+ pseud_abc_d: users[:user_abc_d].default_pseud,
+ pseud_abc_d_2: create(:pseud, user: users[:user_abc_d], name: "Abc_ D"),
+ pseud_abc_num: users[:user_abc_num].default_pseud,
+ pseud_abc_num_2: create(:pseud, user: users[:user_abc_num], name: "Abc 123 Pseud"),
+ pseud_foo: users[:user_foo].default_pseud,
+ pseud_foo_2: create(:pseud, user: users[:user_foo], name: "bar"),
+ pseud_bar: users[:user_bar].default_pseud,
+ pseud_bar_2: create(:pseud, user: users[:user_bar], name: "foo"),
+ pseud_aisha: create(:pseud, user: users[:user_aisha], name: "عيشة")
+ }
+ run_all_indexing_jobs
+ pseuds
end
- it "matches a pseud with and without numbers ('abc123' matches 'abc123' first, then 'Abc 123 Pseud' and 'abc')" do
- pseud_query = PseudQuery.new(query: "abc123")
- results = pseud_query.search_results
- expect(results[0]).to eq(pseuds[:pseud_abc_num])
- expect(results[1]).to eq(pseuds[:pseud_abc_num_2])
- expect(results[2]).to eq(pseuds[:pseud_abc])
- expect(results).to include(pseuds[:pseud_abc_d])
- expect(results).to include(pseuds[:pseud_abc_d_2])
- end
+ context "Search all fields" do
+ it "performs a case-insensitive search ('AbC' matches 'abc' first, then variations including abc)" do
+ pseud_query = PseudQuery.new(query: "AbC")
+ results = pseud_query.search_results
+ expect(results[0]).to eq(pseuds[:pseud_abc])
+ expect(results[1]).to eq(pseuds[:pseud_abc_num_2])
+ expect(results[2]).to eq(pseuds[:pseud_abc_num])
+ # these two have the same score
+ expect(results).to include(pseuds[:pseud_abc_d_2])
+ expect(results).to include(pseuds[:pseud_abc_d])
+ end
- it "matches both pseud and user ('bar' matches 'foo (bar)' and 'bar (foo)'" do
- pseud_query = PseudQuery.new(query: "bar")
- results = pseud_query.search_results
- expect(results[0]).to eq(pseuds[:pseud_bar])
- expect(results[1]).to eq(pseuds[:pseud_foo_2])
- expect(results[2]).to eq(pseuds[:pseud_bar_2])
- end
- end
+ it "matches a pseud with and without numbers ('abc123' matches 'abc123' first, then 'Abc 123 Pseud' and 'abc')" do
+ pseud_query = PseudQuery.new(query: "abc123")
+ results = pseud_query.search_results
+ expect(results[0]).to eq(pseuds[:pseud_abc_num])
+ expect(results[1]).to eq(pseuds[:pseud_abc_num_2])
+ expect(results[2]).to eq(pseuds[:pseud_abc])
+ expect(results).to include(pseuds[:pseud_abc_d])
+ expect(results).to include(pseuds[:pseud_abc_d_2])
+ end
- context "Name field" do
- it "performs a case-insensitive search ('AbC' matches 'abc' and 'abc123')" do
- pseud_query = PseudQuery.new(name: "AbC")
- results = pseud_query.search_results
- expect(results[0]).to eq(pseuds[:pseud_abc])
- expect(results[1]).to eq(pseuds[:pseud_abc_num_2])
+ it "matches both pseud and user ('bar' matches 'foo (bar)' and 'bar (foo)'" do
+ pseud_query = PseudQuery.new(query: "bar")
+ results = pseud_query.search_results
+ expect(results[0]).to eq(pseuds[:pseud_bar])
+ expect(results[1]).to eq(pseuds[:pseud_foo_2])
+ expect(results[2]).to eq(pseuds[:pseud_bar_2])
+ end
end
- it "matches a pseud with and without numbers ('abc123' matches 'abc123' first, then 'Abc 123 Pseud')" do
- pseud_query = PseudQuery.new(name: "abc123")
- results = pseud_query.search_results
- expect(results[0]).to eq(pseuds[:pseud_abc_num])
- expect(results[1]).to eq(pseuds[:pseud_abc_num_2])
+ context "Name field" do
+ it "performs a case-insensitive search ('AbC' matches 'abc' and 'abc123')" do
+ pseud_query = PseudQuery.new(name: "AbC")
+ results = pseud_query.search_results
+ expect(results[0]).to eq(pseuds[:pseud_abc])
+ expect(results[1]).to eq(pseuds[:pseud_abc_num_2])
+ end
+
+ it "matches a pseud with and without numbers ('abc123' matches 'abc123' first, then 'Abc 123 Pseud')" do
+ pseud_query = PseudQuery.new(name: "abc123")
+ results = pseud_query.search_results
+ expect(results[0]).to eq(pseuds[:pseud_abc_num])
+ expect(results[1]).to eq(pseuds[:pseud_abc_num_2])
+ end
+
+ it "matches multiple pseuds with and without numbers ('abc123, عيشة' matches 'abc123' and 'aisha', then 'Abc 123 Pseud')" do
+ pseud_query = PseudQuery.new(name: "abc123,عيشة")
+ results = pseud_query.search_results
+ expect(results).to include(pseuds[:pseud_aisha])
+ expect(results).to include(pseuds[:pseud_abc_num])
+ expect(results[2]).to eq(pseuds[:pseud_abc_num_2])
+ end
end
+ end
- it "matches multiple pseuds with and without numbers ('abc123, عيشة' matches 'abc123' and 'aisha', then 'Abc 123 Pseud')" do
- pseud_query = PseudQuery.new(name: "abc123,عيشة")
- results = pseud_query.search_results
- expect(results).to include(pseuds[:pseud_aisha])
- expect(results).to include(pseuds[:pseud_abc_num])
- expect(results[2]).to eq(pseuds[:pseud_abc_num_2])
+ describe "#generated_query" do
+ it "matches all pseuds when no search params are specified" do
+ pseud_query = PseudQuery.new
+ expect(pseud_query.generated_query[:query]).to eq({ match_all: {} })
end
end
end
diff --git a/spec/models/search/tag_query_spec.rb b/spec/models/search/tag_query_spec.rb
index a57ef2c1ed7..7ef558b0255 100644
--- a/spec/models/search/tag_query_spec.rb
+++ b/spec/models/search/tag_query_spec.rb
@@ -1,186 +1,195 @@
require "spec_helper"
-describe TagQuery, tag_search: true do
- let!(:time) { Time.current }
- let!(:tags) do
- tags = {
- char_abc: create(:character, name: "abc"),
- char_abc_d_minus: create(:character, name: "abc -d"),
- char_abc_d: create(:character, name: "abc d"),
- fan_abcd: create(:fandom, name: "abcd", created_at: time),
- fan_abc_d_minus: create(:fandom, name: "abc-d"),
- fan_yuri: create(:fandom, name: "Yuri!!! On Ice"),
- free_abcplus: create(:freeform, name: "abc+"),
- free_abccc: create(:freeform, name: "abccc", created_at: time),
- free_abapos: create(:freeform, name: "ab'c d"),
- rel_slash: create(:relationship, name: "ab/cd"),
- rel_space: create(:relationship, name: "ab cd"),
- rel_quotes: create(:relationship, name: "ab \"cd\" ef"),
- rel_unicode: create(:relationship, name: "Dave ♦ Sawbuck")
- }
- run_all_indexing_jobs
- tags
- end
+describe TagQuery do
+ describe "#search_results", tag_search: true do
+ let!(:time) { Time.current }
+ let!(:tags) do
+ tags = {
+ char_abc: create(:character, name: "abc"),
+ char_abc_d_minus: create(:character, name: "abc -d"),
+ char_abc_d: create(:character, name: "abc d"),
+ fan_abcd: create(:fandom, name: "abcd", created_at: time),
+ fan_abc_d_minus: create(:fandom, name: "abc-d"),
+ fan_yuri: create(:fandom, name: "Yuri!!! On Ice"),
+ free_abcplus: create(:freeform, name: "abc+"),
+ free_abccc: create(:freeform, name: "abccc", created_at: time),
+ free_abapos: create(:freeform, name: "ab'c d"),
+ rel_slash: create(:relationship, name: "ab/cd"),
+ rel_space: create(:relationship, name: "ab cd"),
+ rel_quotes: create(:relationship, name: "ab \"cd\" ef"),
+ rel_unicode: create(:relationship, name: "Dave ♦ Sawbuck")
+ }
+ run_all_indexing_jobs
+ tags
+ end
- it "performs a case-insensitive search ('AbC' matches 'abc')" do
- tag_query = TagQuery.new(name: "AbC")
- results = tag_query.search_results
- results.first.should eq(tags[:char_abc])
- results.should include(tags[:free_abcplus])
- end
+ it "performs a case-insensitive search ('AbC' matches 'abc')" do
+ tag_query = TagQuery.new(name: "AbC")
+ results = tag_query.search_results
+ results.first.should eq(tags[:char_abc])
+ results.should include(tags[:free_abcplus])
+ end
- it "performs a query string search ('ab OR cd' matches 'ab cd', 'ab/cd' and 'ab “cd” ef')" do
- tag_query = TagQuery.new(name: "ab OR cd")
- results = tag_query.search_results
- results.should include(tags[:rel_slash])
- results.should include(tags[:rel_space])
- results.should include(tags[:rel_quotes])
- end
+ it "performs a query string search ('ab OR cd' matches 'ab cd', 'ab/cd' and 'ab “cd” ef')" do
+ tag_query = TagQuery.new(name: "ab OR cd")
+ results = tag_query.search_results
+ results.should include(tags[:rel_slash])
+ results.should include(tags[:rel_space])
+ results.should include(tags[:rel_quotes])
+ end
- it "performs an exact match with quotes ('xgh OR “abc d”' matches 'abc d')" do
- tag_query = TagQuery.new(name: 'xgh OR "abc d"')
- results = tag_query.search_results
- results.should include(tags[:char_abc_d])
- end
-
- it "lists closest matches at the top of the results ('abc' result lists 'abc' first)" do
- tag_query = TagQuery.new(name: "abc")
- results = tag_query.search_results
- results.first.should eq(tags[:char_abc])
- results.should include(tags[:char_abc_d])
- results.should include(tags[:free_abcplus])
- results.should include(tags[:fan_abc_d_minus])
- end
-
- it "matches every token in any order ('d abc' matches 'abc d' and 'abc-d', but not 'abc' or 'abc+')" do
- tag_query = TagQuery.new(name: "d abc")
- results = tag_query.search_results
- results.should include(tags[:char_abc_d])
- results.should include(tags[:fan_abc_d_minus])
- results.should_not include(tags[:free_abcplus])
- results.should_not include(tags[:char_abc])
- end
+ it "performs an exact match with quotes ('xgh OR “abc d”' matches 'abc d')" do
+ tag_query = TagQuery.new(name: 'xgh OR "abc d"')
+ results = tag_query.search_results
+ results.should include(tags[:char_abc_d])
+ end
- it "matches tokens with double quotes ('ab \"cd\" ef' matches 'ab \"cd\" ef')" do
- tag_query = TagQuery.new(name: "ab \"cd\" ef")
- results = tag_query.search_results
- results.should include(tags[:rel_quotes])
- end
+ it "lists closest matches at the top of the results ('abc' result lists 'abc' first)" do
+ tag_query = TagQuery.new(name: "abc")
+ results = tag_query.search_results
+ results.first.should eq(tags[:char_abc])
+ results.should include(tags[:char_abc_d])
+ results.should include(tags[:free_abcplus])
+ results.should include(tags[:fan_abc_d_minus])
+ end
- it "matches tokens with single quotes ('ab'c d' matches 'ab'c d')" do
- tag_query = TagQuery.new(name: "ab'c d")
- results = tag_query.search_results
- results.should include(tags[:free_abapos])
- end
-
- it "performs a wildcard search at the end of a term ('abc*' matches 'abcd' and 'abcde')" do
- tag_query = TagQuery.new(name: "abc*")
- results = tag_query.search_results
- results.should include(tags[:char_abc])
- results.should include(tags[:fan_abcd])
- results.should include(tags[:free_abccc])
- results.should_not include(tags[:relationship])
- end
-
- it "performs a wildcard search in the middle of a term ('a*d' matches 'abcd')" do
- tag_query = TagQuery.new(name: "a*d")
- results = tag_query.search_results
- results.should include(tags[:fan_abcd])
- end
-
- it "performs a wildcard search at the beginning of a term ('*cd' matches 'abcd')" do
- tag_query = TagQuery.new(name: "*cd")
- results = tag_query.search_results
- results.should include(tags[:fan_abcd])
- end
-
- it "preserves plus (+) character ('abc+' matches 'abc+' and 'abc', but not 'abccc')" do
- tag_query = TagQuery.new(name: "abc+")
- results = tag_query.search_results
- results.should include(tags[:free_abcplus])
- results.should include(tags[:char_abc])
- results.should_not include(tags[:free_abccc])
- end
-
- it "preserves minus (-) character ('abc-d' matches 'abc-d', 'abc -d', 'abc d' but not 'abc' or 'abcd')" do
- tag_query = TagQuery.new(name: "abc-d")
- results = tag_query.search_results
- results.should include(tags[:fan_abc_d_minus])
- results.should include(tags[:char_abc_d_minus])
- results.should_not include(tags[:fan_abcd])
- results.should_not include(tags[:char_abc])
- end
+ it "matches every token in any order ('d abc' matches 'abc d' and 'abc-d', but not 'abc' or 'abc+')" do
+ tag_query = TagQuery.new(name: "d abc")
+ results = tag_query.search_results
+ results.should include(tags[:char_abc_d])
+ results.should include(tags[:fan_abc_d_minus])
+ results.should_not include(tags[:free_abcplus])
+ results.should_not include(tags[:char_abc])
+ end
- it "preserves minus (-) preceded by a space ('abc -d' matches 'abc -d', 'abc d' and 'abc-d', but not 'abc')" do
- tag_query = TagQuery.new(name: "abc -d")
- results = tag_query.search_results
- results.first.should eq(tags[:char_abc_d_minus])
- results.should include(tags[:char_abc_d])
- results.should include(tags[:fan_abc_d_minus])
- results.should_not include(tags[:char_abc])
- end
-
- it "preserves slashes without quotes ('ab/cd' should match 'ab/cd' and 'ab cd')" do
- tag_query = TagQuery.new(name: "ab/cd")
- results = tag_query.search_results
- results.should include(tags[:rel_slash])
- results.should include(tags[:rel_space])
- end
-
- it "matches tags with canonical punctuation ('yuri!!!' on ice matches 'Yuri!!! On Ice')" do
- tag_query = TagQuery.new(name: "yuri!!! on ice")
- results = tag_query.search_results
- results.should include(tags[:fan_yuri])
- end
+ it "matches tokens with double quotes ('ab \"cd\" ef' matches 'ab \"cd\" ef')" do
+ tag_query = TagQuery.new(name: "ab \"cd\" ef")
+ results = tag_query.search_results
+ results.should include(tags[:rel_quotes])
+ end
- it "matches tags without canonical punctuation ('yuri on ice' matches 'Yuri!!! On Ice')" do
- tag_query = TagQuery.new(name: "yuri on ice")
- results = tag_query.search_results
- results.should include(tags[:fan_yuri])
- end
+ it "matches tokens with single quotes ('ab'c d' matches 'ab'c d')" do
+ tag_query = TagQuery.new(name: "ab'c d")
+ results = tag_query.search_results
+ results.should include(tags[:free_abapos])
+ end
- it "matches unicode tags with unicode character ('Dave ♦ Sawbuck' matches 'Dave ♦ Sawbuck')" do
- tag_query = TagQuery.new(name: "dave ♦ sawbuck")
- results = tag_query.search_results
- results.should include(tags[:rel_unicode])
- end
+ it "performs a wildcard search at the end of a term ('abc*' matches 'abcd' and 'abcde')" do
+ tag_query = TagQuery.new(name: "abc*")
+ results = tag_query.search_results
+ results.should include(tags[:char_abc])
+ results.should include(tags[:fan_abcd])
+ results.should include(tags[:free_abccc])
+ results.should_not include(tags[:relationship])
+ end
- it "matches unicode tags without unicode character ('dave sawbuck' matches 'Dave ♦ Sawbuck')" do
- tag_query = TagQuery.new(name: "dave sawbuck")
- results = tag_query.search_results
- results.should include(tags[:rel_unicode])
- end
+ it "performs a wildcard search in the middle of a term ('a*d' matches 'abcd')" do
+ tag_query = TagQuery.new(name: "a*d")
+ results = tag_query.search_results
+ results.should include(tags[:fan_abcd])
+ end
- it "defaults to TAGS_PER_SEARCH_PAGE to determine the number of results" do
- allow(ArchiveConfig).to receive(:TAGS_PER_SEARCH_PAGE).and_return(5)
- tag_query = TagQuery.new(name: "a*")
- results = tag_query.search_results
- expect(results.size).to eq 5
- end
+ it "performs a wildcard search at the beginning of a term ('*cd' matches 'abcd')" do
+ tag_query = TagQuery.new(name: "*cd")
+ results = tag_query.search_results
+ results.should include(tags[:fan_abcd])
+ end
- it "filters tags by multiple fandom ids" do
- q = TagQuery.new(fandom_ids: [6, 7])
- expect(q.filters).to include({ term: { fandom_ids: 6 } }, { term: { fandom_ids: 7 } })
- end
+ it "preserves plus (+) character ('abc+' matches 'abc+' and 'abc', but not 'abccc')" do
+ tag_query = TagQuery.new(name: "abc+")
+ results = tag_query.search_results
+ results.should include(tags[:free_abcplus])
+ results.should include(tags[:char_abc])
+ results.should_not include(tags[:free_abccc])
+ end
- it "allows you to sort by Date Created" do
- q = TagQuery.new(sort_column: "created_at")
- expect(q.generated_query[:sort]).to eq([{ "created_at" => { order: "desc", unmapped_type: "date" } }, { id: { order: "desc" } }])
- end
+ it "preserves minus (-) character ('abc-d' matches 'abc-d', 'abc -d', 'abc d' but not 'abc' or 'abcd')" do
+ tag_query = TagQuery.new(name: "abc-d")
+ results = tag_query.search_results
+ results.should include(tags[:fan_abc_d_minus])
+ results.should include(tags[:char_abc_d_minus])
+ results.should_not include(tags[:fan_abcd])
+ results.should_not include(tags[:char_abc])
+ end
+
+ it "preserves minus (-) preceded by a space ('abc -d' matches 'abc -d', 'abc d' and 'abc-d', but not 'abc')" do
+ tag_query = TagQuery.new(name: "abc -d")
+ results = tag_query.search_results
+ results.first.should eq(tags[:char_abc_d_minus])
+ results.should include(tags[:char_abc_d])
+ results.should include(tags[:fan_abc_d_minus])
+ results.should_not include(tags[:char_abc])
+ end
+
+ it "preserves slashes without quotes ('ab/cd' should match 'ab/cd' and 'ab cd')" do
+ tag_query = TagQuery.new(name: "ab/cd")
+ results = tag_query.search_results
+ results.should include(tags[:rel_slash])
+ results.should include(tags[:rel_space])
+ end
+
+ it "matches tags with canonical punctuation ('yuri!!!' on ice matches 'Yuri!!! On Ice')" do
+ tag_query = TagQuery.new(name: "yuri!!! on ice")
+ results = tag_query.search_results
+ results.should include(tags[:fan_yuri])
+ end
+
+ it "matches tags without canonical punctuation ('yuri on ice' matches 'Yuri!!! On Ice')" do
+ tag_query = TagQuery.new(name: "yuri on ice")
+ results = tag_query.search_results
+ results.should include(tags[:fan_yuri])
+ end
- it "allows you to sort by Date Created in ascending order" do
- q = TagQuery.new(sort_column: "created_at", sort_direction: "asc")
- expect(q.generated_query[:sort]).to eq([{ "created_at" => { order: "asc", unmapped_type: "date" } }, { id: { order: "asc" } }])
+ it "matches unicode tags with unicode character ('Dave ♦ Sawbuck' matches 'Dave ♦ Sawbuck')" do
+ tag_query = TagQuery.new(name: "dave ♦ sawbuck")
+ results = tag_query.search_results
+ results.should include(tags[:rel_unicode])
+ end
+
+ it "matches unicode tags without unicode character ('dave sawbuck' matches 'Dave ♦ Sawbuck')" do
+ tag_query = TagQuery.new(name: "dave sawbuck")
+ results = tag_query.search_results
+ results.should include(tags[:rel_unicode])
+ end
+
+ it "defaults to TAGS_PER_SEARCH_PAGE to determine the number of results" do
+ allow(ArchiveConfig).to receive(:TAGS_PER_SEARCH_PAGE).and_return(5)
+ tag_query = TagQuery.new(name: "a*")
+ results = tag_query.search_results
+ expect(results.size).to eq 5
+ end
+
+ it "keeps sort order of tied tags the same when tag info is updated" do
+ tag_query = TagQuery.new(name: "abc*", sort_column: "created_at")
+ results = tag_query.search_results.map(&:id)
+
+ [tags[:fan_abcd], tags[:free_abccc]].each do |tag|
+ tag.update(canonical: true)
+ run_all_indexing_jobs
+ expect(tag_query.search_results.map(&:id)).to eq(results)
+ end
+ end
end
- it "keeps sort order of tied tags the same when tag info is updated" do
- tag_query = TagQuery.new(name: "abc*", sort_column: "created_at")
- results = tag_query.search_results.map(&:id)
+ describe "#generated_query" do
+ it "matches all tags when no search parameters are specified" do
+ q = TagQuery.new
+ expect(q.generated_query[:query]).to eq({ match_all: {} })
+ end
- [tags[:fan_abcd], tags[:free_abccc]].each do |tag|
- tag.update(canonical: true)
- run_all_indexing_jobs
- expect(tag_query.search_results.map(&:id)).to eq(results)
+ it "filters tags by multiple fandom ids" do
+ q = TagQuery.new(fandom_ids: [6, 7])
+ expect(q.generated_query.dig(:query, :bool, :filter)).to include({ term: { fandom_ids: 6 } }, { term: { fandom_ids: 7 } })
+ end
+
+ it "allows you to sort by Date Created" do
+ q = TagQuery.new(sort_column: "created_at")
+ expect(q.generated_query[:sort]).to eq([{ "created_at" => { order: "desc", unmapped_type: "date" } }, { id: { order: "desc" } }])
+ end
+
+ it "allows you to sort by Date Created in ascending order" do
+ q = TagQuery.new(sort_column: "created_at", sort_direction: "asc")
+ expect(q.generated_query[:sort]).to eq([{ "created_at" => { order: "asc", unmapped_type: "date" } }, { id: { order: "asc" } }])
end
end
end
From c83ab7b6cc4ded88e9056fdf8c1c5a80cdb1d42f Mon Sep 17 00:00:00 2001
From: Ellie Y Cheng
Date: Sat, 12 Aug 2023 23:48:28 -0400
Subject: [PATCH 031/208] AO3-6442 Bypass URL validation for ficbook.net due to
ip block (#4418)
* AO3-6442 Bypass URL validation for ficbook.net due to ip block
* AO3-6442 Address hound
* AO3-6442 Add tests for bypassed urls
* AO3-6442 Address hound
* AO3-6442 Address hound
---
app/validators/url_active_validator.rb | 5 +++--
spec/models/external_work_spec.rb | 16 ++++++++++++++++
spec/spec_helper.rb | 3 ++-
3 files changed, 21 insertions(+), 3 deletions(-)
diff --git a/app/validators/url_active_validator.rb b/app/validators/url_active_validator.rb
index 14058fa489b..9a074766e4a 100644
--- a/app/validators/url_active_validator.rb
+++ b/app/validators/url_active_validator.rb
@@ -5,9 +5,10 @@ class UrlActiveValidator < ActiveModel::EachValidator
# Checks the status of the webpage at the given url
# To speed things up we ONLY request the head and not the entire page.
- # Bypass check for fanfiction.net because of ip block
+ # Bypass check for fanfiction.net and ficbook.net because of ip block
def validate_each(record,attribute,value)
- return true if value.match("fanfiction.net")
+ return true if value.match("fanfiction.net") || value.match("ficbook.net")
+
inactive_url_msg = "could not be reached. If the URL is correct and the site is currently down, please try again later."
inactive_url_timeout = 10 # seconds
begin
diff --git a/spec/models/external_work_spec.rb b/spec/models/external_work_spec.rb
index 9533b273780..95b9c3c3fd2 100644
--- a/spec/models/external_work_spec.rb
+++ b/spec/models/external_work_spec.rb
@@ -26,6 +26,22 @@
end
end
+ context "for bypassed URLs" do
+ BYPASSED_URLS.each do |url|
+ context "for #{url}" do
+ let(:bypassed_url) { build(:external_work, url: url) }
+
+ before do
+ WebMock.stub_request(:any, bypassed_url.url).to_return(status: 403)
+ end
+
+ it "saves" do
+ expect(bypassed_url.save).to be_truthy
+ end
+ end
+ end
+ end
+
context "for valid URLs" do
[200, 301, 302, 307, 308].each do |status|
context "returning #{status}" do
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 064d97fcdf2..b791b4e083d 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -140,7 +140,8 @@
INVALID_URLS = %w[no_scheme.com ftp://ftp.address.com http://www.b@d!35.com https://www.b@d!35.com http://b@d!35.com https://www.b@d!35.com].freeze
VALID_URLS = %w[http://rocksalt-recs.livejournal.com/196316.html https://rocksalt-recs.livejournal.com/196316.html].freeze
INACTIVE_URLS = %w[https://www.iaminactive.com http://www.iaminactive.com https://iaminactive.com http://iaminactive.com].freeze
-
+ BYPASSED_URLS = %w[fanfiction.net ficbook.net].freeze
+
# rspec-rails 3 will no longer automatically infer an example group's spec type
# from the file location. You can explicitly opt-in to the feature using this
# config option.
From 639e50a79e6706f64cc1d55ea8ff4e79fb57dd61 Mon Sep 17 00:00:00 2001
From: salt <60383410+saltlas@users.noreply.github.com>
Date: Sun, 13 Aug 2023 15:49:44 +1200
Subject: [PATCH 032/208] AO3-5961 Stop Rails logger from logging so many
things below level "error" (#4509)
* AO3-5961 Removed unneccesary Rails logger usages
* AO3-5961 Removed unneccesary Rails logger usages
* changes to rails logger messages as per discussion on PR
---
app/controllers/application_controller.rb | 5 -----
app/models/download_writer.rb | 2 +-
app/models/invite_request.rb | 1 -
app/models/pseud.rb | 1 -
app/models/search/async_indexer.rb | 1 -
app/models/search/indexer.rb | 1 -
app/models/tagset_models/tag_nomination.rb | 1 -
app/models/user.rb | 1 -
app/sweepers/collection_sweeper.rb | 2 --
config/environments/production.rb | 2 +-
lib/autocomplete_source.rb | 1 -
lib/html_cleaner.rb | 2 --
12 files changed, 2 insertions(+), 18 deletions(-)
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 9d52b0aed7f..ea852588005 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -201,7 +201,6 @@ def load_tos_popup
before_action :store_location
def store_location
if session[:return_to] == "redirected"
- Rails.logger.debug "Return to back would cause infinite loop"
session.delete(:return_to)
elsif request.fullpath.length > 200
# Sessions are stored in cookies, which has a 4KB size limit.
@@ -210,7 +209,6 @@ def store_location
session.delete(:return_to)
else
session[:return_to] = request.fullpath
- Rails.logger.debug "Return to: #{session[:return_to]}"
end
end
@@ -220,11 +218,9 @@ def redirect_back_or_default(default = root_path)
back = session[:return_to]
session.delete(:return_to)
if back
- Rails.logger.debug "Returning to #{back}"
session[:return_to] = "redirected"
redirect_to(back) and return
else
- Rails.logger.debug "Returning to default (#{default})"
redirect_to(default) and return
end
end
@@ -401,7 +397,6 @@ def is_admin?
def see_adult?
params[:anchor] = "comments" if (params[:show_comments] && params[:anchor].blank?)
- Rails.logger.debug "Added anchor #{params[:anchor]}"
return true if cookies[:view_adult] || logged_in_as_admin?
return false unless current_user
return true if current_user.is_author_of?(@work)
diff --git a/app/models/download_writer.rb b/app/models/download_writer.rb
index a1de8ebf42b..af557a2dd9e 100644
--- a/app/models/download_writer.rb
+++ b/app/models/download_writer.rb
@@ -52,7 +52,7 @@ def generate_ebook_download
exit_status = nil
Open3.popen3(*cmd) { |_stdin, _stdout, _stderr, wait_thread| exit_status = wait_thread.value }
unless exit_status
- Rails.logger.debug "Download generation failed: " + cmd.to_s
+ Rails.logger.warn "Download generation failed: " + cmd.to_s
end
end
end
diff --git a/app/models/invite_request.rb b/app/models/invite_request.rb
index 2517a5704c9..0a4cfc881cd 100644
--- a/app/models/invite_request.rb
+++ b/app/models/invite_request.rb
@@ -45,7 +45,6 @@ def invite_and_remove(creator=nil)
invitation = creator ? creator.invitations.build(invitee_email: self.email, from_queue: true) :
Invitation.new(invitee_email: self.email, from_queue: true)
if invitation.save
- Rails.logger.info "#{invitation.invitee_email} was invited at #{Time.now}"
self.destroy
end
end
diff --git a/app/models/pseud.rb b/app/models/pseud.rb
index e2b53d872d5..6d2e137896f 100644
--- a/app/models/pseud.rb
+++ b/app/models/pseud.rb
@@ -274,7 +274,6 @@ def byline_before_last_save
#
# This is a particular case for the Pseud model
def remove_stale_from_autocomplete_before_save
- Rails.logger.debug "Removing stale from autocomplete: #{autocomplete_search_string_was}"
self.class.remove_from_autocomplete(self.autocomplete_search_string_was, self.autocomplete_prefixes, self.autocomplete_value_was)
end
diff --git a/app/models/search/async_indexer.rb b/app/models/search/async_indexer.rb
index 72e94ddbbfc..acbe66791d8 100644
--- a/app/models/search/async_indexer.rb
+++ b/app/models/search/async_indexer.rb
@@ -7,7 +7,6 @@ class AsyncIndexer
####################
def self.perform(name)
- Rails.logger.info "Blueshirt: Logging use of constantize class self.perform #{name.split(":").first}"
indexer = name.split(":").first.constantize
ids = REDIS.smembers(name)
diff --git a/app/models/search/indexer.rb b/app/models/search/indexer.rb
index 95a205cf1ce..3142110834e 100644
--- a/app/models/search/indexer.rb
+++ b/app/models/search/indexer.rb
@@ -131,7 +131,6 @@ def self.index_from_db
# Add conditions here
def self.indexables
- Rails.logger.info "Blueshirt: Logging use of constantize class self.indexables #{klass}"
klass.constantize
end
diff --git a/app/models/tagset_models/tag_nomination.rb b/app/models/tagset_models/tag_nomination.rb
index 467f1878256..b0d14d29dc7 100644
--- a/app/models/tagset_models/tag_nomination.rb
+++ b/app/models/tagset_models/tag_nomination.rb
@@ -150,7 +150,6 @@ def change_tagname?(new_tagname)
def self.change_tagname!(owned_tag_set_to_change, old_tagname, new_tagname)
TagNomination.for_tag_set(owned_tag_set_to_change).where(tagname: old_tagname).readonly(false).each do |tagnom|
tagnom.tagname = new_tagname
- Rails.logger.info "Tagnom: #{tagnom.tagname} #{tagnom.valid?}"
tagnom.save or return false
end
return true
diff --git a/app/models/user.rb b/app/models/user.rb
index a8370ac93ac..1798931b048 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -578,7 +578,6 @@ def log_email_change
end
def remove_stale_from_autocomplete
- Rails.logger.debug "Removing stale from autocomplete: #{autocomplete_search_string_was}"
self.class.remove_from_autocomplete(self.autocomplete_search_string_was, self.autocomplete_prefixes, self.autocomplete_value_was)
end
diff --git a/app/sweepers/collection_sweeper.rb b/app/sweepers/collection_sweeper.rb
index d6c3d49dfb3..867bc047baa 100644
--- a/app/sweepers/collection_sweeper.rb
+++ b/app/sweepers/collection_sweeper.rb
@@ -7,9 +7,7 @@ def after_create(record)
def after_update(record)
if record.is_a?(Collection) && (record.saved_change_to_name? || record.saved_change_to_title?)
- Rails.logger.debug "Removing renamed collection from autocomplete: #{record.autocomplete_search_string_before_last_save}"
record.remove_stale_from_autocomplete
- Rails.logger.debug "Adding renamed collection to autocomplete: #{record.autocomplete_search_string}"
record.add_to_autocomplete
end
end
diff --git a/config/environments/production.rb b/config/environments/production.rb
index dc1ca6d1470..31aeb442dfe 100644
--- a/config/environments/production.rb
+++ b/config/environments/production.rb
@@ -27,7 +27,7 @@
# See everything in the log (default is now :debug)
# config.log_level = :debug
- config.log_level = :info
+ config.log_level = :error
# Use a different logger for distributed setups
# config.logger = SyslogLogger.new
diff --git a/lib/autocomplete_source.rb b/lib/autocomplete_source.rb
index 8ac81caf349..8dd8bbff427 100644
--- a/lib/autocomplete_source.rb
+++ b/lib/autocomplete_source.rb
@@ -84,7 +84,6 @@ def remove_from_autocomplete
end
def remove_stale_from_autocomplete
- Rails.logger.debug "Removing stale from autocomplete: #{autocomplete_search_string_before_last_save}"
self.class.remove_from_autocomplete(self.autocomplete_search_string_before_last_save, self.autocomplete_prefixes, self.autocomplete_value_before_last_save)
end
diff --git a/lib/html_cleaner.rb b/lib/html_cleaner.rb
index d0544858503..c0886270eac 100644
--- a/lib/html_cleaner.rb
+++ b/lib/html_cleaner.rb
@@ -8,11 +8,9 @@ def sanitize_field(object, fieldname)
sanitizer_version = object.try("#{fieldname}_sanitizer_version")
if sanitizer_version && sanitizer_version >= ArchiveConfig.SANITIZER_VERSION
# return the field without sanitizing
- Rails.logger.debug "Already sanitized #{fieldname} on #{object.class.name} (id #{object.id})"
object.send(fieldname)
else
# no sanitizer version information, so re-sanitize
- Rails.logger.debug "Sanitizing without saving #{fieldname} on #{object.class.name} (id #{object.id})"
sanitize_value(fieldname, object.send(fieldname))
end
end
From 70411f3f7fd4730cf9d08bbee068fd4aed5f0cb9 Mon Sep 17 00:00:00 2001
From: Bilka
Date: Sun, 13 Aug 2023 05:50:12 +0200
Subject: [PATCH 033/208] AO3-5248 Change nonexistent user pages to 404 (#4508)
* AO3-5248 Change nonexistent user pages to 404
Move the test for nonexistent user pages to RSpec
* AO3-5248 Fix indentation
* AO3-5248 Remove unused test step
* AO3-5248 Change to using find_by! in load_user
---
app/controllers/users_controller.rb | 7 +------
features/other_b/errors.feature | 7 -------
features/step_definitions/web_steps.rb | 5 -----
spec/controllers/users_controller_spec.rb | 19 +++++++++++++++++++
4 files changed, 20 insertions(+), 18 deletions(-)
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 3fdb0d65e85..a1150badecc 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -8,7 +8,7 @@ class UsersController < ApplicationController
skip_before_action :store_location, only: [:end_first_login]
def load_user
- @user = User.find_by(login: params[:id])
+ @user = User.find_by!(login: params[:id])
@check_ownership_of = @user
end
@@ -19,11 +19,6 @@ def index
# GET /users/1
def show
- if @user.blank?
- flash[:error] = ts('Sorry, could not find this user.')
- redirect_to(search_people_path) && return
- end
-
@page_subtitle = @user.login
visible = visible_items(current_user)
diff --git a/features/other_b/errors.feature b/features/other_b/errors.feature
index 3f773b0a139..a7dff1b681b 100644
--- a/features/other_b/errors.feature
+++ b/features/other_b/errors.feature
@@ -24,10 +24,3 @@ Some pages with non existent things raise errors
And visiting "/tags/UnknownTag/works" should fail with a not found error
When I am logged in as "wranglerette"
And visiting "/tags/NonexistentTag/edit" should fail with a not found error
-
- Scenario: Some pages with non existent things give flash warnings
- Given the user "KnownUser" exists and is activated
- And the following activated tag wrangler exists
- | login |
- | wranglerette |
- Then visiting "/users/UnknownUser" should fail with "Sorry, could not find this user."
diff --git a/features/step_definitions/web_steps.rb b/features/step_definitions/web_steps.rb
index 5ce950c9525..3a3de0125ff 100644
--- a/features/step_definitions/web_steps.rb
+++ b/features/step_definitions/web_steps.rb
@@ -111,11 +111,6 @@ def with_scope(locator)
}.to raise_error(ActiveRecord::RecordNotFound)
end
-Then /^visiting "([^"]*)" should fail with "([^"]*)"$/ do |path, flash_error|
- visit path
- step %{I should see "#{flash_error}" within ".flash"}
-end
-
Then /^(?:|I )should see JSON:$/ do |expected_json|
require 'json'
expected = JSON.pretty_generate(JSON.parse(expected_json))
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 7993b004d50..1fdb7159a74 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -39,4 +39,23 @@
end
end
end
+
+ describe "show" do
+ let(:user) { create(:user) }
+
+ context "with a valid username" do
+ it "renders the show template" do
+ get :show, params: { id: user }
+ expect(response).to render_template(:show)
+ end
+ end
+
+ context "with an invalid username" do
+ it "raises an error" do
+ expect do
+ get :show, params: { id: "nobody" }
+ end.to raise_error ActiveRecord::RecordNotFound
+ end
+ end
+ end
end
From 189660609dec0a9e6c2cd176517db643f39fadf0 Mon Sep 17 00:00:00 2001
From: potpotkettle <40988246+potpotkettle@users.noreply.github.com>
Date: Sun, 13 Aug 2023 14:28:28 +0900
Subject: [PATCH 034/208] AO3-4494 Remove time of day from draft deletion
warning message (#4270)
* AO3-4494 Remove time of day from draft deletion warning message
Also add some tests for time_in_zone
* AO3-4494 (add _html to new i18n keys)
* AO3-4494 (revise tests)
* AO3-4494 (i18n)
* AO3-4494 (split files, remove pending test, per review)
* AO3-4494 (remove no_date_specified)
* AO3-4994 normalize messages
* AO3-4994 add hint for i18n-tasks
* AO3-4494 reorder i18n entries after merging
---
app/controllers/chapters_controller.rb | 2 +-
app/controllers/works_controller.rb | 2 +-
app/helpers/application_helper.rb | 2 ++
app/helpers/date_helper.rb | 10 ++++++++++
app/views/works/_work_module.html.erb | 2 +-
app/views/works/show.html.erb | 4 +---
config/locales/controllers/en.yml | 3 +++
config/locales/views/en.yml | 7 +++++++
features/works/work_drafts.feature | 4 ++--
spec/helpers/date_helper_spec.rb | 18 ++++++++++++++++++
10 files changed, 46 insertions(+), 8 deletions(-)
create mode 100644 spec/helpers/date_helper_spec.rb
diff --git a/app/controllers/chapters_controller.rb b/app/controllers/chapters_controller.rb
index f6cad1bcbde..0d20be9332b 100644
--- a/app/controllers/chapters_controller.rb
+++ b/app/controllers/chapters_controller.rb
@@ -98,7 +98,7 @@ def edit
end
def draft_flash_message(work)
- flash[:notice] = work.posted ? ts("This is a draft chapter in a posted work. It will be kept unless the work is deleted.") : ts("This is a draft chapter in an unposted work. The work will be automatically deleted on #{view_context.time_in_zone(work.created_at + 1.month)}.").html_safe
+ flash[:notice] = work.posted ? t("chapters.draft_flash.posted_work") : t("chapters.draft_flash.unposted_work_html", deletion_date: view_context.date_in_zone(work.created_at + 29.days)).html_safe
end
# POST /work/:work_id/chapters
diff --git a/app/controllers/works_controller.rb b/app/controllers/works_controller.rb
index 3d6fd0f4607..a18a6be9e51 100755
--- a/app/controllers/works_controller.rb
+++ b/app/controllers/works_controller.rb
@@ -318,7 +318,7 @@ def create
if @work.save
if params[:preview_button]
- flash[:notice] = ts("Draft was successfully created. It will be automatically deleted on %{deletion_date}", deletion_date: view_context.time_in_zone(@work.created_at + 1.month)).html_safe
+ flash[:notice] = ts("Draft was successfully created. It will be scheduled for deletion on %{deletion_date}.", deletion_date: view_context.date_in_zone(@work.created_at + 29.days)).html_safe
in_moderated_collection
redirect_to preview_work_path(@work)
else
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 183c3bac11e..130a0a93b82 100755
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -296,6 +296,8 @@ def link_to_remove_section(linktext, form, class_of_section_to_remove="removeme"
link_to_function(linktext, "remove_section(this, \"#{class_of_section_to_remove}\")", class: "hidden showme")
end
+ # show time in the time zone specified by the first argument
+ # add the user's time when specified in preferences
def time_in_zone(time, zone = nil, user = User.current_user)
return ts("(no time specified)") if time.blank?
diff --git a/app/helpers/date_helper.rb b/app/helpers/date_helper.rb
index c0b9ce2a7d3..ac6d502e522 100644
--- a/app/helpers/date_helper.rb
+++ b/app/helpers/date_helper.rb
@@ -23,4 +23,14 @@ def date_in_user_time_zone(datetime)
end
end
+ # show date in the time zone specified
+ # note: this does *not* append timezone and does *not* reflect user preferences
+ def date_in_zone(time, zone = nil)
+ zone ||= Time.zone.name
+ return nil if time.blank?
+
+ time_in_zone = time.in_time_zone(zone)
+ I18n.l(time_in_zone, format: :date_short_html).html_safe
+ # i18n-tasks-use t('time.formats.date_short_html')
+ end
end
diff --git a/app/views/works/_work_module.html.erb b/app/views/works/_work_module.html.erb
index db0c11070f2..8c8b2c5e644 100644
--- a/app/views/works/_work_module.html.erb
+++ b/app/views/works/_work_module.html.erb
@@ -48,7 +48,7 @@
<% if !work.posted? %>
- <%= ts('This draft will be automatically deleted on %{time}', time: time_in_zone(work.created_at + 1.month)).html_safe %>
+ <%= t('.draft_deletion_notice_html', deletion_date: date_in_zone(work.created_at + 29.days)) %>
<% end %>
<%= ts("Tags") %>
diff --git a/app/views/works/show.html.erb b/app/views/works/show.html.erb
index 7dd60cef43c..cb49e254890 100755
--- a/app/views/works/show.html.erb
+++ b/app/views/works/show.html.erb
@@ -4,9 +4,7 @@
<%= ts("Skip header") %>
<% if !@work.posted? %>
-
- <%= ts("This work is a draft and has not been posted. The draft will be") %> <%= ts("automatically deleted") %> <%= ts("on") %> <%= time_in_zone(@work.created_at + 1.month) %>
-
+ <%= t(".unposted_deletion_notice_html", deletion_date: date_in_zone(@work.created_at + 29.days)) %>
<% end %>
<% if @work.unrevealed? %>
<%= render "works/work_unrevealed_notice" %>
diff --git a/config/locales/controllers/en.yml b/config/locales/controllers/en.yml
index ff90b0ec19c..8fd4df071fd 100644
--- a/config/locales/controllers/en.yml
+++ b/config/locales/controllers/en.yml
@@ -15,6 +15,9 @@ en:
chapters:
destroy:
only_chapter: You can't delete the only chapter in your work. If you want to delete the work, choose "Delete Work".
+ draft_flash:
+ posted_work: This is a draft chapter in a posted work. It will be kept unless the work is deleted.
+ unposted_work_html: This is a draft chapter in an unposted work. The work will be scheduled for deletion on %{deletion_date}.
show:
anonymous: Anonymous
chapter_position: " - Chapter %{position}"
diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml
index bd30557f484..b98fc70857f 100644
--- a/config/locales/views/en.yml
+++ b/config/locales/views/en.yml
@@ -556,6 +556,9 @@ en:
random: These are some random tags used on the Archive. To find more tags, %{search_tags_link}.
random_in_collection: These are some random tags used in the collection.
search_tags: try our tag search
+ time:
+ formats:
+ date_short_html: %a %d %b %Y
troubleshooting:
show:
fix_associations:
@@ -683,3 +686,7 @@ en:
multiple_works_restricted: Only show to registered users
restricted: Only show your work to registered users
unrestricted: Show to all
+ show:
+ unposted_deletion_notice_html: This work is a draft and has not been posted. The draft will be scheduled for deletion on %{deletion_date}.
+ work_module:
+ draft_deletion_notice_html: This draft will be scheduled for deletion on %{deletion_date}.
diff --git a/features/works/work_drafts.feature b/features/works/work_drafts.feature
index bbd535d6469..168daf7753d 100644
--- a/features/works/work_drafts.feature
+++ b/features/works/work_drafts.feature
@@ -19,7 +19,7 @@ Feature: Work Drafts
And I fill in "Work Title" with "Draft Dodging"
And I fill in "content" with "Klinger lay under his porch."
And I press "Preview"
- Then I should see "Draft was successfully created. It will be automatically deleted on"
+ Then I should see "Draft was successfully created. It will be scheduled for deletion on"
When I press "Edit"
Then I should see "Edit Work"
And I fill in "content" with "Klinger, in Uncle Gus's Aunt Gussie dress, lay under his porch."
@@ -42,7 +42,7 @@ Feature: Work Drafts
And I follow "Add Chapter"
And I fill in "content" with "this is second chapter content"
And I press "Preview"
- Then I should see "This is a draft chapter in an unposted work. The work will be automatically deleted on"
+ Then I should see "This is a draft chapter in an unposted work. The work will be scheduled for deletion on"
Scenario: Purging old drafts
Given I am logged in as "drafter" with password "something"
diff --git a/spec/helpers/date_helper_spec.rb b/spec/helpers/date_helper_spec.rb
new file mode 100644
index 00000000000..5d6f4907e31
--- /dev/null
+++ b/spec/helpers/date_helper_spec.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+describe DateHelper do
+ describe "#date_in_zone" do
+ let(:time) { Time.rfc3339("1999-12-31T16:00:00Z") }
+ let(:zone_tokyo) { Time.find_zone("Asia/Tokyo") }
+
+ it "is html safe" do
+ expect(helper.date_in_zone(time).html_safe?).to eq(true)
+ end
+
+ it "formats UTC date without timezone identifier" do
+ expect(strip_tags(helper.date_in_zone(time, zone_tokyo))).to eq("Sat 01 Jan 2000")
+ end
+ end
+end
From f8d25f3f284fbc5c4962d34c61ba66722d82840d Mon Sep 17 00:00:00 2001
From: EchoEkhi
Date: Mon, 14 Aug 2023 10:24:12 +0800
Subject: [PATCH 035/208] AO3-6540 Suspended and banned users should not be
able to update chapter positions (#4602)
AO3-6540 Restrict chapter update_positions for suspended and banned users
---
app/controllers/chapters_controller.rb | 2 +-
spec/controllers/chapters_controller_spec.rb | 22 ++++++++++++++++++++
2 files changed, 23 insertions(+), 1 deletion(-)
diff --git a/app/controllers/chapters_controller.rb b/app/controllers/chapters_controller.rb
index 0d20be9332b..3fe5a4478ad 100644
--- a/app/controllers/chapters_controller.rb
+++ b/app/controllers/chapters_controller.rb
@@ -1,7 +1,7 @@
class ChaptersController < ApplicationController
# only registered users and NOT admin should be able to create new chapters
before_action :users_only, except: [ :index, :show, :destroy, :confirm_delete ]
- before_action :check_user_status, only: [:new, :create, :update]
+ before_action :check_user_status, only: [:new, :create, :update, :update_positions]
before_action :check_user_not_suspended, only: [:edit, :confirm_delete, :destroy]
before_action :load_work
# only authors of a work should be able to edit its chapters
diff --git a/spec/controllers/chapters_controller_spec.rb b/spec/controllers/chapters_controller_spec.rb
index cdca361e7b4..c07750e065b 100644
--- a/spec/controllers/chapters_controller_spec.rb
+++ b/spec/controllers/chapters_controller_spec.rb
@@ -862,6 +862,28 @@
expect(chapter4.reload.position).to eq(4)
end
end
+
+ context "when the logged in user is suspended" do
+ before do
+ fake_login_known_user(suspended_user)
+ end
+
+ it "errors and redirects to user page" do
+ post :update_positions, params: { work_id: suspended_users_work.id, chapter: [suspended_users_work_chapter2, suspended_users_work.chapters.first] }
+ expect(flash[:error]).to include("Your account has been suspended")
+ end
+ end
+
+ context "when the logged in user is banned" do
+ before do
+ fake_login_known_user(banned_user)
+ end
+
+ it "errors and redirects to user page" do
+ post :update_positions, params: { work_id: banned_users_work.id, chapter: [banned_users_work_chapter2, banned_users_work.chapters.first] }
+ expect(flash[:error]).to include("Your account has been banned")
+ end
+ end
end
end
From c38e9c73338d5e6243eb4705561f69f99bf11e84 Mon Sep 17 00:00:00 2001
From: weeklies <80141759+weeklies@users.noreply.github.com>
Date: Mon, 14 Aug 2023 10:28:12 +0800
Subject: [PATCH 036/208] AO3-5052 Update series byline on username or pseud
update (#4605)
* Adding tests (TDD!)
* AO3-5052 AO3-5052: Touch series on pseud update
* AO3-5052 AO3-5052: Touch series on user update
* Add spec
---------
Co-authored-by: tlee911
---
app/models/pseud.rb | 3 ++-
app/models/user.rb | 1 +
features/other_a/pseuds.feature | 18 ++++++++++++++++++
features/users/user_rename.feature | 16 ++++++++++++++++
spec/models/pseud_spec.rb | 14 ++++++++++++++
5 files changed, 51 insertions(+), 1 deletion(-)
diff --git a/app/models/pseud.rb b/app/models/pseud.rb
index 6d2e137896f..550ed3dc963 100644
--- a/app/models/pseud.rb
+++ b/app/models/pseud.rb
@@ -399,7 +399,8 @@ def check_default_pseud
def expire_caches
if saved_change_to_name?
- self.works.each{ |work| work.touch }
+ works.touch_all
+ series.touch_all
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 1798931b048..f33abd06949 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -158,6 +158,7 @@ class User < ApplicationRecord
def expire_caches
return unless saved_change_to_login?
+ series.touch_all
self.works.each do |work|
work.touch
work.expire_caches
diff --git a/features/other_a/pseuds.feature b/features/other_a/pseuds.feature
index b35f9414982..23b3cb5f131 100644
--- a/features/other_a/pseuds.feature
+++ b/features/other_a/pseuds.feature
@@ -173,3 +173,21 @@ Scenario: Many pseuds
When there are 10 pseuds per page
And I view my profile
Then I should see "Zaphod, Agrajag, Betelgeuse, and Slartibartfast" within "dl.meta"
+
+Scenario: Edit pseud updates series blurbs
+
+ Given I am logged in as "Myself"
+ And I add the work "Great Work" to series "Best Series" as "Me2"
+ When I go to the dashboard page for user "Myself" with pseud "Me2"
+ And I follow "Series"
+ Then I should see "Best Series by Me2 (Myself)"
+
+ When I go to my profile page
+ And I follow "Manage My Pseuds"
+ And I follow "Edit Me2"
+ And I fill in "Name" with "Me3"
+ And I press "Update"
+ Then I should see "Pseud was successfully updated."
+
+ When I follow "Series"
+ Then I should see "Best Series by Me3 (Myself)"
diff --git a/features/users/user_rename.feature b/features/users/user_rename.feature
index a5b2f4f13ad..3a292d2f870 100644
--- a/features/users/user_rename.feature
+++ b/features/users/user_rename.feature
@@ -147,3 +147,19 @@ Feature:
And I view the work "Interesting" with comments
Then I should see "after" within ".comment h4.byline"
And I should not see "mine (before)"
+
+ Scenario: Changing username updates series blurbs
+ Given I have no users
+ And I am logged in as "oldusername" with password "password"
+ And I add the work "Great Work" to series "Best Series"
+ When I go to the dashboard page for user "oldusername" with pseud "oldusername"
+ And I follow "Series"
+ Then I should see "Best Series by oldusername"
+ When I visit the change username page for oldusername
+ And I fill in "New user name" with "newusername"
+ And I fill in "Password" with "password"
+ And I press "Change User Name"
+ Then I should get confirmation that I changed my username
+ And I should see "Hi, newusername"
+ When I follow "Series"
+ Then I should see "Best Series by newusername"
diff --git a/spec/models/pseud_spec.rb b/spec/models/pseud_spec.rb
index eac4a07b777..c8b5ade0d80 100644
--- a/spec/models/pseud_spec.rb
+++ b/spec/models/pseud_spec.rb
@@ -66,6 +66,20 @@
end
end
+ describe "expire_caches" do
+ let(:pseud) { create(:pseud) }
+ let(:series) { create(:series, authors: [pseud]) }
+
+ it "modifies the updated_at of associated series" do
+ pseud.reload
+ series.reload
+ travel(1.day)
+ expect do
+ pseud.update(name: "New Name")
+ end.to change { series.reload.updated_at }
+ end
+ end
+
describe ".default_alphabetical" do
let(:user) { create(:user, login: "Zaphod") }
let(:subject) { user.pseuds.default_alphabetical }
From 381eb602ac14a86d64bff85948c743ececd785c4 Mon Sep 17 00:00:00 2001
From: sarken
Date: Sun, 13 Aug 2023 23:05:14 -0400
Subject: [PATCH 037/208] AO3-6480 Update text on adult content warning to
improve accessibility (#4557)
* AO3-6480 Add missing landmark heading to adult content notice
Add display: block style for h2.landmark to remove extra whitespace due to h2 being display: inline
* AO3-6480 i18n adult content notice
* AO3-6480 Update adult content warning phrasing
* AO3-6480 Update works/_adult.html.erb quotation mark style
* AO3-6480 Add comma
Co-authored-by: Brian Austin <13002992+brianjaustin@users.noreply.github.com>
---------
Co-authored-by: Brian Austin <13002992+brianjaustin@users.noreply.github.com>
---
app/views/works/_adult.html.erb | 14 ++++++++------
config/locales/views/en.yml | 8 ++++++++
features/importing/work_import.feature | 8 ++++----
features/other_a/reading.feature | 2 +-
features/works/work_browse.feature | 4 ++--
public/stylesheets/site/2.0/08-actions.css | 4 ++++
6 files changed, 27 insertions(+), 13 deletions(-)
diff --git a/app/views/works/_adult.html.erb b/app/views/works/_adult.html.erb
index 625dc46b8a3..04345ff382f 100644
--- a/app/views/works/_adult.html.erb
+++ b/app/views/works/_adult.html.erb
@@ -1,27 +1,29 @@
+<%= t(".page_title") %>
+
- <%= ts('This work could have adult content. If you proceed you have agreed that you are willing to see such content.') %>
+ <%= t(".caution") %>
- <%= link_to ts('Proceed'), current_path_with(view_adult: true) %>
+ <%= link_to t(".navigation.continue"), current_path_with(view_adult: true) %>
- <%= link_to ts('Go Back'), :back %>
+ <%= link_to t(".navigation.back"), :back %>
<% if logged_in? %>
- <%= link_to ts('Set your preferences now'), user_preferences_path(current_user) %>
+ <%= link_to t(".navigation.preferences"), user_preferences_path(current_user) %>
<% end %>
- <%= render 'works/work_blurb', work: @work %>
+ <%= render "works/work_blurb", work: @work %>
diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml
index b98fc70857f..dc26d4e2b3b 100644
--- a/config/locales/views/en.yml
+++ b/config/locales/views/en.yml
@@ -673,6 +673,14 @@ en:
link_tos: Terms of Service
welcome_text: Hi! It looks like you've just logged into the Archive for the first time. %{help_link} or dismiss this message permanently.
works:
+ adult:
+ caution: This work could have adult content. If you continue, you have agreed that you are willing to see such content.
+ footnote: If you accept cookies from our site and you choose "Yes, Continue", you will not be asked again during this session (that is, until you close your browser). If you log in you can store your preference and never be asked again.
+ navigation:
+ back: No, Go Back
+ continue: Yes, Continue
+ preferences: Set your preferences now
+ page_title: Adult Content Warning
meta:
original_creators:
one: 'Original Creator ID:'
diff --git a/features/importing/work_import.feature b/features/importing/work_import.feature
index 1e7c55a93b3..b28e5b5f42d 100644
--- a/features/importing/work_import.feature
+++ b/features/importing/work_import.feature
@@ -162,7 +162,7 @@ Feature: Import Works
And I press "Post"
When I am logged out
And I go to the "Detected Title" work page
- And I follow "Proceed"
+ And I follow "Yes, Continue"
Then I should see "Guest name:"
Scenario: Imported works can have comments disabled to guests
@@ -172,7 +172,7 @@ Feature: Import Works
And I press "Post"
When I am logged out
And I go to the "Detected Title" work page
- And I follow "Proceed"
+ And I follow "Yes, Continue"
Then I should see "Sorry, this work doesn't allow non-Archive users to comment."
Scenario: Imported works can have comments disabled
@@ -190,7 +190,7 @@ Feature: Import Works
And I press "Post"
When I am logged out
And I go to the "Detected Title" work page
- And I follow "Proceed"
+ And I follow "Yes, Continue"
Then I should not see "This work's creator has chosen to moderate comments on the work."
Scenario: Imported works can have comment moderation on
@@ -200,7 +200,7 @@ Feature: Import Works
And I press "Post"
When I am logged out
And I go to the "Detected Title" work page
- And I follow "Proceed"
+ And I follow "Yes, Continue"
Then I should see "This work's creator has chosen to moderate comments on the work."
@work_import_multi_work_backdate
diff --git a/features/other_a/reading.feature b/features/other_a/reading.feature
index 12e0107ae0d..3ce2eabfe9a 100644
--- a/features/other_a/reading.feature
+++ b/features/other_a/reading.feature
@@ -82,7 +82,7 @@ Feature: Reading count
And I am on testuser2 works page
And I follow "fifth"
And I should see "fifth by testuser2"
- And I follow "Proceed"
+ And I follow "Yes, Continue"
And the readings are saved to the database
When I go to fandomer's reading page
Then I should see "History" within "div#dashboard"
diff --git a/features/works/work_browse.feature b/features/works/work_browse.feature
index 02af31f073c..4540fa91cf8 100644
--- a/features/works/work_browse.feature
+++ b/features/works/work_browse.feature
@@ -63,7 +63,7 @@ content notice to visitors who are not logged in
And I browse the "Canonical Fandom" works
And I follow the recent chapter link for the work "WIP"
Then I should see "adult content"
- When I follow "Proceed"
+ When I follow "Yes, Continue"
Then I should be on the 3rd chapter of the work "WIP"
Scenario: The recent chapter link in a work's blurb should honor the logged-in
@@ -82,7 +82,7 @@ user's "Show me adult content without checking" preference
And I browse the "Canonical Fandom" works
And I follow the recent chapter link for the work "WIP"
Then I should see "adult content"
- When I follow "Proceed"
+ When I follow "Yes, Continue"
Then I should be on the 2nd chapter of the work "WIP"
Scenario: The recent chapter link in a work's blurb should point to
diff --git a/public/stylesheets/site/2.0/08-actions.css b/public/stylesheets/site/2.0/08-actions.css
index 19c6f4fe426..880ff948414 100644
--- a/public/stylesheets/site/2.0/08-actions.css
+++ b/public/stylesheets/site/2.0/08-actions.css
@@ -140,6 +140,10 @@ ul#skiplinks, .landmark, .landmark a, .index .heading.landmark {
opacity: 0;
}
+h2.landmark {
+ display: block;
+}
+
.secondary {
background: #fff;
position: absolute;
From 6c3c86481ec395434399bafc71101da4819afe38 Mon Sep 17 00:00:00 2001
From: weeklies <80141759+weeklies@users.noreply.github.com>
Date: Mon, 14 Aug 2023 14:20:50 +0800
Subject: [PATCH 038/208] AO3-5732 Show error when searching invitations by
email has 0 results (#4603)
* Added new if statement to check for an empty return container
* Tabs -> Spaces
* Add conciseness to check and a test
* Modify views to make the test pass
* Hound
* Normalize
* Fix old tests
* Try again
* Feedback
* AO3-5732-new Hound
* Normalize
---------
Co-authored-by: EliotWL
Co-authored-by: El <39184025+EliotWL@users.noreply.github.com>
---
.../admin/admin_invitations_controller.rb | 13 +++++++------
app/views/admin/admin_invitations/find.html.erb | 8 ++++----
config/locales/controllers/en.yml | 2 ++
config/locales/views/en.yml | 4 ++++
features/admins/admin_invitations.feature | 10 +++++++---
features/support/paths.rb | 2 +-
6 files changed, 25 insertions(+), 14 deletions(-)
diff --git a/app/controllers/admin/admin_invitations_controller.rb b/app/controllers/admin/admin_invitations_controller.rb
index fbe21562450..e48140f9fac 100644
--- a/app/controllers/admin/admin_invitations_controller.rb
+++ b/app/controllers/admin/admin_invitations_controller.rb
@@ -43,13 +43,14 @@ def find
end
if !invitation_params[:token].blank?
@invitation = Invitation.find_by(token: invitation_params[:token])
- elsif !invitation_params[:invitee_email].blank?
- @invitations = Invitation.where('invitee_email LIKE ?', "%#{invitation_params[:invitee_email]}%")
- @invitation = @invitations.first if @invitations.length == 1
- end
- unless @user || @invitation || @invitations
- flash.now[:error] = t('user_not_found', default: "No results were found. Try another search.")
+ elsif invitation_params[:invitee_email].present?
+ @invitations = Invitation.where("invitee_email LIKE ?", "%#{invitation_params[:invitee_email]}%")
+ @invitation = @invitations.first
end
+
+ return if @user || @invitation.present? || @invitations.present?
+
+ flash.now[:error] = t(".user_not_found")
end
private
diff --git a/app/views/admin/admin_invitations/find.html.erb b/app/views/admin/admin_invitations/find.html.erb
index dc4a51e6342..5d426feadfc 100644
--- a/app/views/admin/admin_invitations/find.html.erb
+++ b/app/views/admin/admin_invitations/find.html.erb
@@ -8,12 +8,12 @@
<%= form_tag url_for(:controller => 'admin/admin_invitations', :action => 'find'), :method => :get do %>
- <%= label_tag :user_name, t('.find_user_name', :default => 'Enter a user name:') %>
+ <%= label_tag "invitation[user_name]", t(".user_name") %>:
<%= text_field_tag "invitation[user_name]", params[:invitation][:user_name] %>
- <%= label_tag :token, t('.find_token', :default => 'Enter an invite token:') %>
+ <%= label_tag "invitation[token]", t(".token") %>:
<%= text_field_tag "invitation[token]", params[:invitation][:token] %>
- <%= label_tag :invitee_email, t('.find_email', :default => 'Enter all or part of an email address:') %>
- <%= text_field_tag "invitation[invitee_email]", params[:invitation][:invitee_email], id: "invitee_email" %>
+ <%= label_tag "invitee_email", t(".email") %>:
+ <%= text_field_tag "invitation[invitee_email]", params[:invitation][:invitee_email], id: "invitee_email" %>
<%= submit_tag "Go" %>
<% end %>
diff --git a/config/locales/controllers/en.yml b/config/locales/controllers/en.yml
index 8fd4df071fd..70333a567e2 100644
--- a/config/locales/controllers/en.yml
+++ b/config/locales/controllers/en.yml
@@ -2,6 +2,8 @@
en:
admin:
admin_invitations:
+ find:
+ user_not_found: No results were found. Try another search.
invite_from_queue:
success:
one: "%{count} person from the invite queue is being invited."
diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml
index dc26d4e2b3b..d4095da40df 100644
--- a/config/locales/views/en.yml
+++ b/config/locales/views/en.yml
@@ -60,6 +60,10 @@ en:
violation_html: You have encountered content on the Archive that violates the %{tos_link}.
admin:
admin_invitations:
+ find:
+ email: Enter all or part of an email address
+ token: Enter an invite token
+ user_name: Enter a user name
index:
navigation:
queue: Manage Queue
diff --git a/features/admins/admin_invitations.feature b/features/admins/admin_invitations.feature
index ed44bed2e01..c1dc52c2b87 100644
--- a/features/admins/admin_invitations.feature
+++ b/features/admins/admin_invitations.feature
@@ -320,6 +320,10 @@ Feature: Admin Actions to Manage Invitations
When I fill in "Enter a user name" with "dax"
And I press "Go"
Then I should see "No results were found. Try another search"
+ When I fill in "Enter a user name" with ""
+ And I fill in "Enter all or part of an email address" with "nonexistent@domain.com"
+ And I press "Go"
+ Then I should see "No results were found. Try another search"
Scenario: An admin can invite people from the queue
Given I am logged in as an admin
@@ -342,7 +346,7 @@ Feature: Admin Actions to Manage Invitations
And press "Invite from queue"
Then I should see "1 person from the invite queue is being invited"
When I press "Go"
- And I fill in "Enter all or part of an email address:" with "test@example.com"
+ And I fill in "Enter all or part of an email address" with "test@example.com"
And I press "Go"
Then I should see "Sender testadmin-support"
@@ -358,9 +362,9 @@ Feature: Admin Actions to Manage Invitations
And I fill in "Enter an invite token" with "dax's" invite code
And I press "Go"
Then I should see "copy and use"
- When I fill in "invitation_invitee_email" with "oldman@ds9.com"
+ When I fill in "Enter an email address" with "oldman@ds9.com"
And I press "Update Invitation"
- Then I should see "oldman@ds9.com" in the "invitation_invitee_email" input
+ Then I should see "oldman@ds9.com" in the "Enter an email address" input
Scenario: An admin can search the invitation queue, and search parameters are
kept even if deleting without JavaScript
diff --git a/features/support/paths.rb b/features/support/paths.rb
index a60ab8ad3e4..95df95966e3 100644
--- a/features/support/paths.rb
+++ b/features/support/paths.rb
@@ -132,7 +132,7 @@ def path_to(page_name)
when /^(.*?)(?:'s)? user page$/i
user_path(id: $1)
when /^(.*?)(?:'s)? "(.*)" pseud page$/i
- # TODO: Avoid this in favor of 'the (user|dashboard) page for user "(.*)" with pseud "(.*)', and eventually remove.
+ # TODO: Avoid this in favor of 'the (user|dashboard) page for user "(.*)" with pseud "(.*)', and eventually remove.
user_pseud_path(user_id: $1, id: $2)
when /^the (user|dashboard) page for user "(.*?)" with pseud "(.*?)"$/i
user_pseud_path(user_id: Regexp.last_match(2), id: Regexp.last_match(3))
From 538664f7b1de73539a5c61b28c37c72ca7a0821d Mon Sep 17 00:00:00 2001
From: weeklies <80141759+weeklies@users.noreply.github.com>
Date: Tue, 15 Aug 2023 13:34:53 +0800
Subject: [PATCH 039/208] AO3-5732 Fix partial email search not displaying all
invitations (#4606)
fix-multiple-invitations Fix partial email search not displaying all invitations
---
.../admin/admin_invitations_controller.rb | 4 ++--
app/views/admin/admin_invitations/index.html.erb | 2 +-
config/locales/views/en.yml | 1 +
features/admins/admin_invitations.feature | 15 +++++++++++++++
4 files changed, 19 insertions(+), 3 deletions(-)
diff --git a/app/controllers/admin/admin_invitations_controller.rb b/app/controllers/admin/admin_invitations_controller.rb
index e48140f9fac..28fe4fd2053 100644
--- a/app/controllers/admin/admin_invitations_controller.rb
+++ b/app/controllers/admin/admin_invitations_controller.rb
@@ -45,10 +45,10 @@ def find
@invitation = Invitation.find_by(token: invitation_params[:token])
elsif invitation_params[:invitee_email].present?
@invitations = Invitation.where("invitee_email LIKE ?", "%#{invitation_params[:invitee_email]}%")
- @invitation = @invitations.first
+ @invitation = @invitations.first if @invitations.length == 1
end
- return if @user || @invitation.present? || @invitations.present?
+ return if @user || @invitation || @invitations.present?
flash.now[:error] = t(".user_not_found")
end
diff --git a/app/views/admin/admin_invitations/index.html.erb b/app/views/admin/admin_invitations/index.html.erb
index de9aac45de0..7800a0a3230 100644
--- a/app/views/admin/admin_invitations/index.html.erb
+++ b/app/views/admin/admin_invitations/index.html.erb
@@ -52,7 +52,7 @@
<%= text_field_tag "invitation[user_name]" %>
<%= label_tag "invitation[token]", ts('Enter an invite token') %>:
<%= text_field_tag "invitation[token]" %>
- <%= label_tag "invitation[invitee_email]", ts('Enter all or part of an email address') %>:
+ <%= label_tag "track_invitation_invitee_email", t(".email") %>:
<%= text_field_tag "invitation[invitee_email]", nil, id: "track_invitation_invitee_email" %>
Submit
<%= submit_tag "Go" %>
diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml
index d4095da40df..a20d3aa086e 100644
--- a/config/locales/views/en.yml
+++ b/config/locales/views/en.yml
@@ -65,6 +65,7 @@ en:
token: Enter an invite token
user_name: Enter a user name
index:
+ email: Enter all or part of an email address
navigation:
queue: Manage Queue
requests: Manage Requests
diff --git a/features/admins/admin_invitations.feature b/features/admins/admin_invitations.feature
index c1dc52c2b87..30e5d1de468 100644
--- a/features/admins/admin_invitations.feature
+++ b/features/admins/admin_invitations.feature
@@ -314,6 +314,21 @@ Feature: Admin Actions to Manage Invitations
And I press "Go"
Then I should see "copy and use"
+ Scenario: An admin can find all invitations via email partial match
+ Given I am logged in as an admin
+ And an invitation request for "fred@bedrock.com"
+ And an invitation request for "barney@bedrock.com"
+ And all emails have been delivered
+ And I follow "Invite New Users"
+ Then I should see "There are 2 requests in the queue."
+ When I fill in "Number of people to invite" with "2"
+ And I press "Invite from queue"
+ Then I should see "2 people from the invite queue are being invited"
+ When I fill in "Enter all or part of an email address" with "@"
+ And I press "Go"
+ Then I should see "fred@bedrock.com"
+ And I should see "barney@bedrock.com"
+
Scenario: An admin can't find a invitation for a nonexistent user
Given I am logged in as an admin
And I follow "Invite New Users"
From 72caa938719e30f6fd273b7408b0935a3f762a19 Mon Sep 17 00:00:00 2001
From: weeklies <80141759+weeklies@users.noreply.github.com>
Date: Fri, 18 Aug 2023 11:04:23 +0800
Subject: [PATCH 040/208] AO3-5052 Delete cached series byline when updating
pseud or username (#4609)
AO3-5052-spr Change how series byline expiration works
---
app/models/pseud.rb | 2 +-
app/models/series.rb | 6 ++++++
app/models/user.rb | 2 +-
spec/models/pseud_spec.rb | 14 --------------
4 files changed, 8 insertions(+), 16 deletions(-)
diff --git a/app/models/pseud.rb b/app/models/pseud.rb
index 550ed3dc963..5ce87de8f96 100644
--- a/app/models/pseud.rb
+++ b/app/models/pseud.rb
@@ -400,7 +400,7 @@ def check_default_pseud
def expire_caches
if saved_change_to_name?
works.touch_all
- series.touch_all
+ series.each(&:expire_byline_cache)
end
end
diff --git a/app/models/series.rb b/app/models/series.rb
index b31714c7c1c..5daecb15bcd 100644
--- a/app/models/series.rb
+++ b/app/models/series.rb
@@ -150,6 +150,12 @@ def expire_caches
self.works.each(&:touch) if saved_change_to_title?
end
+ def expire_byline_cache
+ [true, false].each do |only_path|
+ Rails.cache.delete("#{cache_key}/byline-nonanon/#{only_path}")
+ end
+ end
+
# Change the positions of the serial works in the series
def reorder_list(positions)
SortableList.new(self.serial_works.in_order).reorder_list(positions)
diff --git a/app/models/user.rb b/app/models/user.rb
index f33abd06949..6d51edcd8db 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -158,7 +158,7 @@ class User < ApplicationRecord
def expire_caches
return unless saved_change_to_login?
- series.touch_all
+ series.each(&:expire_byline_cache)
self.works.each do |work|
work.touch
work.expire_caches
diff --git a/spec/models/pseud_spec.rb b/spec/models/pseud_spec.rb
index c8b5ade0d80..eac4a07b777 100644
--- a/spec/models/pseud_spec.rb
+++ b/spec/models/pseud_spec.rb
@@ -66,20 +66,6 @@
end
end
- describe "expire_caches" do
- let(:pseud) { create(:pseud) }
- let(:series) { create(:series, authors: [pseud]) }
-
- it "modifies the updated_at of associated series" do
- pseud.reload
- series.reload
- travel(1.day)
- expect do
- pseud.update(name: "New Name")
- end.to change { series.reload.updated_at }
- end
- end
-
describe ".default_alphabetical" do
let(:user) { create(:user, login: "Zaphod") }
let(:subject) { user.pseuds.default_alphabetical }
From 03cafdeaf1ec9ab8aeb89a0c038f0a36cf8228f8 Mon Sep 17 00:00:00 2001
From: Bilka
Date: Sat, 19 Aug 2023 02:50:58 +0200
Subject: [PATCH 041/208] AO3-6590 Add reviewdog for linting ERB files (#4604)
* AO3-6590 Add reviewdog ERB lint runner
* AO3-6590 Remove erb lint from Hound
It doesn't work. And if that would be fixed in the future, it would be
linting twice.
* AO3-6590 Rename reviewdog workflow to be more generic
---
.github/workflows/reviewdog.yml | 27 +++++++++++++++++++++++++++
.hound.yml | 4 ----
2 files changed, 27 insertions(+), 4 deletions(-)
create mode 100644 .github/workflows/reviewdog.yml
diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml
new file mode 100644
index 00000000000..c86a098d193
--- /dev/null
+++ b/.github/workflows/reviewdog.yml
@@ -0,0 +1,27 @@
+# Based on https://github.com/tk0miya/action-erblint/blob/main/README.md#example-usage
+
+name: Reviewdog
+
+on: [pull_request]
+
+permissions:
+ checks: write
+
+jobs:
+ erb-lint:
+ name: ERB Lint runner
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v3
+
+ - name: Set up Ruby and run bundle install
+ uses: ruby/setup-ruby@v1
+ with:
+ bundler-cache: true
+
+ - name: erb-lint
+ uses: tk0miya/action-erblint@667687e73b44e7b7a710a1204b180f49f80ebb5e
+ with:
+ use_bundler: true
+ reporter: github-pr-check # default
diff --git a/.hound.yml b/.hound.yml
index f544a38eee4..42909ccedb6 100644
--- a/.hound.yml
+++ b/.hound.yml
@@ -1,10 +1,6 @@
# Available linter versions:
# http://help.houndci.com/en/articles/2461415-supported-linters
-erblint:
- enabled: true
- config_file: .erb-lint.yml
-
jshint:
config_file: .jshintrc
ignore_file: .jshintignore
From cb58c3ae4947e7ab5469d9d43c637e8d5e91c739 Mon Sep 17 00:00:00 2001
From: Brian Austin <13002992+brianjaustin@users.noreply.github.com>
Date: Fri, 18 Aug 2023 23:26:42 -0400
Subject: [PATCH 042/208] AO3-6595 Disallow ChatGPT-User robots (#4611)
---
public/robots.public.txt | 3 +++
1 file changed, 3 insertions(+)
diff --git a/public/robots.public.txt b/public/robots.public.txt
index 5ff24d7ec7e..c8aa30a0210 100644
--- a/public/robots.public.txt
+++ b/public/robots.public.txt
@@ -33,5 +33,8 @@ Disallow: /
User-agent: GPTBot
Disallow: /
+User-agent: ChatGPT-User
+Disallow: /
+
User-agent: Slurp
Crawl-delay: 30
From 801ce0931c0dbb144f9631eed5e9cf743a6c8d00 Mon Sep 17 00:00:00 2001
From: sarken
Date: Sun, 20 Aug 2023 06:02:31 -0400
Subject: [PATCH 043/208] AO3-6553 Use div with a role rather than nav element
(#4608)
The two column layout breaks with nav because it relies on nth-of-type, which can't be
used with a class as the sole selector.
---
app/views/home/index.html.erb | 4 ++--
public/stylesheets/site/2.0/26-media-narrow.css | 2 +-
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb
index 0bafaa432d0..16036c095aa 100644
--- a/app/views/home/index.html.erb
+++ b/app/views/home/index.html.erb
@@ -15,13 +15,13 @@
<% else %>
- " class="browse module">
+ " class="browse module">
<%= t(".find_your_favorites") %>
<% if logged_in? %>
<%= t(".browse_or_favorite", count: ArchiveConfig.MAX_FAVORITE_TAGS) %>
<% end %>
<%= render 'fandoms' %>
-
+
<% end %>
<% if @homepage.admin_posts.present? %>
diff --git a/public/stylesheets/site/2.0/26-media-narrow.css b/public/stylesheets/site/2.0/26-media-narrow.css
index 0fb07fc582e..aee2a1722be 100644
--- a/public/stylesheets/site/2.0/26-media-narrow.css
+++ b/public/stylesheets/site/2.0/26-media-narrow.css
@@ -182,7 +182,7 @@ body .narrow-shown {
padding: 0;
}
-.splash div.module, .splash nav.module, .logged-in .splash div.module, .logged-in .splash nav.module {
+.splash div.module, .logged-in .splash div.module {
clear: both;
margin-left: 0;
margin-right: 0;
From 342a8b8b527e9d5b8bb99531ab9c167e9a2dd232 Mon Sep 17 00:00:00 2001
From: tickinginstant
Date: Mon, 28 Aug 2023 04:24:52 -0400
Subject: [PATCH 044/208] AO3-6571 Prevent relative image paths. (#4598)
* AO3-6571 Prevent relative image paths.
* AO3-6571 Use APP_URL instead of APP_HOST.
* AO3-6571 Typo.
* AO3-6571 Typo.
---
config/config.yml | 2 +-
.../gem-plugin_config/sanitizer_config.rb | 11 ++++++++++-
features/other_a/parser.feature | 4 ++--
lib/html_cleaner.rb | 5 ++++-
spec/lib/html_cleaner_spec.rb | 14 ++++++++++++++
5 files changed, 31 insertions(+), 5 deletions(-)
diff --git a/config/config.yml b/config/config.yml
index e52da538648..b248ac170ff 100644
--- a/config/config.yml
+++ b/config/config.yml
@@ -277,7 +277,7 @@ ANONYMOUS_THRESHOLD_COUNT: 10
COMMENT_MODERATION_THRESHOLD: 10
# SANITIZER VERSION
-SANITIZER_VERSION: 3
+SANITIZER_VERSION: 4
# parameters that must be natural integers and their default values
NONZERO_INTEGER_PARAMETERS:
diff --git a/config/initializers/gem-plugin_config/sanitizer_config.rb b/config/initializers/gem-plugin_config/sanitizer_config.rb
index 932d515b788..5f94296e2ab 100644
--- a/config/initializers/gem-plugin_config/sanitizer_config.rb
+++ b/config/initializers/gem-plugin_config/sanitizer_config.rb
@@ -33,7 +33,7 @@ module Config
protocols: {
"a" => { "href" => ["ftp", "http", "https", "mailto", :relative] },
"blockquote" => { "cite" => ["http", "https", :relative] },
- "img" => { "src" => ["http", "https", :relative] },
+ "img" => { "src" => ["http", "https"] },
"q" => { "cite" => ["http", "https", :relative] }
},
@@ -58,5 +58,14 @@ module Config
env[:node]["open"] = "open" if env[:node].has_attribute?("open")
end
+
+ # On img elements, convert relative paths to absolute:
+ RELATIVE_IMAGE_PATH_TRANSFORMER = lambda do |env|
+ return unless env[:node_name] == "img" && env[:node]["src"]
+
+ env[:node]["src"] = URI.join(ArchiveConfig.APP_URL, env[:node]["src"])
+ rescue URI::InvalidURIError
+ # do nothing, the sanitizer will handle it
+ end
end
end
diff --git a/features/other_a/parser.feature b/features/other_a/parser.feature
index 5578cf2669a..8ae49c88499 100644
--- a/features/other_a/parser.feature
+++ b/features/other_a/parser.feature
@@ -42,7 +42,7 @@ Feature: Parsing HTML
And I fill in "content" with " You better work
"
And I press "Preview"
Then I should see "Draft was successfully created."
- And I should see the image "src" text "britney.gif"
+ And I should see the image "src" text "http://www.example.org/britney.gif"
And I should see the image "alt" text "Britney Spears"
When I press "Edit"
Then the "Summary" field should not contain "myclass"
@@ -51,7 +51,7 @@ Feature: Parsing HTML
And the "content" field should contain "size-10"
When I press "Post"
Then I should see "Work was successfully posted."
- And I should see the image "src" text "britney.gif"
+ And I should see the image "src" text "http://www.example.org/britney.gif"
Scenario: Chapter notes and content HTML keep classes when previewing before posting
Given I am logged in as a random user
diff --git a/lib/html_cleaner.rb b/lib/html_cleaner.rb
index c0886270eac..f2636d00a16 100644
--- a/lib/html_cleaner.rb
+++ b/lib/html_cleaner.rb
@@ -56,7 +56,10 @@ def sanitize_value(field, value)
end
if ArchiveConfig.FIELDS_ALLOWING_HTML.include?(field.to_s)
# We're allowing users to use HTML in this field
- transformers = [Sanitize::Config::OPEN_ATTRIBUTE_TRANSFORMER]
+ transformers = [
+ Sanitize::Config::OPEN_ATTRIBUTE_TRANSFORMER,
+ Sanitize::Config::RELATIVE_IMAGE_PATH_TRANSFORMER
+ ]
if ArchiveConfig.FIELDS_ALLOWING_VIDEO_EMBEDS.include?(field.to_s)
transformers << OtwSanitize::EmbedSanitizer.transformer
transformers << OtwSanitize::MediaSanitizer.transformer
diff --git a/spec/lib/html_cleaner_spec.rb b/spec/lib/html_cleaner_spec.rb
index 22f8b801fc8..21d9ca47e85 100644
--- a/spec/lib/html_cleaner_spec.rb
+++ b/spec/lib/html_cleaner_spec.rb
@@ -455,6 +455,20 @@
end
end
end
+
+ context "when given an tag with a relative src" do
+ it "converts the src value to an absolute URL" do
+ content = sanitize_value(field, " ")
+ expect(content).to eq("\n \n
")
+ end
+ end
+
+ context "when given an tag with an absolute src" do
+ it "doesn't modify the src value" do
+ content = sanitize_value(field, " ")
+ expect(content).to eq("\n \n
")
+ end
+ end
end
end
From 49581ccbc42a05805fdb697f50aad1fbfb4af9c0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=89ric=20B?=
Date: Mon, 28 Aug 2023 10:25:13 +0200
Subject: [PATCH 045/208] AO3-6467 Record Fannish Next of Kin changes in user
history (#4582)
* Adding
https://otwarchive.atlassian.net/browse/AO3-6467
* Remove
* Replace many ifelse with case
* Display logs on admin page
* Forgot to log admin identity
* Also show logs on the FNOK side
* Test for display
Keeping it simple by not checking id for now
* Add Alterity
* Forgot to commit previously
* Hound
* Hound (bis)
* Hound (ter)
* Add i18n keys to exceptions
* Remove Alterity actually
Superseded by https://github.com/otwcode/otwarchive/pull/4528
* Add pt-online-schema-change syntax for prod
* Remove explicit foreign key
Similar migrations don't appear to set one
* Implicit NULL default value
* Test id too
* Add user destroy controller test
Apparently didn't exist before?
* Log automatic removal of fnok on deletion
* Hound
* Use classical I18n syntax
* Simplify fnok name action
* Clean other I18n keys
* Hound
* reviewdog
* Normalize i18n
* Syntax more compliant with i18n-tasks
* Wordings
* Explicit over succinct
---
.../admin/admin_users_controller.rb | 20 +++++-
app/helpers/users_helper.rb | 72 ++++++++++++-------
app/models/log_item.rb | 2 +
app/models/user.rb | 10 +++
.../admin/admin_users/_user_history.html.erb | 2 +-
config/config.yml | 2 +
config/i18n-tasks.yml | 13 ----
config/locales/helpers/en.yml | 19 +++++
...717161221_add_fnok_user_id_to_log_items.rb | 51 +++++++++++++
factories/fannish_next_of_kin.rb | 9 +++
features/admins/users/admin_fnok.feature | 7 ++
features/step_definitions/admin_steps.rb | 10 +++
.../admin/admin_users_controller_spec.rb | 33 +++++++++
spec/controllers/users_controller_spec.rb | 18 +++++
spec/models/user_spec.rb | 17 +++++
15 files changed, 244 insertions(+), 41 deletions(-)
create mode 100644 db/migrate/20230717161221_add_fnok_user_id_to_log_items.rb
create mode 100644 factories/fannish_next_of_kin.rb
diff --git a/app/controllers/admin/admin_users_controller.rb b/app/controllers/admin/admin_users_controller.rb
index a319a032716..5031bef95fa 100644
--- a/app/controllers/admin/admin_users_controller.rb
+++ b/app/controllers/admin/admin_users_controller.rb
@@ -47,7 +47,7 @@ def show
@user = authorize User.find_by!(login: params[:id])
@hide_dashboard = true
@page_subtitle = t(".page_title", login: @user.login)
- @log_items = @user.log_items.sort_by(&:created_at).reverse
+ log_items
end
# POST admin/users/update
@@ -75,6 +75,12 @@ def update_next_of_kin
if kin.blank? && kin_email.blank?
if fnok.present?
fnok.destroy
+ @user.create_log_item({
+ action: ArchiveConfig.ACTION_REMOVE_FNOK,
+ fnok_user_id: fnok.kin.id,
+ admin_id: current_admin.id,
+ note: "Change made by #{current_admin.login}"
+ })
flash[:notice] = ts("Fannish next of kin was removed.")
end
redirect_to admin_user_path(@user)
@@ -84,11 +90,17 @@ def update_next_of_kin
fnok = @user.build_fannish_next_of_kin if fnok.blank?
fnok.assign_attributes(kin: kin, kin_email: kin_email)
if fnok.save
+ @user.create_log_item({
+ action: ArchiveConfig.ACTION_ADD_FNOK,
+ fnok_user_id: fnok.kin.id,
+ admin_id: current_admin.id,
+ note: "Change made by #{current_admin.login}"
+ })
flash[:notice] = ts("Fannish next of kin was updated.")
redirect_to admin_user_path(@user)
else
@hide_dashboard = true
- @log_items = @user.log_items.sort_by(&:created_at).reverse
+ log_items
render :show
end
end
@@ -168,4 +180,8 @@ def activate
redirect_to action: :show
end
end
+
+ def log_items
+ @log_items ||= (@user.log_items + LogItem.where(fnok_user_id: @user.id)).sort_by(&:created_at).reverse
+ end
end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index b6a6b173a2b..8c3b1d38dcc 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -119,34 +119,56 @@ def authored_items(pseud, work_counts = {}, rec_counts = {})
items.html_safe
end
- def log_item_action_name(action)
- if action == ArchiveConfig.ACTION_ACTIVATE
- t('users_helper.log_validated', default: 'Account Validated')
- elsif action == ArchiveConfig.ACTION_ADD_ROLE
- t('users_helper.log_role_added', default: 'Role Added: ')
- elsif action == ArchiveConfig.ACTION_REMOVE_ROLE
- t('users_helper.log_role_removed', default: 'Role Removed: ')
- elsif action == ArchiveConfig.ACTION_SUSPEND
- t('users_helper.log_suspended', default: 'Suspended until ')
- elsif action == ArchiveConfig.ACTION_UNSUSPEND
- t('users_helper.log_lift_suspension', default: 'Suspension Lifted')
- elsif action == ArchiveConfig.ACTION_BAN
- t('users_helper.log_ban', default: 'Suspended Permanently')
- elsif action == ArchiveConfig.ACTION_WARN
- t('users_helper.log_warn', default: 'Warned')
- elsif action == ArchiveConfig.ACTION_RENAME
- t('users_helper.log_rename', default: 'Username Changed')
- elsif action == ArchiveConfig.ACTION_PASSWORD_RESET
- t('users_helper.log_password_change', default: 'Password Changed')
- elsif action == ArchiveConfig.ACTION_NEW_EMAIL
- t('users_helper.log_email_change', default: 'Email Changed')
- elsif action == ArchiveConfig.ACTION_TROUBLESHOOT
- t('users_helper.log_troubleshot', default: 'Account Troubleshot')
- elsif action == ArchiveConfig.ACTION_NOTE
- t('users_helper.log_note', default: 'Note Added')
+ def log_item_action_name(item, user)
+ action = item.action
+
+ return fnok_action_name(item, user) if [ArchiveConfig.ACTION_ADD_FNOK, ArchiveConfig.ACTION_REMOVE_FNOK].include?(action)
+
+ case action
+ when ArchiveConfig.ACTION_ACTIVATE
+ t("users_helper.log.validated")
+ when ArchiveConfig.ACTION_ADD_ROLE
+ t("users_helper.log.role_added")
+ when ArchiveConfig.ACTION_REMOVE_ROLE
+ t("users_helper.log.role_removed")
+ when ArchiveConfig.ACTION_SUSPEND
+ t("users_helper.log.suspended")
+ when ArchiveConfig.ACTION_UNSUSPEND
+ t("users_helper.log.lift_suspension")
+ when ArchiveConfig.ACTION_BAN
+ t("users_helper.log.ban")
+ when ArchiveConfig.ACTION_WARN
+ t("users_helper.log.warn")
+ when ArchiveConfig.ACTION_RENAME
+ t("users_helper.log.rename")
+ when ArchiveConfig.ACTION_PASSWORD_RESET
+ t("users_helper.log.password_change")
+ when ArchiveConfig.ACTION_NEW_EMAIL
+ t("users_helper.log.email_change")
+ when ArchiveConfig.ACTION_TROUBLESHOOT
+ t("users_helper.log.troubleshot")
+ when ArchiveConfig.ACTION_NOTE
+ t("users_helper.log.note")
end
end
+ def fnok_action_name(item, user)
+ action = item.action == ArchiveConfig.ACTION_REMOVE_FNOK ? "removed" : "added"
+
+ if item.fnok_user_id == user.id
+ user_id = item.user_id
+ action_leaf = "was_#{action}"
+ else
+ user_id = item.fnok_user_id
+ action_leaf = "has_#{action}"
+ end
+
+ t(
+ "users_helper.log.fnok.#{action_leaf}",
+ user_id: user_id
+ )
+ end
+
# Give the TOS field in the new user form a different name in non-production environments
# so that it can be filtered out of the log, for ease of debugging
def tos_field_name
diff --git a/app/models/log_item.rb b/app/models/log_item.rb
index 0730fa300cf..3c14592975d 100644
--- a/app/models/log_item.rb
+++ b/app/models/log_item.rb
@@ -7,6 +7,8 @@ class LogItem < ApplicationRecord
belongs_to :role
+ belongs_to :fnok_user, class_name: "User"
+
validates_presence_of :note
validates_presence_of :action
diff --git a/app/models/user.rb b/app/models/user.rb
index 6d51edcd8db..20830ef4e88 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -36,6 +36,7 @@ class User < ApplicationRecord
has_many :external_authors, dependent: :destroy
has_many :external_creatorships, foreign_key: "archivist_id"
+ before_destroy :log_removal_as_next_of_kin
has_many :fannish_next_of_kins, dependent: :delete_all, inverse_of: :kin, foreign_key: :kin_id
has_one :fannish_next_of_kin, dependent: :destroy
@@ -171,6 +172,15 @@ def remove_user_from_kudos
Kudo.where(user: self).update_all(user_id: nil)
end
+ def log_removal_as_next_of_kin
+ fannish_next_of_kins.each do |fnok|
+ fnok.user.create_log_item({
+ action: ArchiveConfig.ACTION_REMOVE_FNOK,
+ fnok_user_id: self.id
+ })
+ end
+ end
+
def read_inbox_comments
inbox_comments.where(read: true)
end
diff --git a/app/views/admin/admin_users/_user_history.html.erb b/app/views/admin/admin_users/_user_history.html.erb
index 0c19185ec7f..6e0e80a6fb9 100644
--- a/app/views/admin/admin_users/_user_history.html.erb
+++ b/app/views/admin/admin_users/_user_history.html.erb
@@ -36,7 +36,7 @@
@log_items.each do |item| %>
<%= item.created_at %>
- <%= log_item_action_name(item.action) %><%= item.role.name if item.role %><%= item.enddate if item.enddate %>
+ <%= log_item_action_name(item, @user) %><%= item.role&.name %><%= item.enddate %>
<%= item.note %>
<% end %>
diff --git a/config/config.yml b/config/config.yml
index b248ac170ff..32e8607c621 100644
--- a/config/config.yml
+++ b/config/config.yml
@@ -428,6 +428,8 @@ ACTION_PASSWORD_RESET: 8
ACTION_NEW_EMAIL: 9
ACTION_TROUBLESHOOT: 10
ACTION_NOTE: 11
+ACTION_ADD_FNOK: 12
+ACTION_REMOVE_FNOK: 13
# Elasticsearch index prefix
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index 9d82c6ac91a..becfe095384 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -153,19 +153,6 @@ ignore_missing:
- successfully_sent # should be feedbacks.create.successfully_sent
# Files: app/controllers/languages_controller.rb and app/controllers/locales_controller.rb
- successfully_added # should be languages.create.successfully_added and locales.create.successfully_added
- # File: app/helpers/users_helper.rb
- - users_helper.log_ban
- - users_helper.log_email_change
- - users_helper.log_lift_suspension
- - users_helper.log_note
- - users_helper.log_password_change
- - users_helper.log_rename
- - users_helper.log_role_added
- - users_helper.log_role_removed
- - users_helper.log_suspended
- - users_helper.log_troubleshot
- - users_helper.log_validated
- - users_helper.log_warn
# Files: app/views/admin/_admin_nav.html.erb and app/views/admin_posts/show.html.erb
- admin.admin_nav.delete
# File: app/views/admin/admin_invitations/find.html.erb
diff --git a/config/locales/helpers/en.yml b/config/locales/helpers/en.yml
index f8bcc4e26be..0ce91162447 100644
--- a/config/locales/helpers/en.yml
+++ b/config/locales/helpers/en.yml
@@ -20,3 +20,22 @@ en:
user:
bookmark: Bookmarker approval status
work: Creator approval status
+ users_helper:
+ log:
+ ban: Suspended Permanently
+ email_change: Email Changed
+ fnok:
+ has_added: 'Fannish Next of Kin Added: %{user_id}'
+ has_removed: 'Fannish Next of Kin Removed: %{user_id}'
+ was_added: 'Added as Fannish Next of Kin for: %{user_id}'
+ was_removed: 'Removed as Fannish Next of Kin for: %{user_id}'
+ lift_suspension: Suspension Lifted
+ note: Note Added
+ password_change: Password Changed
+ rename: Username Changed
+ role_added: 'Role Added: '
+ role_removed: 'Role Removed: '
+ suspended: 'Suspended until '
+ troubleshot: Account Troubleshot
+ validated: Account Validated
+ warn: Warned
diff --git a/db/migrate/20230717161221_add_fnok_user_id_to_log_items.rb b/db/migrate/20230717161221_add_fnok_user_id_to_log_items.rb
new file mode 100644
index 00000000000..9d0adb23dee
--- /dev/null
+++ b/db/migrate/20230717161221_add_fnok_user_id_to_log_items.rb
@@ -0,0 +1,51 @@
+class AddFnokUserIdToLogItems < ActiveRecord::Migration[6.1]
+ def up
+ if Rails.env.staging? || Rails.env.production?
+ database = LogItem.connection.current_database
+
+ puts <<~PTOSC
+ Schema Change Command:
+
+ pt-online-schema-change D=#{database},t=log_items \\
+ --alter "ADD `fnok_user_id` int,
+ ADD INDEX `index_log_items_on_fnok_user_id` (`fnok_user_id`)" \\
+ --no-drop-old-table \\
+ -uroot --ask-pass --chunk-size=5k --max-flow-ctl 0 --pause-file /tmp/pauseme \\
+ --max-load Threads_running=15 --critical-load Threads_running=100 \\
+ --set-vars innodb_lock_wait_timeout=2 --alter-foreign-keys-method=auto \\
+ --execute
+
+ Table Deletion Command:
+
+ DROP TABLE IF EXISTS `#{database}`.`_log_items_old`;
+ PTOSC
+ else
+ add_column :log_items, :fnok_user_id, :integer, nullable: true
+ add_index :log_items, :fnok_user_id
+ end
+ end
+
+ def down
+ if Rails.env.staging? || Rails.env.production?
+ database = LogItem.connection.current_database
+
+ puts <<~PTOSC
+ Schema Change Command:
+
+ pt-online-schema-change D=#{database},t=log_items \\
+ --alter "DROP COLUMN `fnok_user_id`"\\
+ --no-drop-old-table \\
+ -uroot --ask-pass --chunk-size=5k --max-flow-ctl 0 --pause-file /tmp/pauseme \\
+ --max-load Threads_running=15 --critical-load Threads_running=100 \\
+ --set-vars innodb_lock_wait_timeout=2 --alter-foreign-keys-method=auto \\
+ --execute
+
+ Table Deletion Command:
+
+ DROP TABLE IF EXISTS `#{database}`.`_log_items_old`;
+ PTOSC
+ else
+ remove_column :log_items, :fnok_user_id
+ end
+ end
+end
diff --git a/factories/fannish_next_of_kin.rb b/factories/fannish_next_of_kin.rb
new file mode 100644
index 00000000000..3dc5a390663
--- /dev/null
+++ b/factories/fannish_next_of_kin.rb
@@ -0,0 +1,9 @@
+require "faker"
+
+FactoryBot.define do
+ factory :fannish_next_of_kin do
+ user { create(:user) }
+ kin { create(:user) }
+ kin_email { |u| u.kin.email }
+ end
+end
diff --git a/features/admins/users/admin_fnok.feature b/features/admins/users/admin_fnok.feature
index d1e80ebfd4d..45096b1cbe5 100644
--- a/features/admins/users/admin_fnok.feature
+++ b/features/admins/users/admin_fnok.feature
@@ -15,6 +15,7 @@ Feature: Admin Fannish Next Of Kind actions
And I fill in "Fannish next of kin's email" with "testy@foo.com"
And I press "Update Fannish Next of Kin"
Then I should see "Fannish next of kin was updated."
+ And the history table should show that "libby" was added as next of kin
When I go to the manage users page
And I fill in "Name" with "harrykim"
@@ -24,6 +25,9 @@ Feature: Admin Fannish Next Of Kind actions
When I follow "libby"
Then I should be on libby's user page
+ When I go to the user administration page for "libby"
+ Then the history table should show they were added as next of kin of "harrykim"
+
Scenario: An invalid Fannish Next of Kin username is added
Given the fannish next of kin "libby" for the user "harrykim"
And I am logged in as a "support" admin
@@ -66,6 +70,9 @@ Feature: Admin Fannish Next Of Kind actions
And I fill in "Fannish next of kin's email" with ""
And I press "Update Fannish Next of Kin"
Then I should see "Fannish next of kin was removed."
+ And the history table should show that "libby" was removed as next of kin
+ When I go to the user administration page for "libby"
+ Then the history table should show they were removed as next of kin of "harrykim"
Scenario: A Fannish Next of Kin updates when the next of kin user changes their username
Given the fannish next of kin "libby" for the user "harrykim"
diff --git a/features/step_definitions/admin_steps.rb b/features/step_definitions/admin_steps.rb
index 398da294f58..34bb795a145 100644
--- a/features/step_definitions/admin_steps.rb
+++ b/features/step_definitions/admin_steps.rb
@@ -504,3 +504,13 @@
expect(page).to have_selector(".original_creators",
text: "#{user.id} (#{creator})")
end
+
+Then "the history table should show that {string} was {word} as next of kin" do |username, action|
+ user_id = User.find_by(login: username).id
+ step %{I should see "Fannish Next of Kin #{action.capitalize}: #{user_id}" within "#user_history"}
+end
+
+Then "the history table should show they were {word} as next of kin of {string}" do |action, username|
+ user_id = User.find_by(login: username).id
+ step %{I should see "#{action.capitalize} as Fannish Next of Kin for: #{user_id}" within "#user_history"}
+end
diff --git a/spec/controllers/admin/admin_users_controller_spec.rb b/spec/controllers/admin/admin_users_controller_spec.rb
index 13b750cef07..ef301528a2b 100644
--- a/spec/controllers/admin/admin_users_controller_spec.rb
+++ b/spec/controllers/admin/admin_users_controller_spec.rb
@@ -226,6 +226,39 @@
it_behaves_like "authorized admin can add next of kin"
end
end
+
+ it "logs adding a fannish next of kin" do
+ admin = create(:support_admin)
+ fake_login_admin(admin)
+
+ post :update_next_of_kin, params: {
+ user_login: user.login, next_of_kin_name: kin.login, next_of_kin_email: kin.email
+ }
+ user.reload
+ expect(user.fannish_next_of_kin.kin).to eq(kin)
+ log_item = user.log_items.last
+ expect(log_item.action).to eq(ArchiveConfig.ACTION_ADD_FNOK)
+ expect(log_item.fnok_user.id).to eq(kin.id)
+ expect(log_item.admin_id).to eq(admin.id)
+ expect(log_item.note).to eq("Change made by #{admin.login}")
+ end
+
+ it "logs removing a fannish next of kin" do
+ admin = create(:support_admin)
+ fake_login_admin(admin)
+ kin_user_id = create(:fannish_next_of_kin, user: user).kin_id
+
+ post :update_next_of_kin, params: {
+ user_login: user.login
+ }
+ user.reload
+ expect(user.fannish_next_of_kin).to be_nil
+ log_item = user.log_items.last
+ expect(log_item.action).to eq(ArchiveConfig.ACTION_REMOVE_FNOK)
+ expect(log_item.fnok_user.id).to eq(kin_user_id)
+ expect(log_item.admin_id).to eq(admin.id)
+ expect(log_item.note).to eq("Change made by #{admin.login}")
+ end
end
describe "POST #update_status" do
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 1fdb7159a74..927aa6cf937 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -2,6 +2,7 @@
describe UsersController do
include RedirectExpectationHelper
+ include LoginMacros
describe "GET #activate" do
let(:user) { create(:user, confirmed_at: nil) }
@@ -58,4 +59,21 @@
end
end
end
+
+ describe "destroy" do
+ let(:user) { create(:user) }
+
+ before do
+ fake_login_known_user(user)
+ end
+
+ context "no log items" do
+ it "successfully destroys and redirects with success message" do
+ login = user.login
+ delete :destroy, params: { id: login }
+ it_redirects_to_with_notice(delete_confirmation_path, "You have successfully deleted your account.")
+ expect(User.find_by(login: login)).to be_nil
+ end
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index cdb6a1b55fd..49feee04370 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -15,6 +15,23 @@
end
end
end
+
+ context "when the user is set as someone else's fnok" do
+ let(:fnok) { create(:fannish_next_of_kin) }
+ let(:user) { fnok.kin }
+ let(:person) { fnok.user }
+
+ it "removes the relationship and creates a log item of the removal" do
+ user_id = user.id
+ user.destroy!
+ expect(person.reload.fannish_next_of_kin).to be_nil
+ log_item = person.log_items.last
+ expect(log_item.action).to eq(ArchiveConfig.ACTION_REMOVE_FNOK)
+ expect(log_item.fnok_user_id).to eq(user_id)
+ expect(log_item.admin_id).to be_nil
+ expect(log_item.note).to eq("System Generated")
+ end
+ end
end
describe "#save" do
From 1665c1d9e8811cee8682229f59b8115314284aba Mon Sep 17 00:00:00 2001
From: Brian Austin <13002992+brianjaustin@users.noreply.github.com>
Date: Mon, 28 Aug 2023 19:25:12 -0400
Subject: [PATCH 046/208] AO3-6040 Allow banning certain usernames (#4601)
* Consolidate username validations
* Add validation
* Internationalisation
* Filter comment names
* Missing newline
* Shared validator
* Normalize i18n
* I18n improvements from tickinginstant
* fix: missing translation
* fix: only validate if name changed
* test: log in, change username
* fix: convince i18n-tasks that message is used
* test things
* more test things
* suspicious
* combinable conditional format
---
app/models/comment.rb | 2 +-
app/models/user.rb | 25 +++++++-----------
.../not_forbidden_name_validator.rb | 9 +++++++
config/config.yml | 4 +++
config/locales/models/en.yml | 5 ++++
features/step_definitions/user_steps.rb | 4 +++
features/users/user_rename.feature | 14 ++++++++++
spec/models/comment_spec.rb | 26 +++++++++++++++++++
spec/models/user_spec.rb | 18 +++++++++++++
9 files changed, 91 insertions(+), 16 deletions(-)
create mode 100644 app/validators/not_forbidden_name_validator.rb
diff --git a/app/models/comment.rb b/app/models/comment.rb
index 8f59e26d54e..b0bb724af3c 100644
--- a/app/models/comment.rb
+++ b/app/models/comment.rb
@@ -14,7 +14,7 @@ class Comment < ApplicationRecord
has_many :thread_comments, class_name: 'Comment', foreign_key: :thread
- validates_presence_of :name, unless: :pseud_id
+ validates :name, presence: { unless: :pseud_id }, not_forbidden_name: { if: :will_save_change_to_name? }
validates :email, email_format: { on: :create, unless: :pseud_id }, email_blacklist: { on: :create, unless: :pseud_id }
validates_presence_of :comment_content
diff --git a/app/models/user.rb b/app/models/user.rb
index 20830ef4e88..563b4b4f34a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -196,13 +196,16 @@ def unread_inbox_comments_count
scope :valid, -> { where(banned: false, suspended: false) }
scope :out_of_invites, -> { where(out_of_invites: true) }
- ## used in app/views/users/new.html.erb
- validates_length_of :login,
- within: ArchiveConfig.LOGIN_LENGTH_MIN..ArchiveConfig.LOGIN_LENGTH_MAX,
- too_short: ts("^User name is too short (minimum is %{min_login} characters)",
- min_login: ArchiveConfig.LOGIN_LENGTH_MIN),
- too_long: ts("^User name is too long (maximum is %{max_login} characters)",
- max_login: ArchiveConfig.LOGIN_LENGTH_MAX)
+ validates :login,
+ length: { within: ArchiveConfig.LOGIN_LENGTH_MIN..ArchiveConfig.LOGIN_LENGTH_MAX },
+ format: {
+ with: /\A[A-Za-z0-9]\w*[A-Za-z0-9]\Z/,
+ min_login: ArchiveConfig.LOGIN_LENGTH_MIN,
+ max_login: ArchiveConfig.LOGIN_LENGTH_MAX
+ },
+ uniqueness: true,
+ not_forbidden_name: { if: :will_save_change_to_login? }
+ validate :username_is_not_recently_changed, if: :will_save_change_to_login?
# allow nil so can save existing users
validates_length_of :password,
@@ -213,14 +216,6 @@ def unread_inbox_comments_count
too_long: ts("is too long (maximum is %{max_pwd} characters)",
max_pwd: ArchiveConfig.PASSWORD_LENGTH_MAX)
- validates_format_of :login,
- message: ts("^User name must be %{min_login} to %{max_login} characters (A-Z, a-z, _, 0-9 only), no spaces, cannot begin or end with underscore (_).",
- min_login: ArchiveConfig.LOGIN_LENGTH_MIN,
- max_login: ArchiveConfig.LOGIN_LENGTH_MAX),
- with: /\A[A-Za-z0-9]\w*[A-Za-z0-9]\Z/
- validates :login, uniqueness: { message: ts("^User name has already been taken") }
- validate :login, :username_is_not_recently_changed, if: :will_save_change_to_login?
-
validates :email, email_format: true, uniqueness: true
# Virtual attribute for age check and terms of service
diff --git a/app/validators/not_forbidden_name_validator.rb b/app/validators/not_forbidden_name_validator.rb
new file mode 100644
index 00000000000..54541ff4c80
--- /dev/null
+++ b/app/validators/not_forbidden_name_validator.rb
@@ -0,0 +1,9 @@
+class NotForbiddenNameValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ return if value.nil?
+ return unless ArchiveConfig.FORBIDDEN_USERNAMES.include?(value.downcase)
+
+ # i18n-tasks-use t("activerecord.errors.messages.forbidden")
+ record.errors.add(attribute, :forbidden, **options.merge(value: value))
+ end
+end
diff --git a/config/config.yml b/config/config.yml
index 32e8607c621..53448e74ca7 100644
--- a/config/config.yml
+++ b/config/config.yml
@@ -521,6 +521,10 @@ WRANGLING_REPORT_LIMIT: 1000
# something below 1 -- or commenting it out -- will turn off comment disabling.
ADMIN_POST_COMMENTING_EXPIRATION_DAYS: 14
+# Usernames in this list are not allowed to avoid potential confusion (like
+# a user who has the username 'admin', for example).
+FORBIDDEN_USERNAMES: []
+
# The arguments to pass to pt-online-schema-change:
PERCONA_ARGS: >
--chunk-size=5k
diff --git a/config/locales/models/en.yml b/config/locales/models/en.yml
index fc8aa2470d6..8b3e55b3378 100644
--- a/config/locales/models/en.yml
+++ b/config/locales/models/en.yml
@@ -64,6 +64,8 @@ en:
series/creatorships:
base: 'Invalid creator:'
pseud_id: Pseud
+ user:
+ login: User name
work:
chapter_total_display: Chapters
summary: Summary
@@ -79,6 +81,8 @@ en:
title: The title of a parent work outside the archive
url: Parent work URL
errors:
+ messages:
+ forbidden: "%{value} is not allowed"
models:
abuse_report:
attributes:
@@ -145,6 +149,7 @@ en:
changed_too_recently:
one: can only be changed once per day. You last changed your user name on %{renamed_at}.
other: can only be changed once every %{count} days. You last changed your user name on %{renamed_at}.
+ invalid: must be %{min_login} to %{max_login} characters (A-Z, a-z, _, 0-9 only), no spaces, cannot begin or end with underscore (_).
password_confirmation:
confirmation: doesn't match new password.
work:
diff --git a/features/step_definitions/user_steps.rb b/features/step_definitions/user_steps.rb
index 0b6ee9ed2a2..2f53bd44812 100644
--- a/features/step_definitions/user_steps.rb
+++ b/features/step_definitions/user_steps.rb
@@ -136,6 +136,10 @@
page.driver.reset!
end
+Given "the user name {string} is on the forbidden list" do |username|
+ allow(ArchiveConfig).to receive(:FORBIDDEN_USERNAMES).and_return([username])
+end
+
# TODO: This should eventually be removed in favor of the "I log out" step,
# which does the same thing (but has a shorter and less passive name).
Given /^I am logged out$/ do
diff --git a/features/users/user_rename.feature b/features/users/user_rename.feature
index 3a292d2f870..e6b7552789c 100644
--- a/features/users/user_rename.feature
+++ b/features/users/user_rename.feature
@@ -163,3 +163,17 @@ Feature:
And I should see "Hi, newusername"
When I follow "Series"
Then I should see "Best Series by newusername"
+
+ Scenario: Changing the username from a forbidden name to non-forbidden
+ Given I have no users
+ And the following activated user exists
+ | login | password |
+ | forbidden | secret |
+ And the user name "forbidden" is on the forbidden list
+ When I am logged in as "forbidden" with password "secret"
+ And I visit the change username page for forbidden
+ And I fill in "New user name" with "notforbidden"
+ And I fill in "Password" with "secret"
+ And I press "Change User Name"
+ Then I should get confirmation that I changed my username
+ And I should see "Hi, notforbidden"
diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb
index 24e6a2d3247..697fc58cee1 100644
--- a/spec/models/comment_spec.rb
+++ b/spec/models/comment_spec.rb
@@ -3,6 +3,32 @@
require "spec_helper"
describe Comment do
+ describe "validations" do
+ context "with a forbidden guest name" do
+ subject { build(:comment, email: Faker::Internet.email) }
+ let(:forbidden_name) { Faker::Lorem.characters(number: 8) }
+
+ before do
+ allow(ArchiveConfig).to receive(:FORBIDDEN_USERNAMES).and_return([forbidden_name])
+ end
+
+ it { is_expected.not_to allow_values(forbidden_name, forbidden_name.swapcase).for(:name) }
+
+ it "does not prevent saving when the name is unchanged" do
+ subject.name = forbidden_name
+ subject.save!(validate: false)
+ expect(subject.save).to be_truthy
+ end
+
+ it "does not prevent deletion" do
+ subject.name = forbidden_name
+ subject.save!(validate: false)
+ subject.destroy
+ expect { subject.reload }
+ .to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
context "with an existing comment from the same user" do
let(:first_comment) { create(:comment) }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 49feee04370..d25d96a0ade 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1,6 +1,24 @@
require "spec_helper"
describe User do
+ describe "validations" do
+ context "with a forbidden user name" do
+ let(:forbidden_username) { Faker::Lorem.characters(number: 8) }
+
+ before do
+ allow(ArchiveConfig).to receive(:FORBIDDEN_USERNAMES).and_return([forbidden_username])
+ end
+
+ it { is_expected.not_to allow_values(forbidden_username, forbidden_username.swapcase).for(:login) }
+
+ it "does not prevent saving when the name is unchanged" do
+ existing_user = build(:user, login: forbidden_username)
+ existing_user.save!(validate: false)
+ expect(existing_user.save).to be_truthy
+ end
+ end
+ end
+
describe "#destroy" do
context "on a user with kudos" do
let(:user) { create(:user) }
From a2305c24994cf4e4991a342c3534a85547f62177 Mon Sep 17 00:00:00 2001
From: james_ <1606304+zz9pzza@users.noreply.github.com>
Date: Tue, 29 Aug 2023 00:29:47 +0100
Subject: [PATCH 047/208] AO3-6599 Apply local config on schedulers (#4624)
Apply local config on schedulers
---
config/deploy.rb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/config/deploy.rb b/config/deploy.rb
index d3fc84b4c8c..fc5bf0fbf59 100644
--- a/config/deploy.rb
+++ b/config/deploy.rb
@@ -86,7 +86,7 @@
end
desc "Get the config files"
- task :update_configs, roles: [:app, :web, :workers] do
+ task :update_configs, roles: [:app, :web, :workers, :schedulers] do
run "/home/ao3app/bin/create_links_on_install"
end
From 18e283a4ca694d955f4dcd1899927d888908e73a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=89ric=20B?=
Date: Tue, 29 Aug 2023 04:22:44 +0200
Subject: [PATCH 048/208] AO3-6467 Update add_fnok_user_id_to_log_items
migration to new syntax (#4625)
Update migration to new syntax
---
...717161221_add_fnok_user_id_to_log_items.rb | 49 ++-----------------
1 file changed, 5 insertions(+), 44 deletions(-)
diff --git a/db/migrate/20230717161221_add_fnok_user_id_to_log_items.rb b/db/migrate/20230717161221_add_fnok_user_id_to_log_items.rb
index 9d0adb23dee..cc4f77e0687 100644
--- a/db/migrate/20230717161221_add_fnok_user_id_to_log_items.rb
+++ b/db/migrate/20230717161221_add_fnok_user_id_to_log_items.rb
@@ -1,51 +1,12 @@
class AddFnokUserIdToLogItems < ActiveRecord::Migration[6.1]
- def up
- if Rails.env.staging? || Rails.env.production?
- database = LogItem.connection.current_database
-
- puts <<~PTOSC
- Schema Change Command:
-
- pt-online-schema-change D=#{database},t=log_items \\
- --alter "ADD `fnok_user_id` int,
- ADD INDEX `index_log_items_on_fnok_user_id` (`fnok_user_id`)" \\
- --no-drop-old-table \\
- -uroot --ask-pass --chunk-size=5k --max-flow-ctl 0 --pause-file /tmp/pauseme \\
- --max-load Threads_running=15 --critical-load Threads_running=100 \\
- --set-vars innodb_lock_wait_timeout=2 --alter-foreign-keys-method=auto \\
- --execute
+ uses_departure! if Rails.env.staging? || Rails.env.production?
- Table Deletion Command:
-
- DROP TABLE IF EXISTS `#{database}`.`_log_items_old`;
- PTOSC
- else
- add_column :log_items, :fnok_user_id, :integer, nullable: true
- add_index :log_items, :fnok_user_id
- end
+ def up
+ add_column :log_items, :fnok_user_id, :integer, nullable: true
+ add_index :log_items, :fnok_user_id
end
def down
- if Rails.env.staging? || Rails.env.production?
- database = LogItem.connection.current_database
-
- puts <<~PTOSC
- Schema Change Command:
-
- pt-online-schema-change D=#{database},t=log_items \\
- --alter "DROP COLUMN `fnok_user_id`"\\
- --no-drop-old-table \\
- -uroot --ask-pass --chunk-size=5k --max-flow-ctl 0 --pause-file /tmp/pauseme \\
- --max-load Threads_running=15 --critical-load Threads_running=100 \\
- --set-vars innodb_lock_wait_timeout=2 --alter-foreign-keys-method=auto \\
- --execute
-
- Table Deletion Command:
-
- DROP TABLE IF EXISTS `#{database}`.`_log_items_old`;
- PTOSC
- else
- remove_column :log_items, :fnok_user_id
- end
+ remove_column :log_items, :fnok_user_id
end
end
From b3ea63163d7e2701ac365f0d2c2a633068ac7b77 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=89ric=20B?=
Date: Mon, 4 Sep 2023 02:56:10 +0200
Subject: [PATCH 049/208] AO3-6487 Fix autocomplete for fandoms made synonyms
(#4623)
* AO3-6487 Fix autocomplete for synced fandoms
https://otwarchive.atlassian.net/browse/AO3-6487
Ref: https://otwarchive.atlassian.net/browse/AO3-3632
* Only run the right part of after_update
---
app/models/common_tagging.rb | 5 +++++
app/models/tag.rb | 13 +++++++++----
spec/models/tag_wrangling_spec.rb | 25 +++++++++++++++++++++++++
3 files changed, 39 insertions(+), 4 deletions(-)
diff --git a/app/models/common_tagging.rb b/app/models/common_tagging.rb
index 2765bbadebc..7f013574f65 100644
--- a/app/models/common_tagging.rb
+++ b/app/models/common_tagging.rb
@@ -19,6 +19,7 @@ class CommonTagging < ApplicationRecord
after_create :update_wrangler
after_create :inherit_parents
after_create :remove_uncategorized_media
+ after_create :update_child_autocomplete
after_commit :update_search
@@ -28,6 +29,10 @@ def update_wrangler
end
end
+ def update_child_autocomplete
+ common_tag.refresh_autocomplete
+ end
+
# A relationship should inherit its characters' fandoms
def inherit_parents
if common_tag.is_a?(Relationship) && filterable.is_a?(Character)
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 3239774ebac..24e979d29cb 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -1083,10 +1083,8 @@ def after_update
# decanonicalised tag
tag.remove_from_autocomplete
end
- elsif tag.canonical
- # clean up the autocomplete
- tag.remove_stale_from_autocomplete
- tag.add_to_autocomplete
+ else
+ tag.refresh_autocomplete
end
# Expire caching when a merger is added or removed
@@ -1114,6 +1112,13 @@ def after_update
end
end
+ def refresh_autocomplete
+ return unless canonical
+
+ remove_stale_from_autocomplete
+ add_to_autocomplete
+ end
+
before_destroy :before_destroy
def before_destroy
tag = self
diff --git a/spec/models/tag_wrangling_spec.rb b/spec/models/tag_wrangling_spec.rb
index 7a53d378c2e..194e1168e2d 100644
--- a/spec/models/tag_wrangling_spec.rb
+++ b/spec/models/tag_wrangling_spec.rb
@@ -185,6 +185,31 @@
expect(synonym.children.reload).to contain_exactly
end
+ describe "with asynchronous jobs run asynchronously" do
+ include ActiveJob::TestHelper
+
+ it "transfers the subtags to the new parent autocomplete" do
+ child = create(:canonical_character)
+ synonym.add_association(child)
+ synonym.reload
+
+ fandom_redis_key = Tag.transliterate("autocomplete_fandom_#{fandom.name.downcase}_character")
+
+ expect(REDIS_AUTOCOMPLETE.exists(fandom_redis_key)).to be false
+
+ synonym.update!(syn_string: fandom.name)
+
+ User.current_user = nil # No current user in asynchronous context (?)
+ perform_enqueued_jobs
+
+ expect(fandom.children.reload).to contain_exactly(child)
+ expect(synonym.children.reload).to be_empty
+
+ expect(REDIS_AUTOCOMPLETE.exists(fandom_redis_key)).to be true
+ expect(REDIS_AUTOCOMPLETE.zrange(fandom_redis_key, 0, -1)).to eq(["#{child.id}: #{child.name}"])
+ end
+ end
+
it "transfers favorite tags" do
user = create(:user)
user.favorite_tags.create(tag: synonym)
From 90f07ef1bb74f287f3d336070fea67ddcd78d4f0 Mon Sep 17 00:00:00 2001
From: eliahhecht
Date: Sun, 3 Sep 2023 20:58:53 -0400
Subject: [PATCH 050/208] Omit -b option on macOS (#4616)
* Omit -b option on macOS
* Adopt suggestion to manually create backups
---
script/docker/init.sh | 9 ++++++---
1 file changed, 6 insertions(+), 3 deletions(-)
diff --git a/script/docker/init.sh b/script/docker/init.sh
index 49ab29fc6f0..ead2e61376d 100755
--- a/script/docker/init.sh
+++ b/script/docker/init.sh
@@ -5,9 +5,12 @@ set -ex
# Change directory to root of the repo
cd "$(dirname "$0")/../.."
-cp -b config/docker/database.yml config/database.yml
-cp -b config/docker/redis.yml config/redis.yml
-cp -b config/docker/local.yml config/local.yml
+for file in 'database.yml' 'redis.yml' 'local.yml'
+do
+ # Manual backup as the --backup option is not available for all versions of cp
+ test -f "config/$file" && cp "config/$file" "config/$file~"
+ cp "config/docker/$file" "config/$file"
+done
docker-compose up -d
From 7c923dbbb8ceefb97bff721e08952a1de6444ffc Mon Sep 17 00:00:00 2001
From: Brian Austin <13002992+brianjaustin@users.noreply.github.com>
Date: Mon, 4 Sep 2023 20:23:02 -0400
Subject: [PATCH 051/208] AO3-5522 Limit number of password resets (#4626)
* AO3-5522: Limit number of password resets
* Style fixes
* Updated password reset limit code per review
* Added trailing newline
* Simplify role check
* Bump migration timestamp, use departure
* Test organization improvements
* doggo
---------
Co-authored-by: Hunter Smith <1946720+hunternet93@users.noreply.github.com>
---
app/controllers/users/passwords_controller.rb | 19 +++-
.../concerns/password_resets_limitable.rb | 28 ++++++
app/models/user.rb | 1 +
config/config.yml | 7 ++
config/locales/controllers/en.yml | 1 +
...903180114_add_resets_requested_to_users.rb | 12 +++
features/step_definitions/user_steps.rb | 7 ++
features/users/authenticate_users.feature | 30 +++++--
spec/models/user_spec.rb | 90 +++++++++++++++++++
9 files changed, 185 insertions(+), 10 deletions(-)
create mode 100644 app/models/concerns/password_resets_limitable.rb
create mode 100644 db/migrate/20230903180114_add_resets_requested_to_users.rb
diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb
index 37748179f57..3fbbb238e8f 100644
--- a/app/controllers/users/passwords_controller.rb
+++ b/app/controllers/users/passwords_controller.rb
@@ -7,12 +7,27 @@ class Users::PasswordsController < Devise::PasswordsController
layout "session"
def create
- if User.find_for_authentication(resource_params.permit(:login))&.prevent_password_resets?
+ user = User.find_for_authentication(resource_params.permit(:login))
+
+ if user&.prevent_password_resets?
flash[:error] = t(".reset_blocked", contact_abuse_link: view_context.link_to(t(".contact_abuse"), new_abuse_report_path)).html_safe
redirect_to root_path and return
+ elsif user&.password_resets_limit_reached?
+ available_time = ApplicationController.helpers.time_in_zone(
+ user.password_resets_available_time, nil, user
+ )
+
+ flash[:error] = t(".reset_cooldown", reset_available_time: available_time).html_safe
+ redirect_to root_path and return
end
+
super do |user|
- flash.now[:notice] = ts("We couldn't find an account with that email address or username. Please try again?") if user.nil? || user.new_record?
+ if user.nil? || user.new_record?
+ flash.now[:notice] = ts("We couldn't find an account with that email address or username. Please try again?")
+ else
+ user.update_password_resets_requested
+ user.save
+ end
end
end
end
diff --git a/app/models/concerns/password_resets_limitable.rb b/app/models/concerns/password_resets_limitable.rb
new file mode 100644
index 00000000000..34b847bba2d
--- /dev/null
+++ b/app/models/concerns/password_resets_limitable.rb
@@ -0,0 +1,28 @@
+module PasswordResetsLimitable
+ extend ActiveSupport::Concern
+
+ included do
+ def password_resets_limit_reached?
+ self.resets_requested >= ArchiveConfig.PASSWORD_RESET_LIMIT && self.last_reset_within_cooldown?
+ end
+
+ def password_resets_available_time
+ self.reset_password_sent_at + ArchiveConfig.PASSWORD_RESET_COOLDOWN_HOURS.hours
+ end
+
+ def update_password_resets_requested
+ if self.resets_requested.positive? && !self.last_reset_within_cooldown?
+ self.resets_requested = 1
+ else
+ self.resets_requested += 1
+ end
+ end
+ end
+
+ private
+
+ def last_reset_within_cooldown?
+ self.reset_password_sent_at.nil? ||
+ self.reset_password_sent_at > ArchiveConfig.PASSWORD_RESET_COOLDOWN_HOURS.hours.ago
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 563b4b4f34a..ce230a38247 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,6 +1,7 @@
class User < ApplicationRecord
audited
include WorksOwner
+ include PasswordResetsLimitable
devise :database_authenticatable,
:confirmable,
diff --git a/config/config.yml b/config/config.yml
index 53448e74ca7..f7de3254ad3 100644
--- a/config/config.yml
+++ b/config/config.yml
@@ -22,6 +22,13 @@ DAYS_UNTIL_RESET_PASSWORD_LINK_EXPIRES: 7
# This also affects the link included in the admin account creation email.
DAYS_UNTIL_ADMIN_RESET_PASSWORD_LINK_EXPIRES: 5
+# If more than PASSWORD_RESET_LIMIT password reset emails are sent within
+# PASSWORD_RESET_COOLDOWN_HOURS time, then password resets for that user
+# will be prevented until PASSWORD_RESET_COOLDOWN_HOURS after the last
+# password reset.
+PASSWORD_RESET_LIMIT: 3
+PASSWORD_RESET_COOLDOWN_HOURS: 12
+
# email addresses
RETURN_ADDRESS: 'do-not-reply@example.org'
SPAM_ALERT_ADDRESS: 'abuse-discuss@example.org'
diff --git a/config/locales/controllers/en.yml b/config/locales/controllers/en.yml
index 70333a567e2..46c6989309b 100644
--- a/config/locales/controllers/en.yml
+++ b/config/locales/controllers/en.yml
@@ -73,3 +73,4 @@ en:
create:
contact_abuse: contact Policy & Abuse
reset_blocked: Password resets are disabled for that user. For more information, please %{contact_abuse_link}.
+ reset_cooldown: You cannot reset your password at this time. Please try again after %{reset_available_time}.
diff --git a/db/migrate/20230903180114_add_resets_requested_to_users.rb b/db/migrate/20230903180114_add_resets_requested_to_users.rb
new file mode 100644
index 00000000000..3814ef7a6b4
--- /dev/null
+++ b/db/migrate/20230903180114_add_resets_requested_to_users.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+class AddResetsRequestedToUsers < ActiveRecord::Migration[6.1]
+ uses_departure! if Rails.env.staging? || Rails.env.production?
+
+ def change
+ change_table :users, bulk: true do |t|
+ t.column :resets_requested, :integer, default: 0, null: false
+ t.index :resets_requested
+ end
+ end
+end
diff --git a/features/step_definitions/user_steps.rb b/features/step_definitions/user_steps.rb
index 2f53bd44812..39793320b9b 100644
--- a/features/step_definitions/user_steps.rb
+++ b/features/step_definitions/user_steps.rb
@@ -239,6 +239,13 @@
user.creatorships.unapproved.each(&:accept!)
end
+When "I request a password reset for {string}" do |login|
+ step(%{I am on the login page})
+ step(%{I follow "Reset password"})
+ step(%{I fill in "Email address or user name" with "#{login}"})
+ step(%{I press "Reset Password"})
+end
+
# THEN
Then /^I should get the error message for wrong username or password$/ do
diff --git a/features/users/authenticate_users.feature b/features/users/authenticate_users.feature
index 8c958ef7ffa..afc40e4cb7d 100644
--- a/features/users/authenticate_users.feature
+++ b/features/users/authenticate_users.feature
@@ -92,10 +92,7 @@ Feature: User Authentication
| login | email | password |
| sam | sam@example.com | password |
And all emails have been delivered
- When I am on the login page
- And I follow "Reset password"
- And I fill in "Email address or user name" with "sam@example.com"
- And I press "Reset Password"
+ When I request a password reset for "sam@example.com"
Then I should see "Check your email for instructions on how to reset your password."
And 1 email should be delivered
When I start a new session
@@ -112,10 +109,7 @@ Feature: User Authentication
| login | password |
| sam | password |
And all emails have been delivered
- When I am on the login page
- And I follow "Reset password"
- And I fill in "Email address or user name" with "sam"
- And I press "Reset Password"
+ When I request a password reset for "sam"
Then I should see "Check your email for instructions on how to reset your password."
And 1 email should be delivered
When it is currently 2 weeks from now
@@ -130,6 +124,26 @@ Feature: User Authentication
And I should not see "Your password has been changed"
And I should not see "Hi, sam!"
+ Scenario: Forgot password, with enough attempts to trigger password reset cooldown
+ Given I have no users
+ And the following activated user exists
+ | login | password |
+ | sam | password |
+ And all emails have been delivered
+ When I request a password reset for "sam"
+ And I request a password reset for "sam"
+ And I request a password reset for "sam"
+ Then I should see "Check your email for instructions on how to reset your password."
+ And 3 emails should be delivered
+ When all emails have been delivered
+ And I request a password reset for "sam"
+ Then I should see "You cannot reset your password at this time. Please try again after"
+ And 0 emails should be delivered
+ When it is currently 12 hours from now
+ And I request a password reset for "sam"
+ Then I should see "Check your email for instructions on how to reset your password."
+ And 1 email should be delivered
+
Scenario: User is locked out
Given I have no users
And the following activated user exists
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index d25d96a0ade..b8b9b9729ed 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -271,4 +271,94 @@
expect(duplicates).to eq(3)
end
end
+
+ describe "#password_resets_limit_reached?" do
+ context "with 0 resets requested" do
+ let(:user) { build(:user, resets_requested: 0) }
+
+ it "has not reached the requests limit" do
+ expect(user.password_resets_limit_reached?).to be_falsy
+ end
+ end
+
+ context "with the maximum number of password resets requested" do
+ let(:user) { build(:user, resets_requested: ArchiveConfig.PASSWORD_RESET_LIMIT) }
+
+ context "when the cooldown period has passed" do
+ before do
+ user.reset_password_sent_at = ArchiveConfig.PASSWORD_RESET_COOLDOWN_HOURS.hours.ago
+ end
+
+ it "has not reached the requests limit" do
+ expect(user.password_resets_limit_reached?).to be_falsy
+ end
+ end
+
+ context "when the cooldown period has not passed" do
+ before do
+ user.reset_password_sent_at = Time.current
+ end
+
+ it "has reached the requests limit" do
+ expect(user.password_resets_limit_reached?).to be_truthy
+ end
+ end
+ end
+ end
+
+ describe "#update_password_resets_requested" do
+ context "with 0 resets requested" do
+ let(:user) { build(:user, resets_requested: 0) }
+
+ it "increments the password reset requests field" do
+ expect { user.update_password_resets_requested }
+ .to change { user.resets_requested }
+ .to(1)
+ end
+ end
+
+ context "with under the maximum number of password resets requested" do
+ let(:user) { build(:user, resets_requested: ArchiveConfig.PASSWORD_RESET_LIMIT - 1) }
+
+ context "when the cooldown period has passed" do
+ before do
+ user.reset_password_sent_at = ArchiveConfig.PASSWORD_RESET_COOLDOWN_HOURS.hours.ago
+ end
+
+ it "resets the password reset request field to 1" do
+ expect { user.update_password_resets_requested }
+ .to change { user.resets_requested }
+ .to(1)
+ end
+ end
+
+ context "when the cooldown period has not passed" do
+ before do
+ user.reset_password_sent_at = Time.current
+ end
+
+ it "increments the password reset requests field" do
+ expect { user.update_password_resets_requested }
+ .to change { user.resets_requested }
+ .by(1)
+ end
+ end
+ end
+
+ context "with the maximum number of password resets requested" do
+ let(:user) { build(:user, resets_requested: ArchiveConfig.PASSWORD_RESET_LIMIT) }
+
+ context "when the cooldown period has passed" do
+ before do
+ user.reset_password_sent_at = ArchiveConfig.PASSWORD_RESET_COOLDOWN_HOURS.hours.ago
+ end
+
+ it "resets the password reset request field to 1" do
+ expect { user.update_password_resets_requested }
+ .to change { user.resets_requested }
+ .to(1)
+ end
+ end
+ end
+ end
end
From 943f585818005be8df269d84ca454af478150e75 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 8 Sep 2023 20:16:17 -0400
Subject: [PATCH 052/208] AO3-6603 Bump actions/checkout from 3 to 4 (#4627)
Bump actions/checkout from 3 to 4
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)
---
updated-dependencies:
- dependency-name: actions/checkout
dependency-type: direct:production
update-type: version-update:semver-major
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
.github/workflows/automated-tests.yml | 2 +-
.github/workflows/brakeman-scan.yml | 2 +-
.github/workflows/reviewdog.yml | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/automated-tests.yml b/.github/workflows/automated-tests.yml
index 119e3a50100..647cc25aa21 100644
--- a/.github/workflows/automated-tests.yml
+++ b/.github/workflows/automated-tests.yml
@@ -94,7 +94,7 @@ jobs:
steps:
- name: Check out code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Run apt-get update
run: sudo apt-get update
diff --git a/.github/workflows/brakeman-scan.yml b/.github/workflows/brakeman-scan.yml
index abb2ef64bbd..b1efbdc0366 100644
--- a/.github/workflows/brakeman-scan.yml
+++ b/.github/workflows/brakeman-scan.yml
@@ -24,7 +24,7 @@ jobs:
steps:
# Checkout the repository to the GitHub Actions runner
- name: Checkout
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Setup Ruby and run bundle install
uses: ruby/setup-ruby@v1
diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml
index c86a098d193..7e65dbd8a8c 100644
--- a/.github/workflows/reviewdog.yml
+++ b/.github/workflows/reviewdog.yml
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
- uses: actions/checkout@v3
+ uses: actions/checkout@v4
- name: Set up Ruby and run bundle install
uses: ruby/setup-ruby@v1
From 0d99729649be615431f0b90dae9dc7b8b4485c6d Mon Sep 17 00:00:00 2001
From: weeklies <80141759+weeklies@users.noreply.github.com>
Date: Wed, 13 Sep 2023 10:49:04 +0800
Subject: [PATCH 053/208] AO3-6321 Text changes to TOS FAQ-FNOK section (#4629)
* AO3-6321 Changed links and text to Support instead of Abuse
* AO3-6321 Remove redundant quotation marks
* Fix?
---------
Co-authored-by: tararosenthal
---
app/views/home/tos_faq.html.erb | 10 +++++-----
config/locales/views/en.yml | 3 +++
2 files changed, 8 insertions(+), 5 deletions(-)
diff --git a/app/views/home/tos_faq.html.erb b/app/views/home/tos_faq.html.erb
index 291e48f5206..e590acf96e0 100644
--- a/app/views/home/tos_faq.html.erb
+++ b/app/views/home/tos_faq.html.erb
@@ -431,7 +431,7 @@ Please use our search functions for this rather than creating a separate work.
That's up to you. It should be someone reliable, someone you trust to make decisions about your fanworks.
What do I do once I've chosen someone?
- Both you and your fannish next-of-kin need to send a message to our <%= link_to ts("Abuse team"), new_abuse_report_path %>, which handles next of kin requests, indicating that you want to have them as your fannish next-of-kin and that they agree. This doesn't open an Abuse case. You need to provide your Archive usernames for our records. When we receive matching requests, we will confirm that a fannish next-of-kin arrangement is in place.
+
Both you and your fannish next-of-kin need to send a message to our <%= link_to t(".fnok.support"), new_feedback_report_path %>, which handles next of kin requests, indicating that you want to have them as your fannish next-of-kin and that they agree. You need to provide your Archive usernames for our records. When we receive matching requests, we will confirm that a fannish next-of-kin arrangement is in place.
If they're my next-of-kin, am I theirs ?
The relationship can be reciprocal if you want, but it doesn't have to be. However, you can only have one person as your fannish next-of-kin at a time.
@@ -443,18 +443,18 @@ Please use our search functions for this rather than creating a separate work.
You would need to choose a new person. Your fannish next-of-kin can also designate someone else as their own fannish next-of-kin. If A designates B as a fannish next-of-kin, then dies, and B designates C as B's fannish next-of-kin, when B dies C can control all the accounts that B controlled, which at that point would include A's.
What if my fannish next-of-kin decides they're tired of being my fannish next-of-kin?
- Either party can revoke a fannish next-of-kin agreement by <%= link_to ts("sending a message to our Abuse team"), new_abuse_report_path %> (this will not be an Abuse case, but our Abuse team handles the process). We will inform the other party that the agreement has been ended. Please include your username and the username of the other person involved in the agreement so we can find the right record.
+
Either party can revoke a fannish next-of-kin agreement by sending a message to our <%= link_to t(".fnok.support"), new_feedback_report_path %>. We will inform the other party that the agreement has been ended. Please include your username and the username of the other person involved in the agreement so we can find the right record.
When a fan is dead or incapacitated, the fannish next-of-kin will have control of the account and can make any decisions about it, including handing it off to someone else; the Archive cannot control whether or not anyone shares password information with anyone else. If the fannish next-of-kin lets us know that s/he wants to stop managing the account, we will permanently suspend the account, which means that all existing content will stay in place, but nothing may be changed or added.
What if I decide I don't like my fannish next-of-kin agreement?
- Send an e-mail saying that you want to terminate the agreement. Please include your username and the username of the other person involved in the agreement so we can find the right record. The Abuse Committee will e-mail the parties involved in the agreement to let them know. Either party in an agreement can terminate it. You are free to choose a new fannish next-of-kin.
+
Send an e-mail saying that you want to terminate the agreement. Please include your username and the username of the other person involved in the agreement so we can find the right record. The Support Committee will e-mail the parties involved in the agreement to let them know. Either party in an agreement can terminate it. You are free to choose a new fannish next-of-kin.
What if my fannish next-of-kin does something I wouldn't like?
Please choose someone you trust. It would be difficult or impossible for the Archive to enforce the exact terms of your agreement. All we will do is verify your status and transfer account control to the appropriate person.
How can my fannish next-of-kin get control of my fanworks?
- A fannish next-of-kin can activate the agreement by <%= link_to ts("sending a message to our Abuse team"), new_abuse_report_path %> that you are dead or permanently incapacitated (this will not be an Abuse case, but our Abuse team handles the process). The Archive will send a message to the email address associated with your account. If we do not receive a response from that address within ten days, we will transfer control of your account to your fannish next-of-kin. The Archive will not do any independent investigation into whether you are dead or incapacitated.
+
A fannish next-of-kin can activate the agreement by sending a message to our <%= link_to t(".fnok.support"), new_feedback_report_path %> that you are dead or permanently incapacitated. The Archive will send a message to the email address associated with your account. If we do not receive a response from that address within ten days, we will transfer control of your account to your fannish next-of-kin. The Archive will not do any independent investigation into whether you are dead or incapacitated.
Why won't the Archive check to see whether I am really dead or incapacitated?
We don't want to be in the position of collecting and possessing personal information of the kind that we'd need to confirm what your fannish next-of-kin says. It is your responsibility to choose someone you trust. If you want a custom arrangement, we suggest you make private arrangements with someone you trust to handle your passwords and accounts in the event of your death or incapacity.
@@ -465,7 +465,7 @@ Please use our search functions for this rather than creating a separate work.
Why should I bother choosing someone?
It can be useful to have someone you trust as a fellow fan to make decisions about your account.
Help! Something went wrong: control of my account has been transfered, but I'm still hale and hearty!
- If rumors of your death were greatly exaggerated, please <%= link_to ts("contact our Abuse team"), new_abuse_report_path %> right away. Whether you've turned up after being lost in the Amazon for a decade or whether someone is just trying to pull a fast one, we'll do our best to get you your account back ASAP.
+
If rumors of your death were greatly exaggerated, please contact our <%= link_to t(".fnok.support"), new_feedback_report_path %> right away. Whether you've turned up after being lost in the Amazon for a decade or whether someone is just trying to pull a fast one, we'll do our best to get you your account back ASAP.
Orphaning
You mention that an orphaning request might come from someone who doesn't have an active account. How could that happen?
diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml
index a20d3aa086e..465ec018d5b 100644
--- a/config/locales/views/en.yml
+++ b/config/locales/views/en.yml
@@ -442,6 +442,9 @@ en:
other: Browse fandoms by media or favorite up to %{count} tags to have them listed here!
find_your_favorites: Find your favorites
media_navigation_label: Media
+ tos_faq:
+ fnok:
+ support: Support team
invitations:
invitation:
email_address_label: Enter an email address
From 2f7308e8bb44fdf87e1b86ee539a676048da2cba Mon Sep 17 00:00:00 2001
From: Brian Austin <13002992+brianjaustin@users.noreply.github.com>
Date: Tue, 12 Sep 2023 22:50:54 -0400
Subject: [PATCH 054/208] AO3-5522 Password reset limit fixes (#4630)
* Add more info to success message
* Handle logged-in password change
It seems that Devise will reset the reset_password_sent_at to `nil` when a user changes
their password: https://github.com/heartcombo/devise/blob/9f80dc2562524f744e8633b8562f2a0114efb32b/lib/devise/models/recoverable.rb#L103
In order to prevent this causing a 500 on subsequent password resets, we can treat this as
'resetting the clock', meaning the maximum number of reset requests are allowed again.
* Avoid setting reset_sent_at before updating counter
* Use custom translation key
* Simplify control flow
* Please i18n tasks
* Error instead of info
* Improve documentation/comments
---
app/controllers/users/passwords_controller.rb | 36 +++-
.../concerns/password_resets_limitable.rb | 22 +-
config/locales/controllers/en.yml | 8 +
features/users/authenticate_users.feature | 4 +-
.../password_resets_limitable_spec.rb | 189 ++++++++++++++++++
spec/models/user_spec.rb | 90 ---------
6 files changed, 245 insertions(+), 104 deletions(-)
create mode 100644 spec/models/concerns/password_resets_limitable_spec.rb
diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb
index 3fbbb238e8f..a96166674d6 100644
--- a/app/controllers/users/passwords_controller.rb
+++ b/app/controllers/users/passwords_controller.rb
@@ -8,11 +8,15 @@ class Users::PasswordsController < Devise::PasswordsController
def create
user = User.find_for_authentication(resource_params.permit(:login))
+ if user.nil? || user.new_record?
+ flash[:error] = t(".user_not_found")
+ redirect_to new_user_password_path and return
+ end
- if user&.prevent_password_resets?
+ if user.prevent_password_resets?
flash[:error] = t(".reset_blocked", contact_abuse_link: view_context.link_to(t(".contact_abuse"), new_abuse_report_path)).html_safe
redirect_to root_path and return
- elsif user&.password_resets_limit_reached?
+ elsif user.password_resets_limit_reached?
available_time = ApplicationController.helpers.time_in_zone(
user.password_resets_available_time, nil, user
)
@@ -21,13 +25,25 @@ def create
redirect_to root_path and return
end
- super do |user|
- if user.nil? || user.new_record?
- flash.now[:notice] = ts("We couldn't find an account with that email address or username. Please try again?")
- else
- user.update_password_resets_requested
- user.save
- end
- end
+ user.update_password_resets_requested
+ user.save
+
+ super
+ end
+
+ protected
+
+ # We need to include information about the user (the remaining reset attempts)
+ # in addition to the configured reset cooldown in the success message.
+ # Otherwise, we would just override `devise_i18n_options` instead of this method.
+ def successfully_sent?(resource)
+ return super if Devise.paranoid
+ return unless resource.errors.empty?
+
+ flash[:notice] = t("users.passwords.create.send_instructions",
+ send_times_remaining: t("users.passwords.create.send_times_remaining",
+ count: resource.password_resets_remaining),
+ send_cooldown_period: t("users.passwords.create.send_cooldown_period",
+ count: ArchiveConfig.PASSWORD_RESET_COOLDOWN_HOURS))
end
end
diff --git a/app/models/concerns/password_resets_limitable.rb b/app/models/concerns/password_resets_limitable.rb
index 34b847bba2d..350c7536743 100644
--- a/app/models/concerns/password_resets_limitable.rb
+++ b/app/models/concerns/password_resets_limitable.rb
@@ -2,8 +2,15 @@ module PasswordResetsLimitable
extend ActiveSupport::Concern
included do
+ def password_resets_remaining
+ return ArchiveConfig.PASSWORD_RESET_LIMIT unless self.last_reset_within_cooldown?
+
+ limit_delta = ArchiveConfig.PASSWORD_RESET_LIMIT - self.resets_requested
+ limit_delta.positive? ? limit_delta : 0
+ end
+
def password_resets_limit_reached?
- self.resets_requested >= ArchiveConfig.PASSWORD_RESET_LIMIT && self.last_reset_within_cooldown?
+ password_resets_remaining.zero?
end
def password_resets_available_time
@@ -17,12 +24,23 @@ def update_password_resets_requested
self.resets_requested += 1
end
end
+
+ protected
+
+ # Resets the resets_requested count to the default value -- zero -- when a user successfully _completes_
+ # the reset process. This extends the existing Devise method, which sets `reset_password_sent_at` to `nil`.
+ # If we don't also reset `resets_requested`, we will not know whether the number of resets means further
+ # reset requests should be limited or not.
+ def clear_reset_password_token
+ super
+ self.resets_requested = 0
+ end
end
private
def last_reset_within_cooldown?
- self.reset_password_sent_at.nil? ||
+ self.reset_password_sent_at.present? &&
self.reset_password_sent_at > ArchiveConfig.PASSWORD_RESET_COOLDOWN_HOURS.hours.ago
end
end
diff --git a/config/locales/controllers/en.yml b/config/locales/controllers/en.yml
index 46c6989309b..22a22836485 100644
--- a/config/locales/controllers/en.yml
+++ b/config/locales/controllers/en.yml
@@ -74,3 +74,11 @@ en:
contact_abuse: contact Policy & Abuse
reset_blocked: Password resets are disabled for that user. For more information, please %{contact_abuse_link}.
reset_cooldown: You cannot reset your password at this time. Please try again after %{reset_available_time}.
+ send_cooldown_period:
+ one: After that, you will need to wait %{count} hour before requesting another reset.
+ other: After that, you will need to wait %{count} hours before requesting another reset.
+ send_instructions: Check your email for instructions on how to reset your password. %{send_times_remaining} %{send_cooldown_period}
+ send_times_remaining:
+ one: You may reset your password %{count} more time.
+ other: You may reset your password %{count} more times.
+ user_not_found: We couldn't find an account with that email address or username. Please try again.
diff --git a/features/users/authenticate_users.feature b/features/users/authenticate_users.feature
index afc40e4cb7d..c51eabbfa86 100644
--- a/features/users/authenticate_users.feature
+++ b/features/users/authenticate_users.feature
@@ -133,7 +133,7 @@ Feature: User Authentication
When I request a password reset for "sam"
And I request a password reset for "sam"
And I request a password reset for "sam"
- Then I should see "Check your email for instructions on how to reset your password."
+ Then I should see "Check your email for instructions on how to reset your password. You may reset your password 0 more times."
And 3 emails should be delivered
When all emails have been delivered
And I request a password reset for "sam"
@@ -141,7 +141,7 @@ Feature: User Authentication
And 0 emails should be delivered
When it is currently 12 hours from now
And I request a password reset for "sam"
- Then I should see "Check your email for instructions on how to reset your password."
+ Then I should see "Check your email for instructions on how to reset your password. You may reset your password 2 more times."
And 1 email should be delivered
Scenario: User is locked out
diff --git a/spec/models/concerns/password_resets_limitable_spec.rb b/spec/models/concerns/password_resets_limitable_spec.rb
new file mode 100644
index 00000000000..3e383cc91b2
--- /dev/null
+++ b/spec/models/concerns/password_resets_limitable_spec.rb
@@ -0,0 +1,189 @@
+# frozen_string_literal: true
+
+require "spec_helper"
+
+shared_examples "a password resets limitable" do
+ describe "#password_resets_remaining" do
+ shared_examples "return the maximum number of attempts" do
+ it "returns the maximum number of attempts" do
+ expect(subject.password_resets_remaining).to eq(ArchiveConfig.PASSWORD_RESET_LIMIT)
+ end
+ end
+
+ context "with 0 resets requested" do
+ it_behaves_like "return the maximum number of attempts"
+ end
+
+ context "with under the maximum number of resets requested" do
+ before do
+ subject.resets_requested = ArchiveConfig.PASSWORD_RESET_LIMIT - 1
+ end
+
+ context "when the last reset request time is not set" do
+ it_behaves_like "return the maximum number of attempts"
+ end
+
+ context "when the cooldown period has not passed" do
+ before do
+ subject.reset_password_sent_at = Time.current
+ end
+
+ it "returns the expected number of attempts" do
+ expect(subject.password_resets_remaining).to eq(1)
+ end
+ end
+
+ context "when the cooldown period has passed" do
+ before do
+ subject.reset_password_sent_at = ArchiveConfig.PASSWORD_RESET_COOLDOWN_HOURS.hours.ago
+ end
+
+ it_behaves_like "return the maximum number of attempts"
+ end
+ end
+
+ shared_examples "no more reset requests left" do
+ context "when the last reset request time is not set" do
+ it_behaves_like "return the maximum number of attempts"
+ end
+
+ context "when the cooldown period has not passed" do
+ before do
+ subject.reset_password_sent_at = Time.current
+ end
+
+ it "returns 0 remaining attempts" do
+ expect(subject.password_resets_remaining).to eq(0)
+ end
+ end
+
+ context "when the cooldown period has passed" do
+ before do
+ subject.reset_password_sent_at = ArchiveConfig.PASSWORD_RESET_COOLDOWN_HOURS.hours.ago
+ end
+
+ it_behaves_like "return the maximum number of attempts"
+ end
+ end
+
+ context "with the maximum number of resets requested" do
+ before do
+ subject.resets_requested = ArchiveConfig.PASSWORD_RESET_LIMIT
+ end
+
+ it_behaves_like "no more reset requests left"
+ end
+
+ context "with over the maximum number of resets requested" do
+ before do
+ subject.resets_requested = ArchiveConfig.PASSWORD_RESET_LIMIT + 1
+ end
+
+ it_behaves_like "no more reset requests left"
+ end
+ end
+
+ describe "#password_resets_limit_reached?" do
+ shared_examples "limit not yet reached" do
+ it "has not reached the requests limit" do
+ expect(subject.password_resets_limit_reached?).to be_falsy
+ end
+ end
+
+ context "with 0 resets requested" do
+ it_behaves_like "limit not yet reached"
+ end
+
+ context "with the maximum number of password resets requested" do
+ before do
+ subject.resets_requested = ArchiveConfig.PASSWORD_RESET_LIMIT
+ end
+
+ context "when the last reset request time is not set" do
+ it_behaves_like "limit not yet reached"
+ end
+
+ context "when the cooldown period has passed" do
+ before do
+ subject.reset_password_sent_at = ArchiveConfig.PASSWORD_RESET_COOLDOWN_HOURS.hours.ago
+ end
+
+ it "has not reached the requests limit" do
+ expect(subject.password_resets_limit_reached?).to be_falsy
+ end
+ end
+
+ context "when the cooldown period has not passed" do
+ before do
+ subject.reset_password_sent_at = Time.current
+ end
+
+ it "has reached the requests limit" do
+ expect(subject.password_resets_limit_reached?).to be_truthy
+ end
+ end
+ end
+ end
+
+ describe "#update_password_resets_requested" do
+ context "with 0 resets requested" do
+ it "increments the password reset requests field" do
+ expect { subject.update_password_resets_requested }
+ .to change { subject.resets_requested }
+ .to(1)
+ end
+ end
+
+ context "with under the maximum number of password resets requested" do
+ before do
+ subject.resets_requested = ArchiveConfig.PASSWORD_RESET_LIMIT - 1
+ end
+
+ context "when the cooldown period has passed" do
+ before do
+ subject.reset_password_sent_at = ArchiveConfig.PASSWORD_RESET_COOLDOWN_HOURS.hours.ago
+ end
+
+ it "resets the password reset request field to 1" do
+ expect { subject.update_password_resets_requested }
+ .to change { subject.resets_requested }
+ .to(1)
+ end
+ end
+
+ context "when the cooldown period has not passed" do
+ before do
+ subject.reset_password_sent_at = Time.current
+ end
+
+ it "increments the password reset requests field" do
+ expect { subject.update_password_resets_requested }
+ .to change { subject.resets_requested }
+ .by(1)
+ end
+ end
+ end
+
+ context "with the maximum number of password resets requested" do
+ before do
+ subject.resets_requested = ArchiveConfig.PASSWORD_RESET_LIMIT
+ end
+
+ context "when the cooldown period has passed" do
+ before do
+ subject.reset_password_sent_at = ArchiveConfig.PASSWORD_RESET_COOLDOWN_HOURS.hours.ago
+ end
+
+ it "resets the password reset request field to 1" do
+ expect { subject.update_password_resets_requested }
+ .to change { subject.resets_requested }
+ .to(1)
+ end
+ end
+ end
+ end
+end
+
+describe User do
+ it_behaves_like "a password resets limitable"
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index b8b9b9729ed..d25d96a0ade 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -271,94 +271,4 @@
expect(duplicates).to eq(3)
end
end
-
- describe "#password_resets_limit_reached?" do
- context "with 0 resets requested" do
- let(:user) { build(:user, resets_requested: 0) }
-
- it "has not reached the requests limit" do
- expect(user.password_resets_limit_reached?).to be_falsy
- end
- end
-
- context "with the maximum number of password resets requested" do
- let(:user) { build(:user, resets_requested: ArchiveConfig.PASSWORD_RESET_LIMIT) }
-
- context "when the cooldown period has passed" do
- before do
- user.reset_password_sent_at = ArchiveConfig.PASSWORD_RESET_COOLDOWN_HOURS.hours.ago
- end
-
- it "has not reached the requests limit" do
- expect(user.password_resets_limit_reached?).to be_falsy
- end
- end
-
- context "when the cooldown period has not passed" do
- before do
- user.reset_password_sent_at = Time.current
- end
-
- it "has reached the requests limit" do
- expect(user.password_resets_limit_reached?).to be_truthy
- end
- end
- end
- end
-
- describe "#update_password_resets_requested" do
- context "with 0 resets requested" do
- let(:user) { build(:user, resets_requested: 0) }
-
- it "increments the password reset requests field" do
- expect { user.update_password_resets_requested }
- .to change { user.resets_requested }
- .to(1)
- end
- end
-
- context "with under the maximum number of password resets requested" do
- let(:user) { build(:user, resets_requested: ArchiveConfig.PASSWORD_RESET_LIMIT - 1) }
-
- context "when the cooldown period has passed" do
- before do
- user.reset_password_sent_at = ArchiveConfig.PASSWORD_RESET_COOLDOWN_HOURS.hours.ago
- end
-
- it "resets the password reset request field to 1" do
- expect { user.update_password_resets_requested }
- .to change { user.resets_requested }
- .to(1)
- end
- end
-
- context "when the cooldown period has not passed" do
- before do
- user.reset_password_sent_at = Time.current
- end
-
- it "increments the password reset requests field" do
- expect { user.update_password_resets_requested }
- .to change { user.resets_requested }
- .by(1)
- end
- end
- end
-
- context "with the maximum number of password resets requested" do
- let(:user) { build(:user, resets_requested: ArchiveConfig.PASSWORD_RESET_LIMIT) }
-
- context "when the cooldown period has passed" do
- before do
- user.reset_password_sent_at = ArchiveConfig.PASSWORD_RESET_COOLDOWN_HOURS.hours.ago
- end
-
- it "resets the password reset request field to 1" do
- expect { user.update_password_resets_requested }
- .to change { user.resets_requested }
- .to(1)
- end
- end
- end
- end
end
From 0138d2881ae19a759d7420d13cb75134b90fa672 Mon Sep 17 00:00:00 2001
From: sarken
Date: Wed, 13 Sep 2023 17:13:13 -0400
Subject: [PATCH 055/208] AO3-4595 Allow certain admins to update pseud
descriptions and icons (#4542)
* AO3-4595 Allow admins to edit pseud descriptions and remove icons
* AO3-4595 Mass-assignment protection with Pundit
* AO3-4595 Tidy admin_activity_target_link
* AO3-4595 Update new to use params defined in pseud policy
* AO3-4595 Typo in code comment
* AO3-4595 Fix #create permitted params
* AO3-4595 Refine specs
* AO3-4595 i18n pseud blurb
* AO3-4595 Use safe operator
* AO3-4595 Normalize en.yml
* AO3-4595 admin_activity_spec
* AO3-4595 Style fixes
* AO3-4595 Move character counter inside conditional
* AO3-4595 Code style fixes
* AO3-4595 Remove unnecessary &. and prevent ensure_user from encountering issues if it is used when already logged in as admin, like find_or_create_new_user
* AO3-4595 Update _pseud_blurb.html.erb with code style fixes
* AO3-4595 Update _pseuds_form.html.erb with code style fix
* AO3-4595 Update _pseud_blurb.html.erb with code style fixes
* AO3-4595 Update _pseuds_form.html.erb with code style fixes
* AO3-4595 Update pseuds.feature to add newline at end of file
---
app/controllers/pseuds_controller.rb | 24 +-
app/helpers/admin_helper.rb | 20 ++
app/models/admin_activity.rb | 6 +-
app/models/pseud.rb | 1 +
app/policies/pseud_policy.rb | 25 ++
app/views/admin/activities/index.html.erb | 32 ++-
app/views/admin/activities/show.html.erb | 19 +-
app/views/pseuds/_pseud_blurb.html.erb | 43 +++-
app/views/pseuds/_pseuds_form.html.erb | 53 ++--
config/locales/views/en.yml | 49 ++++
features/admins/admin_works.feature | 5 +-
features/other_a/pseuds.feature | 35 +++
.../other_a/pseuds_special_characters.feature | 2 +-
features/support/user.rb | 10 +
spec/controllers/pseuds_controller_spec.rb | 232 ++++++++++++++++--
spec/models/admin_activity_spec.rb | 36 +++
spec/models/concerns/justifiable_spec.rb | 7 +
spec/models/tag_wrangling_spec.rb | 39 +--
18 files changed, 535 insertions(+), 103 deletions(-)
create mode 100644 app/policies/pseud_policy.rb
create mode 100644 spec/models/admin_activity_spec.rb
diff --git a/app/controllers/pseuds_controller.rb b/app/controllers/pseuds_controller.rb
index f1e03bb0d32..77265c9cca7 100644
--- a/app/controllers/pseuds_controller.rb
+++ b/app/controllers/pseuds_controller.rb
@@ -2,7 +2,8 @@ class PseudsController < ApplicationController
cache_sweeper :pseud_sweeper
before_action :load_user
- before_action :check_ownership, only: [:create, :edit, :destroy, :new, :update]
+ before_action :check_ownership, only: [:create, :destroy, :new]
+ before_action :check_ownership_or_admin, only: [:edit, :update]
before_action :check_user_status, only: [:new, :create, :edit, :update]
def load_user
@@ -74,12 +75,13 @@ def new
# GET /pseuds/1/edit
def edit
@pseud = @user.pseuds.find_by(name: params[:id])
+ authorize @pseud if logged_in_as_admin?
end
# POST /pseuds
# POST /pseuds.xml
def create
- @pseud = Pseud.new(pseud_params)
+ @pseud = Pseud.new(permitted_attributes(Pseud))
if @user.pseuds.where(name: @pseud.name).blank?
@pseud.user_id = @user.id
old_default = @user.default_pseud
@@ -104,8 +106,14 @@ def create
# PUT /pseuds/1.xml
def update
@pseud = @user.pseuds.find_by(name: params[:id])
+ authorize @pseud if logged_in_as_admin?
default = @user.default_pseud
- if @pseud.update(pseud_params)
+ if @pseud.update(permitted_attributes(@pseud))
+ if logged_in_as_admin? && @pseud.ticket_url.present?
+ link = view_context.link_to("Ticket ##{@pseud.ticket_number}", @pseud.ticket_url)
+ summary = "#{link} for User ##{@pseud.user_id}"
+ AdminActivity.log_action(current_admin, @pseud, action: "edit pseud", summary: summary)
+ end
# if setting this one as default, unset the attribute of the current default pseud
if @pseud.is_default and not(default == @pseud)
# if setting this one as default, unset the attribute of the current active pseud
@@ -145,14 +153,4 @@ def destroy
redirect_to(user_pseuds_path(@user))
end
-
- private
-
- def pseud_params
- params.require(:pseud).permit(
- :name, :description, :is_default, :icon, :delete_icon,
- :icon_alt_text, :icon_comment_text
- )
- end
-
end
diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb
index 7e39c510f59..1b085c21061 100644
--- a/app/helpers/admin_helper.rb
+++ b/app/helpers/admin_helper.rb
@@ -5,6 +5,26 @@ def admin_activity_login_string(activity)
activity.admin.nil? ? ts("Admin deleted") : activity.admin_login
end
+ def admin_activity_target_link(activity)
+ url = if activity.target.is_a?(Pseud)
+ user_pseuds_path(activity.target.user)
+ else
+ activity.target
+ end
+ link_to(activity.target_name, url)
+ end
+
+ # Summaries for profile and pseud edits, which contain links, need to be
+ # handled differently from summaries that use item.inspect (and thus contain
+ # angle brackets).
+ def admin_activity_summary(activity)
+ if activity.action == "edit pseud" || activity.action == "edit profile"
+ raw sanitize_field(activity, :summary)
+ else
+ activity.summary
+ end
+ end
+
def admin_setting_disabled?(field)
return unless logged_in_as_admin?
diff --git a/app/models/admin_activity.rb b/app/models/admin_activity.rb
index 3c917ac9096..1cc3aca03b1 100644
--- a/app/models/admin_activity.rb
+++ b/app/models/admin_activity.rb
@@ -16,6 +16,10 @@ def self.log_action(admin, target, options={})
end
def target_name
- "#{target_type} #{target_id}"
+ if target.is_a?(Pseud)
+ "Pseud #{target.name} (#{target&.user&.login})"
+ else
+ "#{target_type} #{target_id}"
+ end
end
end
diff --git a/app/models/pseud.rb b/app/models/pseud.rb
index 5ce87de8f96..eab42fd1994 100644
--- a/app/models/pseud.rb
+++ b/app/models/pseud.rb
@@ -1,6 +1,7 @@
class Pseud < ApplicationRecord
include Searchable
include WorksOwner
+ include Justifiable
has_attached_file :icon,
styles: { standard: "100x100>" },
diff --git a/app/policies/pseud_policy.rb b/app/policies/pseud_policy.rb
new file mode 100644
index 00000000000..62ebd3ac693
--- /dev/null
+++ b/app/policies/pseud_policy.rb
@@ -0,0 +1,25 @@
+class PseudPolicy < ApplicationPolicy
+ # Roles that allow updating a pseud.
+ EDIT_ROLES = %w[superadmin policy_and_abuse].freeze
+
+ def can_edit?
+ user_has_roles?(EDIT_ROLES)
+ end
+
+ # Define which roles can update which attributes.
+ ALLOWED_ATTRIBUTES_BY_ROLES = {
+ "superadmin" => [:delete_icon, :description, :ticket_number],
+ "policy_and_abuse" => [:delete_icon, :description, :ticket_number]
+ }.freeze
+
+ def permitted_attributes
+ if user.is_a?(Admin)
+ ALLOWED_ATTRIBUTES_BY_ROLES.values_at(*user.roles).compact.flatten
+ else
+ [:name, :description, :is_default, :icon, :delete_icon, :icon_alt_text,
+ :icon_comment_text]
+ end
+ end
+
+ alias update? can_edit?
+end
diff --git a/app/views/admin/activities/index.html.erb b/app/views/admin/activities/index.html.erb
index 0c6139f2d08..6a5948c0089 100644
--- a/app/views/admin/activities/index.html.erb
+++ b/app/views/admin/activities/index.html.erb
@@ -1,6 +1,6 @@
-
<%= ts("View Admin Activity") %>
+
<%= t(".page_heading") %>
@@ -8,32 +8,28 @@
-
">
- <%=ts("Admin Activity") %>
+ ">
+ <%= t(".activities_table.caption") %>
- <%=ts("Date") %>
- <%=ts("Admin") %>
- <%=ts("Action") %>
- <%=ts("Target") %>
+ <%= t(".activities_table.date") %>
+ <%= t(".activities_table.admin") %>
+ <%= t(".activities_table.action") %>
+ <%= t(".activities_table.target") %>
<% for admin_activity in @activities %>
-
- <%= link_to admin_activity.created_at, admin_activity %>
-
- <%= admin_activity_login_string(admin_activity) %>
-
- <%= admin_activity.action %>
-
- <%= link_to admin_activity.target_name, admin_activity.target %>
-
-
+
+ <%= link_to admin_activity.created_at, admin_activity %>
+ <%= admin_activity_login_string(admin_activity) %>
+ <%= admin_activity.action %>
+ <%= admin_activity_target_link(admin_activity) %>
+
<% end %>
@@ -42,4 +38,4 @@
<%= will_paginate @activities %>
-
\ No newline at end of file
+
diff --git a/app/views/admin/activities/show.html.erb b/app/views/admin/activities/show.html.erb
index 2ca7b548146..98264a58d1a 100644
--- a/app/views/admin/activities/show.html.erb
+++ b/app/views/admin/activities/show.html.erb
@@ -1,31 +1,32 @@
-
<%= ts('Admin Activity') %>
+
<%= t(".page_heading") %>
- <%= link_to ts("Back To Index"), admin_activities_path %>
+ <%= link_to t(".navigation.index"), admin_activities_path %>
+
<%= t(".landmark.details") %>
- <%= ts("Date") %>
+ <%= t(".date") %>
<%= @activity.created_at %>
- <%= ts("Admin") %>
+ <%= t(".admin") %>
<%= admin_activity_login_string(@activity) %>
- <%= ts("Action") %>
+ <%= t(".action") %>
<%= @activity.action %>
- <%= ts("Target") %>
- <%= link_to @activity.target_name, @activity.target %>
+ <%= t(".target") %>
+ <%= admin_activity_target_link(@activity) %>
- <%= ts("Summary") %>
- <%=raw sanitize_field(@activity, :summary) %>
+ <%= t(".summary") %>
+ <%= admin_activity_summary(@activity) %>
diff --git a/app/views/pseuds/_pseud_blurb.html.erb b/app/views/pseuds/_pseud_blurb.html.erb
index d9fe922b8fb..fa12d9b3907 100644
--- a/app/views/pseuds/_pseud_blurb.html.erb
+++ b/app/views/pseuds/_pseud_blurb.html.erb
@@ -1,17 +1,44 @@
<%= render "pseud_module", pseud: pseud %>
- <% if current_user == pseud.user %>
- User Actions
+ <% if current_user == pseud.user || policy(pseud).edit? %>
+ <%= t(".user_actions") %>
- <%= link_to "#{h(ts('Edit'))} #{h pseud.name} ".html_safe, [:edit, @user, pseud], :id => "edit_#{pseud.name.downcase.gsub(" ", "_")}" %>
- <% unless pseud.works.blank? %>
- <%= link_to "#{h(ts('Orphan Works'))} by #{h pseud.name} ".html_safe, {:controller => 'orphans', :action => 'new', :pseud_id => pseud.id} %>
+ <%# These links use landmarks so they are distinct for screen readers,
+# e.g., Edit Pseud1, Edit Pseud2. The landmark text " Pseud2" is inside the
+# landmark span, so there's no extra space for sighted users. If your
+# language says "Pseud2 Edit", your landmark text would be "Pseud 2 " and
+# your link string "%{landmark_span}Edit". %>
+ <%= link_to(
+ t(".edit_html",
+ landmark_span: content_tag(:span,
+ t(".edit_landmark_text", pseud: pseud.name),
+ class: "landmark")),
+ edit_user_pseud_path(@user, pseud),
+ id: "edit_#{pseud.name.downcase.gsub(' ', '_')}"
+ ) %>
+ <% if pseud.works.present? && current_user == pseud.user %>
+ <%= link_to(
+ t(".orphan_html",
+ landmark_span: content_tag(:span,
+ t(".orphan_landmark_text", pseud: pseud.name),
+ class: "landmark")),
+ new_orphan_path(pseud_id: pseud.id)
+ ) %>
<% end %>
<% if pseud.is_default? %>
- Default Pseud
- <% elsif @user.login != pseud.name %>
- <%= link_to "#{h(ts('Delete'))} #{h pseud.name} ".html_safe, [@user, pseud], data: {confirm: ts('Are you sure?')}, :id => "delete_#{pseud.name.underscore}", :method => :delete %>
+ <%= t(".default_pseud") %>
+ <% elsif @user.login != pseud.name && current_user == pseud.user %>
+ <%= link_to(
+ t(".delete_html",
+ landmark_span: content_tag(:span,
+ t(".delete_landmark_text", pseud: pseud.name),
+ class: "landmark")),
+ user_pseud_path(@user, pseud),
+ data: { confirm: t(".confirm_delete") },
+ id: "delete_#{pseud.name.underscore}",
+ method: :delete
+ ) %>
<% end %>
<% end %>
diff --git a/app/views/pseuds/_pseuds_form.html.erb b/app/views/pseuds/_pseuds_form.html.erb
index bee6d4fccc8..8da60f58e12 100644
--- a/app/views/pseuds/_pseuds_form.html.erb
+++ b/app/views/pseuds/_pseuds_form.html.erb
@@ -1,65 +1,74 @@
<%= form_for([@user, @pseud], html: { multipart: true }) do |f| %>
- <%= f.label :name, ts("Name") %>
+ <%= f.label :name, t(".name") %>
- <% if @pseud.name && @user.login == @pseud.name %>
+ <% if @pseud&.name == @user.login %>
<%= @pseud.name %>
-
+
<% else %>
- <%= f.text_field :name, class: "observe_textlength" %>
+ <%= f.text_field :name, class: "observe_textlength", disabled: logged_in_as_admin? %>
+ <%= generate_countdown_html("pseud_name", Pseud::NAME_LENGTH_MAX) %>
<% end %>
- <%= generate_countdown_html("pseud_name", Pseud::NAME_LENGTH_MAX) if @pseud.name && @user.login != @pseud.name %>
- <%= f.label :is_default, ts('Make this name default') %>
- <%= f.check_box :is_default, disabled: (@pseud.name && @user.login == @pseud.name && @pseud.is_default?) %>
+ <%= f.label :is_default, t(".make_default") %>
+ <%= f.check_box :is_default, disabled: ((@pseud.name && @user.login == @pseud.name && @pseud.is_default?) || logged_in_as_admin?) %>
- <%= f.label :description, ts("Description") %>
+ <%= f.label :description, t(".description") %>
<%= allowed_html_instructions %>
<%= f.text_area :description, class: "observe_textlength" %>
<%= generate_countdown_html("pseud_description", Pseud::DESCRIPTION_MAX) %>
- <%= ts("Icon") %>
+ <%= t(".icon") %>
<% unless @pseud.new_record? %>
- <%= icon_display(@user, @pseud) %> <%= ts("This is your icon.") %>
+ <%= icon_display(@user, @pseud) %> <%= t(".icon_notes.current") %>
<% end %>
- <%= ts("You can have one icon for each pseud") %>
- <%= ts("Icons can be in png, jpeg or gif form") %>
- <%= ts("Icons should be sized 100x100 pixels for best results") %>
+ <%= t(".icon_notes.limit") %>
+ <%= t(".icon_notes.format") %>
+ <%= t(".icon_notes.size") %>
<% if @pseud.icon_file_name %>
<%= f.check_box :delete_icon, checked: false %>
- <%= f.label :delete_icon, ts("Delete your icon and revert to our default") %>
+ <%= f.label :delete_icon, t(".icon_delete") %>
<% end %>
- <%= f.label :icon, ts("Upload a new icon:") %>
- <%= f.file_field :icon %>
+ <%= f.label :icon, t(".icon_upload") %>
+ <%= f.file_field :icon, disabled: logged_in_as_admin? %>
- <%= f.label :icon_alt_text, ts("Icon alt text:") %>
+ <%= f.label :icon_alt_text, t(".icon_alt") %>
<%= link_to_help "icon-alt-text" %>
- <%= f.text_field :icon_alt_text, class: "observe_textlength" %>
+ <%= f.text_field :icon_alt_text, class: "observe_textlength", disabled: logged_in_as_admin? %>
<%= generate_countdown_html("pseud_icon_alt_text", ArchiveConfig.ICON_ALT_MAX) %>
- <%= f.label :icon_comment_text, ts("Icon comment text") %>
+ <%= f.label :icon_comment_text, t(".icon_comment") %>
<%= link_to_help('pseud-icon-comment') %>
- <%= f.text_field :icon_comment_text, class: "observe_textlength" %>
+ <%= f.text_field :icon_comment_text, class: "observe_textlength", disabled: logged_in_as_admin? %>
<%= generate_countdown_html("pseud_icon_comment_text", ArchiveConfig.ICON_COMMENT_MAX) %>
- <%= ts("Submit") %>
- <%= f.submit button_text %>
+ <% if policy(@pseud).can_edit? %>
+ <%= f.label :ticket_number, class: "required" %>
+
+ <%= f.text_field :ticket_number, class: "required" %>
+ <%= live_validation_for_field("pseud_ticket_number", numericality: true) %>
+
+
+ <% end %>
+
+ <%= t(".submit") %>
+ <%= f.submit button_text %>
<% end %>
diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml
index 465ec018d5b..bc1fc97d100 100644
--- a/config/locales/views/en.yml
+++ b/config/locales/views/en.yml
@@ -59,6 +59,27 @@ en:
suspended: You have been suspended or your work has been removed, and you have not received an email explaining why.
violation_html: You have encountered content on the Archive that violates the %{tos_link}.
admin:
+ activities:
+ index:
+ activities_table:
+ action: Action
+ admin: Admin
+ caption: Admin Activity
+ date: Date
+ summary: List of admin activity with links to full summaries.
+ target: Target
+ page_heading: Admin Activities
+ show:
+ action: Action
+ admin: Admin
+ date: Date
+ landmark:
+ details: Action Details
+ navigation:
+ index: Admin Activities
+ page_heading: Admin Activity
+ summary: Summary
+ target: Target
admin_invitations:
find:
email: Enter all or part of an email address
@@ -549,6 +570,34 @@ en:
transfer:
one: Transfer this bookmark to the default pseud
other: Transfer these bookmarks to the default pseud
+ pseud_blurb:
+ confirm_delete: Are you sure?
+ default_pseud: Default Pseud
+ delete_html: Delete%{landmark_span}
+ delete_landmark_text: " %{pseud}"
+ edit_html: Edit%{landmark_span}
+ edit_landmark_text: " %{pseud}"
+ orphan_html: Orphan Works%{landmark_span}
+ orphan_landmark_text: " by %{pseud}"
+ user_actions: User Actions
+ pseuds_form:
+ change_matching_pseud_html: You cannot change the pseud that matches your user name. However, you can %{change_username_link} instead.
+ change_username: change your user name
+ description: Description
+ icon: Icon
+ icon_alt: Icon alt text
+ icon_comment: Icon comment text
+ icon_delete: Delete your icon and revert to our default
+ icon_notes:
+ current: This is your icon.
+ format: Icons can be in png, jpeg or gif form.
+ limit: You can have one icon for each pseud.
+ size: Icons should be sized 100x100 pixels for best results.
+ icon_upload: Upload a new icon
+ make_default: Make this name default
+ name: Name
+ submit: Submit
+ ticket_footnote: Numbers only.
skins:
confirm_delete:
confirm_html: Are you sure you want to delete the skin "%{skin_title}"?
diff --git a/features/admins/admin_works.feature b/features/admins/admin_works.feature
index 1ed69875b97..eeebd918748 100644
--- a/features/admins/admin_works.feature
+++ b/features/admins/admin_works.feature
@@ -56,6 +56,9 @@ Feature: Admin Actions for Works, Comments, Series, Bookmarks
And 1 email should be delivered
And the email should contain "deleted from the Archive by a site admin"
And the email should not contain "translation missing"
+ When I visit the last activities item
+ Then I should see "destroy"
+ And I should see "# { get :edit, params: { user_id: user, id: pseud } } }
+
+ context "when logged in as admin" do
+ authorized_roles = %w[policy_and_abuse superadmin]
+
+ it_behaves_like "an action unauthorized admins can't access",
+ authorized_roles: authorized_roles
- context "when deleting the default pseud" do
- it "errors and redirects to user_pseuds_path" do
- post :destroy, params: { user_id: user, id: user.default_pseud }
- it_redirects_to_with_error(user_pseuds_path(user), "You cannot delete your default pseudonym, sorry!")
+ authorized_roles.each do |role|
+ context "with role #{role}" do
+ let(:admin) { create(:admin, roles: [role]) }
+
+ before { fake_login_admin(admin) }
+
+ it "renders edit template" do
+ subject.call
+ expect(response).to render_template(:edit)
+ end
+ end
end
end
+ end
- context "when deleting the pseud that matches your username" do
- it "errors and redirects to user_pseuds_path" do
- matching_pseud = user.default_pseud
- matching_pseud.update_attribute(:is_default, false)
- matching_pseud.reload
+ describe "update" do
+ shared_examples "an attribute that can be updated by an admin" do
+ it "redirects to user_pseud_path with notice" do
+ put :update, params: params
+ it_redirects_to_with_notice(user_pseud_path(user, pseud), "Pseud was successfully updated.")
+ end
- post :destroy, params: { user_id: user, id: matching_pseud }
- it_redirects_to_with_error(user_pseuds_path(user), "You cannot delete the pseud matching your user name, sorry!")
+ it "creates admin activity" do
+ expect do
+ put :update, params: params
+ end.to change { AdminActivity.count }
+ .by(1)
+ expect(AdminActivity.last.target).to eq(pseud)
+ expect(AdminActivity.last.admin).to eq(admin)
+ expect(AdminActivity.last.summary).to eq("Ticket #1 for User ##{user.id}")
end
end
+
+ subject { -> { put :update, params: { user_id: user, id: pseud } } }
+
+ context "when logged in as admin" do
+ authorized_roles = %w[policy_and_abuse superadmin]
+
+ before { fake_login_admin(admin) }
+
+ it_behaves_like "an action unauthorized admins can't access",
+ authorized_roles: authorized_roles
+
+ authorized_roles.each do |role|
+ context "with role #{role}" do
+ let(:admin) { create(:admin, roles: [role]) }
+
+ context "with valid ticket number" do
+ let(:ticket_url) { Faker::Internet.url }
+
+ before do
+ allow_any_instance_of(ZohoResourceClient).to receive(:find_ticket)
+ .and_return({ "status" => "Open", "departmentId" => ArchiveConfig.ABUSE_ZOHO_DEPARTMENT_ID })
+ allow_any_instance_of(Pseud).to receive(:ticket_url).and_return(ticket_url)
+ end
+
+ context "with description" do
+ let(:params) { { user_id: user, id: pseud, pseud: { description: "admin edit", ticket_number: 1 } } }
+
+ it_behaves_like "an attribute that can be updated by an admin"
+
+ it "updates pseud description" do
+ expect do
+ put :update, params: params
+ end.to change { pseud.reload.description }
+ .from(nil)
+ .to("admin edit
")
+ end
+ end
+
+ context "with delete_icon" do
+ let(:params) { { user_id: user, id: pseud, pseud: { delete_icon: "1", ticket_number: 1 } } }
+
+ before do
+ pseud.icon = File.new(Rails.root.join("features/fixtures/icon.gif"))
+ pseud.save
+ end
+
+ it_behaves_like "an attribute that can be updated by an admin"
+
+ it "removes pseud icon" do
+ expect do
+ put :update, params: params
+ end.to change { pseud.reload.icon_file_name }
+ .from("icon.gif")
+ .to(nil)
+ end
+ end
+
+ %w[name icon_alt_text icon_comment_text].each do |attr|
+ context "with #{attr}" do
+ let(:params) { { user_id: user, id: pseud, pseud: { "#{attr}": "admin edit", ticket_number: 1 } } }
+
+ it "raises UnpermittedParameters and does not update #{attr} or create admin activity" do
+ expect do
+ put :update, params: params
+ end.to raise_exception(ActionController::UnpermittedParameters)
+ expect(pseud.reload.send(attr)).not_to eq("admin edit")
+ expect(AdminActivity.last).to be_nil
+ end
+ end
+ end
+
+ context "with is_default" do
+ let(:params) { { user_id: user, id: pseud, pseud: { is_default: "0", ticket_number: 1 } } }
+
+ it "raises UnpermittedParameters and does not update is_default or create admin activity" do
+ expect do
+ put :update, params: params
+ end.to raise_exception(ActionController::UnpermittedParameters)
+ expect(pseud.reload.is_default).not_to be_falsy
+ expect(AdminActivity.last).to be_nil
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe "destroy" do
+ subject { -> { post :destroy, params: { user_id: user, id: pseud } } }
+
+ context "when logged in as admin" do
+ it_behaves_like "an action admins can't access"
+ end
+
+ context "when logged in as user" do
+ before do
+ fake_login_known_user(user)
+ end
+
+ context "when deleting the default pseud" do
+ it "errors and redirects to user_pseuds_path" do
+ post :destroy, params: { user_id: user, id: user.default_pseud }
+ it_redirects_to_with_error(user_pseuds_path(user), "You cannot delete your default pseudonym, sorry!")
+ end
+ end
+
+ context "when deleting the pseud that matches your username" do
+ it "errors and redirects to user_pseuds_path" do
+ matching_pseud = user.default_pseud
+ matching_pseud.update_attribute(:is_default, false)
+ matching_pseud.reload
+
+ post :destroy, params: { user_id: user, id: matching_pseud }
+ it_redirects_to_with_error(user_pseuds_path(user), "You cannot delete the pseud matching your user name, sorry!")
+ end
+ end
+ end
+ end
+
+ describe "new" do
+ subject { -> { get :new, params: { user_id: user } } }
+
+ context "when logged in as admin" do
+ it_behaves_like "an action admins can't access"
+ end
+ end
+
+ describe "create" do
+ subject { -> { post :create, params: { user_id: user } } }
+
+ context "when logged in as admin" do
+ it_behaves_like "an action admins can't access"
+ end
end
end
diff --git a/spec/models/admin_activity_spec.rb b/spec/models/admin_activity_spec.rb
new file mode 100644
index 00000000000..690d1fde361
--- /dev/null
+++ b/spec/models/admin_activity_spec.rb
@@ -0,0 +1,36 @@
+require "spec_helper"
+
+describe AdminActivity do
+ it "has a valid factory" do
+ expect(create(:admin_activity)).to be_valid
+ end
+
+ it "is invalid without an admin_id" do
+ expect(build(:admin_activity, admin_id: nil).valid?).to be_falsey
+ end
+
+ describe ".target_name" do
+ context "when target is a Pseud" do
+ let(:pseud) { create(:pseud, name: "aka") }
+ let!(:activity) { create(:admin_activity, target: pseud) }
+
+ it "returns the pseud name and user login for existing pseud" do
+ expect(activity.target_name).to eq("Pseud aka (#{pseud.user.login})")
+ end
+
+ it "returns the pseud ID for a deleted pseud" do
+ pseud.destroy
+ expect(activity.reload.target_name).to eq("Pseud #{pseud.id}")
+ end
+ end
+
+ context "when target is a Work" do
+ let(:work) { create(:work) }
+ let(:activity) { create(:admin_activity, target: work) }
+
+ it "returns the work ID" do
+ expect(activity.target_name).to eq("Work #{work.id}")
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/justifiable_spec.rb b/spec/models/concerns/justifiable_spec.rb
index 02b19881a22..b023e56a6f0 100644
--- a/spec/models/concerns/justifiable_spec.rb
+++ b/spec/models/concerns/justifiable_spec.rb
@@ -122,3 +122,10 @@
let(:attributes) { { about_me: "I stole a fragment of the Rune of Death." } }
end
end
+
+describe Pseud do
+ it_behaves_like "a justifiable model" do
+ let!(:record) { create(:pseud) }
+ let(:attributes) { { description: "Edited by admin." } }
+ end
+end
diff --git a/spec/models/tag_wrangling_spec.rb b/spec/models/tag_wrangling_spec.rb
index 194e1168e2d..ec4f7d03cbd 100644
--- a/spec/models/tag_wrangling_spec.rb
+++ b/spec/models/tag_wrangling_spec.rb
@@ -185,7 +185,7 @@
expect(synonym.children.reload).to contain_exactly
end
- describe "with asynchronous jobs run asynchronously" do
+ context "with asynchronous jobs run asynchronously" do
include ActiveJob::TestHelper
it "transfers the subtags to the new parent autocomplete" do
@@ -210,21 +210,30 @@
end
end
- it "transfers favorite tags" do
- user = create(:user)
- user.favorite_tags.create(tag: synonym)
- synonym.update!(syn_string: fandom.name)
- expect(user.favorite_tags.count).to eq 1
- expect(user.favorite_tags.reload.first.tag).to eq(fandom)
- end
+ context "with favorite tags" do
+ # Can't create a user while User.current_user is an admin due to
+ # restrictions on which properties of a pseud an admin can edit.
+ let(:user) do
+ User.current_user = nil
+ user = create(:user)
+ User.current_user = create(:admin)
+ user
+ end
- it "handles duplicate favorite tags" do
- user = create(:user)
- user.favorite_tags.create(tag: fandom)
- user.favorite_tags.create(tag: synonym)
- synonym.update!(syn_string: fandom.name)
- expect(user.favorite_tags.count).to eq 1
- expect(user.favorite_tags.reload.first.tag).to eq(fandom)
+ it "transfers favorite tags" do
+ user.favorite_tags.create(tag: synonym)
+ synonym.update!(syn_string: fandom.name)
+ expect(user.favorite_tags.count).to eq 1
+ expect(user.favorite_tags.reload.first.tag).to eq(fandom)
+ end
+
+ it "handles duplicate favorite tags" do
+ user.favorite_tags.create(tag: fandom)
+ user.favorite_tags.create(tag: synonym)
+ synonym.update!(syn_string: fandom.name)
+ expect(user.favorite_tags.count).to eq 1
+ expect(user.favorite_tags.reload.first.tag).to eq(fandom)
+ end
end
end
end
From 8d5b1c97839ec1eb736c3387c1e52e2e30786181 Mon Sep 17 00:00:00 2001
From: Bilka
Date: Wed, 13 Sep 2023 23:16:19 +0200
Subject: [PATCH 056/208] AO3-6312 Add preference to prevent guest replies to
comments (#4612)
* AO3-6312 Added preference to prevent guest replies to comments
* AO3-6312 Fix reply button on guest comments
Fix Hound and reviewdog errors
* AO3-6312 Fix replying to guest comments
* AO3-6312 Fix N+1 queries for preference in comment threads
* AO3-6312 Hound
* AO3-6312 Add validation to comment model
* AO3-6312 Normalize locale file
* AO3-6312 Improve comment model validation
* AO3-6312 Hound
* AO3-6312 It helps to fix the right spot
* AO3-6312 Move guest reply check to Comment model
* AO3-6312 Refactor can_reply_to_comment? helper
* AO3-6312 Hound
* AO3-6312 Rename validation method
* AO3-6312 Whitespace
---
app/controllers/comments_controller.rb | 8 +
app/controllers/preferences_controller.rb | 3 +-
app/helpers/comments_helper.rb | 21 +-
app/models/comment.rb | 15 +-
app/views/preferences/index.html.erb | 4 +
config/locales/controllers/en.yml | 2 +
config/locales/models/en.yml | 3 +
config/locales/views/en.yml | 1 +
...500_add_guest_replies_off_to_preference.rb | 7 +
.../guest_comment_replies.feature | 68 +++++
features/other_a/preferences_edit.feature | 1 +
.../step_definitions/preferences_steps.rb | 6 +
public/help/comment-preferences.html | 4 +-
spec/controllers/comments_controller_spec.rb | 273 +++++++++++++++++-
spec/models/comment_spec.rb | 90 +++++-
15 files changed, 491 insertions(+), 15 deletions(-)
create mode 100644 db/migrate/20230819074500_add_guest_replies_off_to_preference.rb
create mode 100644 features/comments_and_kudos/guest_comment_replies.feature
diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb
index 507c99114c4..f4e96e3ca2c 100644
--- a/app/controllers/comments_controller.rb
+++ b/app/controllers/comments_controller.rb
@@ -24,6 +24,7 @@ class CommentsController < ApplicationController
before_action :check_frozen, only: [:new, :create, :add_comment_reply]
before_action :check_hidden_by_admin, only: [:new, :create, :add_comment_reply]
before_action :check_not_replying_to_spam, only: [:new, :create, :add_comment_reply]
+ before_action :check_guest_replies_preference, only: [:new, :create, :add_comment_reply]
before_action :check_permission_to_review, only: [:unreviewed]
before_action :check_permission_to_access_single_unreviewed, only: [:show]
before_action :check_permission_to_moderate, only: [:approve, :reject]
@@ -140,6 +141,13 @@ def check_guest_comment_admin_setting
redirect_back(fallback_location: root_path)
end
+ def check_guest_replies_preference
+ return unless guest? && @commentable.respond_to?(:guest_replies_disallowed?) && @commentable.guest_replies_disallowed?
+
+ flash[:error] = t("comments.check_guest_replies_preference.error")
+ redirect_back(fallback_location: root_path)
+ end
+
def check_unreviewed
return unless @commentable.respond_to?(:unreviewed?) && @commentable.unreviewed?
diff --git a/app/controllers/preferences_controller.rb b/app/controllers/preferences_controller.rb
index ce7089f9b2c..abcff655b9e 100644
--- a/app/controllers/preferences_controller.rb
+++ b/app/controllers/preferences_controller.rb
@@ -67,7 +67,8 @@ def preference_params
:first_login,
:banner_seen,
:allow_cocreator,
- :allow_gifts
+ :allow_gifts,
+ :guest_replies_off
)
end
end
diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb
index 9a4266dc4b6..9d7daa1e79b 100644
--- a/app/helpers/comments_helper.rb
+++ b/app/helpers/comments_helper.rb
@@ -100,15 +100,18 @@ def show_hide_comments_link(commentable, options={})
def can_reply_to_comment?(comment)
admin_settings = AdminSetting.current
-
- !(comment.unreviewed? ||
- comment.iced? ||
- comment.hidden_by_admin? ||
- parent_disallows_comments?(comment) ||
- comment_parent_hidden?(comment) ||
- blocked_by_comment?(comment) ||
- blocked_by?(comment.ultimate_parent) ||
- guest? && admin_settings.guest_comments_off?)
+
+ return false if comment.unreviewed?
+ return false if comment.iced?
+ return false if comment.hidden_by_admin?
+ return false if parent_disallows_comments?(comment)
+ return false if comment_parent_hidden?(comment)
+ return false if blocked_by_comment?(comment)
+ return false if blocked_by?(comment.ultimate_parent)
+
+ return true unless guest?
+
+ !(admin_settings.guest_comments_off? || comment.guest_replies_disallowed?)
end
def can_edit_comment?(comment)
diff --git a/app/models/comment.rb b/app/models/comment.rb
index b0bb724af3c..250a4c0df12 100644
--- a/app/models/comment.rb
+++ b/app/models/comment.rb
@@ -24,6 +24,19 @@ class Comment < ApplicationRecord
delegate :user, to: :pseud, allow_nil: true
+ # Whether the writer of the comment this is replying to allows guest replies
+ validate :guest_can_reply, if: :reply_comment?, unless: :pseud_id, on: :create
+ def guest_can_reply
+ errors.add(:commentable, :guest_replies_off) if commentable.guest_replies_disallowed?
+ end
+
+ # Whether the writer of this comment disallows guest replies
+ def guest_replies_disallowed?
+ return false unless user
+
+ user.preference.guest_replies_off && !user.is_author_of?(ultimate_parent)
+ end
+
# Check if the writer of this comment is blocked by the writer of the comment
# they're replying to:
validates :user, not_blocked: {
@@ -70,7 +83,7 @@ def check_for_spam
scope :for_display, lambda {
includes(
- pseud: { user: [:roles, :block_of_current_user, :block_by_current_user] },
+ pseud: { user: [:roles, :block_of_current_user, :block_by_current_user, :preference] },
parent: { work: [:pseuds, :users] }
)
}
diff --git a/app/views/preferences/index.html.erb b/app/views/preferences/index.html.erb
index 64e884c2ae3..06794be64a6 100644
--- a/app/views/preferences/index.html.erb
+++ b/app/views/preferences/index.html.erb
@@ -108,6 +108,10 @@
<%= f.check_box :kudos_emails_off %>
<%= f.label :kudos_emails_off, ts('Turn off emails about kudos.') %>
+
+ <%= f.check_box :guest_replies_off %>
+ <%= f.label :guest_replies_off, t(".guest_replies_off") %>
+
diff --git a/config/locales/controllers/en.yml b/config/locales/controllers/en.yml
index 22a22836485..9b92f55418f 100644
--- a/config/locales/controllers/en.yml
+++ b/config/locales/controllers/en.yml
@@ -36,6 +36,8 @@ en:
reply: Sorry, you have been blocked by that user.
check_frozen:
error: Sorry, you cannot reply to a frozen comment.
+ check_guest_replies_preference:
+ error: Sorry, this user doesn't allow non-Archive users to reply to their comments.
check_hidden_by_admin:
error: Sorry, you cannot reply to a hidden comment.
check_not_replying_to_spam:
diff --git a/config/locales/models/en.yml b/config/locales/models/en.yml
index 8b3e55b3378..094daa3290e 100644
--- a/config/locales/models/en.yml
+++ b/config/locales/models/en.yml
@@ -100,6 +100,9 @@ en:
format: "%{message}"
comment:
attributes:
+ commentable:
+ format: "%{message}"
+ guest_replies_off: Sorry, this user doesn't allow non-Archive users to reply to their comments.
user:
blocked_comment: Sorry, you have been blocked by one or more of this work's creators.
blocked_reply: Sorry, you have been blocked by that user.
diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml
index bc1fc97d100..292d1b89990 100644
--- a/config/locales/views/en.yml
+++ b/config/locales/views/en.yml
@@ -549,6 +549,7 @@ en:
index:
allow_collection_invitation: Allow others to invite my works to collections.
blocked_users: Blocked Users
+ guest_replies_off: Do not allow guests to reply to my comments on news posts or other users' works (you can still control the comment settings for your works separately).
muted_users: Muted Users
profile:
pseud_list:
diff --git a/db/migrate/20230819074500_add_guest_replies_off_to_preference.rb b/db/migrate/20230819074500_add_guest_replies_off_to_preference.rb
new file mode 100644
index 00000000000..289afccc46b
--- /dev/null
+++ b/db/migrate/20230819074500_add_guest_replies_off_to_preference.rb
@@ -0,0 +1,7 @@
+class AddGuestRepliesOffToPreference < ActiveRecord::Migration[6.1]
+ uses_departure! if Rails.env.staging? || Rails.env.production?
+
+ def change
+ add_column :preferences, :guest_replies_off, :boolean, default: false, null: false
+ end
+end
diff --git a/features/comments_and_kudos/guest_comment_replies.feature b/features/comments_and_kudos/guest_comment_replies.feature
new file mode 100644
index 00000000000..8beec416916
--- /dev/null
+++ b/features/comments_and_kudos/guest_comment_replies.feature
@@ -0,0 +1,68 @@
+Feature: Disallowing guest comment replies
+
+ Scenario Outline: Guests cannot reply to a user who has guest comments off on news posts and other users' works
+ Given
+ And the user "commenter" turns off guest comment replies
+ And a comment "OMG!" by "commenter" on
+ When I view with comments
+ Then I should see a "Comment" button
+ But I should not see a link "Reply"
+ When I am logged in as "reader"
+ And I view with comments
+ Then I should see a "Comment" button
+ And I should see a link "Reply"
+
+ Examples:
+ | commentable |
+ | the work "Aftermath" |
+ | the admin post "Change Log" |
+
+ Scenario: Guests can reply to a user who has guest comments off on their own work
+ Given the work "Aftermath" by "creator"
+ And the user "creator" turns off guest comment replies
+ And a comment "OMG!" by "creator" on the work "Aftermath"
+ When I view the work "Aftermath" with comments
+ Then I should see a "Comment" button
+ And I should see a link "Reply"
+ When I am logged in as "reader"
+ And I view the work "Aftermath" with comments
+ Then I should see a "Comment" button
+ And I should see a link "Reply"
+
+ Scenario: Guests can reply to a user who has guest comments off on works co-created by the user
+ Given the user "nemesis" turns off guest comment replies
+ And the work "Aftermath" by "creator" and "nemesis"
+ And a comment "OMG!" by "nemesis" on the work "Aftermath"
+ When I view the work "Aftermath" with comments
+ Then I should see a "Comment" button
+ And I should see a link "Reply"
+ When I am logged in as "reader"
+ And I view the work "Aftermath" with comments
+ Then I should see a "Comment" button
+ And I should see a link "Reply"
+
+ Scenario: Users can reply to a user who has guest comments off on tags
+ Given the following activated tag wranglers exist
+ | login |
+ | commenter |
+ | wrangler |
+ And a canonical fandom "Controversial"
+ And the user "commenter" turns off guest comment replies
+ And a comment "OMG!" by "commenter" on the tag "Controversial"
+ When I am logged in as "wrangler"
+ And I view the tag "Controversial" with comments
+ And I follow "Reply"
+ And I fill in "Comment" with "Ugh." within ".odd"
+ And I press "Comment" within ".odd"
+ Then I should see "Comment created!"
+
+ Scenario: Guests can reply to guests
+ Given the work "Aftermath"
+ And a guest comment on the work "Aftermath"
+ When I view the work "Aftermath" with comments
+ Then I should see a "Comment" button
+ And I should see a link "Reply"
+ When I am logged in as "reader"
+ And I view the work "Aftermath" with comments
+ Then I should see a "Comment" button
+ And I should see a link "Reply"
diff --git a/features/other_a/preferences_edit.feature b/features/other_a/preferences_edit.feature
index c3d2742c33f..63cf8c8a435 100644
--- a/features/other_a/preferences_edit.feature
+++ b/features/other_a/preferences_edit.feature
@@ -31,6 +31,7 @@ Feature: Edit preferences
And I should see "Turn off messages to your inbox about comments."
And I should see "Turn off copies of your own comments."
And I should see "Turn off emails about kudos."
+ And I should see "Do not allow guests to reply to my comments on news posts or other users' works (you can still control the comment settings for your works separately)."
And I should see "Allow others to invite my works to collections."
And I should see "Turn off emails from collections."
And I should see "Turn off inbox messages from collections."
diff --git a/features/step_definitions/preferences_steps.rb b/features/step_definitions/preferences_steps.rb
index 28df3b2ccc9..8325d4cd1f4 100644
--- a/features/step_definitions/preferences_steps.rb
+++ b/features/step_definitions/preferences_steps.rb
@@ -32,6 +32,12 @@
user.preference.save
end
+When "the user {string} turns off guest comment replies" do |login|
+ user = User.where(login: login).first
+ user = find_or_create_new_user(login, DEFAULT_PASSWORD) if user.nil?
+ user.preference.update!(guest_replies_off: true)
+end
+
Given "the user {string} is hidden from search engines" do |login|
user = User.find_by(login: login)
user.preference.update(minimize_search_engines: true)
diff --git a/public/help/comment-preferences.html b/public/help/comment-preferences.html
index f2768a156ff..62325d77727 100644
--- a/public/help/comment-preferences.html
+++ b/public/help/comment-preferences.html
@@ -9,6 +9,6 @@ Comment Preferences
Enable this option if you prefer not to receive emails for your own comments (for example, when you reply to comments on your own works).
Turn off emails about kudos
Enable this option if you would prefer not to receive email notifications when someone leaves kudos on your work.
- Turn off admin emails
- Occasionally, Archive staff may send out email notices to all users, for example to notify people of proposed changes to the Terms of Service or site downtime. If you would prefer not to receive such emails, please enable this option. Note that if you choose to disable this option, you may miss important information about site changes or events.
+ Do not allow guests to reply to my comments on news posts or other users' works
+ Enable this option if you would prefer not to receive replies from users who aren't logged in to an Archive account on comments you leave on news posts or other creators' works. This setting doesn't apply to comments on your own works; for more on controlling who can interact with you on your works check out the Posting and Editing FAQ .
diff --git a/spec/controllers/comments_controller_spec.rb b/spec/controllers/comments_controller_spec.rb
index 9ce7ed462b9..7fa5dccfc2e 100644
--- a/spec/controllers/comments_controller_spec.rb
+++ b/spec/controllers/comments_controller_spec.rb
@@ -167,6 +167,89 @@
end
end
end
+
+ shared_examples "guest cannot reply to a user with guest replies disabled" do
+ it "redirects guest with an error" do
+ get :add_comment_reply, params: { comment_id: comment.id }
+ it_redirects_to_with_error("/where_i_came_from", "Sorry, this user doesn't allow non-Archive users to reply to their comments.")
+ end
+
+ it "redirects logged in user without an error" do
+ fake_login
+ get :add_comment_reply, params: { comment_id: comment.id }
+ expect(flash[:error]).to be_nil
+ end
+ end
+
+ shared_examples "guest can reply to a user with guest replies disabled on user's work" do
+ it "redirects guest user without an error" do
+ get :add_comment_reply, params: { comment_id: comment.id }
+ expect(flash[:error]).to be_nil
+ end
+
+ it "redirects logged in user without an error" do
+ fake_login
+ get :add_comment_reply, params: { comment_id: comment.id }
+ expect(flash[:error]).to be_nil
+ end
+ end
+
+ context "when user has guest replies disabled" do
+ let(:user) do
+ user = create(:user)
+ user.preference.update!(guest_replies_off: true)
+ user
+ end
+
+ context "when commentable is an admin post" do
+ let(:comment) { create(:comment, :on_admin_post, pseud: user.default_pseud) }
+
+ it_behaves_like "guest cannot reply to a user with guest replies disabled"
+ end
+
+ context "when commentable is a tag" do
+ let(:comment) { create(:comment, :on_tag, pseud: user.default_pseud) }
+
+ it_behaves_like "guest cannot reply to a user with guest replies disabled"
+ end
+
+ context "when commentable is a work" do
+ let(:comment) { create(:comment, pseud: user.default_pseud) }
+
+ it_behaves_like "guest cannot reply to a user with guest replies disabled"
+ end
+
+ context "when commentable is user's work" do
+ let(:work) { create(:work, authors: [user.default_pseud]) }
+ let(:comment) { create(:comment, pseud: user.default_pseud, commentable: work.first_chapter) }
+
+ it_behaves_like "guest can reply to a user with guest replies disabled on user's work"
+ end
+
+ context "when commentable is user's co-creation" do
+ let(:work) { create(:work, authors: [create(:user).default_pseud, user.default_pseud]) }
+ let(:comment) { create(:comment, pseud: user.default_pseud, commentable: work.first_chapter) }
+
+ it_behaves_like "guest can reply to a user with guest replies disabled on user's work"
+ end
+ end
+
+ context "when replying to guests" do
+ let(:comment) { create(:comment, :by_guest) }
+
+ it "redirects guest user without an error" do
+ get :add_comment_reply, params: { comment_id: comment.id }
+ expect(flash[:error]).to be_nil
+ expect(response).to redirect_to(chapter_path(comment.commentable, show_comments: true, anchor: "comment_#{comment.id}"))
+ end
+
+ it "redirects logged in user without an error" do
+ fake_login
+ get :add_comment_reply, params: { comment_id: comment.id }
+ expect(flash[:error]).to be_nil
+ expect(response).to redirect_to(chapter_path(comment.commentable, show_comments: true, anchor: "comment_#{comment.id}"))
+ end
+ end
end
describe "GET #unreviewed" do
@@ -346,6 +429,75 @@
get :new, params: { comment_id: comment.id }
it_redirects_to_with_error("/where_i_came_from", "Sorry, you cannot reply to a hidden comment.")
end
+
+ shared_examples "guest cannot reply to a user with guest replies disabled" do
+ it "redirects guest with an error" do
+ get :new, params: { comment_id: comment.id }
+ it_redirects_to_with_error("/where_i_came_from", "Sorry, this user doesn't allow non-Archive users to reply to their comments.")
+ end
+
+ it "renders the :new template for logged in user" do
+ fake_login
+ get :new, params: { comment_id: comment.id }
+ expect(flash[:error]).to be_nil
+ expect(response).to render_template("new")
+ end
+ end
+
+ shared_examples "guest can reply to a user with guest replies disabled on user's work" do
+ it "renders the :new template for guest" do
+ get :new, params: { comment_id: comment.id }
+ expect(flash[:error]).to be_nil
+ expect(response).to render_template("new")
+ end
+
+ it "renders the :new template for logged in user" do
+ fake_login
+ get :new, params: { comment_id: comment.id }
+ expect(flash[:error]).to be_nil
+ expect(response).to render_template("new")
+ end
+ end
+
+ context "user has guest comment replies disabled" do
+ let(:user) do
+ user = create(:user)
+ user.preference.update!(guest_replies_off: true)
+ user
+ end
+
+ context "when commentable is an admin post" do
+ let(:comment) { create(:comment, :on_admin_post, pseud: user.default_pseud) }
+
+ it_behaves_like "guest cannot reply to a user with guest replies disabled"
+ end
+
+ context "when commentable is a tag" do
+ let(:comment) { create(:comment, :on_tag, pseud: user.default_pseud) }
+
+ it_behaves_like "guest cannot reply to a user with guest replies disabled"
+ end
+
+ context "when commentable is a work" do
+ let(:comment) { create(:comment, pseud: user.default_pseud) }
+
+ it_behaves_like "guest cannot reply to a user with guest replies disabled"
+ end
+
+ context "when comment is on user's work" do
+ let(:work) { create(:work, authors: [user.default_pseud]) }
+ let(:comment) { create(:comment, pseud: user.default_pseud, commentable: work.first_chapter) }
+
+ it_behaves_like "guest can reply to a user with guest replies disabled on user's work"
+ end
+
+ context "when commentable is user's co-creation" do
+ let(:work) { create(:work, authors: [create(:user).default_pseud, user.default_pseud]) }
+ let(:comment) { create(:comment, pseud: user.default_pseud, commentable: work.first_chapter) }
+
+ it_behaves_like "guest can reply to a user with guest replies disabled on user's work"
+ end
+ end
end
describe "POST #create" do
@@ -649,6 +801,80 @@
end
end
end
+
+ shared_examples "guest cannot reply to a user with guest replies disabled" do
+ it "redirects guest with an error" do
+ post :create, params: { comment_id: comment.id, comment: anon_comment_attributes }
+ it_redirects_to_with_error("/where_i_came_from", "Sorry, this user doesn't allow non-Archive users to reply to their comments.")
+ end
+
+ it "redirects logged in user without an error" do
+ comment_attributes = {
+ pseud_id: user.default_pseud_id,
+ comment_content: "Hello fellow human!"
+ }
+ fake_login_known_user(user)
+ post :create, params: { comment_id: comment.id, comment: comment_attributes }
+ expect(flash[:error]).to be_nil
+ end
+ end
+
+ shared_examples "guest can reply to a user with guest replies disabled on user's work" do
+ it "redirects guest without an error" do
+ post :create, params: { comment_id: comment.id, comment: anon_comment_attributes }
+ expect(flash[:error]).to be_nil
+ end
+
+ it "redirects logged in user without an error" do
+ comment_attributes = {
+ pseud_id: user.default_pseud_id,
+ comment_content: "Hello fellow human!"
+ }
+ fake_login_known_user(user)
+ post :create, params: { comment_id: comment.id, comment: comment_attributes }
+ expect(flash[:error]).to be_nil
+ end
+ end
+
+ context "user has guest comment replies disabled" do
+ let(:user) do
+ user = create(:user)
+ user.preference.update!(guest_replies_off: true)
+ user
+ end
+
+ context "when commentable is an admin post" do
+ let(:comment) { create(:comment, :on_admin_post, pseud: user.default_pseud) }
+
+ it_behaves_like "guest cannot reply to a user with guest replies disabled"
+ end
+
+ context "when commentable is a tag" do
+ let(:comment) { create(:comment, :on_tag, pseud: user.default_pseud) }
+
+ it_behaves_like "guest cannot reply to a user with guest replies disabled"
+ end
+
+ context "when commentable is a work" do
+ let(:comment) { create(:comment, pseud: user.default_pseud) }
+
+ it_behaves_like "guest cannot reply to a user with guest replies disabled"
+ end
+
+ context "when comment is on user's work" do
+ let(:work) { create(:work, authors: [user.default_pseud]) }
+ let(:comment) { create(:comment, pseud: user.default_pseud, commentable: work.first_chapter) }
+
+ it_behaves_like "guest can reply to a user with guest replies disabled on user's work"
+ end
+
+ context "when commentable is user's co-creation" do
+ let(:work) { create(:work, authors: [create(:user).default_pseud, user.default_pseud]) }
+ let(:comment) { create(:comment, pseud: user.default_pseud, commentable: work.first_chapter) }
+
+ it_behaves_like "guest can reply to a user with guest replies disabled on user's work"
+ end
+ end
end
describe "PUT #review_all" do
@@ -854,6 +1080,25 @@
"Comment thread successfully frozen!"
)
end
+
+ context "when comment is a guest reply to user who turns off guest replies afterwards" do
+ let(:reply) do
+ reply = create(:comment, :by_guest, commentable: comment)
+ comment.user.preference.update!(guest_replies_off: true)
+ reply
+ end
+
+ it "freezes reply and redirects with success message" do
+ fake_login_admin(admin)
+ put :freeze, params: { id: reply.id }
+
+ expect(reply.reload.iced).to be_truthy
+ it_redirects_to_with_comment_notice(
+ admin_post_path(comment.ultimate_parent, show_comments: true, anchor: :comments),
+ "Comment thread successfully frozen!"
+ )
+ end
+ end
end
context "when logged in as a user" do
@@ -2706,7 +2951,9 @@
it "deletes the comment and redirects to referer with a notice" do
delete :destroy, params: { id: unreviewed_comment.id }
- expect { unreviewed_comment.reload }.to raise_exception(ActiveRecord::RecordNotFound)
+ expect do
+ unreviewed_comment.reload
+ end.to raise_exception(ActiveRecord::RecordNotFound)
it_redirects_to_with_notice("/where_i_came_from", "Comment deleted.")
end
@@ -2719,6 +2966,30 @@
end
end
+ context "when comment is a guest reply to user who turns off guest replies afterwards" do
+ let(:comment) { create(:comment, :on_admin_post) }
+ let(:reply) do
+ reply = create(:comment, :by_guest, commentable: comment)
+ comment.user.preference.update!(guest_replies_off: true)
+ reply
+ end
+
+ it "deletes the reply and redirects with success message" do
+ admin = create(:admin)
+ admin.update(roles: ["superadmin"])
+ fake_login_admin(admin)
+ delete :destroy, params: { id: reply.id }
+
+ it_redirects_to_with_comment_notice(
+ admin_post_path(reply.ultimate_parent, show_comments: true, anchor: "comment_#{comment.id}"),
+ "Comment deleted."
+ )
+ expect do
+ reply.reload
+ end.to raise_exception(ActiveRecord::RecordNotFound)
+ end
+ end
+
context "when comment is frozen" do
context "when ultimate parent is an AdminPost" do
let(:comment) { create(:comment, :on_admin_post, iced: true) }
diff --git a/spec/models/comment_spec.rb b/spec/models/comment_spec.rb
index 697fc58cee1..be7d857cf84 100644
--- a/spec/models/comment_spec.rb
+++ b/spec/models/comment_spec.rb
@@ -40,7 +40,7 @@
it "should be invalid if exactly duplicated" do
expect(second_comment.valid?).to be_falsy
- expect(second_comment.errors.keys).to include(:comment_content)
+ expect(second_comment.errors.attribute_names).to include(:comment_content)
end
it "should not be invalid if in the process of being deleted" do
@@ -193,6 +193,94 @@
end
end
+ context "when user has disabled guest replies" do
+ let(:no_reply_guy) do
+ user = create(:user)
+ user.preference.update!(guest_replies_off: true)
+ user
+ end
+
+ let(:guest_reply) do
+ Comment.new(commentable: comment,
+ pseud: nil,
+ name: "unwelcome guest",
+ email: Faker::Internet.email,
+ comment_content: "I'm a vampire.")
+ end
+
+ let(:user_reply) do
+ Comment.new(commentable: comment,
+ pseud: create(:user).default_pseud,
+ comment_content: "Hmm.")
+ end
+
+ shared_examples "creating guest reply is allowed" do
+ describe "save" do
+ it "allows guest replies" do
+ expect(guest_reply.save).to be_truthy
+ expect(guest_reply.errors.full_messages).to be_blank
+ end
+
+ it "allows user replies" do
+ expect(user_reply.save).to be_truthy
+ expect(user_reply.errors.full_messages).to be_blank
+ end
+ end
+ end
+
+ shared_examples "creating guest reply is not allowed" do
+ describe "save" do
+ it "doesn't allow guest replies" do
+ expect(guest_reply.save).to be_falsey
+ expect(guest_reply.errors.full_messages).to include("Sorry, this user doesn't allow non-Archive users to reply to their comments.")
+ end
+
+ it "allows user replies" do
+ expect(user_reply.save).to be_truthy
+ expect(user_reply.errors.full_messages).to be_blank
+ end
+ end
+ end
+
+ context "comment on a work" do
+ let(:comment) { create(:comment, pseud: no_reply_guy.default_pseud) }
+
+ include_examples "creating guest reply is not allowed"
+ end
+
+ context "comment on an admin post" do
+ let(:comment) { create(:comment, :on_admin_post, pseud: no_reply_guy.default_pseud) }
+
+ include_examples "creating guest reply is not allowed"
+ end
+
+ context "comment on a tag" do
+ let(:comment) { create(:comment, :on_tag, pseud: no_reply_guy.default_pseud) }
+
+ include_examples "creating guest reply is not allowed"
+ end
+
+ context "comment on the user's work" do
+ let(:work) { create(:work, authors: [no_reply_guy.default_pseud]) }
+ let(:comment) { create(:comment, pseud: no_reply_guy.default_pseud, commentable: work.first_chapter) }
+
+ include_examples "creating guest reply is allowed"
+ end
+
+ context "comment on the user's co-creation" do
+ let(:work) { create(:work, authors: [create(:user).default_pseud, no_reply_guy.default_pseud]) }
+ let(:comment) { create(:comment, pseud: no_reply_guy.default_pseud, commentable: work.first_chapter) }
+
+ include_examples "creating guest reply is allowed"
+ end
+
+ context "guest comment" do
+ let(:comment) { create(:comment, :by_guest) }
+
+ include_examples "creating guest reply is allowed"
+ end
+ end
+
describe "#create" do
context "as a tag wrangler" do
let(:tag_wrangler) { create(:tag_wrangler) }
From 317e3e840ce28f1d66e2ede43fdbd3e6bf0218bd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=89ric=20B?=
Date: Tue, 19 Sep 2023 05:59:31 +0200
Subject: [PATCH 057/208] AO3-6467 Track deletion of previous FNOK on update
(#4628)
* AO3-6467 Track deletion of previous fnok on update
https://otwarchive.atlassian.net/browse/AO3-6467?focusedCommentId=360436
* Factorize logging code
* Hound
* Explicit noop if identical fnok
* Guess not needed anymore?
Since we deal with the
"updating no fnok to no fnok"
Above
* Just to be sure
* Simpler code? Maybe?
* Typo
* Factorize logging
* Mark helper method private
Not that kind of helper
* weeklies' proposal
Exact code
Just cannot use Github merge function
Due to having other things in file
* Fix test for new (more coherent?) behavior
* Misc. wording change
Brevity is the soul of wit
---
.../admin/admin_users_controller.rb | 46 +++++---
.../admin/admin_users_controller_spec.rb | 105 +++++++++++++-----
2 files changed, 105 insertions(+), 46 deletions(-)
diff --git a/app/controllers/admin/admin_users_controller.rb b/app/controllers/admin/admin_users_controller.rb
index 5031bef95fa..b90f7abf711 100644
--- a/app/controllers/admin/admin_users_controller.rb
+++ b/app/controllers/admin/admin_users_controller.rb
@@ -68,28 +68,29 @@ def update
def update_next_of_kin
@user = authorize User.find_by!(login: params[:user_login])
- fnok = @user.fannish_next_of_kin
kin = User.find_by(login: params[:next_of_kin_name])
kin_email = params[:next_of_kin_email]
- if kin.blank? && kin_email.blank?
- if fnok.present?
- fnok.destroy
- @user.create_log_item({
- action: ArchiveConfig.ACTION_REMOVE_FNOK,
- fnok_user_id: fnok.kin.id,
- admin_id: current_admin.id,
- note: "Change made by #{current_admin.login}"
- })
- flash[:notice] = ts("Fannish next of kin was removed.")
- end
- redirect_to admin_user_path(@user)
- return
+ fnok = @user.fannish_next_of_kin
+ previous_fnok_user_id = fnok&.kin&.id
+ fnok ||= @user.build_fannish_next_of_kin
+ fnok.assign_attributes(kin: kin, kin_email: kin_email)
+
+ unless fnok.changed?
+ flash[:notice] = ts("No change to fannish next of kin.")
+ redirect_to admin_user_path(@user) and return
+ end
+
+ # Remove FNOK that already exists.
+ if fnok.persisted? && kin.blank? && kin_email.blank?
+ fnok.destroy
+ log_next_of_kin_removed(previous_fnok_user_id)
+ flash[:notice] = ts("Fannish next of kin was removed.")
+ redirect_to admin_user_path(@user) and return
end
- fnok = @user.build_fannish_next_of_kin if fnok.blank?
- fnok.assign_attributes(kin: kin, kin_email: kin_email)
if fnok.save
+ log_next_of_kin_removed(previous_fnok_user_id)
@user.create_log_item({
action: ArchiveConfig.ACTION_ADD_FNOK,
fnok_user_id: fnok.kin.id,
@@ -184,4 +185,17 @@ def activate
def log_items
@log_items ||= (@user.log_items + LogItem.where(fnok_user_id: @user.id)).sort_by(&:created_at).reverse
end
+
+ private
+
+ def log_next_of_kin_removed(user_id)
+ return if user_id.blank?
+
+ @user.create_log_item({
+ action: ArchiveConfig.ACTION_REMOVE_FNOK,
+ fnok_user_id: user_id,
+ admin_id: current_admin.id,
+ note: "Change made by #{current_admin.login}"
+ })
+ end
end
diff --git a/spec/controllers/admin/admin_users_controller_spec.rb b/spec/controllers/admin/admin_users_controller_spec.rb
index ef301528a2b..3fa6393560e 100644
--- a/spec/controllers/admin/admin_users_controller_spec.rb
+++ b/spec/controllers/admin/admin_users_controller_spec.rb
@@ -227,37 +227,82 @@
end
end
- it "logs adding a fannish next of kin" do
- admin = create(:support_admin)
- fake_login_admin(admin)
-
- post :update_next_of_kin, params: {
- user_login: user.login, next_of_kin_name: kin.login, next_of_kin_email: kin.email
- }
- user.reload
- expect(user.fannish_next_of_kin.kin).to eq(kin)
- log_item = user.log_items.last
- expect(log_item.action).to eq(ArchiveConfig.ACTION_ADD_FNOK)
- expect(log_item.fnok_user.id).to eq(kin.id)
- expect(log_item.admin_id).to eq(admin.id)
- expect(log_item.note).to eq("Change made by #{admin.login}")
- end
+ context "when admin has support role" do
+ let(:admin) { create(:support_admin) }
+
+ before { fake_login_admin(admin) }
+
+ it "logs adding a fannish next of kin" do
+ post :update_next_of_kin, params: {
+ user_login: user.login, next_of_kin_name: kin.login, next_of_kin_email: kin.email
+ }
+ user.reload
+ expect(user.fannish_next_of_kin.kin).to eq(kin)
+ log_item = user.log_items.last
+ expect(log_item.action).to eq(ArchiveConfig.ACTION_ADD_FNOK)
+ expect(log_item.fnok_user.id).to eq(kin.id)
+ expect(log_item.admin_id).to eq(admin.id)
+ expect(log_item.note).to eq("Change made by #{admin.login}")
+ end
+
+ it "logs removing a fannish next of kin" do
+ kin_user_id = create(:fannish_next_of_kin, user: user).kin_id
+
+ post :update_next_of_kin, params: {
+ user_login: user.login
+ }
+ user.reload
+ expect(user.fannish_next_of_kin).to be_nil
+ log_item = user.log_items.last
+ expect(log_item.action).to eq(ArchiveConfig.ACTION_REMOVE_FNOK)
+ expect(log_item.fnok_user.id).to eq(kin_user_id)
+ expect(log_item.admin_id).to eq(admin.id)
+ expect(log_item.note).to eq("Change made by #{admin.login}")
+ end
+
+ it "logs updating a fannish next of kin" do
+ previous_kin_user_id = create(:fannish_next_of_kin, user: user).kin_id
- it "logs removing a fannish next of kin" do
- admin = create(:support_admin)
- fake_login_admin(admin)
- kin_user_id = create(:fannish_next_of_kin, user: user).kin_id
-
- post :update_next_of_kin, params: {
- user_login: user.login
- }
- user.reload
- expect(user.fannish_next_of_kin).to be_nil
- log_item = user.log_items.last
- expect(log_item.action).to eq(ArchiveConfig.ACTION_REMOVE_FNOK)
- expect(log_item.fnok_user.id).to eq(kin_user_id)
- expect(log_item.admin_id).to eq(admin.id)
- expect(log_item.note).to eq("Change made by #{admin.login}")
+ post :update_next_of_kin, params: {
+ user_login: user.login, next_of_kin_name: kin.login, next_of_kin_email: kin.email
+ }
+ user.reload
+ expect(user.fannish_next_of_kin.kin).to eq(kin)
+
+ remove_log_item = user.log_items[-2]
+ expect(remove_log_item.action).to eq(ArchiveConfig.ACTION_REMOVE_FNOK)
+ expect(remove_log_item.fnok_user.id).to eq(previous_kin_user_id)
+ expect(remove_log_item.admin_id).to eq(admin.id)
+ expect(remove_log_item.note).to eq("Change made by #{admin.login}")
+
+ add_log_item = user.log_items.last
+ expect(add_log_item.action).to eq(ArchiveConfig.ACTION_ADD_FNOK)
+ expect(add_log_item.fnok_user.id).to eq(kin.id)
+ expect(add_log_item.admin_id).to eq(admin.id)
+ expect(add_log_item.note).to eq("Change made by #{admin.login}")
+ end
+
+ it "does nothing if changing the fnok to themselves" do
+ previous_kin = create(:fannish_next_of_kin, user: user)
+
+ post :update_next_of_kin, params: {
+ user_login: user.login, next_of_kin_name: previous_kin.kin.login, next_of_kin_email: previous_kin.kin_email
+ }
+ it_redirects_to_with_notice(admin_user_path(user), "No change to fannish next of kin.")
+ expect(user.reload.log_items).to be_empty
+ end
+
+ it "errors if trying to add an incomplete fnok" do
+ post :update_next_of_kin, params: {
+ user_login: user.login, next_of_kin_email: ""
+ }
+
+ kin = assigns(:user).fannish_next_of_kin
+ expect(kin).not_to be_valid
+ expect(kin.errors[:kin_email]).to include("can't be blank")
+
+ expect(user.reload.log_items).to be_empty
+ end
end
end
From f21f823d4842860e921909f6043cec3d609271b8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=89ric=20B?=
Date: Mon, 2 Oct 2023 01:09:53 +0200
Subject: [PATCH 058/208] AO3-6467 Log on kin side when a user with a fnok is
deleted (#4633)
* Create explicit "passive" fnok actions
Instead of trying to infer them
From mirroring the "active" actions
* Remove now unneeded index
* Complete controller tests
* Log when user is removed on their fnok side
* Hound
* Hound (bis)
* More explicit variable name
* Move to helper file
Though not a view helper
* Hound and co
* Hound
But like, without forgetting to save before commit
* Useless (check just above)
* Some more cleanup
* Rename
* Merge callbacks
* Cleanup: Move everything to same file
* More consistent name
* More apt concern syntax
---
.../admin/admin_users_controller.rb | 28 ++-------
app/helpers/users_helper.rb | 51 ++++++++++------
app/models/concerns/user_loggable.rb | 61 +++++++++++++++++++
app/models/user.rb | 11 +---
.../admin/admin_users/_user_history.html.erb | 2 +-
config/config.yml | 2 +
...emove_fnok_user_id_index_from_log_items.rb | 5 ++
.../admin/admin_users_controller_spec.rb | 45 ++++++++++----
spec/models/user_spec.rb | 17 ++++++
9 files changed, 157 insertions(+), 65 deletions(-)
create mode 100644 app/models/concerns/user_loggable.rb
create mode 100644 db/migrate/20230920094945_remove_fnok_user_id_index_from_log_items.rb
diff --git a/app/controllers/admin/admin_users_controller.rb b/app/controllers/admin/admin_users_controller.rb
index b90f7abf711..e469d01efc0 100644
--- a/app/controllers/admin/admin_users_controller.rb
+++ b/app/controllers/admin/admin_users_controller.rb
@@ -72,7 +72,7 @@ def update_next_of_kin
kin_email = params[:next_of_kin_email]
fnok = @user.fannish_next_of_kin
- previous_fnok_user_id = fnok&.kin&.id
+ previous_kin = fnok&.kin
fnok ||= @user.build_fannish_next_of_kin
fnok.assign_attributes(kin: kin, kin_email: kin_email)
@@ -84,19 +84,14 @@ def update_next_of_kin
# Remove FNOK that already exists.
if fnok.persisted? && kin.blank? && kin_email.blank?
fnok.destroy
- log_next_of_kin_removed(previous_fnok_user_id)
+ @user.log_removal_of_next_of_kin(previous_kin, admin: current_admin)
flash[:notice] = ts("Fannish next of kin was removed.")
redirect_to admin_user_path(@user) and return
end
if fnok.save
- log_next_of_kin_removed(previous_fnok_user_id)
- @user.create_log_item({
- action: ArchiveConfig.ACTION_ADD_FNOK,
- fnok_user_id: fnok.kin.id,
- admin_id: current_admin.id,
- note: "Change made by #{current_admin.login}"
- })
+ @user.log_removal_of_next_of_kin(previous_kin, admin: current_admin)
+ @user.log_assignment_of_next_of_kin(kin, admin: current_admin)
flash[:notice] = ts("Fannish next of kin was updated.")
redirect_to admin_user_path(@user)
else
@@ -183,19 +178,6 @@ def activate
end
def log_items
- @log_items ||= (@user.log_items + LogItem.where(fnok_user_id: @user.id)).sort_by(&:created_at).reverse
- end
-
- private
-
- def log_next_of_kin_removed(user_id)
- return if user_id.blank?
-
- @user.create_log_item({
- action: ArchiveConfig.ACTION_REMOVE_FNOK,
- fnok_user_id: user_id,
- admin_id: current_admin.id,
- note: "Change made by #{current_admin.login}"
- })
+ @log_items ||= @user.log_items.sort_by(&:created_at).reverse
end
end
diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb
index 8c3b1d38dcc..40cf0133573 100644
--- a/app/helpers/users_helper.rb
+++ b/app/helpers/users_helper.rb
@@ -119,10 +119,10 @@ def authored_items(pseud, work_counts = {}, rec_counts = {})
items.html_safe
end
- def log_item_action_name(item, user)
+ def log_item_action_name(item)
action = item.action
- return fnok_action_name(item, user) if [ArchiveConfig.ACTION_ADD_FNOK, ArchiveConfig.ACTION_REMOVE_FNOK].include?(action)
+ return fnok_action_name(item) if fnok_action?(action)
case action
when ArchiveConfig.ACTION_ACTIVATE
@@ -152,23 +152,6 @@ def log_item_action_name(item, user)
end
end
- def fnok_action_name(item, user)
- action = item.action == ArchiveConfig.ACTION_REMOVE_FNOK ? "removed" : "added"
-
- if item.fnok_user_id == user.id
- user_id = item.user_id
- action_leaf = "was_#{action}"
- else
- user_id = item.fnok_user_id
- action_leaf = "has_#{action}"
- end
-
- t(
- "users_helper.log.fnok.#{action_leaf}",
- user_id: user_id
- )
- end
-
# Give the TOS field in the new user form a different name in non-production environments
# so that it can be filtered out of the log, for ease of debugging
def tos_field_name
@@ -178,4 +161,34 @@ def tos_field_name
'terms_of_service_non_production'
end
end
+
+ private
+
+ def fnok_action?(action)
+ [
+ ArchiveConfig.ACTION_ADD_FNOK,
+ ArchiveConfig.ACTION_REMOVE_FNOK,
+ ArchiveConfig.ACTION_ADDED_AS_FNOK,
+ ArchiveConfig.ACTION_REMOVED_AS_FNOK
+ ].include?(action)
+ end
+
+ def fnok_action_name(item)
+ action_leaf =
+ case item.action
+ when ArchiveConfig.ACTION_ADD_FNOK
+ "has_added"
+ when ArchiveConfig.ACTION_REMOVE_FNOK
+ "has_removed"
+ when ArchiveConfig.ACTION_ADDED_AS_FNOK
+ "was_added"
+ when ArchiveConfig.ACTION_REMOVED_AS_FNOK
+ "was_removed"
+ end
+
+ t(
+ "users_helper.log.fnok.#{action_leaf}",
+ user_id: item.fnok_user_id
+ )
+ end
end
diff --git a/app/models/concerns/user_loggable.rb b/app/models/concerns/user_loggable.rb
new file mode 100644
index 00000000000..6cc6042888f
--- /dev/null
+++ b/app/models/concerns/user_loggable.rb
@@ -0,0 +1,61 @@
+module UserLoggable
+ extend ActiveSupport::Concern
+
+ included do
+ before_destroy :log_removal_of_self_from_fnok_relationships
+ end
+
+ def log_removal_of_self_from_fnok_relationships
+ fannish_next_of_kins.each do |fnok|
+ fnok.user.log_removal_of_next_of_kin(self)
+ end
+
+ successor = fannish_next_of_kin&.kin
+ log_removal_of_next_of_kin(successor)
+ end
+
+ def log_assignment_of_next_of_kin(kin, admin:)
+ log_user_history(
+ ArchiveConfig.ACTION_ADD_FNOK,
+ options: { fnok_user_id: kin.id },
+ admin: admin
+ )
+
+ kin.log_user_history(
+ ArchiveConfig.ACTION_ADDED_AS_FNOK,
+ options: { fnok_user_id: self.id },
+ admin: admin
+ )
+ end
+
+ def log_removal_of_next_of_kin(kin, admin: nil)
+ return if kin.blank?
+
+ log_user_history(
+ ArchiveConfig.ACTION_REMOVE_FNOK,
+ options: { fnok_user_id: kin.id },
+ admin: admin
+ )
+
+ kin.log_user_history(
+ ArchiveConfig.ACTION_REMOVED_AS_FNOK,
+ options: { fnok_user_id: self.id },
+ admin: admin
+ )
+ end
+
+ def log_user_history(action, options: {}, admin: nil)
+ if admin.present?
+ options = {
+ admin_id: admin.id,
+ note: "Change made by #{admin.login}",
+ **options
+ }
+ end
+
+ create_log_item({
+ action: action,
+ **options
+ })
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index ce230a38247..ce899537d40 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -2,6 +2,7 @@ class User < ApplicationRecord
audited
include WorksOwner
include PasswordResetsLimitable
+ include UserLoggable
devise :database_authenticatable,
:confirmable,
@@ -37,7 +38,6 @@ class User < ApplicationRecord
has_many :external_authors, dependent: :destroy
has_many :external_creatorships, foreign_key: "archivist_id"
- before_destroy :log_removal_as_next_of_kin
has_many :fannish_next_of_kins, dependent: :delete_all, inverse_of: :kin, foreign_key: :kin_id
has_one :fannish_next_of_kin, dependent: :destroy
@@ -173,15 +173,6 @@ def remove_user_from_kudos
Kudo.where(user: self).update_all(user_id: nil)
end
- def log_removal_as_next_of_kin
- fannish_next_of_kins.each do |fnok|
- fnok.user.create_log_item({
- action: ArchiveConfig.ACTION_REMOVE_FNOK,
- fnok_user_id: self.id
- })
- end
- end
-
def read_inbox_comments
inbox_comments.where(read: true)
end
diff --git a/app/views/admin/admin_users/_user_history.html.erb b/app/views/admin/admin_users/_user_history.html.erb
index 6e0e80a6fb9..74724d17cb8 100644
--- a/app/views/admin/admin_users/_user_history.html.erb
+++ b/app/views/admin/admin_users/_user_history.html.erb
@@ -36,7 +36,7 @@
@log_items.each do |item| %>
<%= item.created_at %>
- <%= log_item_action_name(item, @user) %><%= item.role&.name %><%= item.enddate %>
+ <%= log_item_action_name(item) %><%= item.role&.name %><%= item.enddate %>
<%= item.note %>
<% end %>
diff --git a/config/config.yml b/config/config.yml
index f7de3254ad3..b0b022493ce 100644
--- a/config/config.yml
+++ b/config/config.yml
@@ -437,6 +437,8 @@ ACTION_TROUBLESHOOT: 10
ACTION_NOTE: 11
ACTION_ADD_FNOK: 12
ACTION_REMOVE_FNOK: 13
+ACTION_ADDED_AS_FNOK: 14
+ACTION_REMOVED_AS_FNOK: 15
# Elasticsearch index prefix
diff --git a/db/migrate/20230920094945_remove_fnok_user_id_index_from_log_items.rb b/db/migrate/20230920094945_remove_fnok_user_id_index_from_log_items.rb
new file mode 100644
index 00000000000..6756f610c7e
--- /dev/null
+++ b/db/migrate/20230920094945_remove_fnok_user_id_index_from_log_items.rb
@@ -0,0 +1,5 @@
+class RemoveFnokUserIdIndexFromLogItems < ActiveRecord::Migration[6.1]
+ def change
+ remove_index :log_items, :fnok_user_id
+ end
+end
diff --git a/spec/controllers/admin/admin_users_controller_spec.rb b/spec/controllers/admin/admin_users_controller_spec.rb
index 3fa6393560e..4d82fdc6f4a 100644
--- a/spec/controllers/admin/admin_users_controller_spec.rb
+++ b/spec/controllers/admin/admin_users_controller_spec.rb
@@ -241,12 +241,16 @@
log_item = user.log_items.last
expect(log_item.action).to eq(ArchiveConfig.ACTION_ADD_FNOK)
expect(log_item.fnok_user.id).to eq(kin.id)
- expect(log_item.admin_id).to eq(admin.id)
- expect(log_item.note).to eq("Change made by #{admin.login}")
+
+ added_log_item = kin.reload.log_items.last
+ expect(added_log_item.action).to eq(ArchiveConfig.ACTION_ADDED_AS_FNOK)
+ expect(added_log_item.fnok_user.id).to eq(user.id)
+
+ expect_changes_made_by(admin, [log_item, added_log_item])
end
it "logs removing a fannish next of kin" do
- kin_user_id = create(:fannish_next_of_kin, user: user).kin_id
+ kin = create(:fannish_next_of_kin, user: user).kin
post :update_next_of_kin, params: {
user_login: user.login
@@ -255,13 +259,17 @@
expect(user.fannish_next_of_kin).to be_nil
log_item = user.log_items.last
expect(log_item.action).to eq(ArchiveConfig.ACTION_REMOVE_FNOK)
- expect(log_item.fnok_user.id).to eq(kin_user_id)
- expect(log_item.admin_id).to eq(admin.id)
- expect(log_item.note).to eq("Change made by #{admin.login}")
+ expect(log_item.fnok_user.id).to eq(kin.id)
+
+ removed_log_item = kin.reload.log_items.last
+ expect(removed_log_item.action).to eq(ArchiveConfig.ACTION_REMOVED_AS_FNOK)
+ expect(removed_log_item.fnok_user.id).to eq(user.id)
+
+ expect_changes_made_by(admin, [log_item, removed_log_item])
end
it "logs updating a fannish next of kin" do
- previous_kin_user_id = create(:fannish_next_of_kin, user: user).kin_id
+ previous_kin = create(:fannish_next_of_kin, user: user).kin
post :update_next_of_kin, params: {
user_login: user.login, next_of_kin_name: kin.login, next_of_kin_email: kin.email
@@ -271,15 +279,28 @@
remove_log_item = user.log_items[-2]
expect(remove_log_item.action).to eq(ArchiveConfig.ACTION_REMOVE_FNOK)
- expect(remove_log_item.fnok_user.id).to eq(previous_kin_user_id)
- expect(remove_log_item.admin_id).to eq(admin.id)
- expect(remove_log_item.note).to eq("Change made by #{admin.login}")
+ expect(remove_log_item.fnok_user.id).to eq(previous_kin.id)
add_log_item = user.log_items.last
expect(add_log_item.action).to eq(ArchiveConfig.ACTION_ADD_FNOK)
expect(add_log_item.fnok_user.id).to eq(kin.id)
- expect(add_log_item.admin_id).to eq(admin.id)
- expect(add_log_item.note).to eq("Change made by #{admin.login}")
+
+ removed_log_item = previous_kin.reload.log_items.last
+ expect(removed_log_item.action).to eq(ArchiveConfig.ACTION_REMOVED_AS_FNOK)
+ expect(removed_log_item.fnok_user.id).to eq(user.id)
+
+ added_log_item = kin.reload.log_items.last
+ expect(added_log_item.action).to eq(ArchiveConfig.ACTION_ADDED_AS_FNOK)
+ expect(added_log_item.fnok_user.id).to eq(user.id)
+
+ expect_changes_made_by(admin, [remove_log_item, add_log_item, removed_log_item, added_log_item])
+ end
+
+ def expect_changes_made_by(admin, log_items)
+ log_items.each do |log_item|
+ expect(log_item.admin_id).to eq(admin.id)
+ expect(log_item.note).to eq("Change made by #{admin.login}")
+ end
end
it "does nothing if changing the fnok to themselves" do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index d25d96a0ade..9f0087e9e1d 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -34,6 +34,23 @@
end
end
+ context "when the user has a fnok" do
+ let(:fnok) { create(:fannish_next_of_kin) }
+ let(:user) { fnok.user }
+ let(:kin) { fnok.kin }
+
+ it "logs the fnok removal on the kin side" do
+ user_id = user.id
+ user.destroy!
+
+ log_item = kin.reload.log_items.last
+ expect(log_item.action).to eq(ArchiveConfig.ACTION_REMOVED_AS_FNOK)
+ expect(log_item.fnok_user_id).to eq(user_id)
+ expect(log_item.admin_id).to be_nil
+ expect(log_item.note).to eq("System Generated")
+ end
+ end
+
context "when the user is set as someone else's fnok" do
let(:fnok) { create(:fannish_next_of_kin) }
let(:user) { fnok.kin }
From e611623379925c72c5d73bf8fd024ac549f20675 Mon Sep 17 00:00:00 2001
From: weeklies <80141759+weeklies@users.noreply.github.com>
Date: Mon, 2 Oct 2023 16:45:42 +0800
Subject: [PATCH 059/208] AO3-6328 Allow tags to be sorted by number of uses
(#4632)
* AO3-6328 Allow tags to be sorted by number of uses
* AO3-6328 Added tests and fixed comparison bug
* AO3-6328 Added sort by uses code and tests
* AO3-6328 Fixed uses sort test and added default direction
* AO3-6328 Speed up test, fix test format, Hound recs
* AO3-6328 Hound fixes for tests, revert unrelated
* AO3-6328 Hound fix for instance variables, fix comment
* AO3-6328 Cleaned up rspec and added step to ensure reindex
* AO3-6328 Hound fix, removed unnecessary comments
* Made spacing more consistent with spec tests
* AO3-6328 Add tests for reindexing, fix IndexQueue key computing
* AO3-6328 Hound fixes
* AO3-6328 Use base_class in IndexQueue
* AO3-6328 Feedback fixes
* AO3-6328 Fix incorrect merge conflict resolution
* AO3-6328 Houndilocks
* AO3-6328 Fix outdated code and tests
* AO3-6328 Feedback
* AO3-6328 Fix broken step
* AO3-6328 hound
---------
Co-authored-by: tararosenthal
---
app/controllers/application_controller.rb | 2 +-
app/controllers/tags_controller.rb | 1 +
app/jobs/tag_count_update_job.rb | 2 +-
app/models/indexing/index_queue.rb | 1 +
app/models/search/tag_query.rb | 2 +-
app/models/search/tag_search_form.rb | 6 +-
app/models/tag.rb | 11 ++-
features/step_definitions/tag_steps.rb | 17 +++++
.../tags_and_wrangling/tag_search.feature | 24 ++++++
spec/models/search/tag_query_spec.rb | 10 +++
spec/models/tag_spec.rb | 74 +++++++++++++++++++
11 files changed, 144 insertions(+), 6 deletions(-)
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index ea852588005..59c49d4ef59 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -485,7 +485,7 @@ def valid_sort_column(param, model='work')
if model.to_s.downcase == 'work'
allowed = %w(author title date created_at word_count hit_count)
elsif model.to_s.downcase == 'tag'
- allowed = %w(name created_at taggings_count_cache)
+ allowed = %w[name created_at taggings_count_cache uses]
elsif model.to_s.downcase == 'collection'
allowed = %w(collections.title collections.created_at)
elsif model.to_s.downcase == 'prompt'
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index 1fac4822833..f7ae2d2b6e9 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -409,6 +409,7 @@ def tag_search_params
:type,
:canonical,
:created_at,
+ :uses,
:sort_column,
:sort_direction
)
diff --git a/app/jobs/tag_count_update_job.rb b/app/jobs/tag_count_update_job.rb
index 87f8e1e139b..15007d2e8d2 100644
--- a/app/jobs/tag_count_update_job.rb
+++ b/app/jobs/tag_count_update_job.rb
@@ -17,7 +17,7 @@ def perform_on_batch(tag_ids)
Tag.transaction do
tag_ids.each do |id|
value = REDIS_GENERAL.get("tag_update_#{id}_value")
- Tag.where(id: id).update_all(taggings_count_cache: value) if value.present?
+ Tag.where(id: id).update(taggings_count_cache: value) if value.present?
end
end
end
diff --git a/app/models/indexing/index_queue.rb b/app/models/indexing/index_queue.rb
index e9ffe3684e3..c1d61cbf913 100644
--- a/app/models/indexing/index_queue.rb
+++ b/app/models/indexing/index_queue.rb
@@ -12,6 +12,7 @@ def self.all
end
def self.get_key(klass, label)
+ klass = klass.is_a?(Class) ? klass.base_class : klass
"index:#{klass.to_s.underscore}:#{label}"
end
diff --git a/app/models/search/tag_query.rb b/app/models/search/tag_query.rb
index 8b90ee7af8a..1e83dab1f7c 100644
--- a/app/models/search/tag_query.rb
+++ b/app/models/search/tag_query.rb
@@ -44,7 +44,7 @@ def per_page
def sort
direction = options[:sort_direction]&.downcase
case options[:sort_column]
- when "taggings_count_cache"
+ when "taggings_count_cache", "uses"
column = "uses"
direction ||= "desc"
when "created_at"
diff --git a/app/models/search/tag_search_form.rb b/app/models/search/tag_search_form.rb
index df04dc1832c..c3bb4dae569 100644
--- a/app/models/search/tag_search_form.rb
+++ b/app/models/search/tag_search_form.rb
@@ -11,6 +11,7 @@ class TagSearchForm
:fandoms,
:type,
:created_at,
+ :uses,
:sort_column,
:sort_direction
]
@@ -53,11 +54,12 @@ def sort_direction
def sort_options
[
%w[Name name],
- ["Date Created", "created_at"]
+ ["Date Created", "created_at"],
+ %w[Uses uses]
]
end
def default_sort_direction
- %w[created_at].include?(sort_column) ? "desc" : "asc"
+ %w[created_at uses].include?(sort_column) ? "desc" : "asc"
end
end
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 24e979d29cb..7f7e5edca3c 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -50,8 +50,17 @@ def taggings_count_cache_key
end
def write_taggings_to_redis(value)
+ # Atomically set the value while extracting the old value.
+ old_redis_value = REDIS_GENERAL.getset("tag_update_#{id}_value", value).to_i
+
+ # If the value hasn't changed from the saved version or the REDIS version,
+ # there's no need to write an update to the database, so let's just bail
+ # out.
+ return value if value == old_redis_value && value == taggings_count_cache
+
+ # If we've reached here, then the value has changed, and we need to make
+ # sure that the new value is written to the database.
REDIS_GENERAL.sadd("tag_update", id)
- REDIS_GENERAL.set("tag_update_#{id}_value", value)
value
end
diff --git a/features/step_definitions/tag_steps.rb b/features/step_definitions/tag_steps.rb
index 3e3b3d247cc..99704c48311 100644
--- a/features/step_definitions/tag_steps.rb
+++ b/features/step_definitions/tag_steps.rb
@@ -59,6 +59,23 @@
end
end
+Given "a set of tags for tag sort by use exists" do
+ {
+ "10 uses" => 10,
+ "8 uses" => 8,
+ "also 8 uses" => 8,
+ "5 uses" => 5,
+ "2 uses" => 2,
+ "0 uses" => 0
+ }.each do |freeform, uses|
+ tag = Freeform.find_or_create_by_name(freeform.dup)
+ tag.taggings_count = uses
+ end
+
+ step "all indexing jobs have been run"
+ step "the periodic tag count task is run"
+end
+
Given /^I have a canonical "([^\"]*)" fandom tag named "([^\"]*)"$/ do |media, fandom|
fandom = Fandom.find_or_create_by_name(fandom)
fandom.update(canonical: true)
diff --git a/features/tags_and_wrangling/tag_search.feature b/features/tags_and_wrangling/tag_search.feature
index a8952d47291..a265a293626 100644
--- a/features/tags_and_wrangling/tag_search.feature
+++ b/features/tags_and_wrangling/tag_search.feature
@@ -196,3 +196,27 @@ Feature: Search Tags
And the 2nd tag result should contain "created second"
And the 3rd tag result should contain "created third"
And the 4th tag result should contain "created fourth"
+
+ Scenario: Search and sort by Uses in descending and ascending order
+ Given a set of tags for tag sort by use exists
+ When I am on the search tags page
+ And I fill in "Tag name" with "uses"
+ And I select "Uses" from "Sort by"
+ And I select "Descending" from "Sort direction"
+ And I press "Search Tags"
+ Then I should see "6 Found"
+ And the 1st tag result should contain "10 uses"
+ And the 2nd tag result should contain "8 uses"
+ And the 3rd tag result should contain "8 uses"
+ And the 4th tag result should contain "5 uses"
+ And the 5th tag result should contain "2 uses"
+ And the 6th tag result should contain "0 uses"
+ When I select "Ascending" from "Sort direction"
+ And I press "Search Tags"
+ Then I should see "6 Found"
+ And the 1st tag result should contain "0 uses"
+ And the 2nd tag result should contain "2 uses"
+ And the 3rd tag result should contain "5 uses"
+ And the 4th tag result should contain "8 uses"
+ And the 5th tag result should contain "8 uses"
+ And the 6th tag result should contain "10 uses"
diff --git a/spec/models/search/tag_query_spec.rb b/spec/models/search/tag_query_spec.rb
index 7ef558b0255..b0e6f4c1b48 100644
--- a/spec/models/search/tag_query_spec.rb
+++ b/spec/models/search/tag_query_spec.rb
@@ -191,5 +191,15 @@
q = TagQuery.new(sort_column: "created_at", sort_direction: "asc")
expect(q.generated_query[:sort]).to eq([{ "created_at" => { order: "asc", unmapped_type: "date" } }, { id: { order: "asc" } }])
end
+
+ it "allows you to sort by Uses" do
+ q = TagQuery.new(sort_column: "uses")
+ expect(q.generated_query[:sort]).to eq([{ "uses" => { order: "desc" } }, { id: { order: "desc" } }])
+ end
+
+ it "allows you to sort by Uses in ascending order" do
+ q = TagQuery.new(sort_column: "uses", sort_direction: "asc")
+ expect(q.generated_query[:sort]).to eq([{ "uses" => { order: "asc" } }, { id: { order: "asc" } }])
+ end
end
end
diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb
index 7270549f734..16daf57b9bb 100644
--- a/spec/models/tag_spec.rb
+++ b/spec/models/tag_spec.rb
@@ -77,6 +77,80 @@
expect(@fandom_tag.taggings_count_cache).to eq 40 * ArchiveConfig.TAGGINGS_COUNT_CACHE_DIVISOR
end
end
+
+ describe "write_redis_to_database" do
+ let(:tag) { create(:fandom) }
+ let!(:work) { create(:work, fandom_string: tag.name) }
+
+ before do
+ RedisJobSpawner.perform_now("TagCountUpdateJob")
+ tag.reload
+ end
+
+ def expect_tag_update_flag_in_redis_to_be(flag)
+ expect(REDIS_GENERAL.sismember("tag_update", tag.id)).to eq flag
+ end
+
+ it "does not write to the database when reading the count" do
+ tag.taggings_count
+ expect_tag_update_flag_in_redis_to_be(false)
+ end
+
+ it "does not write to the database when assigning the same count" do
+ tag.taggings_count = 1
+ expect_tag_update_flag_in_redis_to_be(false)
+ end
+
+ it "writes to the database when assigning a new count" do
+ tag.taggings_count = 2
+ expect_tag_update_flag_in_redis_to_be(true)
+
+ RedisJobSpawner.perform_now("TagCountUpdateJob")
+ tag.reload
+
+ # Actual number of taggings has not changed though count cache has.
+ expect(tag.taggings_count_cache).to eq 2
+ expect(tag.taggings_count).to eq 1
+ end
+
+ it "writes to the database when adding a new work with the same tag" do
+ create(:work, fandom_string: tag.name)
+ expect_tag_update_flag_in_redis_to_be(true)
+
+ RedisJobSpawner.perform_now("TagCountUpdateJob")
+ tag.reload
+
+ expect(tag.taggings_count_cache).to eq 2
+ expect(tag.taggings_count).to eq 2
+ end
+
+ it "does not write to the database with a blank value" do
+ # Blank values will cause errors if assigned earlier due to division
+ # in taggings_count_expiry.
+ REDIS_GENERAL.set("tag_update_#{tag.id}_value", "")
+ REDIS_GENERAL.sadd("tag_update", tag.id)
+
+ RedisJobSpawner.perform_now("TagCountUpdateJob")
+
+ expect(tag.reload.taggings_count_cache).to eq 1
+ end
+
+ it "triggers reindexing of tags which aren't used much" do
+ create(:work, fandom_string: tag.name)
+
+ expect { RedisJobSpawner.perform_now("TagCountUpdateJob") }
+ .to add_to_reindex_queue(tag.reload, :main)
+ end
+
+ it "triggers reindexing of tags which are used significantly" do
+ ArchiveConfig.TAGGINGS_COUNT_MIN_CACHE_COUNT.times do
+ create(:work, fandom_string: tag.name)
+ end
+
+ expect { RedisJobSpawner.perform_now("TagCountUpdateJob") }
+ .to add_to_reindex_queue(tag.reload, :main)
+ end
+ end
end
it "should not be valid without a name" do
From 660a67aae71f2e0dbfc0316727ce7ce160ed607a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=89ric=20B?=
Date: Wed, 4 Oct 2023 07:07:36 +0200
Subject: [PATCH 060/208] AO3-6487 More attempts at fixing autocompletes after
a fandom is made a synonym (#4635)
* Force autocomplete refresh every time
* Explicit refresh after synning
* Calling method on nil error
Similar behavior to update_search
* Actually do what the code is supposed to do
* Cop
* Only call relevant bit
The "remove_stale" does nothing useful
Outside of a direct ActiveRecord callback
(no distinct "before_save" value)
* Refacto
* Remove final "batch fix"
* Add unit tests
* Hound
* Irrelevant when removing
* Remove overkill safe operator
* Not so useless after all
* Fuse methods
* More verbose but "lazier" code
* Hound
* Hound (bis)
* Review + A bunch of tests for good measure
* Hound
* Dirty method confusion
---
app/models/common_tagging.rb | 16 ++++--
app/models/tag.rb | 40 +++++++++++----
spec/models/tag_wrangling_spec.rb | 83 ++++++++++++++++++++++++++++---
3 files changed, 119 insertions(+), 20 deletions(-)
diff --git a/app/models/common_tagging.rb b/app/models/common_tagging.rb
index 7f013574f65..0a88f045550 100644
--- a/app/models/common_tagging.rb
+++ b/app/models/common_tagging.rb
@@ -19,7 +19,9 @@ class CommonTagging < ApplicationRecord
after_create :update_wrangler
after_create :inherit_parents
after_create :remove_uncategorized_media
- after_create :update_child_autocomplete
+ after_create :add_to_autocomplete
+
+ before_destroy :remove_from_autocomplete
after_commit :update_search
@@ -29,8 +31,16 @@ def update_wrangler
end
end
- def update_child_autocomplete
- common_tag.refresh_autocomplete
+ def add_to_autocomplete
+ return unless filterable.is_a?(Fandom) && common_tag.eligible_for_fandom_autocomplete?
+
+ common_tag.add_to_fandom_autocomplete(filterable)
+ end
+
+ def remove_from_autocomplete
+ return unless filterable.is_a?(Fandom) && common_tag&.was_eligible_for_fandom_autocomplete?
+
+ common_tag.remove_from_fandom_autocomplete(filterable)
end
# A relationship should inherit its characters' fandoms
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 7f7e5edca3c..519a36de453 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -480,30 +480,48 @@ def autocomplete_prefixes
end
def add_to_autocomplete(score = nil)
- score ||= autocomplete_score
- if self.is_a?(Character) || self.is_a?(Relationship)
+ if eligible_for_fandom_autocomplete?
parents.each do |parent|
- REDIS_AUTOCOMPLETE.zadd(self.transliterate("autocomplete_fandom_#{parent.name.downcase}_#{type.downcase}"), score, autocomplete_value) if parent.is_a?(Fandom)
+ add_to_fandom_autocomplete(parent, score) if parent.is_a?(Fandom)
end
end
super
end
+ def add_to_fandom_autocomplete(fandom, score = nil)
+ score ||= autocomplete_score
+ REDIS_AUTOCOMPLETE.zadd(self.transliterate("autocomplete_fandom_#{fandom.name.downcase}_#{type.downcase}"), score, autocomplete_value)
+ end
+
def remove_from_autocomplete
super
- if self.is_a?(Character) || self.is_a?(Relationship)
- parents.each do |parent|
- REDIS_AUTOCOMPLETE.zrem(self.transliterate("autocomplete_fandom_#{parent.name.downcase}_#{type.downcase}"), autocomplete_value) if parent.is_a?(Fandom)
- end
+
+ return unless was_eligible_for_fandom_autocomplete?
+
+ parents.each do |parent|
+ remove_from_fandom_autocomplete(parent) if parent.is_a?(Fandom)
end
end
+ def remove_from_fandom_autocomplete(fandom)
+ REDIS_AUTOCOMPLETE.zrem(self.transliterate("autocomplete_fandom_#{fandom.name.downcase}_#{type.downcase}"), autocomplete_value)
+ end
+
+ def eligible_for_fandom_autocomplete?
+ (self.is_a?(Character) || self.is_a?(Relationship)) && canonical
+ end
+
+ def was_eligible_for_fandom_autocomplete?
+ (self.is_a?(Character) || self.is_a?(Relationship)) && (canonical || canonical_before_last_save)
+ end
+
def remove_stale_from_autocomplete
super
- if self.is_a?(Character) || self.is_a?(Relationship)
- parents.each do |parent|
- REDIS_AUTOCOMPLETE.zrem(self.transliterate("autocomplete_fandom_#{parent.name.downcase}_#{type.downcase}"), autocomplete_value_before_last_save) if parent.is_a?(Fandom)
- end
+
+ return unless was_eligible_for_fandom_autocomplete?
+
+ parents.each do |parent|
+ REDIS_AUTOCOMPLETE.zrem(self.transliterate("autocomplete_fandom_#{parent.name.downcase}_#{type.downcase}"), autocomplete_value_before_last_save) if parent.is_a?(Fandom)
end
end
diff --git a/spec/models/tag_wrangling_spec.rb b/spec/models/tag_wrangling_spec.rb
index ec4f7d03cbd..367db8cc382 100644
--- a/spec/models/tag_wrangling_spec.rb
+++ b/spec/models/tag_wrangling_spec.rb
@@ -193,20 +193,17 @@
synonym.add_association(child)
synonym.reload
- fandom_redis_key = Tag.transliterate("autocomplete_fandom_#{fandom.name.downcase}_character")
-
- expect(REDIS_AUTOCOMPLETE.exists(fandom_redis_key)).to be false
+ expect_autocomplete_to_return(fandom, [])
synonym.update!(syn_string: fandom.name)
- User.current_user = nil # No current user in asynchronous context (?)
+ User.current_user = nil # No current user in asynchronous context
perform_enqueued_jobs
expect(fandom.children.reload).to contain_exactly(child)
expect(synonym.children.reload).to be_empty
- expect(REDIS_AUTOCOMPLETE.exists(fandom_redis_key)).to be true
- expect(REDIS_AUTOCOMPLETE.zrange(fandom_redis_key, 0, -1)).to eq(["#{child.id}: #{child.name}"])
+ expect_autocomplete_to_return(fandom, [child])
end
end
@@ -577,4 +574,78 @@
expect(work.filters.reload).not_to include(meta)
end
end
+
+ describe "associations" do
+ it "updates Redis autocomplete when adding a canon character to a canon fandom" do
+ fandom = create(:canonical_fandom)
+ character = create(:canonical_character)
+
+ expect_autocomplete_to_return(fandom, [])
+
+ fandom.add_association(character)
+ expect_autocomplete_to_return(fandom, [character])
+ end
+
+ it "updates Redis autocomplete when removing a character from a fandom" do
+ fandom = create(:canonical_fandom)
+ character = create(:canonical_character)
+
+ fandom.add_association(character)
+ expect_autocomplete_to_return(fandom, [character])
+
+ fandom.child_taggings.destroy_all
+ expect_autocomplete_to_return(fandom, [])
+ end
+
+ it "updates Redis autocomplete when a character is deleted" do
+ fandom = create(:canonical_fandom)
+ character = create(:canonical_character)
+
+ fandom.add_association(character)
+ expect_autocomplete_to_return(fandom, [character])
+
+ character.destroy
+ expect_autocomplete_to_return(fandom, [])
+ end
+
+ it "adds to autocomplete when a character becomes canonical" do
+ fandom = create(:canonical_fandom)
+ character = create(:character)
+
+ fandom.add_association(character)
+ expect_autocomplete_to_return(fandom, [])
+
+ character.reload.update! canonical: true
+ expect_autocomplete_to_return(fandom, [character])
+ end
+
+ it "removes from autocomplete when a character loses its canonicity" do
+ fandom = create(:canonical_fandom)
+ character = create(:canonical_character)
+
+ fandom.add_association(character)
+ expect_autocomplete_to_return(fandom, [character])
+
+ character.reload.update! canonical: false
+ expect_autocomplete_to_return(fandom, [])
+ end
+
+ it "updates autocomplete when a character name changes" do
+ fandom = create(:canonical_fandom)
+ character = create(:canonical_character)
+
+ fandom.add_association(character)
+ expect_autocomplete_to_return(fandom, [character])
+
+ User.current_user = create(:admin)
+ character.reload.update! name: "Toto"
+ expect_autocomplete_to_return(fandom, [character])
+ end
+ end
+
+ def expect_autocomplete_to_return(fandom, characters)
+ redis_key = Tag.transliterate("autocomplete_fandom_#{fandom.name.downcase}_character")
+ redis_store = REDIS_AUTOCOMPLETE.zrange(redis_key, 0, -1)
+ expect(redis_store).to eq characters.map { |character| "#{character.id}: #{character.name}" }
+ end
end
From 4cacf7be1aa0559b31b075dae9cc14709b7a25ad Mon Sep 17 00:00:00 2001
From: weeklies <80141759+weeklies@users.noreply.github.com>
Date: Mon, 16 Oct 2023 23:44:16 +0100
Subject: [PATCH 061/208] AO3-6328 Try to fix large tags not updating in uses
sort (#4637)
* AO3-6328 Fix large tags (cached tags) not updating in uses sort
* Style
* AO3-6328-spr Hound
* AO3-6328-spr Comment tweak
* AO3-6328-spr Hound?
---
app/models/tag.rb | 3 +++
spec/models/tag_spec.rb | 27 +++++++++++++++------------
2 files changed, 18 insertions(+), 12 deletions(-)
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 519a36de453..c3af4180e39 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -1133,6 +1133,9 @@ def after_update
end
end
+ # Ensure tags with count cache can be reindexed.
+ Rails.cache.delete(taggings_count_cache_key) if tag.saved_change_to_taggings_count_cache?
+
# Reindex immediately to update the unwrangled bin.
if tag.saved_change_to_unwrangleable?
tag.reindex_document
diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb
index 16daf57b9bb..0cb0ca34c0f 100644
--- a/spec/models/tag_spec.rb
+++ b/spec/models/tag_spec.rb
@@ -82,7 +82,9 @@
let(:tag) { create(:fandom) }
let!(:work) { create(:work, fandom_string: tag.name) }
- before do
+ before { run_update_tag_count_job }
+
+ def run_update_tag_count_job
RedisJobSpawner.perform_now("TagCountUpdateJob")
tag.reload
end
@@ -105,8 +107,7 @@ def expect_tag_update_flag_in_redis_to_be(flag)
tag.taggings_count = 2
expect_tag_update_flag_in_redis_to_be(true)
- RedisJobSpawner.perform_now("TagCountUpdateJob")
- tag.reload
+ run_update_tag_count_job
# Actual number of taggings has not changed though count cache has.
expect(tag.taggings_count_cache).to eq 2
@@ -117,8 +118,7 @@ def expect_tag_update_flag_in_redis_to_be(flag)
create(:work, fandom_string: tag.name)
expect_tag_update_flag_in_redis_to_be(true)
- RedisJobSpawner.perform_now("TagCountUpdateJob")
- tag.reload
+ run_update_tag_count_job
expect(tag.taggings_count_cache).to eq 2
expect(tag.taggings_count).to eq 2
@@ -130,16 +130,15 @@ def expect_tag_update_flag_in_redis_to_be(flag)
REDIS_GENERAL.set("tag_update_#{tag.id}_value", "")
REDIS_GENERAL.sadd("tag_update", tag.id)
- RedisJobSpawner.perform_now("TagCountUpdateJob")
+ run_update_tag_count_job
- expect(tag.reload.taggings_count_cache).to eq 1
+ expect(tag.taggings_count_cache).to eq 1
end
it "triggers reindexing of tags which aren't used much" do
create(:work, fandom_string: tag.name)
-
- expect { RedisJobSpawner.perform_now("TagCountUpdateJob") }
- .to add_to_reindex_queue(tag.reload, :main)
+ expect { run_update_tag_count_job }
+ .to add_to_reindex_queue(tag, :main)
end
it "triggers reindexing of tags which are used significantly" do
@@ -147,8 +146,12 @@ def expect_tag_update_flag_in_redis_to_be(flag)
create(:work, fandom_string: tag.name)
end
- expect { RedisJobSpawner.perform_now("TagCountUpdateJob") }
- .to add_to_reindex_queue(tag.reload, :main)
+ expect { run_update_tag_count_job }
+ .to add_to_reindex_queue(tag, :main)
+ expect_tag_update_flag_in_redis_to_be(false)
+
+ create(:work, fandom_string: tag.name)
+ expect_tag_update_flag_in_redis_to_be(true)
end
end
end
From 9705a64461a313afc0f0616d4ab0362c7d868309 Mon Sep 17 00:00:00 2001
From: weeklies <80141759+weeklies@users.noreply.github.com>
Date: Mon, 16 Oct 2023 23:47:24 +0100
Subject: [PATCH 062/208] AO3-6483 Make old password changes deserializable
(#4638)
Make old password changes serializable
---
app/models/user.rb | 2 +-
config/application.rb | 9 +++++++--
spec/models/user_spec.rb | 31 +++++++++++++++++++++++++++++++
3 files changed, 39 insertions(+), 3 deletions(-)
diff --git a/app/models/user.rb b/app/models/user.rb
index ce899537d40..c44fc6628ca 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1,5 +1,5 @@
class User < ApplicationRecord
- audited
+ audited redacted: [:encrypted_password, :password_salt]
include WorksOwner
include PasswordResetsLimitable
include UserLoggable
diff --git a/config/application.rb b/config/application.rb
index 3c926ddd721..3b62231c19d 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -75,8 +75,13 @@ class Application < Rails::Application
# Keeps updated_at in cache keys
config.active_record.cache_versioning = false
- # This class is not allowed by deafult when upgrading Rails to 6.0.5.1 patch
- config.active_record.yaml_column_permitted_classes = [ActiveSupport::TimeWithZone, Time, ActiveSupport::TimeZone]
+ # This class is not allowed by default when upgrading Rails to 6.0.5.1 patch
+ config.active_record.yaml_column_permitted_classes = [
+ ActiveSupport::TimeWithZone,
+ Time,
+ ActiveSupport::TimeZone,
+ BCrypt::Password
+ ]
# handle errors with custom error pages:
config.exceptions_app = self.routes
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 9f0087e9e1d..836a6f93052 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -213,6 +213,37 @@
end
end
+ context "password was recently changed" do
+ before do
+ pw = Faker::Lorem.characters(number: ArchiveConfig.PASSWORD_LENGTH_MIN)
+ existing_user.update!(password: pw, password_confirmation: pw)
+ end
+
+ redacted_value = "[REDACTED]"
+ redacted_arr = Array.new(2, redacted_value)
+
+ it "audits and redacts password changes" do
+ last_change = existing_user.audits.pluck(:audited_changes).last
+
+ expect(last_change["encrypted_password"]).to eq(redacted_arr)
+ end
+
+ it "deserializes old BCrypt password changes" do
+ salt = SecureRandom.urlsafe_base64(15)
+ bcrypt_password = BCrypt::Password.create(
+ ["another_password", salt].flatten.join,
+ cost: ArchiveConfig.BCRYPT_COST || 14
+ )
+
+ existing_user.update!(encrypted_password: bcrypt_password, password_salt: salt)
+
+ last_change = existing_user.audits.pluck(:audited_changes).last
+
+ expect(last_change["encrypted_password"]).to eq(redacted_arr)
+ expect(last_change["password_salt"]).to eq(redacted_arr)
+ end
+ end
+
context "username was changed outside window" do
before do
travel_to ArchiveConfig.USER_RENAME_LIMIT_DAYS.days.ago do
From 848b4b8d1f63464da5311c9531402ef4195623f8 Mon Sep 17 00:00:00 2001
From: sarken
Date: Tue, 17 Oct 2023 19:14:50 -0400
Subject: [PATCH 063/208] Revert "AO3-6328 Try to fix large tags not updating
in uses sort" (#4641)
Revert "AO3-6328 Try to fix large tags not updating in uses sort (#4637)"
This reverts commit 4cacf7be1aa0559b31b075dae9cc14709b7a25ad.
---
app/models/tag.rb | 3 ---
spec/models/tag_spec.rb | 27 ++++++++++++---------------
2 files changed, 12 insertions(+), 18 deletions(-)
diff --git a/app/models/tag.rb b/app/models/tag.rb
index c3af4180e39..519a36de453 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -1133,9 +1133,6 @@ def after_update
end
end
- # Ensure tags with count cache can be reindexed.
- Rails.cache.delete(taggings_count_cache_key) if tag.saved_change_to_taggings_count_cache?
-
# Reindex immediately to update the unwrangled bin.
if tag.saved_change_to_unwrangleable?
tag.reindex_document
diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb
index 0cb0ca34c0f..16daf57b9bb 100644
--- a/spec/models/tag_spec.rb
+++ b/spec/models/tag_spec.rb
@@ -82,9 +82,7 @@
let(:tag) { create(:fandom) }
let!(:work) { create(:work, fandom_string: tag.name) }
- before { run_update_tag_count_job }
-
- def run_update_tag_count_job
+ before do
RedisJobSpawner.perform_now("TagCountUpdateJob")
tag.reload
end
@@ -107,7 +105,8 @@ def expect_tag_update_flag_in_redis_to_be(flag)
tag.taggings_count = 2
expect_tag_update_flag_in_redis_to_be(true)
- run_update_tag_count_job
+ RedisJobSpawner.perform_now("TagCountUpdateJob")
+ tag.reload
# Actual number of taggings has not changed though count cache has.
expect(tag.taggings_count_cache).to eq 2
@@ -118,7 +117,8 @@ def expect_tag_update_flag_in_redis_to_be(flag)
create(:work, fandom_string: tag.name)
expect_tag_update_flag_in_redis_to_be(true)
- run_update_tag_count_job
+ RedisJobSpawner.perform_now("TagCountUpdateJob")
+ tag.reload
expect(tag.taggings_count_cache).to eq 2
expect(tag.taggings_count).to eq 2
@@ -130,15 +130,16 @@ def expect_tag_update_flag_in_redis_to_be(flag)
REDIS_GENERAL.set("tag_update_#{tag.id}_value", "")
REDIS_GENERAL.sadd("tag_update", tag.id)
- run_update_tag_count_job
+ RedisJobSpawner.perform_now("TagCountUpdateJob")
- expect(tag.taggings_count_cache).to eq 1
+ expect(tag.reload.taggings_count_cache).to eq 1
end
it "triggers reindexing of tags which aren't used much" do
create(:work, fandom_string: tag.name)
- expect { run_update_tag_count_job }
- .to add_to_reindex_queue(tag, :main)
+
+ expect { RedisJobSpawner.perform_now("TagCountUpdateJob") }
+ .to add_to_reindex_queue(tag.reload, :main)
end
it "triggers reindexing of tags which are used significantly" do
@@ -146,12 +147,8 @@ def expect_tag_update_flag_in_redis_to_be(flag)
create(:work, fandom_string: tag.name)
end
- expect { run_update_tag_count_job }
- .to add_to_reindex_queue(tag, :main)
- expect_tag_update_flag_in_redis_to_be(false)
-
- create(:work, fandom_string: tag.name)
- expect_tag_update_flag_in_redis_to_be(true)
+ expect { RedisJobSpawner.perform_now("TagCountUpdateJob") }
+ .to add_to_reindex_queue(tag.reload, :main)
end
end
end
From 00f24a8760bb2c3f66d31b9f5e7988d568aef256 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=89ric=20B?=
Date: Wed, 18 Oct 2023 04:14:17 +0200
Subject: [PATCH 064/208] AO3-6621 Update OTW Donation Link (#4639)
* Update donation link
https://otwarchive.atlassian.net/browse/AO3-6621
* i18n keys
* Nicer html_safe syntax
* Move url links and text to keys too
* Lint
* Mail-like i18n structure
---
app/views/home/donate.html.erb | 18 ++++++++++--------
config/locales/views/en.yml | 13 +++++++++++++
features/other_a/homepage.feature | 1 +
features/step_definitions/generic_steps.rb | 4 ++++
4 files changed, 28 insertions(+), 8 deletions(-)
diff --git a/app/views/home/donate.html.erb b/app/views/home/donate.html.erb
index d7ea0a5b2ad..43381eb7631 100644
--- a/app/views/home/donate.html.erb
+++ b/app/views/home/donate.html.erb
@@ -1,12 +1,14 @@
-<%= ts('Donations') %>
-<%= ts('There are two main ways to support the AO3 - donating your time or money.') %>
+<%= t(".general.title") %>
+<%= t(".general.text") %>
-
<%= ts('Donating Your Time') %>
-
<%= ts("The Organization for Transformative Works is the parent organization of the Archive of Our Own. We are often looking for volunteers to contribute to our projects . If you're interested in volunteering specifically to help the Archive of Our Own, the roles to look out for include tag wranglers, testers, coders, Support staff and Abuse staff. We encourage you to browse through the available listings and apply for any roles which match your qualifications and interests.").html_safe %>
+
<%= t(".time.title") %>
+
<%= t(".time.details_html",
+ otw_link: link_to(t(".time.otw"), "http://www.transformativeworks.org"),
+ projects_link: link_to(t(".time.projects"), "http://www.transformativeworks.org/our-projects"),
+ volunteer_link: link_to(t(".time.volunteer"), "http://www.transformativeworks.org/volunteer")) %>
-
<%= ts('Donating Financially') %>
-
<%= ts("The AO3 has ongoing running costs - electricity for the servers and bandwidth so you can reach them -
-and one-off costs such as buying new servers as the number of users and works increases.
-Any donation to the OTW is a big help. (Don't worry, we'll never connect your AO3 username and your financial information.)").html_safe %>
+
<%= t(".money.title") %>
+
<%= t(".money.details_html",
+ donation_link: link_to(t(".money.donation"), "https://donate.transformativeworks.org/otwgive")) %>
diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml
index 292d1b89990..85299af1abe 100644
--- a/config/locales/views/en.yml
+++ b/config/locales/views/en.yml
@@ -454,7 +454,20 @@ en:
twitter: "@AO3_Status Twitter feed"
home:
donate:
+ general:
+ text: There are two main ways to support the AO3 - donating your time or money.
+ title: Donations
+ money:
+ details_html: The AO3 has ongoing running costs - electricity for the servers and bandwidth so you can reach them - and one-off costs such as buying new servers as the number of users and works increases. Any %{donation_link} is a big help. (Don't worry, we'll never connect your AO3 username and your financial information.)
+ donation: donation to the OTW
+ title: Donating Financially
page_title: Donate or Volunteer
+ time:
+ details_html: The %{otw_link} is the parent organization of the Archive of Our Own. We are often looking for volunteers to contribute to %{projects_link}. If you're interested in volunteering specifically to help the Archive of Our Own, the roles to look out for include tag wranglers, testers, coders, Support staff and Abuse staff. We encourage you to browse through the %{volunteer_link} and apply for any roles which match your qualifications and interests.
+ otw: Organization for Transformative Works
+ projects: our projects
+ title: Donating Your Time
+ volunteer: available listings
fandoms:
all_fandoms: All Fandoms
index:
diff --git a/features/other_a/homepage.feature b/features/other_a/homepage.feature
index 3ba09db274e..418a936d11b 100644
--- a/features/other_a/homepage.feature
+++ b/features/other_a/homepage.feature
@@ -24,3 +24,4 @@ Feature: Various things on the homepage
And I follow "Donations"
Then I should see "There are two main ways to support the AO3 - donating your time or money"
And I should see the page title "Donate or Volunteer"
+ And I should see a link "donation to the OTW" to "https://donate.transformativeworks.org/otwgive"
diff --git a/features/step_definitions/generic_steps.rb b/features/step_definitions/generic_steps.rb
index dfb92f099e6..b392f26a921 100644
--- a/features/step_definitions/generic_steps.rb
+++ b/features/step_definitions/generic_steps.rb
@@ -240,6 +240,10 @@ def assure_xpath_not_present(tag, attribute, value, selector)
page.body.should =~ /#{Regexp.escape(text)}/m
end
+Then "I should see a link {string} to {string}" do |text, href|
+ expect(page).to have_link(text, href: href)
+end
+
Then /^I should not see a link "([^\"]*)"$/ do |name|
text = name + ""
page.body.should_not =~ /#{Regexp.escape(text)}/m
From 53fc408f8590e585647f1b9d93a55c2a3c0f2164 Mon Sep 17 00:00:00 2001
From: sarken
Date: Fri, 20 Oct 2023 04:06:25 -0400
Subject: [PATCH 065/208] AO3-6618 Escape characters on bookmark fields fetched
with JavaScript (#4642)
---
app/views/external_works/fetch.js.erb | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/views/external_works/fetch.js.erb b/app/views/external_works/fetch.js.erb
index 2e3ca855796..cb20f7911cb 100644
--- a/app/views/external_works/fetch.js.erb
+++ b/app/views/external_works/fetch.js.erb
@@ -1,6 +1,6 @@
<% unless @external_work.blank? %>
- $j('#external_work_author').val("<%= @external_work.author %>").change();
- $j('#external_work_title').val("<%= @external_work.title %>").change();
+ $j('#external_work_author').val("<%= escape_javascript(@external_work.author.html_safe) %>").change();
+ $j('#external_work_title').val("<%= escape_javascript(@external_work.title) %>").change();
$j('#external_work_summary').val("<%= escape_javascript(@external_work.summary&.html_safe) %>").change();
$j('#fetched').val("<%= @external_work.id %>");
$j('#external_work_rating_string').val("<%= @external_work.rating_string %>");
From d26b22a069dc6e7ea481faad2b4166c219128990 Mon Sep 17 00:00:00 2001
From: Bilka
Date: Sun, 29 Oct 2023 05:46:55 +0100
Subject: [PATCH 066/208] AO3-5776 Remove mention of author from download
afterword (#4615)
* AO3-5776 Remove mention of author from download afterword
* AO3-5776 Capitalize Archive
* AO3-5776 Change i18n to be English only for now
---
app/views/downloads/_download_afterword.html.erb | 2 +-
app/views/downloads/show.html.erb | 4 ++--
config/locales/views/en.yml | 4 ++++
features/works/work_download.feature | 7 +++++++
4 files changed, 14 insertions(+), 3 deletions(-)
diff --git a/app/views/downloads/_download_afterword.html.erb b/app/views/downloads/_download_afterword.html.erb
index 24068709cde..ee37a6ecbba 100644
--- a/app/views/downloads/_download_afterword.html.erb
+++ b/app/views/downloads/_download_afterword.html.erb
@@ -23,5 +23,5 @@
<% end %>
- <%= ts("Please") %> <%= link_to ts("drop by the archive and comment"), new_work_comment_url(@work) %> <%= ts("to let the author know if you enjoyed their work!") %>
+ <%= t(".please_comment_html", work_comment_link: link_to(t(".work_comment", locale: :en), new_work_comment_url(@work)), locale: :en) %>
diff --git a/app/views/downloads/show.html.erb b/app/views/downloads/show.html.erb
index 7267bba6bb3..d709dd8ecd7 100644
--- a/app/views/downloads/show.html.erb
+++ b/app/views/downloads/show.html.erb
@@ -1,4 +1,4 @@
-<%= render 'downloads/download_preface.html' %>
+<%= render "downloads/download_preface" %>
<% if @chapters.size > 1 %>
@@ -14,4 +14,4 @@
<% end %>
-<%= render 'downloads/download_afterword.html' %>
+<%= render "downloads/download_afterword" %>
diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml
index 85299af1abe..314ec125bea 100644
--- a/config/locales/views/en.yml
+++ b/config/locales/views/en.yml
@@ -396,6 +396,10 @@ en:
disable_anon: Sorry, this work doesn't allow non-Archive users to comment.
hidden: Sorry, you can't add or edit comments on a hidden work.
unrevealed: Sorry, you can't add or edit comments on an unrevealed work.
+ downloads:
+ download_afterword:
+ please_comment_html: Please %{work_comment_link} to let the creator know if you enjoyed their work!
+ work_comment: drop by the Archive and comment
feedbacks:
new:
abuse:
diff --git a/features/works/work_download.feature b/features/works/work_download.feature
index 0b2a13e393b..120a0050582 100644
--- a/features/works/work_download.feature
+++ b/features/works/work_download.feature
@@ -114,6 +114,13 @@ Feature: Download a work
And "Words:" should appear before "Chapters:"
And "Chapters:" should appear before "Could be downloaded"
+ Scenario: Downloaded work afterword does not mention author
+
+ Given the work "Downloadable"
+ When I view the work "Downloadable"
+ And I follow "HTML"
+ Then I should not see "to let the author know if you enjoyed"
+ But I should see "to let the creator know if you enjoyed"
Scenario: Download of chaptered works includes chapters
From 86b711593ae989807d5e51794399e5c4897a0a86 Mon Sep 17 00:00:00 2001
From: Brian Austin <13002992+brianjaustin@users.noreply.github.com>
Date: Sun, 29 Oct 2023 00:47:36 -0400
Subject: [PATCH 067/208] AO3-5774 Pluralize tag categories in downloaded work
meta (#4618)
* AO3-5774 Pluralize tags in downloaded work meta
* hardcode locale (for now)
---
app/views/downloads/_download_preface.html.erb | 2 +-
features/works/work_download.feature | 9 ++++-----
2 files changed, 5 insertions(+), 6 deletions(-)
diff --git a/app/views/downloads/_download_preface.html.erb b/app/views/downloads/_download_preface.html.erb
index 2b00ea49b26..ef111f31df8 100644
--- a/app/views/downloads/_download_preface.html.erb
+++ b/app/views/downloads/_download_preface.html.erb
@@ -14,7 +14,7 @@
<% Tag::VISIBLE.each do |type| %>
<% tags = @work.tag_groups[type] %>
<% unless tags.blank? %>
- <%= type.constantize::NAME %>:
+ <%= tags.size == 1 ? type.constantize::NAME : type.constantize::NAME.pluralize(locale: :en) %>:
<%= tags.map {|t| link_to(t.display_name, tag_url(t))}.join(", ").html_safe %>
<% end %>
<% end %>
diff --git a/features/works/work_download.feature b/features/works/work_download.feature
index 120a0050582..bc2230dfb3c 100644
--- a/features/works/work_download.feature
+++ b/features/works/work_download.feature
@@ -88,9 +88,8 @@ Feature: Download a work
And I should see "Archive Warning: No Archive Warnings Apply"
And I should see "Category: Gen"
And I should see "Fandom: Cool Fandom"
- # TODO: Update "Character" and "Relationship" to plural form when AO3-5774 is fixed
- And I should see "Relationship: Character 1/Character 2, Character 1 & Character 3"
- And I should see "Character: Character 1, Character 2, Character 3"
+ And I should see "Relationships: Character 1/Character 2, Character 1 & Character 3"
+ And I should see "Characters: Character 1, Character 2, Character 3"
And I should see "Additional Tags: Modern AU"
And I should see "Language: English"
And I should see "Series: Part 1 of THE DOWN"
@@ -103,8 +102,8 @@ Feature: Download a work
And "Archive Warning:" should appear before "Category"
And "Category:" should appear before "Fandom"
And "Fandom:" should appear before "Relationship"
- And "Relationship:" should appear before "Character"
- And "Character:" should appear before "Additional Tags"
+ And "Relationships:" should appear before "Character"
+ And "Characters:" should appear before "Additional Tags"
And "Additional Tags:" should appear before "Language"
And "Language:" should appear before "Series"
And "Series:" should appear before "Collections"
From 0b64933fabe61713f6c72db08110cdba95c36e88 Mon Sep 17 00:00:00 2001
From: weeklies <80141759+weeklies@users.noreply.github.com>
Date: Sun, 29 Oct 2023 12:56:07 +0000
Subject: [PATCH 068/208] AO3-6058 Hide byline of unrevealed work in notes of
related work (#4607)
* A related work should not break anonymity
* Follow review
* Improve senario title
* Respect unrevealed collections in downloads
* Fix test
* Follow recomendations :)
* fix test
* AO3-6058 i18n and add tests for downloads
* AO3-6058 External works use default formats
* Remove deprecated .html suffix
* Add more tests
* AO3-6058 Hound
* AO3-6058 First draft
* AO3-6058 Fix some logic
* AO3-6058 Make sure download URLS update
* AO3-6058 Feedback
* AO3-6058 Hi Reviewdog
* AO3-6058 Doggo
* AO3-6058 oops
* AO3-6058 Feedback draft
* AO3-6058 Add i18n hints
* AO3-6058 Style fix
* AO3-6058 Hound
* AO3-6058 The sequel
* AO3-6058 Fixing
* AO3-6058 Fix
* AO3-6058 Fix
* AO3-6058 Fix downloads not being able to render restricted notes
* AO3-6058 Avoid periods and update tests
* AO3-6058 Hardcode locale for downloads
---------
Co-authored-by: vagrant
Co-authored-by: zz9pzza
Co-authored-by: sarken
---
app/helpers/works_helper.rb | 33 ++++++++
app/models/work.rb | 10 ++-
.../downloads/_download_afterword.html.erb | 19 +++--
.../downloads/_download_preface.html.erb | 16 ++--
.../works/_work_approved_children.html.erb | 30 +++----
app/views/works/_work_header_notes.html.erb | 32 +++-----
config/i18n-tasks.yml | 2 +-
config/locales/views/en.yml | 33 ++++++++
.../step_definitions/work_related_steps.rb | 21 ++++-
features/works/work_download.feature | 78 +++++++++++++++++++
features/works/work_related.feature | 67 +++++++++++++++-
11 files changed, 279 insertions(+), 62 deletions(-)
diff --git a/app/helpers/works_helper.rb b/app/helpers/works_helper.rb
index f6965a54c7e..4e92daa3515 100644
--- a/app/helpers/works_helper.rb
+++ b/app/helpers/works_helper.rb
@@ -112,6 +112,39 @@ def get_inspired_by(work)
work.approved_related_works.where(translation: false)
end
+ def related_work_note(related_work, relation, download: false)
+ work_link = link_to related_work.title, polymorphic_url(related_work)
+ language = tag.span(related_work.language.name, lang: related_work.language.short) if related_work.language
+ default_locale = download ? :en : nil
+
+ creator_link = if download
+ byline(related_work, visibility: "public", only_path: false)
+ else
+ byline(related_work)
+ end
+
+ if related_work.respond_to?(:unrevealed?) && related_work.unrevealed?
+ if relation == "translated_to"
+ t(".#{relation}.unrevealed_html",
+ language: language)
+ else
+ t(".#{relation}.unrevealed",
+ locale: default_locale)
+ end
+ elsif related_work.restricted? && (download || !logged_in?)
+ t(".#{relation}.restricted_html",
+ language: language,
+ locale: default_locale,
+ creator_link: creator_link)
+ else
+ t(".#{relation}.revealed_html",
+ language: language,
+ locale: default_locale,
+ work_link: work_link,
+ creator_link: creator_link)
+ end
+ end
+
# Can the work be downloaded, i.e. is it posted and visible to all registered
# users.
def downloadable?
diff --git a/app/models/work.rb b/app/models/work.rb
index c4e3edcae86..e6917ecf8cf 100755
--- a/app/models/work.rb
+++ b/app/models/work.rb
@@ -211,7 +211,7 @@ def new_recipients_allow_gifts
after_save :moderate_spam
after_save :notify_of_hiding
- after_save :notify_recipients, :expire_caches, :update_pseud_index, :update_tag_index, :touch_series
+ after_save :notify_recipients, :expire_caches, :update_pseud_index, :update_tag_index, :touch_series, :touch_related_works
after_destroy :expire_caches, :update_pseud_index
before_destroy :send_deleted_work_notification, prepend: true
@@ -923,6 +923,14 @@ def parents_after_saving
parent_work_relationships.reject(&:marked_for_destruction?)
end
+ def touch_related_works
+ return unless saved_change_to_in_unrevealed_collection?
+
+ # Make sure download URLs of child and parent works expire to preserve anonymity.
+ children.touch_all
+ parents_after_saving.each { |rw| rw.parent.touch }
+ end
+
#################################################################################
#
# In this section we define various named scopes that can be chained together
diff --git a/app/views/downloads/_download_afterword.html.erb b/app/views/downloads/_download_afterword.html.erb
index ee37a6ecbba..d139e41dd08 100644
--- a/app/views/downloads/_download_afterword.html.erb
+++ b/app/views/downloads/_download_afterword.html.erb
@@ -10,14 +10,19 @@
<% end %>
- <% unless @work.approved_children.blank? %>
+ <%# i18n-tasks-use t("downloads.download_afterword.inspired_by.restricted_html") %>
+ <%# i18n-tasks-use t("downloads.download_afterword.inspired_by.revealed_html") %>
+ <%# i18n-tasks-use t("downloads.download_afterword.inspired_by.unrevealed") %>
+ <% if @work.approved_children.present? %>
- <%= ts('Works inspired by this one') %>
-
- <%= @work.approved_related_works.where(:translation => false).map{|rw|
-"#{link_to(rw.work.title.html_safe, work_url(rw.work))} #{ts('by')} #{byline(rw.work, visibility: 'public', only_path: false)}"}.join(",
-").html_safe %>
-
+ <%= t(".inspired_by.title") %>
+ <% for child_work in @work.approved_related_works %>
+ <% next if child_work.translation %>
+
+
+ <%= related_work_note(child_work.work, "inspired_by", download: true) %>
+
+ <% end %>
<% end %>
diff --git a/app/views/downloads/_download_preface.html.erb b/app/views/downloads/_download_preface.html.erb
index ef111f31df8..86d692c8a35 100644
--- a/app/views/downloads/_download_preface.html.erb
+++ b/app/views/downloads/_download_preface.html.erb
@@ -65,19 +65,19 @@
<% end %>
<% end %>
+ <%# i18n-tasks-use t("downloads.download_preface.inspired_by.restricted_html") %>
+ <%# i18n-tasks-use t("downloads.download_preface.inspired_by.revealed_html") %>
+ <%# i18n-tasks-use t("downloads.download_preface.inspired_by.unrevealed") %>
+ <%# i18n-tasks-use t("downloads.download_preface.translation_of.restricted_html") %>
+ <%# i18n-tasks-use t("downloads.download_preface.translation_of.revealed_html") %>
+ <%# i18n-tasks-use t("downloads.download_preface.translation_of.unrevealed") %>
<% related_works = @work.parent_work_relationships.reject { |wr| !wr.parent } %>
<% if related_works.length > 0 %>
<% related_works.each do |work| %>
- <% relation_text = work.translation ?
- "A translation of %{work_link} by %{author_link}" :
- "Inspired by %{work_link} by %{author_link}"
- %>
- <% work_link = link_to work.parent.title.html_safe, polymorphic_url(work.parent) %>
- <% author_link = byline(work.parent, visibility: 'public', only_path: false) %>
-
- <%= ts(relation_text, work_link: work_link, author_link: author_link).html_safe %>
+ <% relation = work.translation ? "translation_of" : "inspired_by" %>
+ <%= related_work_note(work.parent, relation, download: true) %>
<% end %>
diff --git a/app/views/works/_work_approved_children.html.erb b/app/views/works/_work_approved_children.html.erb
index 577c3db9b39..9f7ee8f430b 100644
--- a/app/views/works/_work_approved_children.html.erb
+++ b/app/views/works/_work_approved_children.html.erb
@@ -1,27 +1,15 @@
-
<%= ts('Works inspired by this one:') %>
+
<%= t(".inspired_by.title") %>:
- <% for child_work in inspired_by %>
-
- <% if child_work.work.is_a?(ExternalWork) %>
- <%= link_to child_work.work.title.html_safe, child_work.work %> <%= ts("by") %> <%= byline(child_work.work) %>
- <% if child_work.translation %>
- <%= ts("(translation into %{language})", language: tag.span(child_work.work.language.name, lang: child_work.work.language.short)).html_safe %>
- <% end %>
- <% elsif child_work.work.restricted? && !logged_in? %>
- <%= ts("A [Restricted Work] by") %> <%= byline(child_work.work) %>
- <% if child_work.translation %>
- <%= ts("(translation into %{language})", language: tag.span(child_work.work.language.name, lang: child_work.work.language.short)).html_safe %>
- <% end %> <%= ts("Log in to view.") %>
- <% else %>
- <%= link_to child_work.work.title.html_safe, child_work.work %> <%= ts("by") %> <%= byline(child_work.work) %>
- <% if child_work.translation %>
- <%= ts("(translation into %{language})", language: tag.span(child_work.work.language.name, lang: child_work.work.language.short)).html_safe %>
- <% end %>
- <% end %>
-
- <% end %>
+ <%# i18n-tasks-use t("works.work_approved_children.inspired_by.restricted_html") %>
+ <%# i18n-tasks-use t("works.work_approved_children.inspired_by.revealed_html") %>
+ <%# i18n-tasks-use t("works.work_approved_children.inspired_by.unrevealed") %>
+ <% for child_work in inspired_by %>
+
+ <%= related_work_note(child_work.work, "inspired_by") %>
+
+ <% end %>
diff --git a/app/views/works/_work_header_notes.html.erb b/app/views/works/_work_header_notes.html.erb
index df180fa8cfd..3b0bae46d0a 100644
--- a/app/views/works/_work_header_notes.html.erb
+++ b/app/views/works/_work_header_notes.html.erb
@@ -11,16 +11,13 @@
<% end %>
<% # translations %>
+ <%# i18n-tasks-use t("works.work_header_notes.translated_to.restricted_html") %>
+ <%# i18n-tasks-use t("works.work_header_notes.translated_to.revealed_html") %>
+ <%# i18n-tasks-use t("works.work_header_notes.translated_to.unrevealed_html") %>
<% for related_work in @work.approved_related_works %>
<% if related_work.translation %>
- <%= ts("Translation into %{language} available: ",
- language: tag.span(related_work.work.language.name, lang: related_work.work.language.short)).html_safe %>
- <% if related_work.work.restricted? && !logged_in? %>
- <%= ts("[Restricted Work] by") %> <%= byline(related_work.work) %>. <%= ts("Log in to view.") %>
- <% else %>
- <%= link_to related_work.work.title.html_safe, related_work.work %> <%= ts("by") %> <%= byline(related_work.work) %>
- <% end %>
+ <%= related_work_note(related_work.work, "translated_to") %>
<% else %>
<% related_works_link ||= link_to ts("other works inspired by this one"), get_related_works_url %>
@@ -28,22 +25,17 @@
<% end %>
<% # parent works %>
+ <%# i18n-tasks-use t("works.work_header_notes.translation_of.restricted_html") %>
+ <%# i18n-tasks-use t("works.work_header_notes.translation_of.revealed_html") %>
+ <%# i18n-tasks-use t("works.work_header_notes.translation_of.unrevealed") %>
+ <%# i18n-tasks-use t("works.work_header_notes.inspired_by.restricted_html") %>
+ <%# i18n-tasks-use t("works.work_header_notes.inspired_by.revealed_html") %>
+ <%# i18n-tasks-use t("works.work_header_notes.inspired_by.unrevealed") %>
<% for related_work in @work.parents_after_saving %>
<% if related_work.parent %>
- <% if related_work.translation %>
- <%= ts('A translation of') %>
- <% else %>
- <%= ts('Inspired by') %>
- <% end %>
-
- <% if related_work.parent.is_a?(ExternalWork) %>
- <%= link_to related_work.parent.title.html_safe, related_work.parent %> by <%= byline(related_work.parent) %>.
- <% elsif related_work.parent.restricted? && !logged_in? %>
- <%= ts("[Restricted Work] by") %> <%= byline(related_work.parent) %>. <%= ts("Log in to view.") %>
- <% else %>
- <%= link_to related_work.parent.title.html_safe, related_work.parent %> <%= ts("by") %> <%= byline(related_work.parent) %>.
- <% end %>
+ <% relation = related_work.translation ? "translation_of" : "inspired_by" %>
+ <%= related_work_note(related_work.parent, relation) %>
<% end %>
<% end %>
diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml
index becfe095384..23acbf623c4 100644
--- a/config/i18n-tasks.yml
+++ b/config/i18n-tasks.yml
@@ -261,7 +261,7 @@ ignore_missing:
# File: app/views/pseuds/show.html.erb
- pseuds.show.edit_link
- pseuds.show.index_link
- # File: app/views/series/manage.html.er
+ # File: app/views/series/manage.html.erb
- series.manage.manage_series
## Consider these keys used:
diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml
index 314ec125bea..1e964d070f0 100644
--- a/config/locales/views/en.yml
+++ b/config/locales/views/en.yml
@@ -398,8 +398,22 @@ en:
unrevealed: Sorry, you can't add or edit comments on an unrevealed work.
downloads:
download_afterword:
+ inspired_by:
+ restricted_html: "[Restricted Work] by %{creator_link}"
+ revealed_html: "%{work_link} by %{creator_link}"
+ title: Works inspired by this one
+ unrevealed: A work in an unrevealed collection
please_comment_html: Please %{work_comment_link} to let the creator know if you enjoyed their work!
work_comment: drop by the Archive and comment
+ download_preface:
+ inspired_by:
+ restricted_html: Inspired by [Restricted Work] by %{creator_link}
+ revealed_html: Inspired by %{work_link} by %{creator_link}
+ unrevealed: Inspired by a work in an unrevealed collection
+ translation_of:
+ restricted_html: A translation of [Restricted Work] by %{creator_link}
+ revealed_html: A translation of %{work_link} by %{creator_link}
+ unrevealed: A translation of a work in an unrevealed collection
feedbacks:
new:
abuse:
@@ -771,5 +785,24 @@ en:
unrestricted: Show to all
show:
unposted_deletion_notice_html: This work is a draft and has not been posted. The draft will be scheduled for deletion on %{deletion_date}.
+ work_approved_children:
+ inspired_by:
+ restricted_html: "[Restricted Work] by %{creator_link} (Log in to access.)"
+ revealed_html: "%{work_link} by %{creator_link}"
+ title: Works inspired by this one
+ unrevealed: A work in an unrevealed collection
+ work_header_notes:
+ inspired_by:
+ restricted_html: Inspired by [Restricted Work] by %{creator_link} (Log in to access.)
+ revealed_html: Inspired by %{work_link} by %{creator_link}
+ unrevealed: Inspired by a work in an unrevealed collection
+ translated_to:
+ restricted_html: 'Translation into %{language} available: [Restricted Work] by %{creator_link} (Log in to access.)'
+ revealed_html: 'Translation into %{language} available: %{work_link} by %{creator_link}'
+ unrevealed_html: 'Translation into %{language} available: A work in an unrevealed collection'
+ translation_of:
+ restricted_html: A translation of [Restricted Work] by %{creator_link} (Log in to access.)
+ revealed_html: A translation of %{work_link} by %{creator_link}
+ unrevealed: A translation of a work in an unrevealed collection
work_module:
draft_deletion_notice_html: This draft will be scheduled for deletion on %{deletion_date}.
diff --git a/features/step_definitions/work_related_steps.rb b/features/step_definitions/work_related_steps.rb
index 8f62be49e59..bda1c52b4ae 100644
--- a/features/step_definitions/work_related_steps.rb
+++ b/features/step_definitions/work_related_steps.rb
@@ -15,7 +15,7 @@
end
Given /^an inspiring parent work has been posted$/ do
- step %{I post an inspiring parent work as testy}
+ step "I post an inspiring parent work as testuser"
end
# given for remixes / related works
@@ -42,7 +42,7 @@
### WHEN
-When /^I post an inspiring parent work as testy$/ do
+When "I post an inspiring parent work as testuser" do
step %{I am logged in as "testuser"}
step %{I post the work "Parent Work"}
end
@@ -159,12 +159,22 @@
step %{I should see "Followup by remixer" within ".afterword .children"}
end
+Then "I should see the related work listed on the original work" do
+ step %{I should see "See the end of the work for other works inspired by this one"}
+ step %{I should see "Works inspired by this one:"}
+ step %{I should see "Followup by remixer"}
+end
+
Then /^I should not see the related work listed on the original work$/ do
step %{I should not see "See the end of the work for other works inspired by this one"}
step %{I should not see "Works inspired by this one:"}
step %{I should not see "Followup by remixer"}
end
+Then "I should not see the inspiring parent work in the beginning notes" do
+ step %{I should not see "Inspired by Parent Work by testuser" within ".preface .notes"}
+end
+
# then for translations
Then /^a parent translated work should be seen$/ do
@@ -173,11 +183,16 @@
step %{I should see "A translation of Worldbuilding by inspiration" within ".preface .notes"}
end
-Then /^I should see the translation in the beginning notes$/ do
+Then "I should see the translation in the beginning notes" do
step %{I should see "Translation into Deutsch available:" within ".preface .notes"}
step %{I should see "Worldbuilding Translated by translator" within ".preface .notes"}
end
+Then "I should see the translation listed on the original work" do
+ step %{I should see "Translation into Deutsch available:"}
+ step %{I should see "Worldbuilding Translated by translator"}
+end
+
Then /^I should not see the translation listed on the original work$/ do
step %{I should not see "Translation into Deutsch available:"}
step %{I should not see "Worldbuilding Translated by translator"}
diff --git a/features/works/work_download.feature b/features/works/work_download.feature
index bc2230dfb3c..e9d16746e5e 100644
--- a/features/works/work_download.feature
+++ b/features/works/work_download.feature
@@ -208,3 +208,81 @@ Feature: Download a work
When I am logged in as a "policy_and_abuse" admin
And I hide the work "TOS Violation"
Then I should not see "Download"
+
+ Scenario: Downloads of related work update when parent work's anonymity changes.
+
+ Given a hidden collection "Hidden"
+ And I have related works setup
+ And I post a related work as remixer
+ And I post a translation as translator
+ And I log out
+ When I view the work "Followup"
+ And I follow "HTML"
+ Then I should see "Worldbuilding by inspiration"
+ When I view the work "Worldbuilding Translated"
+ And I follow "HTML"
+ Then I should see "Worldbuilding by inspiration"
+ # Going from revealed to unrevealed
+ When I am logged in as "inspiration"
+ And I edit the work "Worldbuilding" to be in the collection "Hidden"
+ And I log out
+ And I view the work "Followup"
+ And I follow "HTML"
+ Then I should not see "inspiration"
+ And I should see "Inspired by a work in an unrevealed collection"
+ When I view the work "Worldbuilding Translated"
+ And I follow "HTML"
+ Then I should not see "inspiration"
+ And I should see "A translation of a work in an unrevealed collection"
+ # Going from unrevealed to revealed
+ When I reveal works for "Hidden"
+ And I log out
+ And I view the work "Followup"
+ And I follow "HTML"
+ Then I should see "Worldbuilding by inspiration"
+ When I view the work "Worldbuilding Translated"
+ And I follow "HTML"
+ Then I should see "Worldbuilding by inspiration"
+
+ Scenario: Downloads of related work update when child work's anonymity changes.
+
+ Given a hidden collection "Hidden"
+ And I have related works setup
+ And a related work has been posted and approved
+ When I view the work "Worldbuilding"
+ And I follow "HTML"
+ Then I should see "Followup by remixer"
+ And I should not see "A work in an unrevealed collection"
+ # Going from revealed to unrevealed
+ When I am logged in as "remixer"
+ And I edit the work "Followup" to be in the collection "Hidden"
+ And I view the work "Worldbuilding"
+ And I follow "HTML"
+ Then I should not see "Followup by remixer"
+ And I should see "A work in an unrevealed collection"
+ # Going from unrevealed to revealed
+ When I reveal works for "Hidden"
+ And I log out
+ And I view the work "Worldbuilding"
+ And I follow "HTML"
+ Then I should see "Followup by remixer"
+ And I should not see "A work in an unrevealed collection"
+
+ Scenario: Downloads hide titles of restricted related works
+
+ Given I have related works setup
+ And a related work has been posted and approved
+ And I am logged in as "remixer"
+ And I lock the work "Followup"
+ When I am logged out
+ And I view the work "Worldbuilding"
+ And I follow "HTML"
+ Then I should see "[Restricted Work] by remixer"
+ When I am logged in as "inspiration"
+ And I lock the work "Worldbuilding"
+ And I am logged in as "remixer"
+ And I unlock the work "Followup"
+ And I am logged out
+ And I view the work "Followup"
+ And I follow "HTML"
+ Then I should see "Inspired by [Restricted Work] by inspiration"
diff --git a/features/works/work_related.feature b/features/works/work_related.feature
index 82fc4a8838d..fe74dec069d 100644
--- a/features/works/work_related.feature
+++ b/features/works/work_related.feature
@@ -322,7 +322,7 @@ Scenario: Restricted works listed as Inspiration show up [Restricted] for guests
And I lock the work "Followup"
When I am logged out
And I view the work "Worldbuilding"
- Then I should see "A [Restricted Work] by remixer"
+ Then I should see "[Restricted Work] by remixer"
When I am logged in as "remixer"
And I unlock the work "Followup"
When I am logged out
@@ -658,3 +658,68 @@ Scenario: When a user is notified that a co-authored work has been inspired by a
And I should not see "A work in an unrevealed collection"
And I should not see "Worldbuilding Translated by translator"
And I should not see "From English to Deutsch"
+
+ Scenario: Notes of related work do not break anonymity of parent work in an unrevealed collection
+ Given a hidden collection "Hidden"
+ And I have related works setup
+ When I am logged in as "inspiration"
+ And I edit the work "Worldbuilding" to be in the collection "Hidden"
+ And I post a related work as remixer
+ And I post a translation as translator
+ And I log out
+ # Check remix
+ When I view the work "Followup"
+ Then I should not see "Worldbuilding"
+ And I should not see "inspiration"
+ And I should see "Inspired by a work in an unrevealed collection"
+ # Check translated work
+ When I view the work "Worldbuilding Translated"
+ Then I should not see "inspiration"
+ And I should see "A translation of a work in an unrevealed collection"
+
+ Scenario: Notes of parent work do not break anonymity of child related works in an unrevealed collection
+ Given a hidden collection "Hidden"
+ And I have related works setup
+ And a translation has been posted and approved
+ And a related work has been posted and approved
+ When I am logged in as "translator"
+ And I edit the work "Worldbuilding Translated" to be in the collection "Hidden"
+ When I am logged in as "remixer"
+ And I edit the work "Followup" to be in the collection "Hidden"
+ And I log out
+ When I view the work "Worldbuilding"
+ Then I should not see "Worldbuilding Translated by translator"
+ And I should not see "Followup by remixer"
+ And I should see "A work in an unrevealed collection"
+
+ Scenario: Work notes updates when anonymity of related works change
+ Given a hidden collection "Hidden"
+ And I have related works setup
+ And an inspiring parent work has been posted
+ And a translation has been posted and approved
+ And a related work has been posted and approved
+ # Going from revealed to unrevealed
+ When I am logged in as "inspiration"
+ And I edit the work "Worldbuilding"
+ And I list the work "Parent Work" as inspiration
+ And I press "Post"
+ And I am logged in as "translator"
+ And I edit the work "Worldbuilding Translated" to be in the collection "Hidden"
+ And I am logged in as "remixer"
+ And I edit the work "Followup" to be in the collection "Hidden"
+ And I am logged in as "testuser"
+ And I edit the work "Parent Work" to be in the collection "Hidden"
+ And I log out
+ And I view the work "Worldbuilding"
+ Then I should not see the inspiring parent work in the beginning notes
+ And I should see "Translation into Deutsch available:"
+ And I should see "A work in an unrevealed collection"
+ And I should not see "Worldbuilding Translated by translator"
+ And I should not see "Followup by remixer"
+ # Going from unrevealed to revealed
+ When I reveal works for "Hidden"
+ And I log out
+ When I view the work "Worldbuilding"
+ Then I should see the inspiring parent work in the beginning notes
+ And I should see the translation listed on the original work
+ And I should see the related work listed on the original work
From 06a0fc40c9211eb63679abf2a2595dacd4f6470c Mon Sep 17 00:00:00 2001
From: sarken
Date: Sun, 29 Oct 2023 08:58:39 -0400
Subject: [PATCH 069/208] AO3-5629 Switch from wkhtmltopdf to Calibre for PDF
downloads (#4614)
* AO3-5629 Use Calibre for PDFs
* AO3-5629 One top, one bottom, not two tops
* AO3-5629 Remove change for local testing
* AO3-5629 Code style fixes: double quotes, %w[], indenting
* AO3-5629 More quotation mark fixes
* AO3-5629 Remove comma at end of array
---
.github/workflows/automated-tests.yml | 7 ---
app/models/download_writer.rb | 91 ++++++++++++++-------------
config/docker/Dockerfile | 1 -
script/gh-actions/ebook_converters.sh | 23 +------
4 files changed, 50 insertions(+), 72 deletions(-)
diff --git a/.github/workflows/automated-tests.yml b/.github/workflows/automated-tests.yml
index 647cc25aa21..bd6f3c5a326 100644
--- a/.github/workflows/automated-tests.yml
+++ b/.github/workflows/automated-tests.yml
@@ -104,13 +104,6 @@ jobs:
sudo apt-get install -y redis-server
./script/gh-actions/multiple_redis.sh
- - name: Cache wkhtmltopdf package
- if: ${{ matrix.tests.ebook }}
- uses: actions/cache@v3
- with:
- path: wkhtmltopdf
- key: wkhtmltopdf-${{ hashFiles('script/gh-actions/ebook_converters.sh') }}
-
- name: Install ebook converters
if: ${{ matrix.tests.ebook }}
run: ./script/gh-actions/ebook_converters.sh
diff --git a/app/models/download_writer.rb b/app/models/download_writer.rb
index af557a2dd9e..ffaf219e777 100644
--- a/app/models/download_writer.rb
+++ b/app/models/download_writer.rb
@@ -1,4 +1,4 @@
-require 'open3'
+require "open3"
class DownloadWriter
attr_reader :download, :work
@@ -19,8 +19,8 @@ def generate_html
http_host: ArchiveConfig.APP_HOST
)
renderer.render(
- template: 'downloads/show',
- layout: 'barebones',
+ template: "downloads/show",
+ layout: "barebones",
assigns: {
work: work,
page_title: download.page_title,
@@ -40,7 +40,7 @@ def generate_html_download
# transform HTML version into ebook version
def generate_ebook_download
- return unless %w(azw3 epub mobi pdf).include?(download.file_type)
+ return unless %w[azw3 epub mobi pdf].include?(download.file_type)
return if download.exists?
cmds = get_commands
@@ -59,21 +59,7 @@ def generate_ebook_download
# Get the version of the command we need to execute
def get_commands
- download.file_type == "pdf" ? [get_pdf_command] :
- [get_web2disk_command, get_zip_command, get_calibre_command]
- end
-
- # We're sticking with wkhtmltopdf for PDF files since using calibre for PDF requires the use of xvfb
- def get_pdf_command
- [
- 'wkhtmltopdf',
- '--encoding', 'utf-8',
- '--disable-javascript',
- '--disable-smart-shrinking',
- '--log-level', 'none',
- '--title', download.file_name,
- download.html_file_path, download.file_path
- ]
+ [get_web2disk_command, get_zip_command, get_calibre_command]
end
# Create the format-specific command-line call to calibre/ebook-convert
@@ -81,37 +67,56 @@ def get_calibre_command
# Add info about first series if any
series = []
if meta[:series_title].present?
- series = ['--series', meta[:series_title], '--series-index', meta[:series_position]]
+ series = ["--series", meta[:series_title],
+ "--series-index", meta[:series_position]]
end
### Format-specific options
# epub: don't generate a cover image
- epub = download.file_type == "epub" ? ['--no-default-epub-cover'] : []
+ epub = download.file_type == "epub" ? ["--no-default-epub-cover"] : []
+ # pdf: decrease margins from 72pt default
+ pdf = []
+ if download.file_type == "pdf"
+ pdf = [
+ "--pdf-page-margin-top", "36",
+ "--pdf-page-margin-right", "36",
+ "--pdf-page-margin-bottom", "36",
+ "--pdf-page-margin-left", "36",
+ "--pdf-default-font-size", "17"
+ ]
+ end
+
+ ### CSS options
+ # azw3, epub, and mobi get a special stylesheet
+ css = []
+ if %w[azw3 epub mobi].include?(download.file_type)
+ css = ["--extra-css",
+ Rails.public_path.join("stylesheets/ebooks.css").to_s]
+ end
[
- 'ebook-convert',
+ "ebook-convert",
download.zip_path,
download.file_path,
- '--input-encoding', 'utf-8',
+ "--input-encoding", "utf-8",
# Prevent it from turning links to endnotes into entries for the table of
# contents on works with fewer than the specified number of chapters.
- '--toc-threshold', '0',
- '--use-auto-toc',
- '--title', meta[:title],
- '--title-sort', meta[:sortable_title],
- '--authors', meta[:authors],
- '--author-sort', meta[:sortable_authors],
- '--comments', meta[:summary],
- '--tags', meta[:tags],
- '--pubdate', meta[:pubdate],
- '--publisher', ArchiveConfig.APP_NAME,
- '--language', meta[:language],
- '--extra-css', Rails.public_path.join('stylesheets/ebooks.css').to_s,
+ "--toc-threshold", "0",
+ "--use-auto-toc",
+ "--title", meta[:title],
+ "--title-sort", meta[:sortable_title],
+ "--authors", meta[:authors],
+ "--author-sort", meta[:sortable_authors],
+ "--comments", meta[:summary],
+ "--tags", meta[:tags],
+ "--pubdate", meta[:pubdate],
+ "--publisher", ArchiveConfig.APP_NAME,
+ "--language", meta[:language],
# XPaths for detecting chapters are overly specific to make sure we don't grab
# anything inputted by the user. First path is for single-chapter works,
# second for multi-chapter, and third for the preface and afterword
- '--chapter', "//h:body/h:div[@id='chapters']/h:h2[@class='toc-heading'] | //h:body/h:div[@id='chapters']/h:div[@class='meta group']/h:h2[@class='heading'] | //h:body/h:div[@id='preface' or @id='afterword']/h:h2[@class='toc-heading']"
- ] + series + epub
+ "--chapter", "//h:body/h:div[@id='chapters']/h:h2[@class='toc-heading'] | //h:body/h:div[@id='chapters']/h:div[@class='meta group']/h:h2[@class='heading'] | //h:body/h:div[@id='preface' or @id='afterword']/h:h2[@class='toc-heading']"
+ ] + series + css + epub + pdf
end
# Grab the HTML file and any images and put them in --base-dir.
@@ -120,10 +125,10 @@ def get_calibre_command
# creating an empty stylesheets directory.
def get_web2disk_command
[
- 'web2disk',
- '--base-dir', download.assets_path,
- '--max-recursions', '0',
- '--dont-download-stylesheets',
+ "web2disk",
+ "--base-dir", download.assets_path,
+ "--max-recursions", "0",
+ "--dont-download-stylesheets",
"file://#{download.html_file_path}"
]
end
@@ -131,8 +136,8 @@ def get_web2disk_command
# Zip the directory containing the HTML file and images.
def get_zip_command
[
- 'zip',
- '-r',
+ "zip",
+ "-r",
download.zip_path,
download.assets_path
]
diff --git a/config/docker/Dockerfile b/config/docker/Dockerfile
index e3eb3e4bb10..bee750f6e93 100644
--- a/config/docker/Dockerfile
+++ b/config/docker/Dockerfile
@@ -6,7 +6,6 @@ RUN apt-get update && \
calibre \
default-mysql-client \
shared-mime-info \
- wkhtmltopdf \
zip
# Clean and mount repository at /otwa
diff --git a/script/gh-actions/ebook_converters.sh b/script/gh-actions/ebook_converters.sh
index ac1d1b60d83..7572b891235 100755
--- a/script/gh-actions/ebook_converters.sh
+++ b/script/gh-actions/ebook_converters.sh
@@ -1,24 +1,5 @@
#!/bin/bash
-# We can't use apt-get to install wkhtmltopdf because we use several options
-# (--log-level and --disable-smart-shrinking) that are not available on the
-# version currently available through apt-get. So we have to install
-# xfonts-75dpi and xfonts-base (which are dependencies for wkhtmltox) through
-# apt-get, but install wkhtmltox itself through a direct link to the package.
+sudo apt-get install -y calibre zip
-sudo apt-get install -y calibre zip xfonts-75dpi xfonts-base
-
-mkdir -p wkhtmltopdf
-pushd wkhtmltopdf
-
-FILE="wkhtmltox_0.12.5-1.bionic_amd64.deb"
-
-if [[ ! -e $FILE ]]; then
- wget -N https://media.archiveofourown.org/systems/$FILE
-fi
-
-sudo dpkg -i $FILE
-
-popd
-
-ebook-convert --version && wkhtmltopdf --version
+ebook-convert --version
From 86b4fed219d71ab6822a603d98d14b582dc25121 Mon Sep 17 00:00:00 2001
From: sarken
Date: Sun, 29 Oct 2023 09:07:07 -0400
Subject: [PATCH 070/208] AO3-6625 Replace whitespace in download filenames
with underscores (#4645)
* AO3-6625 Don't include whitespace in download filenames
* AO3-6625 Remove redundant assignment and prefer zero? to == 0
* AO3-6625 Test more spacing and use more succinct code
---
app/models/download.rb | 9 ++-
spec/mailers/user_mailer_spec.rb | 10 ++-
spec/models/download_spec.rb | 74 ++++++++++++++++---
.../shared_examples/mailer_shared_examples.rb | 10 ++-
4 files changed, 83 insertions(+), 20 deletions(-)
diff --git a/app/models/download.rb b/app/models/download.rb
index 0d6eb4abd3e..d7d2c799f4c 100644
--- a/app/models/download.rb
+++ b/app/models/download.rb
@@ -51,10 +51,12 @@ def file_type_from_mime(mime)
end
end
- # The base name of the file (eg, "War and Peace")
+ # The base name of the file (e.g., "War_and_Peace")
def file_name
name = clean(work.title)
- name += " Work #{work.id}" if name.length < 3
+ # If the file name is 1-2 characters, append "_Work_#{work.id}".
+ # If the file name is blank, name the file "Work_#{work.id}".
+ name = [name, "Work_#{work.id}"].compact_blank.join("_") if name.length < 3
name.strip
end
@@ -125,6 +127,7 @@ def chapters
# squash spaces
# strip all non-alphanumeric
# truncate to 24 chars at a word boundary
+ # replace whitespace with underscore for bug with epub table of contents on Kindle (AO3-6625)
def clean(string)
# get rid of any HTML entities to avoid things like "amp" showing up in titles
string = string.gsub(/\&(\w+)\;/, '')
@@ -133,6 +136,6 @@ def clean(string)
string = string.gsub(/ +/, " ")
string = string.strip
string = string.truncate(24, separator: ' ', omission: '')
- string
+ string.gsub(/\s/, "_")
end
end
diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb
index bbb18c92b1d..ab675ab826d 100644
--- a/spec/mailers/user_mailer_spec.rb
+++ b/spec/mailers/user_mailer_spec.rb
@@ -1110,10 +1110,11 @@
end
it "has the correct attachments" do
+ filename = work.title.gsub(/\s/, "_")
expect(email.attachments.length).to eq(2)
expect(email.attachments).to contain_exactly(
- an_object_having_attributes(filename: "#{work.title}.html"),
- an_object_having_attributes(filename: "#{work.title}.txt")
+ an_object_having_attributes(filename: "#{filename}.html"),
+ an_object_having_attributes(filename: "#{filename}.txt")
)
end
@@ -1162,10 +1163,11 @@
end
it "has the correct attachments" do
+ filename = work.title.gsub(/\s/, "_")
expect(email.attachments.length).to eq(2)
expect(email.attachments).to contain_exactly(
- an_object_having_attributes(filename: "#{work.title}.html"),
- an_object_having_attributes(filename: "#{work.title}.txt")
+ an_object_having_attributes(filename: "#{filename}.html"),
+ an_object_having_attributes(filename: "#{filename}.txt")
)
end
diff --git a/spec/models/download_spec.rb b/spec/models/download_spec.rb
index bb745368287..25c200b27c5 100644
--- a/spec/models/download_spec.rb
+++ b/spec/models/download_spec.rb
@@ -13,36 +13,92 @@
# Arabic
work.title = "هذا عمل جديد"
- expect(Download.new(work).file_name).to eq("hdh ml jdyd")
+ expect(Download.new(work).file_name).to eq("hdh_ml_jdyd")
# Chinese
work.title = "我哥好像被奇怪的人盯上了怎么破"
- expect(Download.new(work).file_name).to eq("Wo Ge Hao Xiang Bei Qi")
+ expect(Download.new(work).file_name).to eq("Wo_Ge_Hao_Xiang_Bei_Qi")
# Japanese
work.title = "二重スパイは接点を持つ"
- expect(Download.new(work).file_name).to eq("Er Zhong supaihaJie Dian")
+ expect(Download.new(work).file_name).to eq("Er_Zhong_supaihaJie_Dian")
# Hebrew
work.title = "לחזור הביתה"
- expect(Download.new(work).file_name).to eq("lkhzvr hbyth")
+ expect(Download.new(work).file_name).to eq("lkhzvr_hbyth")
end
it "removes HTML entities and emojis" do
work.title = "Two of Hearts <3 & >.< &"
- expect(Download.new(work).file_name).to eq("Two of Hearts 3")
+ expect(Download.new(work).file_name).to eq("Two_of_Hearts_3")
work.title = "Emjoi 🤩 Yay 🥳"
- expect(Download.new(work).file_name).to eq("Emjoi Yay")
+ expect(Download.new(work).file_name).to eq("Emjoi_Yay")
+ end
+
+ it "strips leading space" do
+ work.title = " Blank Space Baby"
+ expect(Download.new(work).file_name).to eq("Blank_Space_Baby")
+ end
+
+ it "strips trailing space" do
+ work.title = "Write your name: "
+ expect(Download.new(work).file_name).to eq("Write_your_name")
+ end
+
+ it "replaces multiple spaces with single underscore" do
+ work.title = "Space Opera"
+ expect(Download.new(work).file_name).to eq("Space_Opera")
+ end
+
+ it "replaces unicode space with underscores" do
+ work.title = "No-break Space"
+ expect(Download.new(work).file_name).to eq("No-break_Space")
+
+ work.title = "En Quad Space"
+ expect(Download.new(work).file_name).to eq("En_Quad_Space")
+
+ work.title = "Em Quad Space"
+ expect(Download.new(work).file_name).to eq("Em_Quad_Space")
+
+ work.title = "En Space"
+ expect(Download.new(work).file_name).to eq("En_Space")
+
+ work.title = "Em Space"
+ expect(Download.new(work).file_name).to eq("Em_Space")
+
+ work.title = "3 Per Em Space"
+ expect(Download.new(work).file_name).to eq("3_Per_Em_Space")
+
+ work.title = "4 Per Em Space"
+ expect(Download.new(work).file_name).to eq("4_Per_Em_Space")
+
+ work.title = "6 Per Em Space"
+ expect(Download.new(work).file_name).to eq("6_Per_Em_Space")
+
+ work.title = "Figure Space"
+ expect(Download.new(work).file_name).to eq("Figure_Space")
+
+ work.title = "Punctuation Space"
+ expect(Download.new(work).file_name).to eq("Punctuation_Space")
+
+ work.title = "Thin Space"
+ expect(Download.new(work).file_name).to eq("Thin_Space")
+
+ work.title = "Hair Space"
+ expect(Download.new(work).file_name).to eq("Hair_Space")
+
+ work.title = "Narrow No-Break Space"
+ expect(Download.new(work).file_name).to eq("Narrow_No-Break_Space")
end
it "appends work ID if too short" do
work.id = 999_999
work.title = "Uh"
- expect(Download.new(work).file_name).to eq("Uh Work 999999")
+ expect(Download.new(work).file_name).to eq("Uh_Work_999999")
work.title = ""
- expect(Download.new(work).file_name).to eq("Work 999999")
+ expect(Download.new(work).file_name).to eq("Work_999999")
work.title = "wat"
expect(Download.new(work).file_name).to eq("wat")
@@ -53,7 +109,7 @@
expect(Download.new(work).file_name).to eq("123456789-123456789-1234")
work.title = "123456789 123456789 123456789"
- expect(Download.new(work).file_name).to eq("123456789 123456789")
+ expect(Download.new(work).file_name).to eq("123456789_123456789")
end
end
diff --git a/spec/support/shared_examples/mailer_shared_examples.rb b/spec/support/shared_examples/mailer_shared_examples.rb
index a02c3380ab9..9281178c94f 100644
--- a/spec/support/shared_examples/mailer_shared_examples.rb
+++ b/spec/support/shared_examples/mailer_shared_examples.rb
@@ -41,16 +41,18 @@
shared_examples "an email with a deleted work with draft chapters attached" do
it "has html and txt attachments" do
+ filename = work.title.gsub(/\s/, "_")
expect(email.attachments.length).to eq(2)
expect(email.attachments).to contain_exactly(
- an_object_having_attributes(filename: "#{work.title}.html"),
- an_object_having_attributes(filename: "#{work.title}.txt")
+ an_object_having_attributes(filename: "#{filename}.html"),
+ an_object_having_attributes(filename: "#{filename}.txt")
)
end
it "includes draft chapters in attachments" do
- html_attachment = email.attachments["#{work.title}.html"].body.raw_source
- txt_attachment = email.attachments["#{work.title}.txt"].body.raw_source
+ filename = work.title.gsub(/\s/, "_")
+ html_attachment = email.attachments["#{filename}.html"].body.raw_source
+ txt_attachment = email.attachments["#{filename}.txt"].body.raw_source
decoded_html_content = Base64.decode64(html_attachment)
decoded_txt_content = Base64.decode64(txt_attachment)
From f2de69b0f3de9b3ab3c01ece61b85ebc2c0c961c Mon Sep 17 00:00:00 2001
From: neuroalien <105230050+neuroalien@users.noreply.github.com>
Date: Sun, 29 Oct 2023 19:02:15 +0000
Subject: [PATCH 071/208] AO3-5626 Fix overlapping text when download has child
related works (#4649)
Co-authored-by: Sarken
---
public/stylesheets/ebooks.css | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/public/stylesheets/ebooks.css b/public/stylesheets/ebooks.css
index 81f40f88c52..2d2445deb84 100644
--- a/public/stylesheets/ebooks.css
+++ b/public/stylesheets/ebooks.css
@@ -20,6 +20,11 @@
margin-bottom: 0;
}
+/* list child related works under the labeling dt */
+#afterword .meta dd {
+ margin: 1em 0 0 1em;
+}
+
#chapters, .userstuff {
font-family: serif;
padding: 0;
From d87652399df73fa5e3b829b0089529501a381538 Mon Sep 17 00:00:00 2001
From: sarken
Date: Tue, 31 Oct 2023 04:19:31 -0400
Subject: [PATCH 072/208] AO3-5626 Actually fix overlay text when download has
child related works (#4650)
* AO3-5626 Fix overlapping text when download has child related works
* AO3-5626 Remove it from the other file
---
app/views/layouts/barebones.html.erb | 2 ++
public/stylesheets/ebooks.css | 5 -----
2 files changed, 2 insertions(+), 5 deletions(-)
diff --git a/app/views/layouts/barebones.html.erb b/app/views/layouts/barebones.html.erb
index fa3d869809a..8bb75af2af0 100644
--- a/app/views/layouts/barebones.html.erb
+++ b/app/views/layouts/barebones.html.erb
@@ -13,6 +13,8 @@
.meta dl.tags { border: 1px solid; padding: 1em; }
.meta dd { margin: -1em 0 0 10em; }
.meta .endnote-link { font-size: .8em; }
+ /* List child related works under the labeling dt */
+ #afterword .meta dd { margin: 1em 0 0 1em; }
#chapters { font-family: "Nimbus Roman No9 L", "Times New Roman", serif; padding: 1em; }
.userstuff { font-family: "Nimbus Roman No9 L", "Times New Roman", serif; padding: 1em; }
/* Invisible headings to help Calibre make a Table of Contents */
diff --git a/public/stylesheets/ebooks.css b/public/stylesheets/ebooks.css
index 2d2445deb84..81f40f88c52 100644
--- a/public/stylesheets/ebooks.css
+++ b/public/stylesheets/ebooks.css
@@ -20,11 +20,6 @@
margin-bottom: 0;
}
-/* list child related works under the labeling dt */
-#afterword .meta dd {
- margin: 1em 0 0 1em;
-}
-
#chapters, .userstuff {
font-family: serif;
padding: 0;
From bd0f170263eb407a2aa33ca9ce4de4494c6a2560 Mon Sep 17 00:00:00 2001
From: Brian Austin <13002992+brianjaustin@users.noreply.github.com>
Date: Sun, 5 Nov 2023 23:00:19 -0500
Subject: [PATCH 073/208] AO3-6058 Add 'translation into' to download preface
(#4652)
* AO3-6058 Add 'translation into' to download preface
* Remove full stop
---
.../downloads/_download_preface.html.erb | 11 +++++-
config/locales/views/en.yml | 4 +++
features/works/work_download.feature | 35 +++++++++++++++++++
3 files changed, 49 insertions(+), 1 deletion(-)
diff --git a/app/views/downloads/_download_preface.html.erb b/app/views/downloads/_download_preface.html.erb
index 86d692c8a35..7bfe5737ffe 100644
--- a/app/views/downloads/_download_preface.html.erb
+++ b/app/views/downloads/_download_preface.html.erb
@@ -65,15 +65,24 @@
<% end %>
<% end %>
+ <%# i18n-tasks-use t("downloads.download_preface.translated_to.restricted_html") %>
+ <%# i18n-tasks-use t("downloads.download_preface.translated_to.revealed_html") %>
+ <%# i18n-tasks-use t("downloads.download_preface.translated_to.unrevealed_html") %>
<%# i18n-tasks-use t("downloads.download_preface.inspired_by.restricted_html") %>
<%# i18n-tasks-use t("downloads.download_preface.inspired_by.revealed_html") %>
<%# i18n-tasks-use t("downloads.download_preface.inspired_by.unrevealed") %>
<%# i18n-tasks-use t("downloads.download_preface.translation_of.restricted_html") %>
<%# i18n-tasks-use t("downloads.download_preface.translation_of.revealed_html") %>
<%# i18n-tasks-use t("downloads.download_preface.translation_of.unrevealed") %>
+ <% translations = @work.approved_related_works.where(translation: true) %>
<% related_works = @work.parent_work_relationships.reject { |wr| !wr.parent } %>
- <% if related_works.length > 0 %>
+ <% if translations.any? || related_works.any? %>
+ <% translations.each do |related_work| %>
+
+ <%= related_work_note(related_work.work, "translated_to", download: true) %>
+
+ <% end %>
<% related_works.each do |work| %>
<% relation = work.translation ? "translation_of" : "inspired_by" %>
diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml
index 1e964d070f0..f21a12d21ee 100644
--- a/config/locales/views/en.yml
+++ b/config/locales/views/en.yml
@@ -410,6 +410,10 @@ en:
restricted_html: Inspired by [Restricted Work] by %{creator_link}
revealed_html: Inspired by %{work_link} by %{creator_link}
unrevealed: Inspired by a work in an unrevealed collection
+ translated_to:
+ restricted_html: 'Translation into %{language} available: [Restricted Work] by %{creator_link}'
+ revealed_html: 'Translation into %{language} available: %{work_link} by %{creator_link}'
+ unrevealed_html: 'Translation into %{language} available: A work in an unrevealed collection'
translation_of:
restricted_html: A translation of [Restricted Work] by %{creator_link}
revealed_html: A translation of %{work_link} by %{creator_link}
diff --git a/features/works/work_download.feature b/features/works/work_download.feature
index e9d16746e5e..52079544e50 100644
--- a/features/works/work_download.feature
+++ b/features/works/work_download.feature
@@ -286,3 +286,38 @@ Feature: Download a work
And I view the work "Followup"
And I follow "HTML"
Then I should see "Inspired by [Restricted Work] by inspiration"
+
+ Scenario: Downloads of translated work update when translation's revealed status changes.
+
+ Given a hidden collection "Hidden"
+ And I have related works setup
+ And a translation has been posted and approved
+ And I log out
+ When I view the work "Worldbuilding"
+ And I follow "HTML"
+ Then I should see "Worldbuilding Translated by translator"
+ # Going from revealed to unrevealed
+ When I am logged in as "translator"
+ And I edit the work "Worldbuilding Translated" to be in the collection "Hidden"
+ And I log out
+ And I view the work "Worldbuilding"
+ And I follow "HTML"
+ Then I should not see "Worldbuilding Translated by translator"
+ And I should see "A work in an unrevealed collection"
+ # Going from unrevealed to revealed
+ When I reveal works for "Hidden"
+ And I log out
+ And I view the work "Worldbuilding"
+ And I follow "HTML"
+ Then I should see "Worldbuilding Translated by translator"
+
+ Scenario: Downloads hide titles of restricted work translations
+
+ Given I have related works setup
+ And a translation has been posted and approved
+ And I am logged in as "translator"
+ And I lock the work "Worldbuilding Translated"
+ When I am logged out
+ And I view the work "Worldbuilding"
+ And I follow "HTML"
+ Then I should see "[Restricted Work] by translator"
From de957be1deb051aaddbfc2bc2dd73b7bf5e116a3 Mon Sep 17 00:00:00 2001
From: Brian Austin <13002992+brianjaustin@users.noreply.github.com>
Date: Sun, 12 Nov 2023 21:59:48 -0500
Subject: [PATCH 074/208] AO3-6605 Add Reviewdog for Rubocop check (#4631)
* AO3-6605 Add Reviewdog for Rubocop check
* Fix checkout version
* Remove rubocop from Hound
* Explicitly disable Hound
* Efficiency improvement
* Remove unneeded lines
---------
Co-authored-by: Bilka
---
.github/workflows/reviewdog.yml | 19 +++++++++++++++++++
.hound.yml | 3 +--
2 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml
index 7e65dbd8a8c..dd46beec8ef 100644
--- a/.github/workflows/reviewdog.yml
+++ b/.github/workflows/reviewdog.yml
@@ -8,6 +8,25 @@ permissions:
checks: write
jobs:
+ rubocop:
+ name: Rubocop
+ runs-on: ubuntu-latest
+ steps:
+ - name: Check out code
+ uses: actions/checkout@v4
+
+ - name: Set up Ruby and run bundle install
+ uses: ruby/setup-ruby@v1
+ with:
+ bundler-cache: true
+
+ - name: rubocop
+ uses: reviewdog/action-rubocop@e70b014b8062c447d6b515ee0209f834ea93e696
+ with:
+ use_bundler: true
+ reporter: github-pr-check
+ skip_install: true
+
erb-lint:
name: ERB Lint runner
runs-on: ubuntu-latest
diff --git a/.hound.yml b/.hound.yml
index 42909ccedb6..236f7f409ba 100644
--- a/.hound.yml
+++ b/.hound.yml
@@ -6,5 +6,4 @@ jshint:
ignore_file: .jshintignore
rubocop:
- version: 1.22.1
- config_file: .rubocop.yml
+ enabled: false
From 1477313cc8465af95e73d05cbf913946b1039c79 Mon Sep 17 00:00:00 2001
From: Bilka
Date: Mon, 13 Nov 2023 04:05:15 +0100
Subject: [PATCH 075/208] AO3-6601 Fix flaky orphan work tests (#4656)
---
features/other_a/orphan_work.feature | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/features/other_a/orphan_work.feature b/features/other_a/orphan_work.feature
index 54a8723cd56..070ed1dc6f1 100644
--- a/features/other_a/orphan_work.feature
+++ b/features/other_a/orphan_work.feature
@@ -24,6 +24,8 @@ Feature: Orphan work
When I follow "Edit"
Then I should see "Edit Work"
And I should see "Orphan Work"
+ # Delay before orphaning to make sure the cache is expired
+ And it is currently 1 second from now
When I follow "Orphan Work"
Then I should see "Read More About The Orphaning Process"
When I choose "Take my pseud off as well"
@@ -46,6 +48,8 @@ Feature: Orphan work
When I follow "Edit"
Then I should see "Edit Work"
And I should see "Orphan Work"
+ # Delay before orphaning to make sure the cache is expired
+ And it is currently 1 second from now
When I follow "Orphan Work"
Then I should see "Read More About The Orphaning Process"
When I choose "Leave a copy of my pseud on"
@@ -131,6 +135,8 @@ Feature: Orphan work
And I should see "Glorious"
And I should see "Excellent"
And I should not see "Lovely"
+ # Delay before orphaning to make sure the cache is expired
+ And it is currently 1 second from now
When I follow "Orphan Works Instead"
Then I should see "Orphaning a work removes it from your account and re-attaches it to the specially created orphan_account."
When I press "Yes, I'm sure"
From 2fd1aaf478d808395b0d116d46524f8949ac469d Mon Sep 17 00:00:00 2001
From: Bilka
Date: Mon, 13 Nov 2023 04:25:38 +0100
Subject: [PATCH 076/208] AO3-6633 Fix flaky series co-creator test (#4657)
---
features/other_b/series.feature | 2 ++
1 file changed, 2 insertions(+)
diff --git a/features/other_b/series.feature b/features/other_b/series.feature
index 88b2078109d..f1208224c8c 100644
--- a/features/other_b/series.feature
+++ b/features/other_b/series.feature
@@ -207,6 +207,8 @@ Feature: Create and Edit Series
Then I should see "Work was successfully updated."
And "moon" should be a creator of the series "Ponies"
And "son" should be a creator on the series "Ponies"
+ # Delay to make sure the cache is expired
+ And it is currently 1 second from now
When I follow "Remove Me As Co-Creator"
Then I should see "You have been removed as a creator from the series and its works."
And "moon" should not be the creator of the series "Ponies"
From 99be16ccad1ed49dac068a2d976d7e97386f1fe7 Mon Sep 17 00:00:00 2001
From: Bilka
Date: Mon, 13 Nov 2023 11:43:55 +0100
Subject: [PATCH 077/208] AO3-6632 Fix flaky collection anonymity tests (#4660)
---
features/collections/collection_anonymity.feature | 13 ++++++++++++-
1 file changed, 12 insertions(+), 1 deletion(-)
diff --git a/features/collections/collection_anonymity.feature b/features/collections/collection_anonymity.feature
index 25340d74de8..071ece51849 100755
--- a/features/collections/collection_anonymity.feature
+++ b/features/collections/collection_anonymity.feature
@@ -60,6 +60,8 @@ Feature: Collection
And all emails have been delivered
When I am logged in as "first_user"
And I post the work "First Snippet" to the collection "Hidden Treasury" as a gift for "third_user"
+ # Delay before posting to make sure first work is clearly older
+ And it is currently 1 second from now
And I post the work "Second Snippet" to the collection "Hidden Treasury" as a gift for "fourth_user"
And subscription notifications are sent
Then 0 emails should be delivered
@@ -160,6 +162,8 @@ Feature: Collection
And the user "third_user" allows gifts
And I am logged in as "first_user"
And I post the work "First Snippet" to the collection "Anonymous Hugs" as a gift for "third_user"
+ # Delay before posting to make sure first work is clearly older
+ And it is currently 1 second from now
And I post the work "Second Snippet" to the collection "Anonymous Hugs" as a gift for "not a user"
When subscription notifications are sent
Then "second_user" should not be emailed
@@ -167,6 +171,8 @@ Feature: Collection
And I view the approved collection items page for "Anonymous Hugs"
# items listed in date order so checking the second will reveal the older work
And I uncheck the 2nd checkbox with id matching "collection_items_\d+_anonymous"
+ # Delay before submitting to make sure the cache is expired
+ And it is currently 1 second from now
And I submit
Then the author of "First Snippet" should be publicly visible
When subscription notifications are sent
@@ -388,6 +394,8 @@ Feature: Collection
When I am logged in as the owner of "Anonymous Collection"
And I go to "Anonymous Collection" collection edit page
And I follow "Delete Collection"
+ # Delay before deleting to make sure the cache is expired
+ And it is currently 1 second from now
And I press "Yes, Delete Collection"
And I go to creator's works page
Then I should see "Secret Work"
@@ -418,6 +426,8 @@ Feature: Collection
When I am logged in as the owner of "Anonymous Collection"
And I view the approved collection items page for "Anonymous Collection"
And I check "Remove"
+ # Delay before submitting to make sure the cache is expired
+ And it is currently 1 second from now
And I submit
And I go to creator's works page
Then I should see "Secret Work"
@@ -451,8 +461,9 @@ Feature: Collection
When I edit the work "Secret Work"
And I fill in "Collections" with "Holidays,Fluffy"
+ # Delay before posting to make sure the cache is expired
+ And it is currently 1 second from now
And I press "Post"
- And all indexing jobs have been run
And I go to my works page
Then I should see "Secret Work"
From bba3552e90254ea003cd8e67191a0c207fc1ea40 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=89ric=20B?=
Date: Wed, 15 Nov 2023 06:01:43 +0100
Subject: [PATCH 078/208] AO3-6638 Intermittent failure in cucumber
tag_wrangling, orphan_pseud, and user_delete features (#4658)
* Fix grandfather flakiness
* Helpful (?) comment
* Fix another intermittent test
Inspired by https://github.com/otwcode/otwarchive/pull/4656
* Alternative fix
More consistent with other formerly intermittent tests
* Fix more orphan tests
---
features/other_a/orphan_pseud.feature | 4 ++++
features/tags_and_wrangling/tag_wrangling.feature | 2 ++
features/users/user_delete.feature | 9 ++++++---
3 files changed, 12 insertions(+), 3 deletions(-)
diff --git a/features/other_a/orphan_pseud.feature b/features/other_a/orphan_pseud.feature
index 069980c1491..a144c7ac1c1 100644
--- a/features/other_a/orphan_pseud.feature
+++ b/features/other_a/orphan_pseud.feature
@@ -18,6 +18,8 @@ Feature: Orphan pseud
When I follow "Back To Pseuds"
Then I should see "orphanpseud"
And I should see "2 works"
+ # Delay before orphaning to make sure the cache is expired
+ And it is currently 1 second from now
When I follow "Orphan Works"
Then I should see "Orphan All Works by orphanpseud"
When I choose "Take my pseud off as well"
@@ -44,6 +46,8 @@ Feature: Orphan pseud
When I follow "Back To Pseuds"
Then I should see "orphanpseud"
And I should see "2 works"
+ # Delay before orphaning to make sure the cache is expired
+ And it is currently 1 second from now
When I follow "Orphan Works"
Then I should see "Orphan All Works by orphanpseud"
When I choose "Leave a copy of my pseud on"
diff --git a/features/tags_and_wrangling/tag_wrangling.feature b/features/tags_and_wrangling/tag_wrangling.feature
index 833b9ee1dc0..eebd9dcb3c2 100644
--- a/features/tags_and_wrangling/tag_wrangling.feature
+++ b/features/tags_and_wrangling/tag_wrangling.feature
@@ -330,6 +330,8 @@ Feature: Tag wrangling
When I edit the tag "Child"
And I check the 1st checkbox with id matching "MetaTag"
And I fill in "tag_meta_tag_string" with "Grandparent"
+ # Ensure a new cache key will be used
+ And it is currently 1 second from now
And I press "Save changes"
Then I should see "Tag was updated"
And I should see "Grandparent" within "#parent_MetaTag_associations_to_remove_checkboxes"
diff --git a/features/users/user_delete.feature b/features/users/user_delete.feature
index c43bfdf3e68..e47f9a1af54 100644
--- a/features/users/user_delete.feature
+++ b/features/users/user_delete.feature
@@ -45,6 +45,8 @@ Scenario: Allow a user to orphan their works when deleting their account
When I try to delete my account as orphaner
Then I should see "What do you want to do with your works?"
When I choose "Change my pseud to "orphan" and attach to the orphan account"
+ # Delay before orphaning to make sure the cache is expired
+ And it is currently 1 second from now
And I press "Save"
Then I should see "You have successfully deleted your account."
And 0 emails should be delivered
@@ -66,6 +68,8 @@ Scenario: Delete a user with a collection
When I try to delete my account as moderator
Then I should see "You have 1 collection(s) under the following pseuds: moderator."
When I choose "Change my pseud to "orphan" and attach to the orphan account"
+ # Delay before orphaning to make sure the cache is expired
+ And it is currently 1 second from now
And I press "Save"
Then I should see "You have successfully deleted your account."
And 0 emails should be delivered
@@ -73,9 +77,8 @@ Scenario: Delete a user with a collection
And a user account should not exist for "moderator"
When I go to the collections page
Then I should see "fake"
- # TODO: And a caching bug is fixed...
- # And I should see "orphan_account"
- # And I should not see "moderator"
+ And I should see "orphan_account"
+ And I should not see "moderator"
Scenario: Delete a user who has coauthored a work
Given the following activated users exist
From 690e9c0486030516009f6028618f3b8a4b9c59d7 Mon Sep 17 00:00:00 2001
From: Brian Austin <13002992+brianjaustin@users.noreply.github.com>
Date: Sun, 19 Nov 2023 22:37:35 -0500
Subject: [PATCH 079/208] AO3-6611 Prevent autoloading in initialization
(#4634)
* Avoid constant references in factories
* Move locale setup to `after_initialize`
* Move admin_setting setup to `after_initialize`
* Move mailer setup to `after_initialize`
* doggo
* other doggo
---
config/initializers/archive_config/locale.rb | 4 ++--
config/initializers/archive_config/settings_for_admin.rb | 2 +-
config/initializers/monkeypatches/mailers_controller.rb | 5 +++--
factories/challenges.rb | 2 +-
factories/user_invite_requests.rb | 3 +--
5 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/config/initializers/archive_config/locale.rb b/config/initializers/archive_config/locale.rb
index f06f39668a8..8508c0ed573 100644
--- a/config/initializers/archive_config/locale.rb
+++ b/config/initializers/archive_config/locale.rb
@@ -1,4 +1,4 @@
-begin
+Rails.application.config.after_initialize do
ActiveRecord::Base.connection
# try to set language and locale using models (which use Archive Config)
Language.default
@@ -10,7 +10,7 @@
rescue
# ArchiveConfig didn't work, try to set it manually
if Language.table_exists? && Locale.table_exists?
- language = Language.find_or_create_by(short: 'en', name: 'English')
+ language = Language.find_or_create_by(short: "en", name: "English")
Locale.set_base_locale(iso: "en", name: "English (US)", language_id: language.id)
end
end
diff --git a/config/initializers/archive_config/settings_for_admin.rb b/config/initializers/archive_config/settings_for_admin.rb
index a45368a9a41..f5f9e29451a 100644
--- a/config/initializers/archive_config/settings_for_admin.rb
+++ b/config/initializers/archive_config/settings_for_admin.rb
@@ -1,4 +1,4 @@
-begin
+Rails.application.config.after_initialize do
# If we have no database, fall through to rescue
ActiveRecord::Base.connection
AdminSetting.default if AdminSetting.table_exists?
diff --git a/config/initializers/monkeypatches/mailers_controller.rb b/config/initializers/monkeypatches/mailers_controller.rb
index bed94e324b4..ca8f9417899 100644
--- a/config/initializers/monkeypatches/mailers_controller.rb
+++ b/config/initializers/monkeypatches/mailers_controller.rb
@@ -6,5 +6,6 @@ module MailersController
skip_rack_dev_mark
end
end
-
-::Rails::MailersController.include MailersController
+Rails.application.config.after_initialize do
+ ::Rails::MailersController.include MailersController
+end
diff --git a/factories/challenges.rb b/factories/challenges.rb
index abfed0a2f26..0372abda42c 100644
--- a/factories/challenges.rb
+++ b/factories/challenges.rb
@@ -16,7 +16,7 @@
offers_attributes { [attributes_for(:offer)] }
end
- factory :prompt_meme_signup, class: ChallengeSignup do
+ factory :prompt_meme_signup, class: "ChallengeSignup" do
pseud { create(:user).default_pseud }
collection { create(:collection, challenge: create(:prompt_meme)) }
requests_attributes { [attributes_for(:request)] }
diff --git a/factories/user_invite_requests.rb b/factories/user_invite_requests.rb
index 7c29e87c715..7572faf44dd 100644
--- a/factories/user_invite_requests.rb
+++ b/factories/user_invite_requests.rb
@@ -1,7 +1,6 @@
require 'faker'
FactoryBot.define do
-
- factory :user_invite_requests, class: UserInviteRequest do
+ factory :user_invite_requests, class: "UserInviteRequest" do
user_id { FactoryBot.create(:user).id }
quantity { 5 }
reason { "Because reasons!" }
From 942f4a3bd2d66e9212d25b7b7d2ffec7dd710ded Mon Sep 17 00:00:00 2001
From: sarken
Date: Mon, 20 Nov 2023 02:48:06 -0500
Subject: [PATCH 080/208] AO3-6636 Add speculation rules next-chapter prefetch
(#4661) (#4665)
* AO3-6636 Add speculation rules next-chapter prefetch
* Add lint config
* Indent
* Put behind a rollout
Co-authored-by: Domenic Denicola
---
.erb-lint.yml | 4 ++++
app/views/chapters/_chapter.html.erb | 14 ++++++++++++++
2 files changed, 18 insertions(+)
diff --git a/.erb-lint.yml b/.erb-lint.yml
index df80cf9a83f..3a52bb710a6 100644
--- a/.erb-lint.yml
+++ b/.erb-lint.yml
@@ -1,6 +1,10 @@
---
EnableDefaultLinters: true
linters:
+ AllowedScriptType:
+ allowed_types:
+ - "text/javascript"
+ - "speculationrules"
DeprecatedClasses:
enabled: true
rule_set:
diff --git a/app/views/chapters/_chapter.html.erb b/app/views/chapters/_chapter.html.erb
index 5c520be1f7e..6ec4f7d6f48 100644
--- a/app/views/chapters/_chapter.html.erb
+++ b/app/views/chapters/_chapter.html.erb
@@ -79,6 +79,20 @@
<% end %>
+ <% if @next_chapter && $rollout.active?(:prefetch_next_chapter) %>
+
+ <% end %>
+
<% end %>
From 819c435f5690a457ece0cbcb6b86dae197a1ba21 Mon Sep 17 00:00:00 2001
From: Domenic Denicola
Date: Tue, 21 Nov 2023 12:29:58 +0900
Subject: [PATCH 081/208] AO3-6636 Fixup speculation rules next-chapter
prefetch (#4667)
942f4a3bd2d66e9212d25b7b7d2ffec7dd710ded accidentally used the "eagerness" field, which is only supported in Chrome 121+. It isn't necessary in our case (the value we used, "eager", is already the default) so we can just remove it to fix prefetching in earlier versions of Chrome.
---
app/views/chapters/_chapter.html.erb | 1 -
1 file changed, 1 deletion(-)
diff --git a/app/views/chapters/_chapter.html.erb b/app/views/chapters/_chapter.html.erb
index 6ec4f7d6f48..5b1fb0df4f2 100644
--- a/app/views/chapters/_chapter.html.erb
+++ b/app/views/chapters/_chapter.html.erb
@@ -85,7 +85,6 @@
"prefetch": [
{
"source": "list",
- "eagerness": "eager",
"urls": ["<%= work_chapter_path(@work, @next_chapter, anchor: "workskin") %>"]
}
]
From 4ec559fb011ce2eba4b164a6b799f71eac9b2eec Mon Sep 17 00:00:00 2001
From: Domenic Denicola
Date: Wed, 22 Nov 2023 18:26:42 +0900
Subject: [PATCH 082/208] AO3-6636 Update chapter cache key (#4670)
This ensures that speculation rules prefetching is output, and cached, for all chapters.
---
app/views/chapters/_chapter.html.erb | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/app/views/chapters/_chapter.html.erb b/app/views/chapters/_chapter.html.erb
index 5b1fb0df4f2..f2a1c41a7be 100644
--- a/app/views/chapters/_chapter.html.erb
+++ b/app/views/chapters/_chapter.html.erb
@@ -17,7 +17,7 @@
<%= render "chapters/chapter_management", chapter: chapter, work: @work %>
<% end %>
-<% cache_if(!@preview_mode, "#{@work.cache_key}/#{chapter.cache_key}-show-v4", skip_digest: true) do %>
+<% cache_if(!@preview_mode, "#{@work.cache_key}/#{chapter.cache_key}-show-v5", skip_digest: true) do %>
From ee5f41eaba9d909aa132c3b90d2fbaa4435d2f0d Mon Sep 17 00:00:00 2001
From: Domenic Denicola
Date: Mon, 27 Nov 2023 13:46:46 +0900
Subject: [PATCH 083/208] AO3-6636 Remove rollout guard for speculation rules
(#4674)
See https://otwarchive.atlassian.net/browse/AO3-6636?focusedCommentId=360670.
---
app/views/chapters/_chapter.html.erb | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/views/chapters/_chapter.html.erb b/app/views/chapters/_chapter.html.erb
index f2a1c41a7be..db73ee0ef4b 100644
--- a/app/views/chapters/_chapter.html.erb
+++ b/app/views/chapters/_chapter.html.erb
@@ -17,7 +17,7 @@
<%= render "chapters/chapter_management", chapter: chapter, work: @work %>
<% end %>
-<% cache_if(!@preview_mode, "#{@work.cache_key}/#{chapter.cache_key}-show-v5", skip_digest: true) do %>
+<% cache_if(!@preview_mode, "#{@work.cache_key}/#{chapter.cache_key}-show-v6", skip_digest: true) do %>
@@ -79,7 +79,7 @@
<% end %>
- <% if @next_chapter && $rollout.active?(:prefetch_next_chapter) %>
+ <% if @next_chapter %>
+<% end %>
+
diff --git a/app/views/invite_requests/_no_invitation.html.erb b/app/views/invite_requests/_no_invitation.html.erb
new file mode 100644
index 00000000000..dc55ef502f2
--- /dev/null
+++ b/app/views/invite_requests/_no_invitation.html.erb
@@ -0,0 +1,3 @@
+
+ <%= t(".email_not_found") %>
+
diff --git a/app/views/invite_requests/show.html.erb b/app/views/invite_requests/show.html.erb
index 3cc3537b524..aa36bc95fac 100644
--- a/app/views/invite_requests/show.html.erb
+++ b/app/views/invite_requests/show.html.erb
@@ -1,5 +1,11 @@
-<%= render "invite_request", invite_request: @invite_request %>
+<% if @invite_request %>
+ <%= render "invite_request", invite_request: @invite_request %>
+<% elsif @invitation %>
+ <%= render "invitation", invitation: @invitation %>
+<% else %>
+ <%= render "no_invitation" %>
+<% end %>
- <%= ts("To check on the status of your invitation, go to the %{status_page} and enter your email in the space provided!", status_page: link_to("Invitation Request Status page", status_invite_requests_path)).html_safe %>
+ <%= t(".instructions_html", status_link: link_to("Invitation Request Status page", status_invite_requests_path)) %>
diff --git a/app/views/invite_requests/show.js.erb b/app/views/invite_requests/show.js.erb
index c6ead4bd5b0..ac4b7fe396f 100644
--- a/app/views/invite_requests/show.js.erb
+++ b/app/views/invite_requests/show.js.erb
@@ -1,5 +1,7 @@
<% if @invite_request %>
$j("#invite-status").html("<%= escape_javascript(render "invite_requests/invite_request", invite_request: @invite_request) %>");
+<% elsif @invitation %>
+ $j("#invite-status").html("<%= escape_javascript(render "invitation", invitation: @invitation) %>");
<% else %>
- $j("#invite-status").html("Sorry, we can't find the email address you entered. If you had used it to join our invitation queue, it's possible that your invitation may have already been emailed to you; please check your spam folder, as your spam filters may have placed it there.
");
+ $j("#invite-status").html("<%= escape_javascript(render "no_invitation") %>");
<% end %>
diff --git a/config/config.yml b/config/config.yml
index b0b022493ce..846ebcb45ed 100644
--- a/config/config.yml
+++ b/config/config.yml
@@ -78,6 +78,8 @@ DELIMITER_FOR_OUTPUT: ', '
INVITE_FROM_QUEUE_ENABLED: true
INVITE_FROM_QUEUE_NUMBER: 10
INVITE_FROM_QUEUE_FREQUENCY: 7
+
+HOURS_BEFORE_RESEND_INVITATION: 24
# this is whether or not people without invitations can create accounts
ACCOUNT_CREATION_ENABLED: false
DAYS_TO_PURGE_UNACTIVATED: 7
diff --git a/config/locales/controllers/en.yml b/config/locales/controllers/en.yml
index 9b92f55418f..701779766bf 100644
--- a/config/locales/controllers/en.yml
+++ b/config/locales/controllers/en.yml
@@ -61,6 +61,11 @@ en:
error: Sorry, that comment could not be unhidden.
permission_denied: Sorry, you don't have permission to unhide that comment.
success: Comment successfully unhidden!
+ invite_requests:
+ resend:
+ not_found: Could not find an invitation associated with that email.
+ not_yet: You cannot resend an invitation that was sent in the last %{count} hours.
+ success: Invitation resent to %{email}.
kudos:
create:
success: Thank you for leaving kudos!
diff --git a/config/locales/models/en.yml b/config/locales/models/en.yml
index 094daa3290e..2bc3ecf93b4 100644
--- a/config/locales/models/en.yml
+++ b/config/locales/models/en.yml
@@ -115,6 +115,11 @@ en:
attributes:
user_defined_tags_count:
at_most: must not add up to more than %{count}. You have entered %{value} of these tags, so you must remove %{diff} of them.
+ invitation:
+ attributes:
+ base:
+ format: "%{message}"
+ notification_could_not_be_sent: 'Notification email could not be sent: %{error}'
kudo:
attributes:
commentable:
diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml
index f21a12d21ee..040a8b695bb 100644
--- a/config/locales/views/en.yml
+++ b/config/locales/views/en.yml
@@ -505,10 +505,31 @@ en:
invitation:
email_address_label: Enter an email address
invite_requests:
+ invitation:
+ after_cooldown_period:
+ not_resent:
+ one: Because your invitation was sent more than an hour ago, you can have your invitation resent.
+ other: Because your invitation was sent more than %{count} hours ago, you can have your invitation resent.
+ resent:
+ one: Because your invitation was resent more than an hour ago, you can have your invitation resent again, or you may want to %{contact_support_link}.
+ other: Because your invitation was resent more than %{count} hours ago, you can have your invitation resent again, or you may want to %{contact_support_link}.
+ before_cooldown_period:
+ one: If it has been more than an hour since you should have received your invitation, but you have not received it after checking your spam folder, you can visit this page to resend the invitation.
+ other: If it has been more than %{count} hours since you should have received your invitation, but you have not received it after checking your spam folder, you can visit this page to resend the invitation.
+ contact_support: contact Support
+ info:
+ not_resent: Your invitation was emailed to this address on %{sent_at}. If you can't find it, please check your email spam folder as your spam filters may have placed it there.
+ resent: Your invitation was emailed to this address on %{sent_at} and resent on %{resent_at}. If you can't find it, please check your email spam folder as your spam filters may have placed it there.
+ resend_button: Resend Invitation
+ title: Invitation Status for %{email}
invite_request:
date: 'At our current rate, you should receive an invitation on or around: %{date}.'
position_html: You are currently number %{position} on our waiting list!
title: Invitation Status for %{email}
+ no_invitation:
+ email_not_found: Sorry, we can't find the email address you entered.
+ show:
+ instructions_html: To check on the status of your invitation, go to the %{status_link} and enter your email in the space provided.
kudos:
guest_header:
one: "%{count} guest has also left kudos"
diff --git a/config/routes.rb b/config/routes.rb
index e0c027fe8f0..1c2bc1a2352 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -81,6 +81,7 @@
collection do
get :manage
get :status
+ post :resend
end
end
diff --git a/db/migrate/20231027172035_add_resent_at_to_invitations.rb b/db/migrate/20231027172035_add_resent_at_to_invitations.rb
new file mode 100644
index 00000000000..c95aa1ff884
--- /dev/null
+++ b/db/migrate/20231027172035_add_resent_at_to_invitations.rb
@@ -0,0 +1,7 @@
+class AddResentAtToInvitations < ActiveRecord::Migration[6.1]
+ uses_departure! if Rails.env.staging? || Rails.env.production?
+
+ def change
+ add_column :invitations, :resent_at, :datetime
+ end
+end
diff --git a/features/other_a/invite_queue.feature b/features/other_a/invite_queue.feature
index 902b43cfdc6..12c6bfccd44 100644
--- a/features/other_a/invite_queue.feature
+++ b/features/other_a/invite_queue.feature
@@ -57,7 +57,7 @@ Feature: Invite queue management
# check your place in the queue - invalid address
When I check how long "testttt@archiveofourown.org" will have to wait in the invite request queue
Then I should see "Invitation Request Status"
- And I should see "If you can't find it, your invitation may have already been emailed to that address; please check your email spam folder as your spam filters may have placed it there."
+ And I should see "Sorry, we can't find the email address you entered."
And I should not see "You are currently number"
# check your place in the queue - correct address
@@ -98,7 +98,7 @@ Feature: Invite queue management
Then 1 email should be delivered to test@archiveofourown.org
When I check how long "test@archiveofourown.org" will have to wait in the invite request queue
Then I should see "Invitation Request Status"
- And I should see "If you can't find it, your invitation may have already been emailed to that address;"
+ And I should see "If you can't find it, please check your email spam folder as your spam filters may have placed it there."
# invite can be used
When I am logged in as an admin
@@ -155,3 +155,29 @@ Feature: Invite queue management
And I fill in "invite_request_email" with "fred@bedrock.com"
And I press "Add me to the list"
Then I should see "Email is already being used by an account holder."
+
+ Scenario: Users can resend their invitation after enough time has passed
+ Given account creation is enabled
+ And the invitation queue is enabled
+ And account creation requires an invitation
+ And the invite_from_queue_at is yesterday
+ And an invitation request for "invitee@example.org"
+ When the scheduled check_invite_queue job is run
+ Then 1 email should be delivered to invitee@example.org
+
+ When I check how long "invitee@example.org" will have to wait in the invite request queue
+ Then I should see "Invitation Request Status"
+ And I should see "If you can't find it, please check your email spam folder as your spam filters may have placed it there."
+ And I should not see "Because your invitation was sent more than 24 hours ago, you can have your invitation resent."
+ And I should not see a "Resend Invitation" button
+
+ When all emails have been delivered
+ And it is currently 25 hours from now
+ And I check how long "invitee@example.org" will have to wait in the invite request queue
+ Then I should see "Invitation Request Status"
+ And I should see "If you can't find it, please check your email spam folder as your spam filters may have placed it there."
+ And I should see "Because your invitation was sent more than 24 hours ago, you can have your invitation resent."
+ And I should see a "Resend Invitation" button
+
+ When I press "Resend Invitation"
+ Then 1 email should be delivered to invitee@example.org
diff --git a/spec/controllers/invite_requests_controller_spec.rb b/spec/controllers/invite_requests_controller_spec.rb
index c7d42457936..e223ba681ea 100644
--- a/spec/controllers/invite_requests_controller_spec.rb
+++ b/spec/controllers/invite_requests_controller_spec.rb
@@ -17,21 +17,14 @@
describe "GET #show" do
context "when given invalid emails" do
- it "redirects to index with error" do
- message = "You can search for the email address you signed up with below. If you can't find it, your invitation may have already been emailed to that address; please check your email spam folder as your spam filters may have placed it there."
- get :show, params: { id: 0 }
- it_redirects_to_with_error(status_invite_requests_path, message)
- expect(assigns(:invite_request)).to be_nil
- get :show, params: { id: 0, email: "mistressofallevil@example.org" }
- it_redirects_to_with_error(status_invite_requests_path, message)
+ it "renders" do
+ get :show, params: { email: "mistressofallevil@example.org" }
+ expect(response).to render_template("show")
expect(assigns(:invite_request)).to be_nil
end
it "renders for an ajax call" do
- get :show, params: { id: 0 }, xhr: true
- expect(response).to render_template("show")
- expect(assigns(:invite_request)).to be_nil
- get :show, params: { id: 0, email: "mistressofallevil@example.org" }, xhr: true
+ get :show, params: { email: "mistressofallevil@example.org" }, xhr: true
expect(response).to render_template("show")
expect(assigns(:invite_request)).to be_nil
end
@@ -41,19 +34,51 @@
let(:invite_request) { create(:invite_request) }
it "renders" do
- get :show, params: { id: 0, email: invite_request.email }
+ get :show, params: { email: invite_request.email }
expect(response).to render_template("show")
expect(assigns(:invite_request)).to eq(invite_request)
end
it "renders for an ajax call" do
- get :show, params: { id: 0, email: invite_request.email }, xhr: true
+ get :show, params: { email: invite_request.email }, xhr: true
expect(response).to render_template("show")
expect(assigns(:invite_request)).to eq(invite_request)
end
end
end
+ describe "POST #resend" do
+ context "when the email doesn't match any invitations" do
+ it "redirects with an error" do
+ post :resend, params: { email: "test@example.org" }
+ it_redirects_to_with_error(status_invite_requests_path,
+ "Could not find an invitation associated with that email.")
+ end
+ end
+
+ context "when the invitation is too recent" do
+ let(:invitation) { create(:invitation) }
+
+ it "redirects with an error" do
+ post :resend, params: { email: invitation.invitee_email }
+ it_redirects_to_with_error(status_invite_requests_path,
+ "You cannot resend an invitation that was sent in the last 24 hours.")
+ end
+ end
+
+ context "when the email and time are valid" do
+ let!(:invitation) { create(:invitation) }
+
+ it "redirects with a success message" do
+ travel_to((1 + ArchiveConfig.HOURS_BEFORE_RESEND_INVITATION).hours.from_now)
+ post :resend, params: { email: invitation.invitee_email }
+
+ it_redirects_to_with_notice(status_invite_requests_path,
+ "Invitation resent to #{invitation.invitee_email}.")
+ end
+ end
+ end
+
describe "POST #create" do
it "redirects to index with error given invalid emails" do
post :create, params: { invite_request: { email: "wat" } }
From 0cbe3c3ac137a0e1fe16b67b888031dc0dfbd654 Mon Sep 17 00:00:00 2001
From: Paul Lemus
Date: Sun, 3 Dec 2023 16:15:38 -0800
Subject: [PATCH 093/208] AO3-6579 Set @page_title to Create Account not New
Registration (#4675)
AO3-6579 Set page_title to Create Account
The heading in New User Registrations page says "Create Account",
however the browser tab said "New Registration". In the new action of
the controller, I added @page_title = 'Create Account' to make the
browser tab match.
Note: This is different from setting @title, which would change the
heading.
---
app/controllers/users/registrations_controller.rb | 1 +
1 file changed, 1 insertion(+)
diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb
index 13f48b394b5..5f11db3c066 100644
--- a/app/controllers/users/registrations_controller.rb
+++ b/app/controllers/users/registrations_controller.rb
@@ -3,6 +3,7 @@ class Users::RegistrationsController < Devise::RegistrationsController
before_action :configure_permitted_parameters
def new
+ @page_title = "Create Account" # Displays "New Registration" otherwise
super do |resource|
if params[:invitation_token]
@invitation = Invitation.find_by(token: params[:invitation_token])
From e7979ef354d9f800dcd54133b72d1972421e67c9 Mon Sep 17 00:00:00 2001
From: Albert Pedersen
Date: Mon, 4 Dec 2023 01:15:50 +0100
Subject: [PATCH 094/208] AO3-6643 Fix error when sorting by columns without a
pretty name (#4666)
* Add fallback to sort column name
* Add spec for sorting by column without a pretty name
* Update app/models/search/work_search_form.rb
Co-authored-by: weeklies <80141759+weeklies@users.noreply.github.com>
* Trying to make reviewdog happy
* Is reviewdog happy now? Woof
---------
Co-authored-by: weeklies <80141759+weeklies@users.noreply.github.com>
---
app/models/search/work_search_form.rb | 11 ++++++++---
spec/models/search/work_search_form_spec.rb | 8 ++++++++
2 files changed, 16 insertions(+), 3 deletions(-)
diff --git a/app/models/search/work_search_form.rb b/app/models/search/work_search_form.rb
index 77f827a32c1..79d4394a8a3 100644
--- a/app/models/search/work_search_form.rb
+++ b/app/models/search/work_search_form.rb
@@ -160,9 +160,14 @@ def summary
end
end
if @options[:sort_column].present?
- summary << "sort by: #{name_for_sort_column(@options[:sort_column]).downcase}" +
- (@options[:sort_direction].present? ?
- (@options[:sort_direction] == "asc" ? " ascending" : " descending") : "")
+ # Use pretty name if available, otherwise fall back to plain column name
+ pretty_sort_name = name_for_sort_column(@options[:sort_column])
+ direction = if @options[:sort_direction].present?
+ @options[:sort_direction] == "asc" ? " ascending" : " descending"
+ else
+ ""
+ end
+ summary << ("sort by: #{pretty_sort_name&.downcase || @options[:sort_column]}" + direction)
end
summary.join(" ")
end
diff --git a/spec/models/search/work_search_form_spec.rb b/spec/models/search/work_search_form_spec.rb
index f6d529af696..02d4670d85d 100644
--- a/spec/models/search/work_search_form_spec.rb
+++ b/spec/models/search/work_search_form_spec.rb
@@ -108,6 +108,14 @@
expect(searcher.options[:sort_direction]).to eq("desc")
end
end
+
+ context "when sorting by field without pretty name" do
+ it "displays the field name in search summary" do
+ options = { sort_column: "expected_number_of_chapters", sort_direction: "desc" }
+ searcher = WorkSearchForm.new(options)
+ expect(searcher.summary).to eq("sort by: expected_number_of_chapters descending")
+ end
+ end
end
describe "searching" do
From a0b7645c81cb204afe30102e5a58d75327cebb4e Mon Sep 17 00:00:00 2001
From: kwerey
Date: Mon, 4 Dec 2023 00:16:04 +0000
Subject: [PATCH 095/208] AO3-6626: stop tags from being added with Chinese or
Japanese commas (#4663)
* AO3-6626: prevent tags from being created with Chinese or Japanese commas
* AO3-6626: fix indentation per styleguide
* AO3-6626 correct rubocop violations
* AO3-6626 test all forbidden characters
* AO3-6626 update to meet rubocop standards
* AO3-6626 define forbidden tags inline
---------
Co-authored-by: nikobee
---
app/models/tag.rb | 23 +++++++--------
spec/models/tag_spec.rb | 62 ++++++++++++++++++++++++++---------------
2 files changed, 52 insertions(+), 33 deletions(-)
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 519a36de453..9740deb8546 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -110,7 +110,7 @@ def commentable_owners
end
has_many :mergers, foreign_key: 'merger_id', class_name: 'Tag'
- belongs_to :merger, class_name: 'Tag'
+ belongs_to :merger, class_name: "Tag"
belongs_to :fandom
belongs_to :media
belongs_to :last_wrangler, polymorphic: true
@@ -161,17 +161,18 @@ def commentable_owners
has_many :tag_set_associations, dependent: :destroy
has_many :parent_tag_set_associations, class_name: 'TagSetAssociation', foreign_key: 'parent_tag_id', dependent: :destroy
- validates_presence_of :name
+ validates :name, presence: true
validates :name, uniqueness: true
- validates_length_of :name, minimum: 1, message: "cannot be blank."
- validates_length_of :name,
- maximum: ArchiveConfig.TAG_MAX,
- message: "^Tag name '%{value}' is too long -- try using less than %{count} characters or using commas to separate your tags."
- validates_format_of :name,
- with: /\A[^,*<>^{}=`\\%]+\z/,
- message: "^Tag name '%{value}' cannot include the following restricted characters: , ^ * < > { } = ` \\ %"
-
- validates_presence_of :sortable_name
+ validates :name,
+ length: { minimum: 1,
+ message: "cannot be blank." }
+ validates :name,
+ length: { maximum: ArchiveConfig.TAG_MAX,
+ message: "^Tag name '%{value}' is too long -- try using less than %{count} characters or using commas to separate your tags." }
+ validates :name,
+ format: { with: /\A[^,,、*<>^{}=`\\%]+\z/,
+ message: "^Tag name '%{value}' cannot include the following restricted characters: , ^ * < > { } = ` , 、 \\ %" }
+ validates :sortable_name, presence: true
validate :unwrangleable_status
def unwrangleable_status
diff --git a/spec/models/tag_spec.rb b/spec/models/tag_spec.rb
index 16daf57b9bb..340c2d87991 100644
--- a/spec/models/tag_spec.rb
+++ b/spec/models/tag_spec.rb
@@ -6,7 +6,7 @@
User.current_user = nil
end
- context 'checking count caching' do
+ context "checking count caching" do
before(:each) do
# Set the minimal amount of time a tag can be cached for.
ArchiveConfig.TAGGINGS_COUNT_MIN_TIME = 1
@@ -17,15 +17,15 @@
@fandom_tag = FactoryBot.create(:fandom)
end
- context 'without updating taggings_count_cache' do
- it 'should not cache tags which are not used much' do
+ context "without updating taggings_count_cache" do
+ it "should not cache tags which are not used much" do
FactoryBot.create(:work, fandom_string: @fandom_tag.name)
@fandom_tag.reload
expect(@fandom_tag.taggings_count_cache).to eq 0
expect(@fandom_tag.taggings_count).to eq 1
end
- it 'will start caching a when tag when that tag is used significantly' do
+ it "will start caching a when tag when that tag is used significantly" do
(1..ArchiveConfig.TAGGINGS_COUNT_MIN_CACHE_COUNT).each do |try|
FactoryBot.create(:work, fandom_string: @fandom_tag.name)
@fandom_tag.reload
@@ -40,8 +40,8 @@
end
end
- context 'updating taggings_count_cache' do
- it 'should not cache tags which are not used much' do
+ context "updating taggings_count_cache" do
+ it "should not cache tags which are not used much" do
FactoryBot.create(:work, fandom_string: @fandom_tag.name)
RedisJobSpawner.perform_now("TagCountUpdateJob")
@fandom_tag.reload
@@ -49,7 +49,7 @@
expect(@fandom_tag.taggings_count).to eq 1
end
- it 'will start caching a when tag when that tag is used significantly' do
+ it "will start caching a tag when that tag is used significantly" do
(1..ArchiveConfig.TAGGINGS_COUNT_MIN_CACHE_COUNT).each do |try|
FactoryBot.create(:work, fandom_string: @fandom_tag.name)
RedisJobSpawner.perform_now("TagCountUpdateJob")
@@ -65,7 +65,7 @@
expect(@fandom_tag.taggings_count).to eq ArchiveConfig.TAGGINGS_COUNT_MIN_CACHE_COUNT
end
- it "Writes to the database do not happen immeadiately" do
+ it "Writes to the database do not happen immediately" do
(1..40 * ArchiveConfig.TAGGINGS_COUNT_CACHE_DIVISOR - 1).each do |try|
@fandom_tag.taggings_count = try
@fandom_tag.reload
@@ -168,11 +168,29 @@ def expect_tag_update_flag_in_redis_to_be(flag)
expect(tag.errors[:name].join).to match(/too long/)
end
- it "should not be valid with disallowed characters" do
- tag = Tag.new
- tag.name = "badtag",
+ "^tag",
+ "{open",
+ "close}",
+ "not=allowed",
+ "suspicious`character",
+ "no%maths"
+ ].each do |tag|
+ forbidden_tag = Tag.new
+ forbidden_tag.name = tag
+ it "is not saved and receives an error message about restricted characters" do
+ expect(forbidden_tag.save).to be_falsey
+ expect(forbidden_tag.errors[:name].join).to match(/restricted characters/)
+ end
+ end
end
context "unwrangleable" do
@@ -250,20 +268,20 @@ def expect_tag_update_flag_in_redis_to_be(flag)
expect(tag.save).to be_falsey
end
- it 'autocomplete should work' do
- tag_character = FactoryBot.create(:character, canonical: true, name: 'kirk')
- tag_fandom = FactoryBot.create(:fandom, name: 'Star Trek', canonical: true)
+ it "autocomplete should work" do
+ tag_character = FactoryBot.create(:character, canonical: true, name: "kirk")
+ tag_fandom = FactoryBot.create(:fandom, name: "Star Trek", canonical: true)
tag_fandom.add_to_autocomplete
- results = Tag.autocomplete_fandom_lookup(term: 'ki', fandom: 'Star Trek')
+ results = Tag.autocomplete_fandom_lookup(term: "ki", fandom: "Star Trek")
expect(results.include?("#{tag_character.id}: #{tag_character.name}")).to be_truthy
expect(results.include?("brave_sire_robin")).to be_falsey
end
- it 'old tag maker still works' do
- tag_adult = Rating.create_canonical('adult', true)
- tag_normal = ArchiveWarning.create_canonical('other')
- expect(tag_adult.name).to eq('adult')
- expect(tag_normal.name).to eq('other')
+ it "old tag maker still works" do
+ tag_adult = Rating.create_canonical("adult", true)
+ tag_normal = ArchiveWarning.create_canonical("other")
+ expect(tag_adult.name).to eq("adult")
+ expect(tag_normal.name).to eq("other")
expect(tag_adult.adult).to be_truthy
expect(tag_normal.adult).to be_falsey
end
From fa60e9816caa36c858778622a7ba3a666e125648 Mon Sep 17 00:00:00 2001
From: neuroalien <105230050+neuroalien@users.noreply.github.com>
Date: Mon, 4 Dec 2023 00:16:20 +0000
Subject: [PATCH 096/208] AO3-5259 Deduplicate and order admin post tags
(#4654)
---
app/controllers/admin_posts_controller.rb | 2 +-
spec/controllers/admin_posts_controller_spec.rb | 16 ++++++++++++++++
2 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/app/controllers/admin_posts_controller.rb b/app/controllers/admin_posts_controller.rb
index f3db82d2ed3..92637ca4657 100644
--- a/app/controllers/admin_posts_controller.rb
+++ b/app/controllers/admin_posts_controller.rb
@@ -14,7 +14,7 @@ def index
@admin_posts ||= AdminPost
if params[:language_id].present? && (@language = Language.find_by(short: params[:language_id]))
@admin_posts = @admin_posts.where(language_id: @language.id)
- @tags = AdminPostTag.joins(:admin_posts).where(admin_posts: { language_id: @language.id })
+ @tags = AdminPostTag.distinct.joins(:admin_posts).where(admin_posts: { language_id: @language.id }).order(:name)
else
@admin_posts = @admin_posts.non_translated
@tags = AdminPostTag.order(:name)
diff --git a/spec/controllers/admin_posts_controller_spec.rb b/spec/controllers/admin_posts_controller_spec.rb
index 4ab2c65fbd6..6287c888f25 100644
--- a/spec/controllers/admin_posts_controller_spec.rb
+++ b/spec/controllers/admin_posts_controller_spec.rb
@@ -4,6 +4,22 @@
include LoginMacros
include RedirectExpectationHelper
+ describe "GET #index" do
+ context "when filtering by language" do
+ let(:translated_post1) { create(:admin_post, tag_list: "xylophone,aardvark") }
+ let!(:translation_post1) { create(:admin_post, translated_post_id: translated_post1.id, language: create(:language, short: "fr")) }
+ let(:translated_post2) { create(:admin_post, tag_list: "xylophone,aardvark") }
+ let!(:translation_post2) { create(:admin_post, translated_post_id: translated_post2.id, language: translation_post1.language) }
+ let!(:untranslated_post) { create(:admin_post, tag_list: "uncommon tag") }
+
+ it "assigns the admin post tags for the language ordered by name" do
+ get :index, params: { language_id: "fr" }
+
+ expect(assigns[:tags].map(&:name).join(", ")).to eql("aardvark, xylophone")
+ end
+ end
+ end
+
describe "POST #create" do
before { fake_login_admin(create(:admin, roles: ["communications"])) }
From 01a8000bf041021c9fe2a6f0db81f17f8838c5ee Mon Sep 17 00:00:00 2001
From: eliahhecht
Date: Sun, 3 Dec 2023 19:16:49 -0500
Subject: [PATCH 097/208] AO3-3217 Remove aria attributes from char counts
(#4617)
Remove chatty ARIA attributes from char count elements, make those elements tab-focusable instead.
---
app/helpers/application_helper.rb | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 130a0a93b82..8ed84424960 100755
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -243,8 +243,8 @@ def allow_tinymce?(controller)
# see: http://www.w3.org/TR/wai-aria/states_and_properties#aria-valuenow
def generate_countdown_html(field_id, max)
max = max.to_s
- span = content_tag(:span, max, id: "#{field_id}_counter", class: "value", "data-maxlength" => max, "aria-live" => "polite", "aria-valuemax" => max, "aria-valuenow" => field_id)
- content_tag(:p, span + ts(' characters left'), class: "character_counter")
+ span = content_tag(:span, max, id: "#{field_id}_counter", class: "value", "data-maxlength" => max)
+ content_tag(:p, span + ts(' characters left'), class: "character_counter", "tabindex" => 0)
end
# expand/contracts all expand/contract targets inside its nearest parent with the target class (usually index or listbox etc)
From 35260255492dd6db1941b89f11f6b4bfb03dde57 Mon Sep 17 00:00:00 2001
From: Ellie Y Cheng
Date: Sun, 3 Dec 2023 19:17:37 -0500
Subject: [PATCH 098/208] AO3-6533 Validates provided rating string only has
one rating (#4613)
* Validates provided rating string must be one of the allowed strings
* Address hound
* Remove rating tasks that are no longer necessary due to fix
* Use split_tag_string to validate rating_string
* Revert unnecessary changes after new changes
* Add test for validation
* Nit
* Use create_invalid to set up test environment
---
app/models/work.rb | 7 +++++++
spec/lib/tasks/after_tasks.rake_spec.rb | 4 ++--
spec/models/work_spec.rb | 6 ++++++
3 files changed, 15 insertions(+), 2 deletions(-)
diff --git a/app/models/work.rb b/app/models/work.rb
index e6917ecf8cf..4e82d362af9 100755
--- a/app/models/work.rb
+++ b/app/models/work.rb
@@ -146,6 +146,13 @@ def validate_published_at
validates :rating_string,
presence: { message: "^Please choose a rating." }
+ validate :only_one_rating
+ def only_one_rating
+ return unless split_tag_string(rating_string).count > 1
+
+ errors.add(:base, ts("Only one rating is allowed."))
+ end
+
# rephrases the "chapters is invalid" message
after_validation :check_for_invalid_chapters
def check_for_invalid_chapters
diff --git a/spec/lib/tasks/after_tasks.rake_spec.rb b/spec/lib/tasks/after_tasks.rake_spec.rb
index b3d4142a877..8c9b3ab2b34 100644
--- a/spec/lib/tasks/after_tasks.rake_spec.rb
+++ b/spec/lib/tasks/after_tasks.rake_spec.rb
@@ -37,7 +37,7 @@
let!(:canonical_gen_rating) { Rating.find_or_create_by!(name: ArchiveConfig.RATING_GENERAL_TAG_NAME, canonical: true) }
let!(:canonical_teen_rating) { Rating.find_or_create_by!(name: ArchiveConfig.RATING_TEEN_TAG_NAME, canonical: true) }
let!(:work_with_noncanonical_rating) { create(:work, rating_string: noncanonical_teen_rating.name) }
- let!(:work_with_canonical_and_noncanonical_ratings) { create(:work, rating_string: [noncanonical_teen_rating.name, ArchiveConfig.RATING_GENERAL_TAG_NAME].join(",")) }
+ let!(:work_with_canonical_and_noncanonical_ratings) { create_invalid(:work, rating_string: [noncanonical_teen_rating.name, ArchiveConfig.RATING_GENERAL_TAG_NAME].join(",")) }
it "updates the works' ratings to the canonical teen rating" do
subject.invoke
@@ -55,7 +55,7 @@
let!(:canonical_teen_rating) { Rating.find_or_create_by!(name: ArchiveConfig.RATING_TEEN_TAG_NAME, canonical: true) }
let!(:default_rating) { Rating.find_or_create_by!(name: ArchiveConfig.RATING_DEFAULT_TAG_NAME, canonical: true) }
let!(:work_with_noncanonical_rating) { create(:work, rating_string: noncanonical_rating.name) }
- let!(:work_with_canonical_and_noncanonical_ratings) { create(:work, rating_string: [noncanonical_rating.name, canonical_teen_rating.name]) }
+ let!(:work_with_canonical_and_noncanonical_ratings) { create_invalid(:work, rating_string: [noncanonical_rating.name, canonical_teen_rating.name]) }
it "changes and replaces the noncanonical rating tags" do
subject.invoke
diff --git a/spec/models/work_spec.rb b/spec/models/work_spec.rb
index 05cfba69d87..ee6a722ff50 100644
--- a/spec/models/work_spec.rb
+++ b/spec/models/work_spec.rb
@@ -106,6 +106,12 @@
end
end
+ context "invalid rating" do
+ it "cannot have more than one rating" do
+ expect(build(:work, rating_string: "Not Rated, General Audiences")).to be_invalid
+ end
+ end
+
context "validate authors" do
let(:invalid_work) { build(:no_authors) }
From 2be5b548f468cab8a71588d01929d6d9f4034a48 Mon Sep 17 00:00:00 2001
From: tickinginstant
Date: Sun, 3 Dec 2023 19:17:59 -0500
Subject: [PATCH 099/208] AO3-6505 Preload approved_collections and
stat_counter for work blurbs. (#4599)
AO3-6505 Preload collections and stats.
---
app/models/work.rb | 2 +-
spec/requests/works_n_plus_one_spec.rb | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/app/models/work.rb b/app/models/work.rb
index 4e82d362af9..0f024d22924 100755
--- a/app/models/work.rb
+++ b/app/models/work.rb
@@ -1023,7 +1023,7 @@ def self.in_series(series)
}
scope :with_includes_for_blurb, lambda {
- includes(:pseuds)
+ includes(:pseuds, :approved_collections, :stat_counter)
}
scope :for_blurb, -> { with_columns_for_blurb.with_includes_for_blurb }
diff --git a/spec/requests/works_n_plus_one_spec.rb b/spec/requests/works_n_plus_one_spec.rb
index 468e7562ce6..e8f5a2ad02c 100644
--- a/spec/requests/works_n_plus_one_spec.rb
+++ b/spec/requests/works_n_plus_one_spec.rb
@@ -29,7 +29,7 @@
run_all_indexing_jobs
end
- it "performs around 13 queries per work" do
+ it "performs around 11 queries per work" do
# TODO: Ideally, we'd like the uncached work listings to also have a
# constant number of queries, instead of the linear number of queries
# we're checking for here. But we also don't want to put too much
@@ -38,7 +38,7 @@
expect do
subject.call
expect(response.body.scan(' 81254 bytes
.../site/2.0/15-group-comments.css | 8 ++---
.../stylesheets/site/2.0/26-media-narrow.css | 12 ++++---
18 files changed, 109 insertions(+), 104 deletions(-)
create mode 100644 features/comments_and_kudos/guest_comments.feature
diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb
index 9d7daa1e79b..893a21f1ab1 100644
--- a/app/helpers/comments_helper.rb
+++ b/app/helpers/comments_helper.rb
@@ -43,7 +43,7 @@ def get_commenter_pseud_or_name(comment)
link_to comment.pseud.byline, [comment.pseud.user, comment.pseud]
end
else
- comment.name
+ content_tag(:span, comment.name) + content_tag(:span, " #{ts('(Guest)')}", class: "role")
end
end
@@ -328,8 +328,9 @@ def css_classes_for_comment(comment)
unreviewed = "unreviewed" if comment.unreviewed?
commenter = commenter_id_for_css_classes(comment)
official = "official" if commenter && comment&.pseud&.user&.official
+ guest = "guest" unless comment.pseud_id
- "#{unavailable} #{official} #{unreviewed} comment group #{commenter}".squish
+ "#{unavailable} #{official} #{guest} #{unreviewed} comment group #{commenter}".squish
end
# find the parent of the commentable
diff --git a/app/helpers/mailer_helper.rb b/app/helpers/mailer_helper.rb
index ff42cc41ecd..bc9f9c85975 100644
--- a/app/helpers/mailer_helper.rb
+++ b/app/helpers/mailer_helper.rb
@@ -9,6 +9,10 @@ def style_link(body, url, html_options = {})
link_to(body.html_safe, url, html_options)
end
+ def style_role(text)
+ tag.em(tag.strong(text))
+ end
+
# For work, chapter, and series links
def style_creation_link(title, url, html_options = {})
html_options[:style] = "color:#990000"
@@ -176,6 +180,27 @@ def style_work_tag_metadata(tags)
"#{label}#{style_work_tag_metadata_list(tags)}".html_safe
end
+ def commenter_pseud_or_name_link(comment)
+ if comment.comment_owner.nil?
+ style_bold(comment.comment_owner_name) + style_role(t("roles.guest_with_parens"))
+ elsif comment.by_anonymous_creator?
+ style_bold(t("roles.anonymous_creator"))
+ else
+ style_link(comment.comment_owner_name, polymorphic_url(comment.comment_owner, only_path: false))
+ end
+ end
+
+ def commenter_pseud_or_name_text(comment)
+ if comment.comment_owner.nil?
+
+ "#{comment.comment_owner_name} #{t('roles.guest_with_parens')}"
+ elsif comment.by_anonymous_creator?
+ t("roles.anonymous_creator")
+ else
+ "#{comment.comment_owner_name} (#{polymorphic_url(comment.comment_owner, only_path: false)})"
+ end
+ end
+
private
# e.g., 1 word or 50 words
diff --git a/app/views/comment_mailer/comment_notification.html.erb b/app/views/comment_mailer/comment_notification.html.erb
index 4afef2f434c..fb90ab9363f 100644
--- a/app/views/comment_mailer/comment_notification.html.erb
+++ b/app/views/comment_mailer/comment_notification.html.erb
@@ -1,21 +1,15 @@
<% content_for :message do %>
- <% if @comment.comment_owner %>
- <%= style_link(@comment.comment_owner_name, polymorphic_url(@comment.comment_owner, :only_path => false)) %>
- <% else %>
- <%= style_bold(@comment.comment_owner_name) %>
- <% end %>
-
- left the following comment on
+ <%= commenter_pseud_or_name_link(@comment) %> left the following comment on
<% if @comment.ultimate_parent.is_a?(Tag) %>
- the tag
+ the tag
<%= style_link(@comment.ultimate_parent.commentable_name, {:controller => :tags, :action => :show, :id => @comment.ultimate_parent, :only_path => false}) %>:
<% else %>
<%= style_link(@comment.ultimate_parent.commentable_name.html_safe, polymorphic_url(@comment.ultimate_parent, :only_path => false)) %> :
<% end %>
- <%= style_quote(raw @comment.sanitized_content) %>
+ <%= style_quote(raw(@comment.sanitized_content)) %>
<%= render 'comment_notification_footer' %>
diff --git a/app/views/comment_mailer/comment_notification.text.erb b/app/views/comment_mailer/comment_notification.text.erb
index 119baf3cf7d..2257c6f02ce 100644
--- a/app/views/comment_mailer/comment_notification.text.erb
+++ b/app/views/comment_mailer/comment_notification.text.erb
@@ -1,5 +1,5 @@
<% content_for :message do %>
-<%= @comment.comment_owner_name %> <% if @comment.comment_owner then %>(<%= polymorphic_url(@comment.comment_owner, :only_path => false) %>) <% end %>left the following comment on
+<%= commenter_pseud_or_name_text(@comment) %> left the following comment on
<% if @comment.ultimate_parent.is_a?(Tag) then %>the tag "<%= @comment.ultimate_parent.commentable_name %>" (<%= url_for(:controller => :tags, :action => :show, :id => @comment.ultimate_parent, :only_path => false) %>) <% else %>"<%= @comment.ultimate_parent.commentable_name.html_safe %>" (<%= polymorphic_url(@comment.ultimate_parent, :only_path => false) %>)<% end %>:
<%= text_divider %>
diff --git a/app/views/comment_mailer/comment_reply_notification.html.erb b/app/views/comment_mailer/comment_reply_notification.html.erb
index 05df553ca62..ff696aefb71 100644
--- a/app/views/comment_mailer/comment_reply_notification.html.erb
+++ b/app/views/comment_mailer/comment_reply_notification.html.erb
@@ -1,31 +1,22 @@
<% content_for :message do %>
- <% responder = @comment.comment_owner ?
- (
- @comment.by_anonymous_creator? ?
- "Anonymous Creator" :
- style_link(@comment.comment_owner_name,
- polymorphic_url(@comment.comment_owner, :only_path => false))
- ) :
- @comment.comment_owner_name %>
-
- <%= style_bold(responder) %> replied to your comment on
-
+ <%= commenter_pseud_or_name_link(@comment) %> replied to your comment on
+
<% if @comment.ultimate_parent.is_a?(Tag) %>
- the tag
+ the tag
<%= style_link(@comment.ultimate_parent.commentable_name, {:controller => :tags, :action => :show, :id => @comment.ultimate_parent.to_param, :only_path => false}) %>:
- <% else %>
+ <% else %>
<%= style_link(@comment.ultimate_parent.commentable_name.html_safe, polymorphic_url(@comment.ultimate_parent, :only_path => false)) %> :
<% end %>
-
+
You wrote:
<%= style_quote(raw @your_comment.sanitized_content) %>
-
+
- <%= style_bold(responder) %> responded:
- <%= style_quote(raw @comment.sanitized_content) %>
+ <%= commenter_pseud_or_name_link(@comment) %> responded:
+ <%= style_quote(raw(@comment.sanitized_content)) %>
<%= render 'comment_notification_footer' %>
diff --git a/app/views/comment_mailer/comment_reply_notification.text.erb b/app/views/comment_mailer/comment_reply_notification.text.erb
index 251a3fad891..3b4eddb1019 100644
--- a/app/views/comment_mailer/comment_reply_notification.text.erb
+++ b/app/views/comment_mailer/comment_reply_notification.text.erb
@@ -1,12 +1,5 @@
<% content_for :message do %>
-<% responder = @comment.comment_owner ?
- (
- @comment.by_anonymous_creator? ?
- "Anonymous Creator" :
- @comment.comment_owner_name + " (" + polymorphic_url(@comment.comment_owner, :only_path => false) + ")"
- ) :
- @comment.comment_owner_name %>
-<%= responder %> replied to your comment on <% if @comment.ultimate_parent.is_a?(Tag) then %> the tag <%= @comment.ultimate_parent.commentable_name %> (<%= url_for(:controller => :tags, :action => :show, :id => @comment.ultimate_parent.to_param, :only_path => false) %>)<% else %>"<%= @comment.ultimate_parent.commentable_name.html_safe %>" (<%= polymorphic_url(@comment.ultimate_parent, :only_path => false) %>)<% end %>:
+<%= commenter_pseud_or_name_text(@comment) %> replied to your comment on <% if @comment.ultimate_parent.is_a?(Tag) then %> the tag <%= @comment.ultimate_parent.commentable_name %> (<%= url_for(controller: :tags, action: :show, id: @comment.ultimate_parent.to_param, only_path: false) %>)<% else %>"<%= @comment.ultimate_parent.commentable_name.html_safe %>" (<%= polymorphic_url(@comment.ultimate_parent, only_path: false) %>)<% end %>:
You wrote:
<%= text_divider %>
@@ -15,7 +8,7 @@ You wrote:
<%= text_divider %>
-<%= responder %> responded:
+<%= commenter_pseud_or_name_text(@comment) %> responded:
<%= text_divider %>
<%= to_plain_text(raw @comment.sanitized_content) %>
diff --git a/app/views/comment_mailer/comment_reply_sent_notification.html.erb b/app/views/comment_mailer/comment_reply_sent_notification.html.erb
index 3af5cd4a546..a23c6959c11 100644
--- a/app/views/comment_mailer/comment_reply_sent_notification.html.erb
+++ b/app/views/comment_mailer/comment_reply_sent_notification.html.erb
@@ -8,17 +8,8 @@
<%= style_link(@comment.ultimate_parent.commentable_name.html_safe, polymorphic_url(@comment.ultimate_parent, only_path: false)) %> :
<% end %>
- <% commenter = @parent_comment.comment_owner ?
- (
- @parent_comment.by_anonymous_creator? ?
- "Anonymous Creator" :
- style_link(@parent_comment.comment_owner_name,
- polymorphic_url(@parent_comment.comment_owner, only_path: false))
- ) :
- @parent_comment.comment_owner_name %>
-
- <%= style_bold(commenter) %> wrote:
+ <%= commenter_pseud_or_name_link(@parent_comment) %> wrote:
<%= style_quote(raw @parent_comment.sanitized_content) %>
diff --git a/app/views/comment_mailer/comment_reply_sent_notification.text.erb b/app/views/comment_mailer/comment_reply_sent_notification.text.erb
index 9b67e67e7f6..7686ad99f30 100644
--- a/app/views/comment_mailer/comment_reply_sent_notification.text.erb
+++ b/app/views/comment_mailer/comment_reply_sent_notification.text.erb
@@ -1,15 +1,7 @@
<% content_for :message do %>
You replied to a comment on <% if @comment.ultimate_parent.is_a?(Tag) then %>the tag <%= @comment.ultimate_parent.commentable_name %> (<%= tag_url(@comment.ultimate_parent) %>)<% else %>"<%= @comment.ultimate_parent.commentable_name.html_safe %>" (<%= polymorphic_url(@comment.ultimate_parent, only_path: false) %>)<% end %>:
-<% commenter = @parent_comment.comment_owner ?
- (
- @parent_comment.by_anonymous_creator? ?
- "Anonymous Creator" :
- @parent_comment.comment_owner_name + " (" + polymorphic_url(@parent_comment.comment_owner, only_path: false) + ")"
- ) :
- @parent_comment.comment_owner_name %>
-
-<%= commenter %> wrote:
+<%= commenter_pseud_or_name_text(@parent_comment) %> wrote:
<%= text_divider %>
<%= to_plain_text(raw @parent_comment.sanitized_content) %>
diff --git a/app/views/comment_mailer/edited_comment_notification.html.erb b/app/views/comment_mailer/edited_comment_notification.html.erb
index f7fc6aae589..d77fd1e42b4 100644
--- a/app/views/comment_mailer/edited_comment_notification.html.erb
+++ b/app/views/comment_mailer/edited_comment_notification.html.erb
@@ -1,22 +1,16 @@
<% content_for :message do %>
- <% if @comment.comment_owner %>
- <%= style_link(@comment.comment_owner_name, polymorphic_url(@comment.comment_owner, :only_path => false)) %>
- <% else %>
- <%= style_bold(@comment.comment_owner_name) %>
- <% end %>
+ <%= commenter_pseud_or_name_link(@comment) %> edited the following comment on
- edited the following comment on
-
<% if @comment.ultimate_parent.is_a?(Tag) %>
- the tag
+ the tag
<%= style_link(@comment.ultimate_parent.commentable_name, {:controller => :tags, :action => :show, :id => @comment.ultimate_parent.to_param, :only_path => false}) %>:
- <% else %>
+ <% else %>
<%= style_link(@comment.ultimate_parent.commentable_name.html_safe, polymorphic_url(@comment.ultimate_parent, :only_path => false)) %> :
<% end %>
- <%= style_quote(raw @comment.sanitized_content) %>
+ <%= style_quote(raw(@comment.sanitized_content)) %>
<%= render 'comment_notification_footer' %>
diff --git a/app/views/comment_mailer/edited_comment_notification.text.erb b/app/views/comment_mailer/edited_comment_notification.text.erb
index f8b37575a7d..8f557ca4908 100644
--- a/app/views/comment_mailer/edited_comment_notification.text.erb
+++ b/app/views/comment_mailer/edited_comment_notification.text.erb
@@ -1,5 +1,5 @@
<% content_for :message do %>
-<%= @comment.comment_owner_name %> <% if @comment.comment_owner then %>(<%= polymorphic_url(@comment.comment_owner, :only_path => false) %>) <% end %>edited the following comment on
+<%= commenter_pseud_or_name_text(@comment) %> edited the following comment on
<% if @comment.ultimate_parent.is_a?(Tag) then %>the tag "<%= @comment.ultimate_parent.commentable_name %>" (<%= url_for(:controller => :tags, :action => :show, :id => @comment.ultimate_parent, :only_path => false) %>) <% else %>"<%= @comment.ultimate_parent.commentable_name.html_safe %>" (<%= polymorphic_url(@comment.ultimate_parent, :only_path => false) %>)<% end %>:
<%= text_divider %>
diff --git a/app/views/comment_mailer/edited_comment_reply_notification.html.erb b/app/views/comment_mailer/edited_comment_reply_notification.html.erb
index a3034ee1ade..fd067f5b1f1 100644
--- a/app/views/comment_mailer/edited_comment_reply_notification.html.erb
+++ b/app/views/comment_mailer/edited_comment_reply_notification.html.erb
@@ -1,31 +1,22 @@
<% content_for :message do %>
- <% responder = @comment.comment_owner ?
- (
- @comment.by_anonymous_creator? ?
- "Anonymous Creator" :
- style_link(@comment.comment_owner_name,
- polymorphic_url(@comment.comment_owner, :only_path => false))
- ) :
- @comment.comment_owner_name %>
-
- <%= style_bold(responder) %> edited their reply to your comment on
-
+ <%= commenter_pseud_or_name_link(@comment) %> edited their reply to your comment on
+
<% if @comment.ultimate_parent.is_a?(Tag) %>
- the tag
+ the tag
<%= style_link(@comment.ultimate_parent.commentable_name, {:controller => :tags, :action => :show, :id => @comment.ultimate_parent.to_param, :only_path => false}) %>:
- <% else %>
+ <% else %>
<%= style_link(@comment.ultimate_parent.commentable_name.html_safe, polymorphic_url(@comment.ultimate_parent, :only_path => false)) %> :
<% end %>
-
+
You wrote:
<%= style_quote(raw @your_comment.sanitized_content) %>
-
+
- <%= style_bold(responder) %> edited their response to:
- <%= style_quote(raw @comment.sanitized_content) %>
+ <%= commenter_pseud_or_name_link(@comment) %> edited their response to:
+ <%= style_quote(raw(@comment.sanitized_content)) %>
<%= render 'comment_notification_footer' %>
diff --git a/app/views/comment_mailer/edited_comment_reply_notification.text.erb b/app/views/comment_mailer/edited_comment_reply_notification.text.erb
index 0dc010aaca7..bbee53050a6 100644
--- a/app/views/comment_mailer/edited_comment_reply_notification.text.erb
+++ b/app/views/comment_mailer/edited_comment_reply_notification.text.erb
@@ -1,12 +1,5 @@
<% content_for :message do %>
-<% responder = @comment.comment_owner ?
- (
- @comment.by_anonymous_creator? ?
- "Anonymous Creator" :
- @comment.comment_owner_name + " (" + polymorphic_url(@comment.comment_owner, :only_path => false) + ")"
- ) :
- @comment.comment_owner_name %>
-<%= responder %> edited their reply to your comment on <% if @comment.ultimate_parent.is_a?(Tag) then %> the tag <%= @comment.ultimate_parent.commentable_name %> (<%= url_for(:controller => :tags, :action => :show, :id => @comment.ultimate_parent.to_param, :only_path => false) %>)<% else %>"<%= @comment.ultimate_parent.commentable_name.html_safe %>" (<%= polymorphic_url(@comment.ultimate_parent, :only_path => false) %>)<% end %>:
+<%= commenter_pseud_or_name_text(@comment) %> edited their reply to your comment on <% if @comment.ultimate_parent.is_a?(Tag) then %> the tag <%= @comment.ultimate_parent.commentable_name %> (<%= url_for(controller: :tags, action: :show, id: @comment.ultimate_parent.to_param, only_path: false) %>)<% else %>"<%= @comment.ultimate_parent.commentable_name.html_safe %>" (<%= polymorphic_url(@comment.ultimate_parent, only_path: false) %>)<% end %>:
You wrote:
<%= text_divider %>
@@ -15,7 +8,7 @@ You wrote:
<%= text_divider %>
-<%= responder %> edited their response to:
+<%= commenter_pseud_or_name_text(@comment) %> edited their response to:
<%= text_divider %>
<%= to_plain_text(raw @comment.sanitized_content) %>
diff --git a/config/locales/mailers/en.yml b/config/locales/mailers/en.yml
index d00811114e5..70538ad7d15 100644
--- a/config/locales/mailers/en.yml
+++ b/config/locales/mailers/en.yml
@@ -81,6 +81,9 @@ en:
open_doors: The Open Doors team
parent_org: Organization for Transformative Works
support: The AO3 Support team
+ roles:
+ anonymous_creator: Anonymous Creator
+ guest_with_parens: "(Guest)"
user_mailer:
abuse_report:
copy:
diff --git a/features/comments_and_kudos/guest_comments.feature b/features/comments_and_kudos/guest_comments.feature
new file mode 100644
index 00000000000..73cfb31f8d4
--- /dev/null
+++ b/features/comments_and_kudos/guest_comments.feature
@@ -0,0 +1,33 @@
+@comments
+Feature: Read guest comments
+ In order to tell guest comments from logged-in users' comments
+ As a user
+ I'd like to see the "guest" sign
+
+Scenario: View guest comments in homepage, inbox and works
+ Given I am logged in as "normal_user"
+ And I post the work "My very meta work about AO3"
+ And I am logged out
+ When I post a guest comment
+ Then I should see "(Guest)"
+ When I am logged in as "normal_user"
+ And I go to the home page
+ Then I should see "(Guest)"
+ When I follow "My Inbox"
+ Then I should see "(Guest)"
+ When I view the work "My very meta work about AO3" with comments
+ Then I should see "(Guest)"
+
+Scenario: View logged-in comments in homepage, inbox and works
+ Given I am logged in as "normal_user"
+ And I post the work "My very meta work about AO3"
+ And I am logged in as "logged_in_user"
+ When I post the comment ":)))))))" on the work "My very meta work about AO3"
+ Then I should not see "(Guest)"
+ When I am logged in as "normal_user"
+ And I go to the home page
+ Then I should not see "(Guest)"
+ When I follow "My Inbox"
+ Then I should not see "(Guest)"
+ When I view the work "My very meta work about AO3" with comments
+ Then I should not see "(Guest)"
diff --git a/features/comments_and_kudos/inbox.feature b/features/comments_and_kudos/inbox.feature
index e36346ca449..ee7d85bc285 100644
--- a/features/comments_and_kudos/inbox.feature
+++ b/features/comments_and_kudos/inbox.feature
@@ -43,11 +43,11 @@ Feature: Get messages in the inbox
And I go to my inbox page
And I choose "Show unread"
And I press "Filter"
- Then I should see "guest on Down for the Count"
+ Then I should see "guest (Guest) on Down for the Count"
And I should see "less than 1 minute ago"
When I choose "Show read"
And I press "Filter"
- Then I should not see "guest on Down for the Count"
+ Then I should not see "guest (Guest) on Down for the Count"
Scenario: I can bulk edit comments in my inbox by clicking 'Select'
Given I am logged in as "boxer"
diff --git a/public/images/imageset.png b/public/images/imageset.png
index cd9754f2fd61e037e715bd6fc00b676d12b8b1dc..066876d029799538915ef58c229f1729d2270f21 100644
GIT binary patch
literal 81254
zcmV)KK)Sz)P)*g3Bq#fVzP2sA1WzuuMa2)wL9F3U?Y=
zZCQ9SEe5z+C^NaOgY-H`6aKCUkXBLg0dDNG*r1EpK
zQoCjM;1DNDgb+GKRxoiHl$w{!YWs%1ebW@gW60a0ge{H?S@8~tT9mFSscF$u@|cuV
z?tk-i(j&u_?ky2$eDJ)NUhr?hXW8>U4m|Jk5bE#YEMN5(B&V@A(aTjWya(;;aB^PN
z^ldoZfUY}Bv$b!jUbZithUa5wO+v>NG%r@N`Z?$577}ani!Nbj7y9IW`_dN(185B(
z^8v&F000SaNLh0L01FZT01FZU(%pXi001BWNklMH1PrMD?BN7C5&o}`?{F#Vb`~N1oqF{5d5z`jU
zPx~4*095{Z3Y5Q}1^B~hU-#XbhtSi<;>BOW@At#%fwfX-t%AWICQdxE$KOMT8Q+J?
zHNaFl*+H7yh$2gOV~8)SUBL23<7kM*@b0J~*fD7o0*8%&x3~bl>N;eu-i)FT7vYGF
zTM*04hN)@o0@enr0dAjy>_Wm1vy+8YEgBNw`A9sDWnX709b5<0RR9CL0i2%K7m?0x
zvX_U4Zt||VW3P}m8jnLU;`lZlfj|I7MMW^p{l8YKs-mKz;()w|4r?zL*3yt?u0x5p
zis-%8hH!h-7?uU65CTPD5G008L;OrVa+^bVVNeOyfA4%;d%;xPFk~oN^NL+&7@xlO
z0s3EiKMKP%m(SDU`#2#8&uba&+KqTNLhdL~!gBo5m0;Vfc*rml+L-Gm^Ky#OXfAczSi
zY<;T`L{a{;=ihe@Vkj7yQ-+0)PXmZM5UGF}BLT^lquhQC$_ZNp2VsFJMnIK{Ah}H;
z+;{Y7jDO%3JbBv5;t+ap+m-wQ~P4gMg+dL=KaXqo_8HzI!49Nen`e
zcy|#`k^eJj1ysZQRw0N=@1wH(i~SGDJg=-*ygSp(dV#*{fB!+y?5y3XUD=M?tp7
zRs@I_#McreLzysvlXNK{({FGijWK^TTK1?Rjrusc{}M0>n+Mq$tLN0XS&H1hRLB
zdl-#j6#e`P+_?50$kZX;)D`Gx&!C@7`&!YLJ^g8xNjAL
z5LFRCtn?J}9Vi4L#dz;{Tny_PhG6Qw+kuz|e&1@$xqko(rkucnk4@fbZX+y|a|SB@
zsS%=VrA2GL<0^q14e!pu+Oy7u+tZJ})X;L9{cvw6#iqbvsNek+M)c~9`VXE)!^syy
z^%vQTWr=w$n}fiVL`x@y{qyHf!G*^^fJ-hu2Gz~q;P>-RM`cqr-v+9U#ym)b$C&)9
zaN!ZpLCvsQWYG7mUSs
zUiv9~aKq!$(M-thiF**E8}ak0If!~-WGq$44jm3An*<#Zf~XKZP>@F0hJPW;L;OaC{E{$^o^m{%
ze7~IVB4i_oAw@69%@>q662)v%#&5acbaBdxvo3-{$PuF#=I>jNsLxHnq+{g?m!Xkn
z8g>$KoTC1SMWf<-o|orm3eUG=08?DRl6S`AvQvL1kYo0mTk*@;6VMQor(pH^Wf(T17p@&&ipEAcpJ^mHNQnLjTJ4W8VOlg8MyR$44K`Hz0y%nP4j#R-5SLzlEg?r4Zn}3VM*mUAF~?7Zn46Szi6Bx*
zHLMWgXB>-x`Mpqj|8MCVAIaZV``Wdm?s(nkvu+ta{BS-lIPD}9-7o`{KYI+C9-!-`
z4kE;jxU@L!+T_CFc|XFr=N^R*R^Ea+>mNt9S8|t*^k6m)X!Od@E*pm{qiZqdu#q^m
z@>$F&yH+5Tq}FU~h_`Evb43j7@EF*6VqXlHHUWc&^drTdl=@HCpz$yBv59n$v!1>k
zrv-ZBv6C*q_%EKsaF0vO#}97U#)fz|IEczzjS!20Uc>&gVlhH=_ILVJzGr|~B8YOJ
z5Tq4rX5}=dV}$D?+%i2A7o2w{O8O6CiQYaT1}j{Hmp@pJ-@URDcb(k_m&~{vf1b4+
zw?43nv~Qw{&CNCkp-m$lhY=&n_aTb8n*e&~;fF{I)!QEt9d*&iW*QIb#jcm%1qea1
zk2)MX5rJ;`g{Z{yGqYo=%6n3?@?t!A-*lAKeTMf|J&s(z{4Sj%gOvKeEg6IVDGK7`
ziPzw>?RO!j)Fi~jW)d4)BZw<(U}=9Z2HkTm{(9Xt&>EXiwQdJ;h84p%?M&=>@O#*@
zbtn2%M)1F5E=LCPv8TKO1L8V@%u8raHdhOd*$p_?=v+1!#K4A-=zar=tFOKq^XAPH
zzq1&{;uL?zT;$wy&&Atszb!sX4~Tn8#5!O)NW7I%UB|SdH}UhkW?EBnsy{n-E2Ja>L%R#D?*Om1YMyBw%5=!(Fn~>fE0>K
z@q|Smg0coeh@!k~akr*jRYJ;B(vazCNzhbQL1;nK=4262nBqE<0L!wK3I18V3pirv
zxyUW>;qjSgBh%fMYp9x}1D1HEw!TGoRuupI@E5rCiu*BT=>OoOjX$KPwsnxW)xsLb
z)}w8iz~~1q#*Z%h4t{&+RoJ?AHJ$G!njS@N|ADybv)|(ALq_BG*WN@o3?SI0}Kn_0ceR>U$uS;iv
zSI6~F``|h5ed5iUm{*#44FS|{gkahWr}WOZrDC(oK@vI|Ngv@W<2Cid-!8thjD!xt
zuh3jsjAM_Uh+wP~6|{*$dNYzkqsQF2rWkuZy9x0v-$7H8{rair&^2poomo^}s8zx_m3XJMR(H
zSheE1D9LS>-`!#^o}UeLX%FTgCZk4#M*9EK6|vZ3{m?@X;f_1*Kz)6^_wLWVUMX<}58zXzgvA!zOnR_@
zj;S}T!RsGT*OG_tkC-%IztK0v9
zGl#s3=1?o!1XFSUHY{7Y5d}HJ1Rx3{0E>!rLd>+ph!a~Z_iH%sqSNusiyxq}xeAKc
zllY#8NqUAxSoh3re?XQu3q?bRBOHmfIEM$%%kzgwS4ww;z3eCr6M1Or*68YZ?6JoZ
zzq2CVS_fg|aFq6r9eGN^+SC(6Q4W+2;wX197f$V8h6^tE4&M5FCzdQES*6IEG}*Rx
zP!+jw;lmqIJH9uL7(E09{a2u>p8XT89-OG7sW1nzgeYSWrp4Zx16+9tMm~5KRzLIr
zGU6G+#M{x~H6r-9WH3%91mR=-@BYfdiI|h`W(qbTIe|)gNlum*8;EklY#DlDu+^&W
z9NhT8Y4G_y;@wW3JPiwW_CwIvfdto9q%N~tgfongQm7?GAhtF+Qz|46g-ZfBY{X!E
z^V(}Pn=Za@M_WMc%55kQ)?m=Fhhf2{?QomER+LK4pZ8>8+MDOmBG^zq9G{MuK)-iG
z#9H%iS|V1ue|MxOIMjasd(o8xsifONcr=XZlY7&fW#Nt|77z!}y9jLJ{{`^N=fA+O
z?-`8i&+Lz1KD&unM5|yCp`0^t%KKP^Ab~gEz>?h$V|?#ojC}V^tUL2`_>4X@XImuk
zSW#>x@Cn}eU7SAg2<(~tF;V;-@G3?0c&2;~6A3+pl?n!KO?62>ht={?a(Xqw?{g4e
zvgwFfk#Gnm!Wfui*l}jgJd%SLN!gs8nZ&kyUN0d&JM7}n+KGu`{>Mg&=3of%h*YUt
z$4>F4^ZW%b<)^)Qq`jbK*ifvUa1;@^a5oU7^RJmZoOj-NEkrH{DOg!wx^yW93>YBH
z!5x`;1!D9Ng7nzD?JMcF>_M{eupt8_TU}L6I)%4mn`pv(qhP`6TKXk~aYJ*ZP;9Dg
zyjiRx7?!OJ(~D(VD!w`AdK~`cOpKd$8tUGdiH+x9jRGq|PvWDYh%gZiVSJw3A2oMh
zk4J93QcO2@>V+g`6R*zuFL~h0~VR=ejM?N>0_Ychb@^YMb^i=WL
z%b#C~U^D`syT$q=+^i$Bgu{cwg0t{vj07C0Xr&N|*v{Q3uc*S5>!#sve|j7R@U?>=
zVZ;yZjbR0SaNkQGi8qV1G7Ttf0vAXY>c-}=Y#0O@gWW@rG<6ULenx~;CB1uGp{D@(
z00m-65z9dk6B$0geSgC?@pSc^Dw)gB+bjgL4F|Do;7V9u6fd&A`WUZW`F+gz
z+n;dad1s+t{RYhW?O#zh?+b+M8xain5t=d)$Def~Eo&SgSSmoY-)5zMt%cUZdi;FhfKrxJ{Mw5
z)obwREdh}cqS+`Fh|>l+SRG>Vy5^c|@Z59H;lJrn1(Yz^BdTRil1CD9$L20UOS}Jh`KSMa5>I9XR4sB
zi$5)w!A%`VjID<%{1~LT3Kcd7r60V3cP~5#ug>{IT=V7kAHsV#U5&CWD+sW1&?L?_
z6X?)-BwmP~Eu1+Qv*x}Jk6*?0M}3KgrZ$3K`xKdwWBuoYFztx@F=^6pJTvb!fk=*s
zrgShgLTX0rnwb$RTR(!^)%Cluhew+*1V?`F8n`A6#LPusp)uTuXr>!Y
z!}D?6jn|`S!l44ByE-DmRrzQ{!-{JX}XOx>xfj0LvH*+{N$SRF=4_`fgFEdcsFwV>Df#8UHS~@jUV3g
zUD7>F_%r>;^k(A7lO|%+q(k8i_%ZAEui%xhSJAU~j&U)hAFZefoyc~p8?pTk9BhGN
zQ?6s#N{#USZ#fj4{F2pbw?Wu|*MB_*=bv(fq-srjuxZNG?y&mbf
zbGuRh)pD4jpoq(AeJp=zXImQrP0L2MGe`azXH33XeAZB7VEO7zsHmzUX@jX^0KIZ5iU4OONWrq7)&X7!>hWC4)xel`ZdJM%KyL(YTeUkc+e@
z{^##&PU0P9(`<`1#%IFG+2)oXZqjM(;0;Stg
zUAiUZvl_fIYc9S&<2=-^ScmFmpV0$mq{xHrc9Ek)`H>zkK#%vq=3DT|rk~)%Lm$BS
zp=aXQV@H5JPvT1O3MtxiYYfkT6G&a@?1WETW
zyHS>f4bB{_h?o-}bMWYS1aTlM--FF%aa?}i*F;#qgz)q^<{uAXQNY!{8K|2`k}7g2
zL}Lxx?y3bc2o*fSax8hhqpIC(1bsh6;J
z$!b)8wpehC{kEjEo?e$6J#YEYk%z*)11xk$B;g!(kqg{2qRc)Vp->3bReO4@E*8&!
zKoGrRjo;E;fo@W+bT?ZeIll?-j0(Zcc^f^VL~+uPh3&6fJzhBOman|fy@4K+tK;D&
z%z7l!<7;*CZ-=+0OzBhl7U#CQFy+rz?%I&ndvLUYh&bSL)8q;Va?f>*ki+BaO_(l2
zD7XlMqF60%YIU)C!eXj$!GV@u2}NN$%Wk-oF#IIk@6CHCmX-Kvwv&J(f==RbxJhs5
zs&HIen6$})sdNRqqHvr}yW!34ErKM)Iz+4MR}Hp$+A)r#FU679bu>3KQdW6m
z8EzDYjW$T)>{Dlj<5E#w)g?agnl|l{pZW`Ai67P3Z>#;naf}#dG%6}l)kQ4q2^tuc
z89`VJq)C)4qN;JMtEHc`rh_vKM6?Y19;%qXqqWj#9o+=D>wMWLA2$igk&{t4VmSPH
zdBCo6M7M8)^7$voU-T&g0#RJ;1D0(FQ0&NOu^9A+S2ywZjRjcQ>sIvdm4}>$l{mTi
zW#nbJL@_2tYEI~~Qi)&+9aV)4BIlA(0Uhso()1U@hMO=ZY(E
zsZEH*YGq)YZR%1)oRlHz=tg51@OU$EZ1xj0$8LIJTlZJYgk7dRYRAa&*_G39$Yqxc
z%BW~MG_O~@v1J%y$(hO&7xn`7RHEVZiv*=@chlP`9M=NC+!j42P`;=SJdKkvIOhzE
z>^Bhp3_t3(*P|-37**EC2<6X1Aj@jORh+_cmH?0#ya?m@h9hy_03;KP~lXhfh=)+ib?$5)5T>(tY-UGi|BhKO>WKmNDX(v*_LAZ|hy!Fv*&LSM~hr{7#W5yFN
z;KjfGgSeQ{X?{?;d!Ck=D8s_mMS0M7Ps7N*r{leer_kx_mfuD|34j2<@(+v;YaD)bqN0@*ks`)p+SPNR$e2kTaB!|UZE2vMU6
zVbLiZC-XnJ$U+=_Y0fi`Isyuft-o7@g62&ybzi4q)w+EuGiyK)sm<>hb}_eQ@Nm%>M4!IYtW
zF?seo_+aLT*m?Hp$VdUU#9A?pH*Imk-Y15D-o6-S&wZH?7m=>+Q2
z+4J;}16cOYu{ita2XWqwXF?&7@Q+{kVTNGOYjIfxP5_Dv>cyPl=uwN|4NpUTX~cdx
zOH?M=*>oEQjkps5uM6RZIfz!>MQ|ISmvIRZDPqM4c$y@BC#p57_R`xFrj-&VC`|bt
z1c@U1xoXL}HJEwzwZgD#rJTc8v1F>XBhy+bvTverTqY<8Q5C;
zHh%ibJ
zRMq6#nG#8aNMu7711s;PvS%47z+5Ys=B(T(&WV%QJ-{ErNhco#KaCYzD)H8w=WzVa
zU&0&6p($*!+Rv$<}{Vg6ZQQ#bsB{!1A)E@uzv`iehoR2rj#T94A3Gn-%!o
z!a>-&aR(NbJ|uw8na3#{CqzF{%E&-49HQrDPFz+MxJ{TFnOoHw~v3av@0=7#H`UnY=4I3LeFU+83Bd!2L|{L{}${<^i8
zHK;#6`qLBQ+#|-0!r}jT8KE|7WeY#_nIgnGS=oX`t*p>CG>0Tnv#K_TEYApu%+s@$
zV9Vd{K{#e%?BGINbp1WpGwn4fp;{Z&WL*QnnQXDTy*+O+EK8I*sH(4#9JNknc6NGA
z7ki%0VEo-huXcFO{R!lFXT?=0t=T9tJW}^u7pWDk&ygHN>P9B(OO`_&brCx@c;u1TeZv1j
zZLAc)?hFC^;Rbx1dm%19?G)_#*+{I|ycJHIs8sHM5dy#osRb&W-rdrPIZE3vRtt
ze3u*WfW7sGeNM8r)qxmH4#64@;hl%1R%
zy4Dk}KuxVAgi7^Vsc_S|5fY~cZG8r@pQ_mQ)cI&6`S`G-PC+ap%fgs;SsBwFo$Yyx
z{bTO8tyJ8vdd12F=h>S*PiOFYhnd2xxdqqXc)8TI%3j9&^?xH`+;-1Zp+qMrf7`)|
zxkk#nTWchpi9Eb@001BWNkl{JAa3vb-ADXDCVQbVTALF8Jg1dtm%nKqm_sr8@Bc!mgY{slT>Aa%@y*t9asDxXeMWE{XRQQRw`>mL
z5J8(dXRA$wATs|vykiNQc%-SFE6Zqpc=CE<=kht2_tiRB8)m^`S;dm`@Z3UoFszRE
zyw%{Nxmi|Psaw8MuE}2Rc{+pbN83Lhha0|k6FlAkNdmj@Tuleic^BiP
zu*VbS3GF&mX&{JWnXzm+H+2ZDr#X~ZsBmRv+w(fOw)OY05~YB}vo#Rc9zW<(OV}x>Jns%
zS4Y=Oz5`>ejc8mthh$qNq5PY0N|G5ZI0zpV6Q^M6k+QVT=l9+#Ot-OrJdZaX`?L7$
z_{%Oq(d|DZ@yBYrCaNkT*gpHg55&0x#vW!{eeAt3&7?uVG;Q(96uN{?&s^WoAdNFy
zJ{1UJ+TZI+9F_H9JfYr>DVbYQ|IBnKo-7gW(o*~}yWY2y{c|LUnq|uq`cPNbwBGY{
z2H$4a8W&C-_d|?4MCv?0n^lr1F4*3lt5QcWx*jQ}g5&~peVv$}k)b_Vk~gis5j71`
z28{)S!m`EUfDj32FqMpUaD8GP+hl1v#oxF%!_It6o-#?yySsL&VWp2WkPJ?JTvV!QgrJh2pB;;A3p^*Uvo98A3s4VJ1LbU6bC_U
z{ZGuV2x%VPBacR}tPE6qwOFi)qm-B3aVzG3^r^W1{KtL^*W}}5Yl(K&Hu&yLtgDb&
zqQ$43F8xhxAxgZIA@v1Qa+B2dcS2$d*4muL619IL!Dif1dmA2|M!L~U(_jXQY$v19
zQfRW@qbprPj1;YV7rkpbI!L>F-qkRrXyq@v9y2bL6Yl4&LF^6c
zc%Y@Jz)F{cq$s4FP!G%2ZARaCD~7LH0MESF;rsMWcxe=@UkJ6Ns8ed>NDir7K#P{3
zf4~0XyYkxQ(pKLIb+JU3Q&M>;rkF_xGUQ7tx(imt8g3#6r<$2piQ;Fa
z$KiiZAv|CB0OI~4yPBnuQ20e9e=`{u)+Hxs6^c@|YPBR9XDbx
zy9v6>kHwpx#EKnX2_sF1Fky+J4XMV|4$NvcPg7bUh|;A2I%yN{$Xq9Ao&CbirJFH)
z>1G`2^GelS*U_~23(VWOTZSEXG7nM;#PbiYmJh$*m!L?N*>Ok$M`i>oGZAg_(~301
z+pDSl>|x8YkvUneRnOs`g+SkE6;?lXI&yyWA>0H|gQq`$wXtHHOcx*|d~TEQx
z)Scm%NMt1~R!YLXm-Ml7Xo;e_U{-uXfT#%*w78DAp+4?v3|M^`>Rq`|SR@nx?sVgl
z9u*l^g>D5YI(2r1i5KZz+j`I2^&~of5~5da(s1(FA7I4DQN$fW`0MrY*-D8-v+Q;QmXdxBg4NMyN)qw5fA|sJCFRN6>RsJwcw}1?h~q{*u4FQ~Tgyl~
z`#LycO}V)7q>qr7pNCIZK7qvz_qRb3Nf68y0IS#>Hby6p-u@95zWO_yc+n5pdw{df
zzaI5(+>~57A)>VkK5@)3DRF0lT-hddZ%N;FJ<=KU>C1zL;D+s+Q9uANZS!`_95O&4
zk8^O1{cKIHK~s5o%KD^)64{U@xS_*GIUc8ai|uWfc2RbY4)5hhQ)~c~rV0Udiv67o
zFR^4bGb_m_(?ttUSr+1W@Qp%|
z5C^O4`$-Y2%h5{2O6VXa(!IW;(c)p;9$|rTrwmso*QSmZ#Z-7vv`S&J=A$rklx(ci
zQGpg-C|4!2^Ze*DumBgHeh2sYX>*=<8wbcO%MMProA=P?5n@1tKfDWHuUQ2r)u!Ry
zCq->8(=nx5Nu|{B(>bP5GldL=R><_$lBrV@I!Nnv^OSDE3)5r~>EVO>WAe=R(VRk9
zs`H95ptm$qlrLDA=&F?zhYUNM$V@2f!WyM&h*WL15m4X5CpOk`^ZdT~_|+YAzY%xDE9}ys%JQFj@jf
zC*;CXBdDs9j;gpBZNFQtaj9-0t>TUJw{d%gcQUi#_Jq|f85sTlI$FJy}d)?ZFJ8{>u#k_zy;
zwQxFMOUtD#WG4VyCS93!^+c0%+-)B!vh_%;3Kc^$qqY;mv_nFige|G?cT{<%
z!+mp;(w~vh?Y(a$3!(0tWAWo#9>*2my%0B?{Q{b3A?mmtM#*g$KVc$b@p`=S8o1-zLM2PkQ$~fh_ID6Vcf7
zi~I568#Be0>?Zv7XA-t!fV=3wI51n00)I7xRq?!G-6{t=@E!tX5zFb?+}P>7);
zhKuM1&F#YI!sDQBxfEAleVI7t*E2`RLLAC|L=cA~>$(>%I}BOrsoJRfv0^u;6)Z0O
zzXbSSbmTJ#hSLjdtEgYQPpP*tLW;u{{zOzu>S~^j9*61aS9jbgejhk;7!LXMqa7j%
z=M1g5;A|{evswIo+3fc)>Gk(<=}EH4#JrnsY>}I3+P&zc1v3fQERqZgL{a?lH3;qA
zAo{rTc@YA>sRi3Hbl1zsc>7|}w%W2qDWbO-a$%6sv0z&{eLS;oi6WcJaP%{Rw&xH5fk_G+jaD#4z*Yb0*AHAX^cc1$7vUnN`
zlw$q5O>+0?8CX&8)w`eg{@G8iL*`zTH|T~AqNZ%i{*i?Qo)_l9=kX;t6Px-N2pUob
zYZ4W7^6JwjA(oku_T1cW@WrJ^qoGn(KA$+|Ox!r(bC@*)gnSycWXL@?@(t3Bbhn;j
z4(WXE2v=Cy3k0xQQ5&U`rDRcg^_eGQ**7bs_;kznaBAruG*7<}Wx7wS3+o%ZX&cuU
z^u>~Muf)zB$?`{E-18%>e|J`!%zmcysxTPHoEy_d5>+u!)okD=J5I%`?n_{WA~M_v
z()VM!+{9Hp3C<%eppNlDMopMjGAcQpJ>vb{^W;Oga@7h9_~Rq{)QZ$Yps{q*yKNNb
zH@~_bE^Y4!mY&Xraymh#zY?*S-8xP0n>7FIqspO?ER>VgOL}`JO~KS%8&OtX+7fU$
zeUmuAsNoawitSud(!_qvAH{vMe7NT54={G*=@>iqXpA{z8y0=}4c2b`jKr>H=w>dC
z8PyM?50&8uF0JdjKS4bQxw#p_SJ0jJ4bVfylqATSe%bP^qZeYc?VJf?5PahuOg?Wq
z#`Y`1*mqvV#;SU(*;I8sr$kDORJunkagOUQ`7Va#
z^udr@e}F&U_M>i^fKq^&-eYb3E
z(3Cj?hS4BWM6n;5*K2VT8=m_Ma{Bg#J39v@nvVW10zUR63!}P);Jqcn|6ie+~Z394LKAsjS@4=^R-!zCQnKY#2KpCp_~k
z296(tVFlSV4r_Dnibo&CXFvR*xK~Dp&tW=}lZ_IGiaL(lW5jLvv
z^t|apwAx#7S1DhS9ve`det)!ds<>PF%y8!6U5P6#QLVKl)zLp(=opT+zSQ+|uyKW3
z8^lxkrKkE%92)`u^t6)}ecRV5MgV%iC`WqxWNqJWZ6tT64cQLOjFvAo^LL#*KOkCg
z@?(}9cRX^34n=nVerVjf4dn|LVE3j?3Exp?vh&$@X`*NY>sprG<3vJ`2^YzoRU1NR`Ka4*h9m#
zk%31bZSE=)n*@?L&Tvzjx}$_$+3Q{v@2C1CRkUOyNO6)tDx3ERH>u*JRx5CVR=T?=
zdN&cHt3xEhdG^m-Iu0KHafh;x8)rJTd2Ic|vXa0q#4S6Q*GgFH>{>XC9I73Nh-Aqm
z!F&`ME}vHX#(&aUpL^BMEVr&Dg5{D6hLrL>j{arKm`)U?mC6U~+7>b#Q?fdeWrmb!
z2;}#}>ecIe>?O8sT8R(*!&?z#lSd*xwwLvyEouz*)5rML_y1}aflSlDBg6-_&Gm_kiguTk&PYXeO4#bbne0K!1|~k(}x6b`Gf&zv|O0%E$Z@KIbJv1
zjc)Ys-#?M2y}yCiAmpyDu0c^zuO7Jv!@I}hfv%EJu4syAa3)t$5gt@&qf*eXI9r68
zY}sB3AHBG)+g)dDc#y$nyaAeo87-;LiRLskm(FRMN-EuPl)UZ%X&Zk+%3rA|J3WCQ
ziDCzKtsI$XMDL`U2|<3{VJo&uvU-WCz*csp8I`Fz1FmUA
zv47pOb{LJ#HPX5VZ=|7PM^l6-sSp30yBf!jEWzlK+)nRQTHB0)oP3o=r1-udeCyQR
z{XIVy6XC8nv`K2xHle_7!z_ye^e!aoap6f=wzdqPe7+hRx9k=Zcswq<7j}}M!l9e8
z7HOiHyefWITWy8X{r$Jv|8s9B?cR8CEYnJeoFXwOQ8kAPB4}tb6pU1>Fy7yYL-O6|
zo#jD9abZWZf;YDr*bvGDo9A_>yA}Hk6v?quJNL@>QT98Ur>;|a_pnX4Hif?Bq*3_8
zoK^7XDuxwhcVh8$_Pi(F!>Nm>UX4N#)dZX%TosZbmdi_?1vISP$h{Jd{Db_E9Gr_&~(
zoP<7|sOTr#n^3l@6iaXJg(cgnaPYho=6xo9s+j?JM~#5Hq*&BI?uvs5;@-jD3!RGq-0$~e@#4k!;DZlDd*_cj4w7X8U>-NhRI1a|B%UpzZ{KD=j1}<{G^Ub8z`ZCnLk}#ggUgv1@k)vNK&c
z<(Of(;H=5`+beUid0ROliQdX`W^)iNJy$ly$M-rNtR4%t9hxU@EN)riV=q@it|b2FkLbdPi5W0
zY9H9eyiTRd4)4)QgtcPry2gJzZ5)0+Ybm~e$|z)e-5mm0$H<{0y0aV=zy2etp8Y2*
zBTR(agZ!&zp!bjOfU9pGx^=7*1hG23hhmHMufF;!?!NnOl$DhwzWd^fFYw@l52CoZ
z7%{p>mk0v!pTjx&}A2^Vc3E9uME*J8znI^<<-#~H^D$GCBaqoj8(AxeP6uBZ@m
zESu6=PNj5FPTqu0iy?s9b}u=2XwXjPX1Q_GRi~r6E{v!D_9j9Ro=X$#cJ8N3am}Tt
zV(@@sY%AR@#I5!e56pzhakKsjb>}NxUfb1mPCcepjU)|+HMH4;8_En?=l6cd*7{vhvRhA#I&0C1t
zgn+yfaab;PtO{Mhu`@I_e>oNuUktSK^Qq=
zyZ4~%_Mf2sy^oM_)CA<7GY$10e2nU6|BhJs9t?Qu_iz;zh@6y;5Tr95ggFOujq9(!
z9!*V6IQ#6gamp#D;L%4P#fvY#h)5)YXPo(9v_LY;QDAKm%aZJb&By|{m6i%Jm)D#!VGsg=Og#1C
zM`A&D^hzD&Xx)qE^J*D-nx7U%Pg#K#pUMIYuBuety0Xt*#^@$^G70=qr5JJKKH*blt-Ui
zwbxGO(kdn=1esXT=*1N^+wj~CrxIejaNlPi!m!*j2gRlwreWH^tt9*rRZ$l_+dsvc
zHy;G-Zn>3{CPKg9msou7379bXB=OlthmFCgUwnWyl_BExdWyKkCS=W4MNGSzquBv)
z*cLkx!(xiwB%K{UVbl;xm&8ktIbiEf5f&
zaSCAT6kccE_Be=WSzwFeK3}rzlk*%&W04{pfo|Gc7PlOPOXVN}K`ckAc9aJtMKrZz
z|9OFY9vwGaejJJlvPc}7i`5&61$y+P!fmw@b`y1%9FF85kOSl`o
z3#Vb)cJLGvv&oCn0dlA*$iew4DlMei+?)_exOG~gJ4z%ffPbxPK#t7l2}0yzGjOUdlHef8
z8dpve6syG^$U0+jTz>InLXK?w-*X>fdszb^ht|T@+@j2q9ha48FOu+fDoG@yhl#}^
z;+|~1GR>vWxbN=J6%4&^MOOdc8LX+$FJ+T_(%oP!4=34aIEO3`Z-VS)O`8VjF%
z$BSDQ6u?E|)(Lsd__Wp!w;jBvD2^Fd{$3N;5sf5Gn1)jq%SzU@HO!Xck7?2Q*#Z1@
zNj2^ewYa?!ImALn<@2ac*=pbq^S6rzA4*E7khqtk*nG@x_tr|pvP;KFYR+eI-7}9L
zjk~I0m94_o${<>VP{3+&Z3eB~V7lg#7nk6f>rQL~yo%~Z{oMIjfAQ5Yn?fQH%c!o!
z#>=i3go(QWm|?juyB>QAd!Bj$!~XFU@=iOAa5>xtc-un|<`9oR{2Vo1>>#x6#>C>m9U%!6BqSY2b?7B>eFlM{vscJ)EEN0-a5rr5&qz|6@`zP2|
zR!7{#l`yMIFEA3-^{hlgE+ru{7Bi!)T&(GOhYl+>m3!ETAqI00Zt5aRbV`H9o`{N5Gb|CSfBwvBj495*+}#$|
zax%IWC#Bj(4$D~+%Z^ji@Wk?ZOfokilg`^&-6&*o)7Ci>?FmvSCL>W+riwK?-8kz{
z?_t*Wr`en%A}vfp%8Gg=wK6x+9Hwz)?O>dZ}ooVY4Ij5WQF
zg?h~_8B>&$#oF|86q7Tz$#5rJ2Q9t#Dx&4P5UDPQ%a@-Zh$dG_v2#)qE%pe2qgCZX
zA89QmbS7d!=!dbiX=X-7h7hw_`_mFiy0b_Q?RG>1C;x+WkKqIJv8TF;#HwBJy7gpb
zK2vE~X415hU4~$XRWpLbO~hkPDIG`FQc>3!#X4ejlO`U8qQV@M@2(LcEUht}=hItQ
zs*)yYN5<@SFq+rf0&!Y!4qLQJAO{V>LF~eBY_Ql7a^dhCSqQk}wqc@4C6|HKzRIuVol7Yikt*Yb;<)tLO-Pl4Ty2=vRctCh4ZJ*bG5
z7EbS%_RCioc3uY?S>llpGW`TV6OWJrn35ucw+2o-KieRp=j?TB@mH)bT2{1B+EeC5
zXp%0y@vX(^F2YN?<2ZH@t;4xk=9T5^<#6gFna)BA5iCZOK1hHbPdc1TD^V>FPWgMg
zW0K`0^V`42=sF(J7>^iXcwAt)sRl5Q~jMqGOSi3ntPQCH7gC4|PtpfDrnW@lpb
zp@R^P#O&+kwQ>$QjPBH7vKQ7al@LvYbKXW~cKUW}>-j>DZ}bTrpR5sErulQiG5Z?JDx6r1bKL^`Wjcc-VL
zX|x(~xi_&Wa{6VE{-oi^o8H1>A1)Ed@$BO5n0)IS#FCql(?2tzf3%%P7GF?o9i;v5
z+J6dk5IE1kp3kf9Ud>6?$|=5B;e3Oo~(SXYF?l=BPG!A!qAPq$FWOjeC=Uk}uv
zq+M-FY1a>OKoLX~FxFHgI+-N$lO@cv4l=M$uAt^$ezRGKNF3a#n5nRCrsxIoPx-MS
zyoanu7@ch%bCOCEJKkRo_ncLvG!ZhyEi?zC!c@qX35gsno6mF?L74w+{o^08@rWZZ
zblNl=bMws@F=GbmmMz2j`|guUJR^p!d!!~gD;)jw(noVPg;1PXgO6_-iIbmN
zg|i-c2eW>BQHv+M;4h012@)E(qj=b&%*Lr^;tfvt858F%C8j!qiyV%>DpR@M@_
zbxO=4$4UhZNeM0u!3u@D{Rr$J%*@PeqsX^*2J+F~c`Xsd&Wy8CV?gY@bXL?g)rH!c8f0f@
z+bz4KLawIzNu1x!l-E=2#x<6j=wa6_R^3VC9i>%;M>Acy);9Apu-7hMj`~%r1n|3P
zk$9->h*o0If(s=*;pH)t;wbAQZWZgQLYVlcWjK0RF3ziqA>6nJq2?yMbjdKRE33gr
zyDU60r;)fw99~z_5~SGe6GUJqi=~G1UOkgtZyBi)eox(_gS{q>m37RK7|Ein$1gi&
zGR$do4U6EZmP#fTu_!VpPqH~(dIuBJ+itrJ-}~P8ggU@~J60-&!Pb%Aj2sNx9Z5L4
zuy{BjCJ7s_gUJf2>q2RDdA?3?TE$6Am+#0XUnUMZf8p2h
zMQJ?_fA~Y`3lf{J%j$KUqpghK9XcGv
zLq~MaFkr>H^#}~`f}nQ;sX7s{?A&U0R#|OBpZW8R+iI|GbG5J{31l>F=U~g8I=0?n
zCS-IoQJA`I-td0i8mnttcY9uPEd?ozH&q2a
zA^s$-WjG-Bup?G`1PvVrUssO4y=R1qhz8c{>zd$lYffDa+kolR?s2>t$r3k?HgC1a
z?NC~zd^*2JIhUX$2ge;cADN!8d&8r4$6YlIcYl5tSnuiR-L4LArS&BSI3xpfLRhzj~n
zPvV|B5*XaG_sGi1lHoHwU`mGQfW^7xP1w4%xyR~aaSWHTef#$RkLuw4=O2d-8-T;2
zGI*V%kl5N4j>*3>t$jDj?kho8_7PgFF76+u?_G3~%jU*T-QKWUcJK0ttOHeMtO^UN
z+Cr-}lz5wGO_6g%h^NaYrdo=G9&@-KT~te745Dc^`RodooXCFg`1c>BrKK1X)p0C*
z$V3n^5@7@JOlbrm#W#z#u~FGKaYEfk2;Wy>#_R$?JeE#xX(FmfmnMt%I!ABC=x3Dc
z8aK(BRd!r*Tv!xUn07Z`E$&Sqhm$9yrBHOwn+|L~3Dr
zMn;X+KN3?>r5r3?{jEDVj|m!4Fe55_B2lYZv?vRDBrs&f_YuJN6+`w>#OjKJG!Z1b
z35dP)o|?&(V=Pn>ov+Xg)YH8;5r<{Y5hJ8ww`YtnDn@!Da}cfwn%A5XvYse`=oBRZ
zKB3k0(>%GvM)K*?TpHPQpA3$SAki&M=+?jvV9kf@_(25u)*Y%eF$qZ0xEcymPnCa<
z!$pF!7TXW={~I|_SJ%7kXCyu_^Uyl59-~<@1IESiWyEQ@Yqe7b
zx3vzEYOib3TnG~f3dJlm65_F*S4eAIsCtpIRGkbak;1x1ARSX%G=Fc;WxVY?@M
z5JA3eN07v-U4|iBJ}VruWRVC!VgT`yTox)~`^G}FeFX8Ime9B}d^HJBWkv+M>Fj24
zPZb#=eTpc*-7ZLaj*uw2uG#{X8duP^%t!L4FCG=wipXkJ#=>z&s(M=1W*VE7xM*-%
zLi{OPItC#YU@1$lPUS5SfHDMI>VnP*wC+_sxHye#_0gU%PcQhr?(v
zr0JED%S4z@oB^5JX~-~rk<%r7wee&dJ{_pnfX2isC5i!tAI0?fAll4a
zF8)Sb#dZydFdzmTx5grZCjQ_k>bR)2%yDQMDgWVr^ZR)ab884`QMf{J(bq-Qeb`8V
zzbY2NmZ(Xbi;v(_N3sVnD{e#xUXUMj5SweJ@z73rPC`APab3|zY
z*{mC)^EfSzo0M*Mi3nYI2$B5U%3I|17<6s~K~~O9_CKlkfYvset`5>uJs$qsjZs
z<IMIxhYnqji5
zQXrN77?;rHJ}-1#%sR+HzI8_y?M@M~gIt#vIj$J$<0fiVH-ctZn1FY>2(Xv}TI}>9
z?n0v%k=fovGP6UPT_w^)G88e{aP!;HT*2@V5J%&-uR=!+vAyktFdWZOC#+b86rJev
zxE=H7vY1ntfKqH+^5&83ma2$&Knz$ws#QcgX-`uX&lcBnV
z8F#mCpn?#yme!kf5boK+T?$0duFz4e^f}1E;{b9Q-V`$mZ!8KQQL_Tgje3_G4a7No
zv~`;yH!#;#An+@vr`tx)R{XVe}!9snFv>0rMS>b7PLeh35n@;H*Y%brdLWxaB$S?pXih+nO>sWD-
zxq7CE2K$5wNk|;=tDN5(m*M*V=J#VGT*Rf4lCEMGy+gHaa&^&jvQi%AV(P>ZGfdf(
zwZHB`v98hklHL-INJpnrFhNyBTDZtPO3a_qLdY(?BP?2Wad6X1LR^IAibseu5mJ&K
zqj6z$&OD59_=?m>qKZ8KAcB0`%)BC-faGSbx1?|}1{OCAQG+Z>n`n}l)Z5TeVtTOK
zief#1SE(YS*V+2Ra!M1;P)egli=vQrxfd()rV78Q{M*auc(y9OBVbani8i)eHIYZi!)D)lcN8^>iE7e0LR^pn91ZO5Bpvp^9BvY=7||<9
zq-hW)1XbuGnkkDCYKBI76pNG=^6B~gv4~KZ*{K(I$$&%~`V&R06P8v&B2CCus`POv
z(!|aGH|*ASuBau#QAkZAE+NykHPZJA326&yeOALvt=oPOLB4H=tdwMmGv$z^3twD@{*+tmlkC76d8jNPOEj%HT&S2N_=H^dmSwe_gKnQDq
zp|!kHFb)G#%>d@19(AB~r#FN(#`hea=%B*RuIyh2%0T7`25F#l_uk
zrE@WpbbvSkK`EqpwTPf{0AXBTQdDyYMKq&X0InTzFHFOZ63qW%=Dro+s3YySK5pw0
zP9+_Fc0;DZ7SnyQIgu%~pA00YF_fOS!f{$b4iSqI((zUs=0ODck3$gB4+Ovh$tpB<
zg?^nKWlD_XdeSopNqi)3aj>7C5T#bPMAlsfDJ~%b&n7xINOMIPSV=#L(Oeu5+1s#l
zvk6NC<+j7jS+r*W?MjB~Dj2a=g-8H%S|BC>lMsPJJqqc$GVG#(;@-B>ooo@X=ournq{a%r
zD1XFAPxs2yND&ua5mSVj7^+k8MNLz~>H6pjfjLja#YPTc_d96C`j0zYmXfIdm0D}o
z*?5z5k+@>W4#fnF9G=okafz+YAmz9evPwu)grB&CUm!^5H4{>?6K@G|pd4aP1*%6x
z+IocY6158fS|XfdUjQ&MXqTZE6BGqnO)YEaZVImAU
zqG>G-BFKN@p<8xyfz*bpiruN0ezNEkAb{g6M3(a#IW}C6qR?=o#PC7!S*VCcu$#_l
zAQlmK>2Q%Onj&!uiP@<^YAsu)oEvd+`6*`wjrfsw(aCLd8zg-P4mJGXukrb5?>#5oGHT>#Y&{K0
zkb)>zhjp`q6AtZ&gjnTb0-s=Rf{75eDeQ$Q&gxW%b)9bCqP1y<+WZX7wJcg-+tlD7
z9hQlG)EA4Fox$W$asW~&g;f_h^aTn{BmN5MtkULz&n_7;DZ%^9V6irfmu*K+$w=0H
z;blpBUjv}wtW?8)z$>66R`c~~w=xtmY9vsy0+4b9$as=ac&=IZpM%gq%aQcn<}?}-
zxIDp`d%%y6Bqg4hC~C07b(lcgVwQ!6JCstMMbwbWn50Xw`VxX4@(Zh2>Kr`D^z(Gz
zdBZP_3Aa>3Lf*9jtpbp81c}iH7GuEcUJ-Srh8t;?Ofi^zO_aW2p20fJS}!HBTWwK?
z8dD)rZ8D^S2$8ec>p6sWiuDnjc0*ecQx#bQiIyNgsC1}?$kRMw6#8wR6n&FaSZ%Gc
z0)Y61Vd9OqAqxppIZ~(skSPEX-zbIsCODocMG0)4)QDnj;z^HQI-Z*%C#|ejFz+Hm
z(YGy1F#wV%K+d4*Ku8O#TVd0ZqHDIpvcOgC$Q*>zLnm>K0`pH|(4}X)>FclnKj0@c
z=3q*HtU31sxK0HiWj4HAaEwRrt*`PzI$4iu^?P{bw`qnE;R)|kwVS55OqFWtkSb4T
zFo-fx!E|$TT@Bq#%@&Qj$>JR>E!8HEPotl$fA#8&NRe_M(lcKL;?a~Jyk
z-Wab>-*xgU9AqkxbdO3aMbNwx)}}U|XWL_Y;x=
z7!scZTC1TtNHdPZdKoXKC%sYa;Ll=>g4%3-w0Ej)h~psBp47W7lhrF
zOhwd6sfyH5I+LVrOze9I{KTcjJdJ8xo9bBQYPW`;tfE4iPw^!?vOJG!2{3IXbpc|GfSCHmO4wq)l|#x_wcMr
zn9{zCsvKoDPFW_u?o*Q!qBkf-i7=ve!;-9o>WOYFOxsdv+QDG0vn*D{cpooGNqkw@
zqOfa{&0yfuZtUVlY&MYUnXbNy`dA-e$uPYmukQ<48Hr
zpmVP>yJpCdz(Lcun3Gu4gbIh^7hx5sx0^v^Wu0}7X|w9ZQiR3JRtB+`>ZFCCFh%$S
z)y!2;FahE7sBh(4KC4!?G{%wUH6D(erl4mr%JyLDxA+;!PmiH^9D~Bjc;YZM`8B@;
z;xuTh;OV}Z(uftZ5fv6;!D2AB@xsuB4$q~`Nq^*#M`+8IEi`M^ELyZ^k^GHBA`^1c
zTUuIZ)v8srcI{f)zI{6-5(#-7J?5rOn_W
zl%%K|nRzA&Fv19uM7{^Vwo_U=D1V5A$vB>X2Kg#TTB@>CIZ$vTKf~{bO$MYl=FYs9
zc2EYABPdX6OYkHLO4zi;Nl}aEl@=hFgc-hwn50iX{j@Z5U%q^~Y;)(%9haMpZl+H@
z`6NC1=%ciA=gz#JX=`hvH{X1d{_&50&_4U@LnoYY0yQ-??Yb&j0g~(*A?oo|Q8eUS
z#C5YpL)VY;SZbgkl!5a99@H%r@=mPUR557Q33G5&D7vTBK~m}#&trPiQL2fjYkGnB
zOWH1MrVP9oOM~f5`nNHtK=w11FGU+5K~Q~UC!bTyO-mBjEdE^7HKYer%2hod$J$#_
zsa3v!L;U%sp+KtKIn=_xeH;I;tWdimv->G8)Omx;M}@nSmq
z=%Z=3-F7S4L`bL8bmyIS(i2ZSk$nxlv2hO5r%xA2(c9Zgn>KBt&6_t1c%OUjIohyc
z1AX8FA1LX$Or?=#)uq!<3MsDEW>?T~l%IGfcZg0G`<@V0Y2I$C=1pm=0t5`%&>Wpa7xcX
zkjw*#f$hj6kECawc}9Tp%U}MI_S|z%I_8*TWO9zBbuWI%-FM%e1qY-Rj(hOI2TOO=
z++zb6Yu2oxhaP%JBni~9n{K*^F1X+Vnloq4$gE4AH!dxQOKJftl<&Z^tjAMMFcg)G
zA!-(NYbEc(5_Lw(HJCwwpehIr(u_)vOZZyvOeblp<10s+BswvH=Ly{FsujA+Qe{z7
zx;VmIp7=3LZrhW|UTcJDI;&?p`Lo?Re9)6xRQ$~h#2!AMEy7hYWU`7ELz5isu^+S6
zA;4GaBqRsG0fPdJ6bY7?TXf0M5KPu2vzY
zTEH8f^h30s$#D;#3i3VXfoJ%RO4mi16wj36&zQBK8wmfFtXNVBR1FLwTPhiP%pn4!
zw9@O5?HD(LiD*!gkJT&jTr3-y=b%9ihEso7BtlCRb8tLGr~M1Juz0ShK2?@{q)mm-+lL$
zi49c;r22>>j-aECI*O{Qs){~VDwPt61o4i^ecEZK2>~xO@EvEJb*?gkr~m*U07*na
zRF+Ias9CF5ua-%_-+ucIyWh5L+vwhV@0GuhP*Z(meBJU`+AW(>!BL8qn5wN=hi>d+LTDAsFgWK8V*9ViHlfdrCq8`cwjAar
zuCx_EDI(icRaoMtB_&l&CnIKKS8^LYl^Dh9O~TBuq|%k#!{9Dq0oPc?2u!)QECxY=
zMqeZcf-%t-Bo#}X*{`izXH1Q|GBQNpT?l^$vj2t77P6Yz#&23jTm{qKL5$Gh*o
zy8xuv0jm}y&Py-7q@qX}V1aZSWbI)9KKS5+bnC6R3bz5c@Yyk$Sc{M#zhMb-pvla@
zPpO_C5>pV++=l6S}aSAavOlpTj6cyNAUwQ%}
zt#?~(oaSa?%(D%O@v2?N62!GU+QH}2$tss^Ra1@ID^*lbqYFL|guS#vedBxOM#EI+
zDx*Tsl0y_dL-mXTx}cH;^?JQPEAtgSM>b?b2F8dGb%!IgC2Y_--e(;jYX{DskJG@S
z%d}DSJ59~jB-F(Uhr@L2vByepc93k4=xeXNR;0;s#~nx2)z!o9kJUf8IFLvq9Z>mZ
z&z>#B9a~RN&!F#wMeNUi{L2^q$LOAW?h&&v{3w`^r<`)iuofoph+s8_uVpj<1t$SGy1Kf=(v=I4S6_XV
zZomC@dHm6jew3CjT{@}22v$SCcrletY0Wl1lF)V4OeWzdUwc?@2tW4>uc%y;?DyYOeGVz5!g~DX;<>u#*=L_Erddo%NQY~#xrW~T
z?sp3}$)}RQyepO!Ru@L&h{Q1gl0c2hsx7a#1)7eFY9F>Wmp!;P{XbV%9)nVLC&R5UV9{
zCNkNt=E?V}7DbqxFxyJlyi9CMtYNKCidbx+7BAGZg?@x)GQhf#Yw!59LH8c?#oKR+
z2i;@y1R+>Z7P~Ii3_|13B$w
z?$P5bs~y5UA`*6?TxKeF$)>{ivz24fay3XCK8`_+JekpEVTYd<6K}%hDQbe~vxL!B
zvXFt?cLu46-sdC^K;KN$EIykVtnWv7JxBBj>Smz}RD!{0psymztENo>R4Qc2r0?}e
zxyFZE2TP!i1C|4g0FW4wg3GRE|l}D0FywAT>i!j0Ehyt_-Ow
zy^}Ippbdm40shF+a+G`0Q<4H3?)=hdjIT?KQY@h+-RlL%9UOfanCu_^@P|d^3JCet
zS6?mW*kB-@Di|-k@Phb4MheIvECu`m9)CC;A*nw2!4H;A7|LjKuVMY^>rWWOZJVNp
zCwo3LK!%hV*z970b^~qQc+}QHCd3#)qZM7W1&HW1FyoqYd0#J1m}ZNGX%S0Mcz*
zeF1_J_ryr=<$Y17Yc+pn8`NEvZnIh4ns0-=qq7tyG^(k1qL!s$Z_cTHYjHK)ZwJz
zrkq@W;9|jHmuun$@y0415Ot_rfB3^6gdbo4Jg`JztXsFPU#Ll;0fPU)0}sge22MqA
z4m^MFd*7P{=A>Sep-B2D0Fzr(4rCR2xThOc+J^Fa8`|Op5P?t^8m9P34C&wJim$-X
z{mRvM#gb{NV-VJ}DjcDpLjkKo;2nnQQHbcLD64Zee;;C=w(zmGA(Ig+I+OWoz6$fg
zFTFdEuZ{JuFdUmO31ih~i5OzAL>ymQLZk5xf-C{3n1jucEuKzMjJIei
zsS}}Gk!S{5vBA&6a(Z?+P7C8TG$$ISE;q^N)Gg}aq^el3AVHm4wrtrT$Kr=R^dY+O
z#v8?70%8tc0={zMi6_!K-ti9FxN)QK5I7z&*$+JMz=D$M=9_QMs#o|9c=mBjn4Jqb
z49kI}i`G(LO_S_Ea1Ri2UPaMQJeskYdl9`{V@fDa6
zrq@lm(Kaxp2EhoP@C%2kQZ6m$N!Q3K*Xz7ozrm9iEuJIcYKo=0MN&D)>tK+m`d2Qk
z=cikkgREdN)W|%eo)K7`Nl2^fZU)FUKHgRb)+|+d(bc3|9_zw+xHd5_+^47-ioq#U
zw4mwCA5G-7|&Nq1N!7~6x3~&t`4-y4Z
zhEV#1A_;Cl4b-1
zrh9c&cWsE4z970l9>8fJ`O25)F?8x>qHF@VETZZ{rWk0dGme;RVX8$2VoD;DeTfBw
zxjMxX%GAA0EFFytJcmU6BHGi4P&{l&v~Zl~K)BbHuzXvL!xpWy)4HgHDRgX70fGq&
zOA@@>`37D;@4WNq_rL!=z468y@*4al00}}@pc+ACf~giE?+9Um6A<9SaY4utdj;tc
z41}eCoWu8m&%ja!3AC#W!&ep>Gsv~zNr+liQk6u^t)9en*|LuNSyfk+7Ai$~Pv$_D
zq4T*7n(c?BbCk)1cNfu9L{@=_Yj_b{q@%iX~)3i1}NY|fiyr!2vjEk0=#4>pUWYK93tu*
zEM@Qr!+XDE$&y`4a*z%ck;0ys^~{dD4b?OUDn^JW6vESS6JbxfB>v;aVc=EN}u|=MMK7N({Rr
zX#vh6c6XgoIB(uO`3%&r>#n;_3x)T3%X>~x;w4y#QmzGsfEy9LLe%JERKSD27#mJ-s>
z6XFg|5q=EImMuwFtg2aFKeA?cWiMBXa5vPrH5^XO|DEB@xS#RZ^QiH6%PknQX_-(ODX#eNo|1%_IYWpz?dl
zqllZK>9J`HL~s#LrmUas2BZ|c81L8PsiG3DZDqql!c5ddtbV1!zJMWV@QUxbQYZoe
zlE{)wYpWH2?&)rEs?L>4t%aWlzZa6jcQu&@7ju=}&w#eU-)y;Rfw3kNATZs6ppP`H
z1JVFD9f67gKrjOT#%Kq8DmVr}ARph}jqs&p@o9PiNTS#t-3LZ@GbC_P%B{O*x
zN~7Pygphuc7#&wPjus;X9<#bujd-(c6CcWkqc|dsJn`wk@z~4c9XZ330uY3HU?L*4
zBp#0!ZSewsKxjzr%Aa=;GnLI8$A(E4)-CLhfk_W?E@GV3F1!b+gZE)}hV;SX+itr}
z;=?AYbWy2>4esSCgDunsBr7>VFGEsMytelAd0Aqwz3wG&grGp2gu3{ujQ}iFl6)oc
zTYNse)Ei7V>JdO_Gk_Q%2>j`ifcK07#u$x1hWw%M#4bXd>Fg#N9Cwf}dtJDJ_w=rBE@IqfpxQ
zN>E}SmFMda(pI*>W~V**mS@R1gBL|YVKEwHCPT%-stwM7Fq(Cs5o0~;t0{KS9B94Utm2O~IOE4fQz&~JF13+K}1861#99d34eK{b-PTnvw
zP+7-zSYL-~f`SB4Z)6qc4F%b3-CjB9RDjQ0hEb9pBuG3G|AV0_7K&2n>Vj`j8Ka1j
z^@K#juigcb4j_K$6imYr-y6I{V!~7hQrVM0dRF~Ze
z2MAVWNDxHj<|`ePMx-@OG)zp)%(X!HWi+sA^bahGdl$HON#F
zL_$7{-2Cm)55#^cEO;tG
zdP|*$NZ5q(y?B<}7uTi(-=yFXKF5Hr<|}VbG(ccwLLoVbfC>@?9`oE|AqqJ6I|uTjyY9Lx>xqYd2BzN$mmtRA
zzax<#Af_mhlj57(&eu6172GYdP>+fkXas4juJWEsP^9t{s;Y`5AR(soQ)hL>HYI%(
zS06kGovIv`W?MZ^N}q}!#8hR4;JXX?rD^k^V#@q17Xb{=_$TN&2-_sAYDow+gajjF
z(K3~^a}k20oSTizNJ10c9|SxCehY+$piUJA)F*_pqh3~!Z=O5g{YG&HNDC0`p$fO{
zzyJQ?_xQ_S{*rwSh2nC}xC12?01I^wpM&Kp$P)qJ5H&m%9f&TJXUi1zrIXpY!@nEX
z3^%)$LQ;N5)GMr(W>A?{Pb_cjv(yWzNuRL{mJSAWFc{les&%-Is<@~tq)6DgtJ+%I
zmZkb4x}K)hWB7j4*MW+LNK;E0w6Z*;!HTiO%1E?xFQlFAitk3qx~XzikUuiZjq+mB
zJ8=Mk`f}fW_X)CLMS_D4fWd3v3H{UrzF;8QhaY~p1Q!-tHwzKI012ejzyZQOP-OyG
zLEv*&A5?<`!4FbyW!gsh1-i-!DC#MG7_!K$LHfz
zTM??_XUJQRAZU@{=Y&zaD?!Ojj|6Z!60#)*UR62MP@y>Wc38~N69y3Q3UCDwb4-4e
z)jAV3BNMdCGZlEA}`?*mzfS~gh#adqcc
z@q?h*rLTM;p0+r_$^u89m`yzupJZsSuL_|^Qz%tUJNyXkU=!s%;qYmO?`LaX
znJSB_pSf2&%kZ#^+0)EA8iU0HueFBK6y{kKNn?Vmx><PSM5z=zQX_IRB*jv3AZBO?SvQiKmCgrqrk9k=7l6-BCLS=g~A50KHpx0wn
z7uz&XK;&->lp0W>4m<2H;TJ>kgrZ0yA0K(_013SH!2q!zIO!3(@~KaKO4KY!Gn^-i
zH{dZc3@5tePf
zx>L2DGz;K+WKkM^7CvMft4}SNq!a-h`l?J3
zyysXLyc|=v;!9Hew4fCMX}mR&s4C(BM&_t^DoaWY`P7BjD^kbzgOmVe50>cRTC$|v
zPhhVYcYr{(0xt+EiNLCqD>Xozks%0GYAnFW9YKgZ)F+UC+z3dfe4GLj1mBO^zOb+%
z-vpK{SjAA?q@1h33#r!XnACxSZqI5=yPuNUPEyr_SK_!2>y;t#P=(YeC79goJVY^R
z!&lM^v$}@Y<|Hgo{2VE;E&SyO8)-+?MU)qH(ke%tQ~+d%GrTax3|9xjj59!RA>epJSbOek7_bn5iy*3YJQ9|`HxLSsuy}x=P)aV&
z0U!lIhZF%fLdGIY!=(alDTZYj{jQ@5Or&f~6Dxh1RirbbR>9*Rv6QbOp?kFhBz}PgI@_U^rqbkxeX8dl8;8}7IS!Jmx_JLq23STU
zu8}pROW{0~JWn&hU$n7f+$9K1wy;j+4iIhx145k|@0!4{fSr8u$?_N~S70t4ZU79h
zV1d9x`oQTpnhK5+%3A`Lun&uoNjoHLgLFtcqz5W@*jW?kuqrBwG5D%vr)#2Wi6s%U
zNG?J?`rD^pz^pD;3~X*tzL2f0OF#X21UKfqE8L%%J)SAarOw!r9?>@G`0vOB1PF
zl{g9mqG}@_som!Z&yppbN$HUGVKf$85A+ie$EZy;c2t0n<5tKns(pB9xyO37K~*67wqLk|nQKWPp@#$~1UmAH+{M7?mZH=i
zy=AGk2(Autk}gahi~%ZzH*NQk`v4rldEtIz8V>Db)v|{7Z`AdBd-?pDrOvP7aq^#zCV-J6#OG3r
zl0UeXIZ+1V0+66Gdf?fHiu0~_y-U=b$?`3LgMecoqYxlPrS^R0V}z{$*syG&1nOve
zn2fjTD)n!b)*}83kYZ%XHG2+qMi7&wyxLe5QJ^4-RTOF71zM4#M<1+Fu;z3y$7n}!
z3v{_f8#FNm+d7uqlcYgkt0(9bWr^9;)SM!xGK<$|3A2-qNDin_RdQ}bb^M*ROspgD
zM=;qJ2OV0nWzv$}2S164GD56=Z7O(i`21~Do6pb=K2`&tNF5(H!6(zM3!MNMz^+uG
zWbT32aGe^>u;hy1oKa@T6m!IQSX%gtp}QmM79`2cv~wZ}O^
znt{|K_6p~V5Eoe30OoQ`$<6hT1Ozy!B#4lwZl;!Lo>U>J$l=NAjG_gYyslZe?|L$1
ziRZh6$?Q&EwRiGyIt-Qgl(9@|O4zF9fNq*5CuE~@l`44ZIiAW8H1Z_yqB;qm0CM7M
zkPgigzm@?N=MxVlm|S}){tj2p-O>@sC`j#xBRuKDV(Q&gfTY8)>_~YZ<{P{iv@!dL
zgyNKDAoOPR_49SEh(svp5EPjG86YTKm9s8j$(IdzuB#E1D7K!7#E{w`q-*gtwS3;mK=O7T6hC6;Mg;H1rEFL
zeZUNylodCmWOH^;n8
zO2_iUJejFq5E@FGHp&Wb&`z{yOFL4TT@1LEs{f7zFnm|Hk(OpZF-NImuh}7b6Kb8S
zLPxs!T*8heb$&ApxTLMxJy-F2jjk%B=b{jIGQ!uSXZ$z_CKp0V%5gw#g2x=&R2w2S
zTrW$up(bGU?&ite!|Nwn1|X~)1=$dA7lOqG@lIHzcvX$`s+?{(8{w=&1rO}k#p+Cw
z`OC&+r&yZO+E*blQXsLuWUtGdR^<$o^+_CgFVS-orEeW$T%
z7KI2i(qsDlGs02oKyx8J-vp9ub0lNhReeh+#~1-_mT?^EP9=?>bqhOLf{-`wc{*Q&
z^zWg5#gA;Nj41{X6+Vs?6EQ?-))P8;#cpQ;+KJq5UV$w)&HBB`x_v}Umhe;?mTKP|
zM<$_f=(b{v2W89IGRFr`{OSQ5Q!hy{&;2!Me5
z`4xanxgjcz0aGl@u&UTur?zCF%%;QAq|SzElZmRY`nso48;ajcxi+myB~(vy#7&VZ
zss?Yl%HM{=&mmVUAuuKTrqb2%BCNZ0;5qQC@NsLH*Gv+Nh~fYMAOJ~3K~&?vb$l*W
zsF)95g{@jdN!c8o@`|RMVVQAbpKeIYwS-@p)P#netUltzT3y6g*}CG9jv^q^=dV2W
zxE5AM1;TiiY8H<(LbVr^CY4_76$vucIf$yh(aTF`i<6cvODU;N$jS?A7myxvLeT#W
zvYnLVrML3UZ{=0Cm)UHyqdPhRs@f=VWC0iPRdsiZRFy@^JTWx?U@0lJdRaQhjvSbR=99LCB2lqhOJI)6aE@LW-T2;aoRSM=|X+@0!aNsYVR&~bs>(e}$jbw(R
z{F|cT2u)*7)yv=80jrj$%E|}9l0n!&(74CZ2~>fDKd5*TMpmH?^;bck6_Q@IL$wF6
zbiG=VuyO?H!B69$--l66J5w2253e~-c1%okeT_1w2=i@UEEGOO>!TL6unN@7Dphwz
zfyk6vRVlIi%L3^cx?v`(Z17g|_a>#dvrDmzLv=ipnk;~WRVn`NsFCD}u9jA~>oVCY
zb!PV8L{3J5sYC6v>Y*9b_ZnV_9Bxzt_*O`KAicsV3~GR73q}S8z{6;uV+valzcXCzvAzTYj5p?9XM8W`^$qP?K#VT`-q5lTC3fhtcnIGmA9KANWbo#0*
zwWcibdyrUS`17%9kuI&>J