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 %> - + <% 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 %>
  • 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 @@ 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 @@
    • <%= link_to ts("Forgot password?"), new_user_password_path %>
    • - <% if @admin_settings.account_creation_enabled? && !@admin_settings.creation_requires_invite? %> + <% if AdminSetting.current.account_creation_enabled? && !AdminSetting.current.creation_requires_invite? %>
    • <%= link_to ts("Create an Account"), signup_path %>
    • - <% elsif @admin_settings.invite_from_queue_enabled? %> + <% elsif AdminSetting.current.invite_from_queue_enabled? %>
    • <%= link_to ts("Get an Invitation"), invite_requests_path %>
    • diff --git a/app/views/users/sessions/new.html.erb b/app/views/users/sessions/new.html.erb index 077fe2a63f2..85215f894b6 100644 --- a/app/views/users/sessions/new.html.erb +++ b/app/views/users/sessions/new.html.erb @@ -3,9 +3,9 @@

      <%= t(".restricted.work_unavailable") %> <%= t(".restricted.account_exists") %> - <% if @admin_settings.invite_from_queue_enabled? && @admin_settings.creation_requires_invite? %> + <% if AdminSetting.current.invite_from_queue_enabled? && AdminSetting.current.creation_requires_invite? %> <%= t(".restricted.no_account", request_invite_link: link_to(t(".restricted.request_invite"), invite_requests_path)).html_safe %> - <% elsif @admin_settings.account_creation_enabled? && !@admin_settings.creation_requires_invite? %> + <% elsif AdminSetting.current.account_creation_enabled? && !AdminSetting.current.creation_requires_invite? %> <%= link_to t(".restricted.signup"), signup_path %> <% end %>

      @@ -14,9 +14,9 @@

      <%= t(".restricted.commenting_unavailable") %> <%= t(".restricted.account_exists") %> - <% if @admin_settings.invite_from_queue_enabled? && @admin_settings.creation_requires_invite? %> + <% if AdminSetting.current.invite_from_queue_enabled? && AdminSetting.current.creation_requires_invite? %> <%= t(".restricted.no_account", request_invite_link: link_to(t(".restricted.request_invite"), invite_requests_path)).html_safe %> - <% elsif @admin_settings.account_creation_enabled? && !@admin_settings.creation_requires_invite? %> + <% elsif AdminSetting.current.account_creation_enabled? && !AdminSetting.current.creation_requires_invite? %> <%= link_to t(".restricted.signup"), signup_path %> <% end %>

      @@ -35,9 +35,9 @@

      <%= t(".login.forgot", reset_password_link: link_to(t(".login.reset_password"), new_user_password_path)).html_safe %> - <% if @admin_settings.invite_from_queue_enabled? && @admin_settings.creation_requires_invite? %> + <% if AdminSetting.current.invite_from_queue_enabled? && AdminSetting.current.creation_requires_invite? %>
      <%= t(".login.no_account", join_link: link_to(t(".login.request_invite"), invite_requests_path)).html_safe %> - <% elsif @admin_settings.account_creation_enabled? && !@admin_settings.creation_requires_invite? %> + <% elsif AdminSetting.current.account_creation_enabled? && !AdminSetting.current.creation_requires_invite? %>
      <%= t(".login.no_account", join_link: link_to(t(".login.create_account"), signup_path)).html_safe %> <% end %>

      diff --git a/app/views/works/_work_header_navigation.html.erb b/app/views/works/_work_header_navigation.html.erb index 7aa7cc9eb7a..7217d15116a 100644 --- a/app/views/works/_work_header_navigation.html.erb +++ b/app/views/works/_work_header_navigation.html.erb @@ -108,7 +108,7 @@ <% end %> - <% if downloadable? && @admin_settings.downloads_enabled? %> + <% if downloadable? && AdminSetting.current.downloads_enabled? %>
    • <%= ts("Download") %> <% end %> +<%= will_paginate @pseuds %> @@ -20,4 +21,6 @@
    - + +<%= 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| %> -

    <%= 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") %>
  • 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 %> 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 @@ <% 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') %>

    +
    + <%= 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") %>

    - <%= ts('If you accept cookies from our site and you choose "Proceed", 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.') %> + <%= t(".footnote") %>

      - <%= 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 %> - + <% 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 "

    Britney SpearsYou 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") %>
    "> + - - - - + + + + <% for admin_activity in @activities %> - - - - - - + + + + + + <% end %>
    <%= 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") %>
    <%= 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) %>
    @@ -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") %>

    +

    <%= 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") %>
    <% 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 %>

    -

    <%= ts("You cannot change the pseud that matches your user name. However, you can change your user name instead.".html_safe) %>

    +

    <%= t(".change_matching_pseud_html", change_username_link: link_to(t(".change_username"), change_username_user_path(@user))) %>

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

    <%= t(".ticket_footnote") %>

    +
    + <% 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? %> diff --git a/public/507.html b/public/507.html index 4d6542e966f..722f08f1a55 100644 --- a/public/507.html +++ b/public/507.html @@ -127,7 +127,7 @@

    Contact Us

    Development

    diff --git a/public/nomaintenance.html b/public/nomaintenance.html index 28d0d8ea51e..b8b37c2ccac 100644 --- a/public/nomaintenance.html +++ b/public/nomaintenance.html @@ -125,7 +125,7 @@

    Contact Us

    Development

    diff --git a/public/status/index.html b/public/status/index.html index f2abe4e72a4..520c1cb73a2 100644 --- a/public/status/index.html +++ b/public/status/index.html @@ -1,5 +1,6 @@ + @@ -7,7 +8,7 @@ + Organization for Transformative Works" /> @@ -23,8 +24,10 @@ } - - + + @@ -34,12 +37,15 @@ - - + + + @@ -184,4 +180,4 @@

    Development

    - \ No newline at end of file + From bf027f3a76210845e05d50478d645a1e8c96b082 Mon Sep 17 00:00:00 2001 From: Claire Carden <96350691+smclairecarden@users.noreply.github.com> Date: Sun, 24 Mar 2024 14:53:04 -0500 Subject: [PATCH 180/208] AO3-6676 Remove participant memoization causing Leave button to appear on all collection blurbs (#4751) * AO3-6676 Remove participant memoisation Too much of a good thing can be harmful - we don't want to memoise the participant for a collection, because the blurb is used on listings of multiple collections. Memoising (persisting the value in a variable instead of computing it each time) is useful when we want to use the same computation during a request. Here, the computation needs to be different from one collection to the next. The participant doesn't need to be an instance variable either. It's not what causes the bug, reusing the value is what ails us, but I have some vague memories of instance variables being less performant than plain variables, and therefore we want to prefer plain variables when it makes no difference. * AO3-6676 - finish cucumber test maybe. * AO#-6676 - add giiven steps * AO#-6676 - fix rubocop error * AO#-6676 - updsted steps * AO#-6676 - fix rubocop error * AO3-6676 - continue editing steps * AO3-6676 - continue editing steps * AO3-6676 - continue editing steps * AO3-6676 - continue editing steps * AO3-6676 - continue editing steps * AO3-6676 - continue editing steps * AO3-6676 - continue editing steps * AO3-6676 - continue editing steps * AO3-6676 - continue editing steps * AO3-6676 - continue editing steps * AO3-6676 - continue editing steps * AO3-6676 - continue editing steps * AO3-6676 - continue editing steps * AO3-6676 - continue editing steps * AO3-6676 - continue editing steps * AO3-6676 - continue editing steps * AO3-6676 - continue editing steps * AO3-6676 - back to the other way * AO3-6676 - are the collections even on the page * AO3-6676 - trying a different way to get the collections to show up * AO3-6676 - fixing syntax error? * AO3-6676 - fixing ambigous match error * AO3-6676 - still fixing ambigous match error * AO3-6676 - fixing syntax error? * AO3-6676 - still fixing ambigous match error * AO3-6676 - still fixing ambigous match error * AO3-6676 - still fixing ambigous match error * AO3-6676 - still fixing ambigous match error * AO3-6676 - rubocop * AO3-6676 - pr feedback * AO3-6676 - button text * AO3-6676 - button text * AO3-6676 - address pr feedback --------- Co-authored-by: neuroalien <> --- app/views/collections/_collection_blurb.html.erb | 4 ++-- features/collections/collection_participants.feature | 9 +++++++++ features/step_definitions/collection_steps.rb | 7 +++++++ features/step_definitions/generic_steps.rb | 4 ++++ 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/app/views/collections/_collection_blurb.html.erb b/app/views/collections/_collection_blurb.html.erb index ea6ea2b066b..055927b3be7 100644 --- a/app/views/collections/_collection_blurb.html.erb +++ b/app/views/collections/_collection_blurb.html.erb @@ -72,8 +72,8 @@ <% end %> <% if !collection.user_is_owner?(current_user) && collection.moderated? && !(collection.challenge && collection.challenge.signup_open) %>
  • - <% if (@participant ||= collection.get_participants_for_user(current_user).first) %> - <%= link_to ts("Leave"), collection_participant_path(collection, @participant), + <% if (participant = collection.get_participants_for_user(current_user).first) %> + <%= link_to ts("Leave"), collection_participant_path(collection, participant), data: {confirm: ts('Are you certain you want to leave this collection?')}, :method => :delete %>
  • <% else %> diff --git a/features/collections/collection_participants.feature b/features/collections/collection_participants.feature index 84d1222b182..868faa442a1 100644 --- a/features/collections/collection_participants.feature +++ b/features/collections/collection_participants.feature @@ -66,3 +66,12 @@ And I follow "Join" Then I should see "You are now a member of Such a nice collection" When I am in the default browser + +Scenario: Collection member should see correct button text + Given I have the moderated collection "ModeratedCollection" + And I have the moderated collection "ModeratedCollectionTheSequel" + And I am logged in as "sam" + And I have joined the collection "ModeratedCollection" as "sam" + When I am on the collections page + Then I should see "Leave" exactly 1 time + And I should see "Join" exactly 1 time \ No newline at end of file diff --git a/features/step_definitions/collection_steps.rb b/features/step_definitions/collection_steps.rb index c01aba626fa..6daa52bf489 100644 --- a/features/step_definitions/collection_steps.rb +++ b/features/step_definitions/collection_steps.rb @@ -117,6 +117,13 @@ step %{I should see "Updated #{name}"} end +Given "I have joined the collection {string} as {string}" do |title, login| + collection = Collection.find_by(title: title) + user = User.find_by(login: login) + FactoryBot.create(:collection_participant, pseud: user.default_pseud, collection: collection, participant_role: "Member") + visit collections_path +end + ### WHEN When /^I set up (?:a|the) collection "([^"]*)"(?: with name "([^"]*)")?$/ do |title, name| diff --git a/features/step_definitions/generic_steps.rb b/features/step_definitions/generic_steps.rb index 7f53e72c35c..db802cc0f50 100644 --- a/features/step_definitions/generic_steps.rb +++ b/features/step_definitions/generic_steps.rb @@ -271,3 +271,7 @@ def assure_xpath_not_present(tag, attribute, value, selector) Time.zone = zone page.body.should =~ /#{Regexp.escape(Time.zone.now.zone)}/ end + +Then "I should see {string} exactly {int} time(s)" do |string, int| + expect(page).to have_content(string).exactly(int) +end From f07d729477026cd8f4e1db54883e773e64e67fc8 Mon Sep 17 00:00:00 2001 From: Claire Carden <96350691+smclairecarden@users.noreply.github.com> Date: Sun, 24 Mar 2024 14:53:10 -0500 Subject: [PATCH 181/208] AO3-6628 Reorder font family names (#4712) * update font family order * 6628 - add new line to the end of tiny_mce_custom.css --- public/help/skins-wizard-font.html | 2 +- public/stylesheets/site/2.0/01-core.css | 2 +- public/stylesheets/site/2.0/02-elements.css | 2 +- public/stylesheets/site/2.0/07-interactions.css | 2 +- public/stylesheets/site/2.0/08-actions.css | 4 ++-- public/stylesheets/site/2.0/09-roles-states.css | 2 +- public/stylesheets/tiny_mce_custom.css | 4 ++-- 7 files changed, 9 insertions(+), 9 deletions(-) diff --git a/public/help/skins-wizard-font.html b/public/help/skins-wizard-font.html index 6ffa816f861..03290e3ed20 100644 --- a/public/help/skins-wizard-font.html +++ b/public/help/skins-wizard-font.html @@ -1,3 +1,3 @@ -

    The default is: 'Lucida Grande', 'Lucida Sans Unicode', 'GNU Unifont', Verdana, Helvetica, sans-serif

    +

    The default is: 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, sans-serif, 'GNU Unifont'

    Put any font name in here, and if it's installed on your computer, it'll work for you. If you use several different devices, specify some fall-back fonts, with commas in between the names, in case one of your devices doesn't have the first font.

    You can use either single or double quotation marks around fonts with multi-word names, e.g. "Lucida Grande" or 'Lucide Sans Unicode'.

    diff --git a/public/stylesheets/site/2.0/01-core.css b/public/stylesheets/site/2.0/01-core.css index 1bd0e33548c..95b203e319f 100644 --- a/public/stylesheets/site/2.0/01-core.css +++ b/public/stylesheets/site/2.0/01-core.css @@ -4,7 +4,7 @@ http://otwcode.github.com/docs/front_end_coding/css-shorthand*/ body, .toggled form, .dynamic form, .secondary, .dropdown { background: #fff; color: #2a2a2a; - font: 100%/1.125 'Lucida Grande', 'Lucida Sans Unicode', 'GNU Unifont', Verdana, Helvetica, sans-serif; + font: 100%/1.125 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, sans-serif, 'GNU Unifont'; margin: 0; padding: 0; } diff --git a/public/stylesheets/site/2.0/02-elements.css b/public/stylesheets/site/2.0/02-elements.css index 4f0201a738e..e24b00c216d 100644 --- a/public/stylesheets/site/2.0/02-elements.css +++ b/public/stylesheets/site/2.0/02-elements.css @@ -19,7 +19,7 @@ html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockq /* The global rules for normal elements */ body { - font: 100%/1.125 'Lucida Grande', 'Lucida Sans Unicode', 'GNU Unifont', Verdana, Helvetica, sans-serif; + font: 100%/1.125 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, sans-serif, 'GNU Unifont'; } a, a:link, a:visited:hover { diff --git a/public/stylesheets/site/2.0/07-interactions.css b/public/stylesheets/site/2.0/07-interactions.css index 66cae9fc64c..66e56c6aa4c 100644 --- a/public/stylesheets/site/2.0/07-interactions.css +++ b/public/stylesheets/site/2.0/07-interactions.css @@ -66,7 +66,7 @@ label { } input, textarea { - font: 100% 'Lucida Grande', 'Lucida Sans Unicode', 'GNU Unifont', Verdana, Helvetica, sans-serif; + font: 100% 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, sans-serif, 'GNU Unifont'; width: 100%; border: 1px solid #bbb; box-shadow: inset 0 1px 2px #ccc; diff --git a/public/stylesheets/site/2.0/08-actions.css b/public/stylesheets/site/2.0/08-actions.css index 880ff948414..aaa3e053026 100644 --- a/public/stylesheets/site/2.0/08-actions.css +++ b/public/stylesheets/site/2.0/08-actions.css @@ -46,7 +46,7 @@ ul.actions { } button { - font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'GNU Unifont', Verdana, Helvetica, sans-serif; + font-family: 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, sans-serif, 'GNU Unifont'; box-sizing: content-box; } @@ -165,7 +165,7 @@ legend .action:link { .heading .actions, .heading .action, .heading span.actions { height: auto; - font: 100 75%/1.286 'Lucida Grande', 'Lucida Sans Unicode', 'GNU Unifont', Verdana, Helvetica, sans-serif; + font: 100 75%/1.286 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, sans-serif, 'GNU Unifont'; padding: 0.15em 0.375em; } diff --git a/public/stylesheets/site/2.0/09-roles-states.css b/public/stylesheets/site/2.0/09-roles-states.css index 11b04fefcdc..e2f5e5c114d 100644 --- a/public/stylesheets/site/2.0/09-roles-states.css +++ b/public/stylesheets/site/2.0/09-roles-states.css @@ -19,7 +19,7 @@ span.unread, .replied, span.claimed, .actions span.defaulted { background: #ccc; color: #900; width: auto; - font: 100 100%/1.286 'Lucida Grande', 'Lucida Sans Unicode', 'GNU Unifont', Verdana, Helvetica, sans-serif; + font: 100 100%/1.286 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, sans-serif, 'GNU Unifont'; height: 1.286em; vertical-align: middle; display: inline-block; diff --git a/public/stylesheets/tiny_mce_custom.css b/public/stylesheets/tiny_mce_custom.css index e90db3123af..bdc80d50d34 100644 --- a/public/stylesheets/tiny_mce_custom.css +++ b/public/stylesheets/tiny_mce_custom.css @@ -1,3 +1,3 @@ body, td, th { - font: 87.5%/1.1286 'Lucida Grande', 'Lucida Sans Unicode', 'GNU Unifont', Verdana, Helvetica, sans-serif; -} \ No newline at end of file + font: 87.5%/1.1286 'Lucida Grande', 'Lucida Sans Unicode', Verdana, Helvetica, sans-serif, 'GNU Unifont'; +} From a4d1e191365b5368137c156c22673b2461a5dea2 Mon Sep 17 00:00:00 2001 From: Brandon Walsh Date: Sun, 24 Mar 2024 15:53:19 -0400 Subject: [PATCH 182/208] AO3-6623 Paginate works in series (#4741) * Paginate works in series #show routes * Create series pagination test * Remove specified selectors on series works * Remove pagination selectors * Try not confirming pagination can be seen * Set Work to have 3 per page * Modify WillPaginate items per page for series test * Add pagination to works on 1 line * Update new series given step definition --------- Co-authored-by: Brian Austin --- app/controllers/series_controller.rb | 2 +- app/views/series/show.html.erb | 9 +++++++++ features/other_b/series.feature | 16 ++++++++++++++++ features/step_definitions/series_steps.rb | 4 ++++ 4 files changed, 30 insertions(+), 1 deletion(-) diff --git a/app/controllers/series_controller.rb b/app/controllers/series_controller.rb index d5b5a9722a1..697bff6d2c6 100644 --- a/app/controllers/series_controller.rb +++ b/app/controllers/series_controller.rb @@ -43,7 +43,7 @@ def index # GET /series/1 # GET /series/1.xml def show - @works = @series.works_in_order.posted.select(&:visible?) + @works = @series.works_in_order.posted.select(&:visible?).paginate(page: params[:page]) # sets the page title with the data for the series @page_title = @series.unrevealed? ? ts("Mystery Series") : get_page_title(@series.allfandoms.collect(&:name).join(', '), @series.anonymous? ? ts("Anonymous") : @series.allpseuds.collect(&:byline).join(', '), @series.title) diff --git a/app/views/series/show.html.erb b/app/views/series/show.html.erb index 7747a0d4f05..4d87bbf92ff 100644 --- a/app/views/series/show.html.erb +++ b/app/views/series/show.html.erb @@ -84,9 +84,18 @@ <% end %>

    <%= ts("Listing Series") %>

    +<% unless @works.blank? %> + + <%= will_paginate @works %> +<% end %>
      <%= render partial: "works/work_blurb", collection: @works, as: :work %>
    + +<% unless @works.blank? %> + + <%= will_paginate @works %> +<% end %> diff --git a/features/other_b/series.feature b/features/other_b/series.feature index c598bf05ed4..ee39b8d37a1 100644 --- a/features/other_b/series.feature +++ b/features/other_b/series.feature @@ -184,6 +184,22 @@ Feature: Create and Edit Series Then I should see "penguins30" When I follow "Next" Then I should see "penguins0" + + Scenario: Series show page with many works + Given I am logged in as "author" + And I post the work "Caesar" as part of a series "Salads" + And I post the work "Chicken" as part of a series "Salads" + And I post the work "Pasta" as part of a series "Salads" + And I post the work "Spring" as part of a series "Salads" + And I post the work "Chef" as part of a series "Salads" + And there are 3 works per series page + When I view the series "Salads" + Then I should see "Caesar" + And I should see "Chicken" + And I should see "Pasta" + When I follow "Next" + Then I should see "Spring" + And I should see "Chef" Scenario: Removing self as co-creator from co-created series when you are the only creator of a work in the series. Given I am logged in as "sun" diff --git a/features/step_definitions/series_steps.rb b/features/step_definitions/series_steps.rb index 998d7769e95..333aea5c9d4 100644 --- a/features/step_definitions/series_steps.rb +++ b/features/step_definitions/series_steps.rb @@ -2,6 +2,10 @@ visit series_path(Series.find_by(title: series)) end +Given "there are {int} works per series page" do |amount| + allow(WillPaginate).to receive(:per_page).and_return(amount) +end + When /^I add the series "([^\"]*)"$/ do |series_title| check("series-options-show") if Series.find_by(title: series_title) From 79824cadd329f4493aed7dfa775e69be81eb5cf3 Mon Sep 17 00:00:00 2001 From: weeklies <80141759+weeklies@users.noreply.github.com> Date: Sun, 24 Mar 2024 19:53:38 +0000 Subject: [PATCH 183/208] AO3-6564 Reduce fields where images are shown (#4729) * Add strip_images to banner * AO3-6564 Bookmarker's notes * AO3-6564 Pseud desc * Abuse and support form description * AO3-6564 Admin post comment * AO3-6564 Add features (overkill?) * AO3-6564 woof * AO3-6564 doggo * AO3-6564 Feedback * AO3-6564 appease dog * Strip images from emails (AdminPost only) --- app/models/comment.rb | 2 +- .../feedback_reporters/abuse_reporter.rb | 4 +- .../feedback_reporters/support_reporter.rb | 4 +- app/views/admin/banners/_banner.html.erb | 2 +- .../bookmarks/_bookmark_blurb_short.html.erb | 4 +- .../bookmarks/_bookmark_user_module.html.erb | 4 +- app/views/comments/_single_comment.html.erb | 4 +- app/views/layouts/_banner.html.erb | 37 +++++++--------- app/views/pseuds/_pseud_module.html.erb | 4 +- app/views/share/_embed_meta_bookmark.erb | 2 +- features/bookmarks/bookmark_create.feature | 12 ++++++ .../comments_and_kudos/add_comment.feature | 15 ++++++- .../comments_adminposts.feature | 11 +++++ features/other_a/pseuds.feature | 12 ++++++ lib/html_cleaner.rb | 20 +++++---- spec/mailers/admin_mailer_spec.rb | 42 +++++++++++++++++++ spec/mailers/comment_mailer_spec.rb | 40 +++++++++++++++++- .../feedback_reporters/abuse_reporter_spec.rb | 8 ++++ .../support_reporter_spec.rb | 8 ++++ 19 files changed, 191 insertions(+), 44 deletions(-) diff --git a/app/models/comment.rb b/app/models/comment.rb index 250a4c0df12..23fb2b02879 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -495,7 +495,7 @@ def mark_unhidden! end def sanitized_content - sanitize_field self, :comment_content + sanitize_field(self, :comment_content, strip_images: ultimate_parent.is_a?(AdminPost)) end include Responder end diff --git a/app/models/feedback_reporters/abuse_reporter.rb b/app/models/feedback_reporters/abuse_reporter.rb index 2d1159ebfb7..628258102e5 100644 --- a/app/models/feedback_reporters/abuse_reporter.rb +++ b/app/models/feedback_reporters/abuse_reporter.rb @@ -30,6 +30,8 @@ def subject end def ticket_description - description.present? ? description.html_safe : "No comment submitted." + return "No comment submitted." if description.blank? + + strip_images(description.html_safe) end end diff --git a/app/models/feedback_reporters/support_reporter.rb b/app/models/feedback_reporters/support_reporter.rb index 70c8a45d090..e4f3074985f 100644 --- a/app/models/feedback_reporters/support_reporter.rb +++ b/app/models/feedback_reporters/support_reporter.rb @@ -31,6 +31,8 @@ def subject end def ticket_description - description.present? ? description.html_safe : "No description submitted." + return "No description submitted." if description.blank? + + strip_images(description.html_safe) end end diff --git a/app/views/admin/banners/_banner.html.erb b/app/views/admin/banners/_banner.html.erb index 3e171555f60..1f8f8444dce 100644 --- a/app/views/admin/banners/_banner.html.erb +++ b/app/views/admin/banners/_banner.html.erb @@ -2,6 +2,6 @@ <% # don't forget to update layouts/banner! %>
    - <%=raw sanitize_field(admin_banner, :content) %> + <%= raw sanitize_field(admin_banner, :content, strip_images: true) %>
    diff --git a/app/views/bookmarks/_bookmark_blurb_short.html.erb b/app/views/bookmarks/_bookmark_blurb_short.html.erb index cc177827d5c..c85c935707b 100755 --- a/app/views/bookmarks/_bookmark_blurb_short.html.erb +++ b/app/views/bookmarks/_bookmark_blurb_short.html.erb @@ -28,10 +28,10 @@ <% end %> - <% unless bookmark.bookmarker_notes.blank? %> + <% if bookmark.bookmarker_notes.present? %>
    <%= ts("Bookmark Notes:") %>
    - <%=raw sanitize_field(bookmark, :bookmarker_notes) %> + <%= raw sanitize_field(bookmark, :bookmarker_notes, strip_images: true) %>
    <% end %> diff --git a/app/views/bookmarks/_bookmark_user_module.html.erb b/app/views/bookmarks/_bookmark_user_module.html.erb index 45faac2fbca..60330591694 100644 --- a/app/views/bookmarks/_bookmark_user_module.html.erb +++ b/app/views/bookmarks/_bookmark_user_module.html.erb @@ -45,10 +45,10 @@ <% end %> - <% unless bookmark.bookmarker_notes.blank? %> + <% if bookmark.bookmarker_notes.present? %>
    <%= ts('Bookmarker\'s Notes') %>
    - <%=raw sanitize_field(bookmark, :bookmarker_notes) %> + <%= raw sanitize_field(bookmark, :bookmarker_notes, strip_images: true) %>
    <% end %> <% # end of information added by the bookmark owner %> diff --git a/app/views/comments/_single_comment.html.erb b/app/views/comments/_single_comment.html.erb index 19d8cb027f6..72085c022c2 100644 --- a/app/views/comments/_single_comment.html.erb +++ b/app/views/comments/_single_comment.html.erb @@ -55,7 +55,9 @@ <% if single_comment.hidden_by_admin? %>

    <%= ts("This comment has been hidden by an admin.") %>

    <% end %> -
    <%=raw sanitize_field(single_comment, :comment_content) %>
    +
    + <%= raw sanitize_field(single_comment, :comment_content, strip_images: single_comment.ultimate_parent.is_a?(AdminPost)) %> +
    <% end %> <% if single_comment.edited_at.present? %>

    diff --git a/app/views/layouts/_banner.html.erb b/app/views/layouts/_banner.html.erb index 745779d2186..25115ff1f6b 100644 --- a/app/views/layouts/_banner.html.erb +++ b/app/views/layouts/_banner.html.erb @@ -1,25 +1,20 @@ -<% # BACK END this seems giant and messy and confusing, pls can we review? - # FRONT END yes let us rewrite this -%> -<% unless current_user && current_user.try(:preference).try(:banner_seen) %> -<% if @admin_banner && @admin_banner.active? %> -<% unless current_user.nil? && session[:hide_banner] %> -

    -
    - <%=raw sanitize_field(@admin_banner, :content) %> -
    - <% if current_user.nil? %> -

    - <%= link_to "×".html_safe, current_path_with(hide_banner: true), :class => 'showme action', :title => ts("hide banner") %> -

    - <% else %> - <%= form_tag end_banner_user_path(current_user), :method => :post, :remote => true do %> +<% if @admin_banner&.active? %> + <% unless session[:hide_banner] || current_user&.preference&.banner_seen %> +
    +
    + <%= raw sanitize_field(@admin_banner, :content, strip_images: true) %> +
    + <% if current_user.nil? %>

    - <%= submit_tag "×".html_safe, :title => ts("hide banner") %> + <%= link_to "×".html_safe, current_path_with(hide_banner: true), class: "showme action", title: ts("hide banner") %>

    + <% else %> + <%= form_tag end_banner_user_path(current_user), method: :post, remote: true do %> +

    + <%= submit_tag "×".html_safe, title: ts("hide banner") %> +

    + <% end %> <% end %> - <% end %> -
    -<% end %> -<% end %> +
    + <% end %> <% end %> diff --git a/app/views/pseuds/_pseud_module.html.erb b/app/views/pseuds/_pseud_module.html.erb index 3c727397998..32258ea5c8e 100644 --- a/app/views/pseuds/_pseud_module.html.erb +++ b/app/views/pseuds/_pseud_module.html.erb @@ -15,8 +15,8 @@

    <%= date %>

    <% end %> -<% unless pseud.description.blank? %> +<% if pseud.description.present? %>
    - <%=raw sanitize_field(pseud, :description) %> + <%= raw sanitize_field(pseud, :description, strip_images: true) %>
    <% end %> diff --git a/app/views/share/_embed_meta_bookmark.erb b/app/views/share/_embed_meta_bookmark.erb index 757688ab8dc..b5f79af897c 100644 --- a/app/views/share/_embed_meta_bookmark.erb +++ b/app/views/share/_embed_meta_bookmark.erb @@ -5,6 +5,6 @@ <% end %> <% if bookmark.bookmarker_notes.present? %> <%= ts("Bookmarker's Notes: ").html_safe %> -<%= raw(sanitize_field(bookmark, :bookmarker_notes)) %> +<%= raw sanitize_field(bookmark, :bookmarker_notes, strip_images: true) %> <% end %> <% end %> diff --git a/features/bookmarks/bookmark_create.feature b/features/bookmarks/bookmark_create.feature index 72d1d8efe72..e56da0f057c 100644 --- a/features/bookmarks/bookmark_create.feature +++ b/features/bookmarks/bookmark_create.feature @@ -108,6 +108,18 @@ Scenario: extra commas in bookmark form (Issue 2284) And I press "Create" Then I should see "created" +Scenario: Bookmark notes do not display images + Given I am logged in as "bookmarkuser" + And I post the work "Some Work" + When I follow "Bookmark" + And I fill in "Notes" with "Fantastic!" + And I press "Create" + And all indexing jobs have been run + Then I should see "Bookmark was successfully created" + When I go to the bookmarks page + Then I should not see the image "src" text "http://example.com/icon.svg" + And I should see "Fantastic!" + Scenario: bookmark added to moderated collection has flash notice only when not approved Given the following activated users exist | login | password | diff --git a/features/comments_and_kudos/add_comment.feature b/features/comments_and_kudos/add_comment.feature index 782700c82f0..c4613c550a8 100644 --- a/features/comments_and_kudos/add_comment.feature +++ b/features/comments_and_kudos/add_comment.feature @@ -133,7 +133,7 @@ Scenario: Comment threading, comment editing And I fill in "Comment" with "B's improved comment (edited)" And I press "Update" Then 0 emails should be delivered to "User_A" - + Scenario: Try to post an invalid comment When I am logged in as "author" @@ -180,6 +180,17 @@ Scenario: Set preference and receive comment notifications of your own comments And "commenter" should be emailed And 1 email should be delivered to "commenter" +Scenario: Work comment displays images + + Given the work "Generic Work" + And I am logged in as "commenter" + And I visit the new comment page for the work "Generic Work" + When I fill in "Comment" with "Fantastic!" + And I press "Comment" + Then I should see "Comment created!" + And I should see "Fantastic!" + And I should see the image "src" text "http://example.com/icon.svg" + Scenario: Try to post a comment with a < angle bracket before a linebreak, without a space before the bracket Given the work "Generic Work" @@ -194,7 +205,7 @@ Scenario: Try to post a comment with a < angle bracket before a linebreak, witho And I press "Comment" Then I should see "Comment created!" -Scenario: Try to post a comment with a < angle bracket before a linebreak, with a space before the bracket +Scenario: Try to post a comment with a < angle bracket before a linebreak, with a space before the bracket Given the work "Generic Work" And I am logged in as "commenter" diff --git a/features/comments_and_kudos/comments_adminposts.feature b/features/comments_and_kudos/comments_adminposts.feature index da130cc1d0e..a5060948daa 100644 --- a/features/comments_and_kudos/comments_adminposts.feature +++ b/features/comments_and_kudos/comments_adminposts.feature @@ -135,3 +135,14 @@ Feature: Commenting on admin posts When I follow "Edit Post" Then I should see "No one can comment" # TODO: Test that the other options aren't available/selected in a non-brittle way + + Scenario: Admin post comment does not display images + Given I have posted an admin post + And I am logged in as "regular" + And I go to the admin-posts page + And I follow "Default Admin Post" + When I fill in "Comment" with "Hi!" + And I press "Comment" + Then I should see "Comment created!" + And I should not see the image "src" text "http://example.com/icon.svg" + And I should see "Hi!" diff --git a/features/other_a/pseuds.feature b/features/other_a/pseuds.feature index 50379fcfc78..077c1407e00 100644 --- a/features/other_a/pseuds.feature +++ b/features/other_a/pseuds.feature @@ -118,6 +118,18 @@ Scenario: Manage pseuds - add, edit And I should see "I wanted to add another fancy name" And I should not see "My new name (editpseuds)" +Scenario: Pseud descriptions do not display images + + Given I am logged in as "myself" + And I go to my pseuds page + When I follow "Edit" + And I fill in "Description" with "Fantastic!" + And I press "Update" + Then I should see "Pseud was successfully updated." + When I follow "Back To Pseuds" + Then I should not see the image "src" text "http://example.com/icon.svg" + And I should see "Fantastic!" + Scenario: Comments reflect pseud changes immediately Given the work "Interesting" diff --git a/lib/html_cleaner.rb b/lib/html_cleaner.rb index f2636d00a16..a70e19517ec 100644 --- a/lib/html_cleaner.rb +++ b/lib/html_cleaner.rb @@ -2,17 +2,21 @@ module HtmlCleaner # If we aren't sure that this field hasn't been sanitized since the last sanitizer version, # we sanitize it before we allow it to pass through (and save it if possible). - def sanitize_field(object, fieldname) + def sanitize_field(object, fieldname, strip_images: false) return "" if object.send(fieldname).nil? sanitizer_version = object.try("#{fieldname}_sanitizer_version") - if sanitizer_version && sanitizer_version >= ArchiveConfig.SANITIZER_VERSION - # return the field without sanitizing - object.send(fieldname) - else - # no sanitizer version information, so re-sanitize - sanitize_value(fieldname, object.send(fieldname)) - end + sanitized_field = + if sanitizer_version && sanitizer_version >= ArchiveConfig.SANITIZER_VERSION + # return the field without sanitizing + object.send(fieldname) + else + # no sanitizer version information, so re-sanitize + sanitize_value(fieldname, object.send(fieldname)) + end + + sanitized_field = strip_images(sanitized_field) if strip_images + sanitized_field end # yank out bad end-of-line characters and evil msword curly quotes diff --git a/spec/mailers/admin_mailer_spec.rb b/spec/mailers/admin_mailer_spec.rb index 9074b1b51d2..2ab35574f50 100644 --- a/spec/mailers/admin_mailer_spec.rb +++ b/spec/mailers/admin_mailer_spec.rb @@ -3,6 +3,48 @@ require "spec_helper" describe AdminMailer do + describe "#comment_notification" do + let(:email) { described_class.comment_notification(comment.id) } + + context "when the comment is on an admin post" do + let(:comment) { create(:comment, :on_admin_post) } + + context "and the comment's contents contain an image" do + let(:image_tag) { "" } + + before do + comment.comment_content += image_tag + comment.save! + end + + it "strips the image from the email message" do + expect(email).not_to have_html_part_content(image_tag) + end + end + end + end + + describe "#edited_comment_notification" do + let(:email) { described_class.edited_comment_notification(comment.id) } + + context "when the comment is on an admin post" do + let(:comment) { create(:comment, :on_admin_post) } + + context "and the comment's contents contain an image" do + let(:image_tag) { "" } + + before do + comment.comment_content += image_tag + comment.save! + end + + it "strips the image from the email message" do + expect(email).not_to have_html_part_content(image_tag) + end + end + end + end + describe "send_spam_alert" do let(:spam_user) { create(:user) } diff --git a/spec/mailers/comment_mailer_spec.rb b/spec/mailers/comment_mailer_spec.rb index 1946bcdd561..c60f4647e8d 100644 --- a/spec/mailers/comment_mailer_spec.rb +++ b/spec/mailers/comment_mailer_spec.rb @@ -71,7 +71,21 @@ end end - describe "comment_notification" do + shared_examples "strips image tags" do + let(:image_tag) { "" } + + before do + comment.comment_content += image_tag + comment.save! + end + + it "strips the image from the email message" do + expect(email).not_to have_html_part_content(image_tag) + expect(email).not_to have_text_part_content(image_tag) + end + end + + describe "#comment_notification" do subject(:email) { CommentMailer.comment_notification(user, comment) } it_behaves_like "an email with a valid sender" @@ -79,6 +93,12 @@ it_behaves_like "a notification email with a link to the comment" it_behaves_like "a notification email with a link to reply to the comment" + context "when the comment is on an admin post" do + let(:comment) { create(:comment, :on_admin_post) } + + it_behaves_like "strips image tags" + end + context "when the comment is a reply to another comment" do let(:comment) { create(:comment, commentable: create(:comment)) } @@ -111,6 +131,12 @@ it_behaves_like "a notification email with a link to the comment" it_behaves_like "a notification email with a link to reply to the comment" + context "when the comment is on an admin post" do + let(:comment) { create(:comment, :on_admin_post) } + + it_behaves_like "strips image tags" + end + context "when the comment is a reply to another comment" do let(:comment) { create(:comment, commentable: create(:comment)) } @@ -147,6 +173,12 @@ it_behaves_like "a notification email with a link to reply to the comment" it_behaves_like "a notification email with a link to the comment's thread" + context "when the comment is on an admin post" do + let(:comment) { create(:comment, :on_admin_post) } + + it_behaves_like "strips image tags" + end + context "when the comment is on a tag" do let(:parent_comment) { create(:comment, :on_tag) } @@ -183,6 +215,12 @@ it_behaves_like "a notification email with a link to reply to the comment" it_behaves_like "a notification email with a link to the comment's thread" + context "when the comment is on an admin post" do + let(:comment) { create(:comment, :on_admin_post) } + + it_behaves_like "strips image tags" + end + context "when the comment is on a tag" do let(:parent_comment) { create(:comment, :on_tag) } diff --git a/spec/models/feedback_reporters/abuse_reporter_spec.rb b/spec/models/feedback_reporters/abuse_reporter_spec.rb index 9bafc6a8aea..0cbd4c06f44 100644 --- a/spec/models/feedback_reporters/abuse_reporter_spec.rb +++ b/spec/models/feedback_reporters/abuse_reporter_spec.rb @@ -79,5 +79,13 @@ expect(subject.report_attributes.fetch("cf").fetch("cf_url")).to eq("Unknown URL") end end + + context "if the report has an image in description" do + it "strips all img tags" do + allow(subject).to receive(:description).and_return("Hi!Bye!") + + expect(subject.report_attributes.fetch("description")).to eq("Hi!Bye!") + end + end end end diff --git a/spec/models/feedback_reporters/support_reporter_spec.rb b/spec/models/feedback_reporters/support_reporter_spec.rb index c14fce8555e..33065485836 100644 --- a/spec/models/feedback_reporters/support_reporter_spec.rb +++ b/spec/models/feedback_reporters/support_reporter_spec.rb @@ -89,5 +89,13 @@ expect(subject.report_attributes.fetch("cf").fetch("cf_user_agent")).to eq("Unknown user agent") end end + + context "if the report has an image in description" do + it "strips all img tags" do + allow(subject).to receive(:description).and_return("Hi!Bye!") + + expect(subject.report_attributes.fetch("description")).to eq("Hi!Bye!") + end + end end end From 323abe1e4155d8c0fa5d9729f97f8d782cad0e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89ric=20B?= Date: Sun, 24 Mar 2024 20:53:50 +0100 Subject: [PATCH 184/208] AO3-6502 Blocked users should not be able to give gift works to blocker (#4721) * AO3-6502 Blocked users gifting works to blocker https://otwarchive.atlassian.net/browse/AO3-6502 * Extend to co-authors * Possibly overkill error message improvement * Cop * Remove ??? break * Actually read/follow the spec * Add challenge exception Based on https://github.com/otwcode/otwarchive/pull/4721#issuecomment-1902728815 * Additional tests * Fix (unrelated) intermittent test * What if the test actually tested the right thing? * Cop * Add translation keys * i18n normalize * Proper RoR syntax * Explicit new restriction on blocked users * Revert "Fix (unrelated) intermittent test" This reverts commit d3839c9985caf0b5ce1e08a1895b93afb71d5b4b. Unable to reproduce on master anymore? --- app/models/work.rb | 40 ++++++++++++-- .../blocked/users/confirm_block.html.erb | 1 + .../blocked/users/confirm_unblock.html.erb | 1 + app/views/blocked/users/index.html.erb | 1 + config/locales/models/en.yml | 2 + config/locales/views/en.yml | 3 ++ .../challenge_giftexchange.feature | 34 ++++++++++++ features/other_a/gift.feature | 54 +++++++++++++++++++ ...challenge_promptmeme_posting_fills.feature | 17 ++++++ features/users/blocking.feature | 14 +++++ 10 files changed, 164 insertions(+), 3 deletions(-) diff --git a/app/models/work.rb b/app/models/work.rb index 292d6957c55..da0501e4f98 100755 --- a/app/models/work.rb +++ b/app/models/work.rb @@ -183,10 +183,34 @@ def new_recipients_allow_gifts self.new_gifts.each do |gift| next if gift.pseud.blank? next if gift.pseud&.user&.preference&.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) + next if challenge_bypass(gift) - self.errors.add(:base, ts("%{byline} does not accept gifts.", byline: gift.pseud.byline)) + self.errors.add(:base, :blocked_gifts, byline: gift.pseud.byline) + end + end + + validate :new_recipients_have_not_blocked_gift_giver + def new_recipients_have_not_blocked_gift_giver + return if self.new_gifts.blank? + + self.new_gifts.each do |gift| + # Already dealt with in #new_recipients_allow_gifts + next if gift.pseud&.user&.preference && !gift.pseud.user.preference.allow_gifts? + + next if challenge_bypass(gift) + + blocked_users = gift.pseud&.user&.blocked_users || [] + next if blocked_users.empty? + + pseuds_after_saving.each do |pseud| + next unless blocked_users.include?(pseud.user) + + if User.current_user == pseud.user + self.errors.add(:base, :blocked_your_gifts, byline: gift.pseud.byline) + else + self.errors.add(:base, :blocked_gifts, byline: gift.pseud.byline) + end + end end end @@ -1258,4 +1282,14 @@ def nonfiction def allow_collection_invitation? users.any? { |user| user.preference.allow_collection_invitation } end + + private + + def challenge_bypass(gift) + self.challenge_assignments.map(&:requesting_pseud).include?(gift.pseud) || + self.challenge_claims + .reject { |c| c.request_prompt.anonymous? } + .map(&:requesting_pseud) + .include?(gift.pseud) + end end diff --git a/app/views/blocked/users/confirm_block.html.erb b/app/views/blocked/users/confirm_block.html.erb index 01ebec6d766..03b6045ec80 100644 --- a/app/views/blocked/users/confirm_block.html.erb +++ b/app/views/blocked/users/confirm_block.html.erb @@ -10,6 +10,7 @@
    • <%= t(".will.commenting") %>
    • <%= t(".will.replying") %>
    • +
    • <%= t(".will.gifting") %>

    <%= t(".will_not.intro") %>

    diff --git a/app/views/blocked/users/confirm_unblock.html.erb b/app/views/blocked/users/confirm_unblock.html.erb index af702be64b2..7e8b2fc2a82 100644 --- a/app/views/blocked/users/confirm_unblock.html.erb +++ b/app/views/blocked/users/confirm_unblock.html.erb @@ -10,6 +10,7 @@
    • <%= t(".resume.commenting") %>
    • <%= t(".resume.replying") %>
    • +
    • <%= t(".resume.gifting") %>
    diff --git a/app/views/blocked/users/index.html.erb b/app/views/blocked/users/index.html.erb index 045c8409c1a..4d00f2877b4 100644 --- a/app/views/blocked/users/index.html.erb +++ b/app/views/blocked/users/index.html.erb @@ -13,6 +13,7 @@
    • <%= t(".will.commenting") %>
    • <%= t(".will.replying") %>
    • +
    • <%= t(".will.gifting") %>

    <%= t(".will_not.intro") %>

    diff --git a/config/locales/models/en.yml b/config/locales/models/en.yml index 8c16681d48c..9730233f056 100644 --- a/config/locales/models/en.yml +++ b/config/locales/models/en.yml @@ -175,6 +175,8 @@ en: attributes: user_defined_tags_count: at_most: must not add up to more than %{count}. Your work has %{value} of these tags, so you must remove %{diff} of them. + blocked_gifts: "%{byline} does not accept gifts." + blocked_your_gifts: "%{byline} does not accept gifts from you." work/parent_work_relationships: format: "%{message}" models: diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml index 12015c85d30..b8951dffe92 100644 --- a/config/locales/views/en.yml +++ b/config/locales/views/en.yml @@ -325,6 +325,7 @@ en: title: Block %{name} will: commenting: commenting or leaving kudos on your works + gifting: giving you gift works outside of challenge assignments and claimed prompts intro: 'Blocking a user prevents them from:' replying: replying to your comments anywhere on the site will_not: @@ -337,6 +338,7 @@ en: cancel: Cancel resume: commenting: commenting or leaving kudos on your works + gifting: giving you gift works outside of challenge assignments and claimed prompts intro: 'Unblocking a user allows them to resume:' replying: replying to your comments anywhere on the site sure_html: Are you sure you want to %{unblock} %{username}? @@ -356,6 +358,7 @@ en: title: Blocked Users will: commenting: commenting or leaving kudos on your works + gifting: giving you gift works outside of challenge assignments and claimed prompts intro: one: 'You can block up to %{block_limit} user. Blocking a user prevents them from:' other: 'You can block up to %{block_limit} users. Blocking a user prevents them from:' diff --git a/features/gift_exchanges/challenge_giftexchange.feature b/features/gift_exchanges/challenge_giftexchange.feature index 424f0c209ac..13c95d5d5bd 100644 --- a/features/gift_exchanges/challenge_giftexchange.feature +++ b/features/gift_exchanges/challenge_giftexchange.feature @@ -641,3 +641,37 @@ Feature: Gift Exchange Challenge And I uncheck "exchange_collection (recip)" And I press "Post" Then I should see "For recip." + + Scenario: If a work is connected to an assignment for a user who blocked the gifter, + user is still automatically added as a gift recipient. The recipient + remains attached even if the work is later disconnected from the assignment. + Given basic tags + And the user "recip" exists and is activated + And the user "recip" allows gifts + And the user "recip" has blocked the user "gifter" + And I am logged in as "gifter" + And I have an assignment for the user "recip" in the collection "exchange_collection" + When I fulfill my assignment + Then I should see "For recip." + When I follow "Edit" + And I uncheck "exchange_collection (recip)" + And I press "Post" + Then I should see "For recip." + + Scenario: A user can explicitly give a gift to a user who blocked the gifter if + the work is connected to an assignment. The recipient remains attached even if + the work is later disconnected from the assignment. + Given basic tags + And the user "recip" exists and is activated + And the user "recip" allows gifts + And the user "recip" has blocked the user "gifter" + And I am logged in as "gifter" + And I have an assignment for the user "recip" in the collection "exchange_collection" + When I start to fulfill my assignment + And I fill in "Gift this work to" with "recip" + And I press "Post" + Then I should see "For recip." + When I follow "Edit" + And I uncheck "exchange_collection (recip)" + And I press "Post" + Then I should see "For recip." diff --git a/features/other_a/gift.feature b/features/other_a/gift.feature index ec39358f45f..8d77e3b5171 100644 --- a/features/other_a/gift.feature +++ b/features/other_a/gift.feature @@ -337,3 +337,57 @@ Feature: Create Gifts And I should not see "by gifter for giftee1" When I view the work "Rude Gift" Then I should not see "For giftee1." + + Scenario: Can't give a gift to a user who has blocked you + Given the user "giftee1" has blocked the user "gifter" + When I am logged in as "gifter" + And I post the work "Rude Gift" as a gift for "giftee1" + Then I should see "Sorry! We couldn't save this work because: giftee1 does not accept gifts from you." + And 0 emails should be delivered to "giftee1@example.com" + + Scenario: Can't gift an existing work to a user who has blocked you + Given the user "giftee1" has blocked the user "gifter" + And I press "Post" + And I follow "Edit" + And I give the work to "giftee1" + When I press "Post" + Then I should see "Sorry! We couldn't save this work because: giftee1 does not accept gifts from you." + + Scenario: Can't gift a work whose co-creator is blocked by recipient + Given I coauthored the work "Collateral" as "gifter" with "gifter2" + And the user "giftee1" has blocked the user "gifter2" + And I edit the work "Collateral" + And I give the work to "giftee1" + When I press "Post" + Then I should see "Sorry! We couldn't save this work because: giftee1 does not accept gifts." + + Scenario: Only see one error message is shown if gifts are disabled and user is blocked* + Given the user "giftee1" disallows gifts + And the user "giftee1" has blocked the user "gifter" + When I am logged in as "gifter" + And I post the work "Rude Gift" as a gift for "giftee1" + Then I should see "Sorry! We couldn't save this work because:" + And I should see "giftee1 does not accept gifts." + And I should not see "giftee1 does not accept gifts from you." + + Scenario: A user can refuse previous gifts from user after blocking them + Given I am logged in as "gifter" + And I post the work "Rude Gift" as a gift for "giftee1" + When I am logged in as "giftee1" + And I go to my gifts page + Then I should see "Rude Gift" + When I go to my blocked users page + And I fill in "blocked_id" with "gifter" + And I press "Block" + And I press "Yes, Block User" + Then I should see "You have blocked the user gifter." + When I go to my gifts page + And it is currently 1 second from now + And I follow "Refuse Gift" + Then I should see "This work will no longer be listed among your gifts." + And I should not see "Rude Gift" + When I follow "Refused Gifts" + Then I should see "Rude Gift" + And I should not see "by gifter for giftee1" + When I view the work "Rude Gift" + Then I should not see "For giftee1." diff --git a/features/prompt_memes_b/challenge_promptmeme_posting_fills.feature b/features/prompt_memes_b/challenge_promptmeme_posting_fills.feature index 38691a84848..e15e8e7c5fb 100755 --- a/features/prompt_memes_b/challenge_promptmeme_posting_fills.feature +++ b/features/prompt_memes_b/challenge_promptmeme_posting_fills.feature @@ -542,3 +542,20 @@ Feature: Prompt Meme Challenge And I fill in "Gift this work to" with "prompter, bystander" And I press "Post" Then I should see "bystander does not accept gifts." + + Scenario: A creator can give a gift to a user who has blocked them if the work is connected to a claim of a non-anonymous prompt belonging to the recipient + + Given I have Battle 12 prompt meme fully set up + And the user "prompter" exists and is activated + And the user "prompter" has blocked the user "gifter" + And "prompter" has signed up for Battle 12 with combination A + When I am logged in as "gifter" + And I claim a prompt from "Battle 12" + And I start to fulfill my claim + And I fill in "Gift this work to" with "prompter" + And I press "Post" + Then I should see "For prompter." + When I follow "Edit" + And I uncheck "Battle 12 (prompter)" + And I press "Post" + Then I should see "For prompter." diff --git a/features/users/blocking.feature b/features/users/blocking.feature index d136e427c9a..d305915dd3f 100644 --- a/features/users/blocking.feature +++ b/features/users/blocking.feature @@ -121,3 +121,17 @@ Feature: Blocking | superadmin | | policy_and_abuse | | support | + + Scenario: Users are told about blocking effects on gift-giving + Given the user "pest" exists and is activated + And I am logged in as "blocker" + When I go to my blocked users page + Then I should see "giving you gift works" + Given the user "unblocker" has blocked the user "improving" + And I am logged in as "unblocker" + When I go to my blocked users page + Then I should see "improving" + And I should see "giving you gift works" + When I follow "Unblock" + Then I should see a "Yes, Unblock User" button + And I should see "giving you gift works" From 03193e15109c9b5e375705f1b76851b6caab1181 Mon Sep 17 00:00:00 2001 From: EchoEkhi Date: Sun, 24 Mar 2024 19:54:02 +0000 Subject: [PATCH 185/208] AO3-6320 Fix Incomplete error message when including invalid tag with ^ symbol (#4206) * AO3-6320 Fix regex error * AO3-6320 Pleasing the hound * AO3-6320 Add unit test * Fixes for merge --- app/helpers/validation_helper.rb | 8 +++++--- features/other_b/errors.feature | 12 ++++++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/app/helpers/validation_helper.rb b/app/helpers/validation_helper.rb index a73e12f55c2..7e7e72fb60d 100644 --- a/app/helpers/validation_helper.rb +++ b/app/helpers/validation_helper.rb @@ -42,9 +42,11 @@ def error_messages_for(object) end def error_messages_formatted(errors, intro = "") - return unless errors && !errors.empty? - error_messages = errors.map { |msg| content_tag(:li, msg.gsub(/^(.*)\^/, '').html_safe) }.join("\n").html_safe - content_tag(:div, intro.html_safe + content_tag(:ul, error_messages), id:"error", class:"error") + return unless errors.present? + + error_messages = errors.map { |msg| content_tag(:li, msg.gsub(/^(.*?)\^/, "").html_safe) } + .join("\n").html_safe + content_tag(:div, intro.html_safe + content_tag(:ul, error_messages), id: "error", class: "error") end # use to make sure we have consistent name throughout diff --git a/features/other_b/errors.feature b/features/other_b/errors.feature index a7dff1b681b..7bc6c78696d 100644 --- a/features/other_b/errors.feature +++ b/features/other_b/errors.feature @@ -1,6 +1,5 @@ @errors -Feature: We need to do something when someone asks for something we don't have -Some pages with non existent things raise errors +Feature: Error messages should work Scenario: Some pages with non existent things raise errors Given the user "KnownUser" exists and is activated @@ -24,3 +23,12 @@ 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: Error messages should be able to display '^' + Given I am logged in as a random user + And I post the work "Work 1" + And I view the work "Work 1" + And I follow "Edit Tags" + When I fill in "Fandoms" with "^" + And I press "Post" + Then I should see "Sorry! We couldn't save this work because: Tag name '^' cannot include the following restricted characters: , ^ * < > { } = ` , 、 \ %" From fab9b24ca0be32223611d62ec18cb9e7601ab4b7 Mon Sep 17 00:00:00 2001 From: Bilka Date: Sun, 24 Mar 2024 20:54:09 +0100 Subject: [PATCH 186/208] AO3-4696 Link the fandom page on tag pages (#4350) * Link the fandom page on tag pages * AO3-4696 Add _link suffix to link variable and update test * AO3-4696 Move text to locale file * AO3-4696 Readd locale --- app/views/tags/show.html.erb | 3 +++ config/locales/views/en.yml | 3 +++ features/other_b/fandoms.feature | 3 ++- features/support/paths.rb | 2 -- 4 files changed, 8 insertions(+), 3 deletions(-) diff --git a/app/views/tags/show.html.erb b/app/views/tags/show.html.erb index 6b2702138d6..1b8b2ccb229 100644 --- a/app/views/tags/show.html.erb +++ b/app/views/tags/show.html.erb @@ -24,6 +24,9 @@

    <%= ts("This tag belongs to the %{tag_class} Category.", :tag_class => @tag.class::NAME) %> <% if @tag.canonical %> + <% if @tag.is_a?(Fandom) %> + <%= t(".list_fandom_tags_html", fandom_relationship_tags_link: link_to(t(".fandom_relationship_tags"), fandom_path(@tag.name))) %> + <% end %> <%= ts("It's a common tag. You can use it to ") %> <%= link_to ts('filter works'), {:controller => :works, :action => :index, :tag_id => @tag} %> <%= ts('and to') %> <%= link_to ts('filter bookmarks'), {:controller => :bookmarks, :action => :index, :tag_id => @tag} %>. diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml index b8951dffe92..f74ce3491ab 100644 --- a/config/locales/views/en.yml +++ b/config/locales/views/en.yml @@ -733,6 +733,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 + show: + fandom_relationship_tags: Relationship tags in this fandom + list_fandom_tags_html: A list of all the %{fandom_relationship_tags_link} is available. time: formats: date_short_html: %a %d %b %Y diff --git a/features/other_b/fandoms.feature b/features/other_b/fandoms.feature index 8fba205aae1..06dcf699f66 100644 --- a/features/other_b/fandoms.feature +++ b/features/other_b/fandoms.feature @@ -34,6 +34,7 @@ Feature: There is a list of unassigned Fandoms And I add the fandom "Steven Universe" to the character "Sapphire (Steven Universe)" And I am logged in as "author" And I post the work "Stronger than you" with fandom "Steven Universe" with character "Ruby (Steven Universe)" with second character "Sapphire (Steven Universe)" with relationship "Ruby/Sapphire (Steven Universe)" - When I go to the "Steven Universe" fandom relationship page + When I go to the "Steven Universe" tag page + And I follow "Relationship tags in this fandom" Then I should see "Ruby (Steven Universe)" And I should see "Sapphire (Steven Universe)" diff --git a/features/support/paths.rb b/features/support/paths.rb index 95df95966e3..01a4bdc2fd7 100644 --- a/features/support/paths.rb +++ b/features/support/paths.rb @@ -285,8 +285,6 @@ def path_to(page_name) edit_tag_path(Tag.find_by(name: Regexp.last_match(1))) when /^the wrangling tools page$/ tag_wranglings_path - when /^the "(.*)" fandom relationship page$/i - fandom_path($1) when /^the new external work page$/i new_external_work_path when /^the external works page$/i From 7c368fffd7ce1f5e48e50ebc6292c18f14b4918b Mon Sep 17 00:00:00 2001 From: sarken Date: Tue, 26 Mar 2024 16:28:25 -0400 Subject: [PATCH 187/208] AO3-6689 Fix JavaScript for dropdowns on status index (#4769) --- public/status/index.html | 1 + 1 file changed, 1 insertion(+) diff --git a/public/status/index.html b/public/status/index.html index 9bfea205714..0888bd84206 100644 --- a/public/status/index.html +++ b/public/status/index.html @@ -160,6 +160,7 @@

    Development

    } +