diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml index c4014453bb1..3af37b18225 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -11,6 +11,8 @@ jobs: rubocop: name: Rubocop runs-on: ubuntu-latest + env: + BUNDLE_ONLY: linters steps: - name: Check out code uses: actions/checkout@v4 @@ -21,7 +23,7 @@ jobs: bundler-cache: true - name: rubocop - uses: reviewdog/action-rubocop@32686543011497c256009cce0c94b73a8179cbdb + uses: reviewdog/action-rubocop@2c8048e3169487eccc1eed812daaa6e5275a809f with: use_bundler: true reporter: github-pr-check @@ -30,6 +32,8 @@ jobs: erb-lint: name: ERB Lint runner runs-on: ubuntu-latest + env: + BUNDLE_ONLY: linters steps: - name: Check out code uses: actions/checkout@v4 diff --git a/.gitignore b/.gitignore index 4cbfe8ab18c..53da1b09dbc 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ .idea stdout *~ +.vscode/ # / /*.tmproj /sphinx diff --git a/.rubocop.yml b/.rubocop.yml index d6fdc3dd71b..6ca41445023 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -6,6 +6,7 @@ require: - rubocop-rails - rubocop-rspec + - ./rubocop/rubocop inherit_mode: merge: @@ -18,6 +19,10 @@ AllCops: Bundler/OrderedGems: Enabled: false +I18n/DeprecatedTranslationKey: + Rules: + name_with_colon: "Prefer `name` with `mailer.general.metadata_label_indicator` over `name_with_colon`" + Layout/DotPosition: EnforcedStyle: leading @@ -72,6 +77,51 @@ Metrics/ParameterLists: Metrics/PerceivedComplexity: Enabled: false +Migration/LargeTableSchemaUpdate: + Tables: + - abuse_reports + - admin_activities + - audits + - bookmarks + - comments + - common_taggings + - collection_items + - collection_participants + - collection_profiles + - collections + - creatorships + - external_works + - favorite_tags + - feedbacks + - filter_counts + - filter_taggings + - gifts + - inbox_comments + - invitations + - kudos + - log_items + - mutes + - preferences + - profiles + - prompts + - pseuds + - readings + - set_taggings + - serial_works + - series + - skins + - stat_counters + - subscriptions + - tag_nominations + - tag_set_associations + - tags + - taggings + - users + - works + +Rails/DefaultScope: + Enabled: true + Rails/DynamicFindBy: AllowedMethods: # Exception for Tag.find_by_name diff --git a/.ruby-version b/.ruby-version index 316881c9f75..71e447d5b6c 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -ruby-3.0.5 +ruby-3.1.4 diff --git a/Gemfile b/Gemfile index a49f26678d0..9b906b676d4 100644 --- a/Gemfile +++ b/Gemfile @@ -1,12 +1,13 @@ source 'https://rubygems.org' -ruby "3.0.5" +ruby "3.1.4" gem 'test-unit', '~> 3.2' gem 'bundler' gem "rails", "~> 6.1.7" + gem "rails-i18n" gem "rack", "~> 2.2" gem "sprockets", "< 4" @@ -66,6 +67,10 @@ gem 'devise' gem 'devise-async' # To mails through queues gem 'bcrypt' +# Needed for modern ssh +gem "ed25519", ">= 1.2", "< 2.0" +gem "bcrypt_pbkdf", ">= 1.0", "< 2.0" + # A highly updated version of the authorization plugin gem 'permit_yo' gem "pundit" @@ -115,6 +120,9 @@ gem "marcel", "1.0.2" # Library for helping run pt-online-schema-change commands: gem "departure", "~> 6.5" +# Ruby 3.1 means we need to specify a version of mail until we get to rails 7.x +gem "mail", ">= 2.8" + group :test do gem "rspec-rails", "~> 4.0.1" gem 'pickle' @@ -126,7 +134,7 @@ group :test do gem 'capybara-screenshot' gem 'cucumber-rails', require: false gem 'launchy' # So you can do Then show me the page - gem 'delorean' + # Record and replay data from external URLs gem 'vcr', '~> 3.0', '>= 3.0.1' gem "webmock" diff --git a/Gemfile.lock b/Gemfile.lock index f6591abb2b0..7fcdc79b48e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -122,6 +122,7 @@ GEM backports (3.23.0) base64 (0.2.0) bcrypt (3.1.16) + bcrypt_pbkdf (1.1.0) better_html (2.0.1) actionview (>= 6.0) activesupport (>= 6.0) @@ -204,8 +205,7 @@ GEM activerecord (>= 5.a) database_cleaner-core (~> 2.0.0) database_cleaner-core (2.0.1) - delorean (2.1.0) - chronic + date (3.3.4) departure (6.5.0) activerecord (>= 5.2.0, < 7.1, != 7.0.0) mysql2 (>= 0.4.0, <= 0.5.5) @@ -223,6 +223,7 @@ GEM docile (1.4.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) + ed25519 (1.3.0) elasticsearch (7.17.1) elasticsearch-api (= 7.17.1) elasticsearch-transport (= 7.17.1) @@ -328,8 +329,11 @@ GEM loofah (2.19.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) - mail (2.7.1) + mail (2.8.1) mini_mime (>= 0.1.1) + net-imap + net-pop + net-smtp marcel (1.0.2) matrix (0.4.2) mechanize (2.8.5) @@ -362,10 +366,19 @@ GEM net-http-digest_auth (1.4.1) net-http-persistent (4.0.1) connection_pool (~> 2.2) + net-imap (0.4.10) + date + net-protocol + net-pop (0.1.2) + net-protocol + net-protocol (0.2.2) + timeout net-scp (3.0.0) net-ssh (>= 2.6.5, < 7.0.0) net-sftp (3.0.0) net-ssh (>= 5.0.0, < 7.0.0) + net-smtp (0.5.0) + net-protocol net-ssh (6.1.0) net-ssh-gateway (2.0.0) net-ssh (>= 4.0.0) @@ -564,6 +577,7 @@ GEM tilt (2.3.0) timecop (0.9.4) timeliness (0.4.4) + timeout (0.4.1) tzinfo (2.0.6) concurrent-ruby (~> 1.0) unf (0.1.4) @@ -616,6 +630,7 @@ DEPENDENCIES awesome_print aws-sdk-s3 bcrypt + bcrypt_pbkdf (>= 1.0, < 2.0) brakeman bullet (>= 5.7.3) bundler @@ -630,10 +645,10 @@ DEPENDENCIES cucumber-timecop dalli database_cleaner - delorean departure (~> 6.5) devise devise-async + ed25519 (>= 1.2, < 2.0) elasticsearch (= 7.17.1) email_spec (= 1.6.0) erb_lint (= 0.4.0) @@ -653,6 +668,7 @@ DEPENDENCIES launchy listen (~> 3.3) lograge + mail (>= 2.8) marcel (= 1.0.2) mechanize minitest @@ -704,7 +720,7 @@ DEPENDENCIES will_paginate (>= 3.0.2) RUBY VERSION - ruby 3.0.5p211 + ruby 3.1.4p223 BUNDLED WITH 2.2.33 diff --git a/README.md b/README.md index 419af3c5881..b91d3e67c3f 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ There is currently no API for the OTW-Archive software. While it is something we License and Acknowledgments ---------- -The Archive code is licensed under [GPL](https://www.gnu.org/licenses/gpl-2.0.html) by the [Organization for Transformative Works](https://www.transformativeworks.org/). +The Archive code is licensed under [GPL-2.0-or-later](https://www.gnu.org/licenses/gpl-2.0.html) by the [Organization for Transformative Works](https://www.transformativeworks.org/). We benefit from software and services that are free to use for Open Source projects, including: diff --git a/app/controllers/admin/spam_controller.rb b/app/controllers/admin/spam_controller.rb index d7f1422fce9..8b652789aa0 100644 --- a/app/controllers/admin/spam_controller.rb +++ b/app/controllers/admin/spam_controller.rb @@ -1,5 +1,4 @@ class Admin::SpamController < Admin::BaseController - def index authorize ModeratedWork @@ -11,7 +10,11 @@ def index else { reviewed: false, approved: false } end - @works = ModeratedWork.where(conditions).order(:created_at).page(params[:page]) + @works = ModeratedWork.where(conditions) + .joins(:work) + .includes(:work) + .order(:created_at) + .page(params[:page]) end def bulk_update diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 556e2bcf8eb..667868faeaa 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -96,17 +96,6 @@ def redirect_to(*args, **kwargs) end end - # Migrates session cookies from encrypted to signed format - before_action :migrate_session_cookie_20231230 - def migrate_session_cookie_20231230 - # If the request contains a valid encrypted session cookie, it means the - # session hasn't yet been migrated to a signed cookie, so we update the - # session with the contents of the encrypted cookie. - if cookies.encrypted[:_otwarchive_session].present? - session.update(cookies.encrypted[:_otwarchive_session]) - end - end - after_action :ensure_admin_credentials def ensure_admin_credentials if logged_in_as_admin? diff --git a/app/controllers/archive_faqs_controller.rb b/app/controllers/archive_faqs_controller.rb index 11a7935be81..b97130558f8 100644 --- a/app/controllers/archive_faqs_controller.rb +++ b/app/controllers/archive_faqs_controller.rb @@ -129,9 +129,8 @@ def default_url_options def set_locale 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].presence || Locale.find(current_user.preference.preferred_locale).iso + if current_user.present? + @i18n_locale = session[:language_id].presence || current_user.preference.locale.iso else @i18n_locale = session[:language_id].presence || I18n.default_locale end diff --git a/app/controllers/bookmarks_controller.rb b/app/controllers/bookmarks_controller.rb index f51722cf50f..ee48b66edbd 100644 --- a/app/controllers/bookmarks_controller.rb +++ b/app/controllers/bookmarks_controller.rb @@ -61,7 +61,7 @@ def search def index if @bookmarkable access_denied unless is_admin? || @bookmarkable.visible? - @bookmarks = @bookmarkable.bookmarks.is_public.paginate(page: params[:page], per_page: ArchiveConfig.ITEMS_PER_PAGE) + @bookmarks = @bookmarkable.bookmarks.is_public.order_by_created_at.paginate(page: params[:page], per_page: ArchiveConfig.ITEMS_PER_PAGE) else base_options = { show_private: (@user.present? && @user == current_user), @@ -286,7 +286,7 @@ def fetch_recent @bookmarkable = @bookmark.bookmarkable respond_to do |format| format.js { - @bookmarks = @bookmarkable.bookmarks.visible.order("created_at DESC").offset(1).limit(4) + @bookmarks = @bookmarkable.bookmarks.order_by_created_at.visible.offset(1).limit(4) set_own_bookmarks } format.html do diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 17c4042834d..98402e86240 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -1,7 +1,8 @@ class HomeController < ApplicationController + before_action :users_only, only: [:first_login_help] skip_before_action :store_location, only: [:first_login_help, :token_dispenser] - + # unicorn_test def unicorn_test end @@ -10,14 +11,14 @@ def unicorn_test def tos render action: "tos", layout: "application" end - + # terms of service faq - def tos_faq + def tos_faq render action: "tos_faq", layout: "application" end # dmca policy - def dmca + def dmca render action: "dmca", layout: "application" end @@ -32,28 +33,28 @@ def token_dispenser format.json { render json: { token: form_authenticity_token } } end end - + # diversity statement - def diversity + def diversity render action: "diversity_statement", layout: "application" end - + # site map - def site_map + def site_map render action: "site_map", layout: "application" end - + # donate def donate @page_subtitle = t(".page_title") render action: "donate", layout: "application" end - + # about def about render action: "about", layout: "application" end - + def first_login_help render action: "first_login_help", layout: false end diff --git a/app/controllers/languages_controller.rb b/app/controllers/languages_controller.rb index 8d1336fefcd..96892070d76 100644 --- a/app/controllers/languages_controller.rb +++ b/app/controllers/languages_controller.rb @@ -2,6 +2,9 @@ class LanguagesController < ApplicationController def index @languages = Language.default_order + @works_counts = Rails.cache.fetch("/v1/languages/work_counts/#{current_user.present?}", expires_in: 1.day) do + WorkQuery.new.works_per_language(@languages.count) + end end def new diff --git a/app/controllers/prompts_controller.rb b/app/controllers/prompts_controller.rb index 8c5bb7693fe..69a084e9a65 100644 --- a/app/controllers/prompts_controller.rb +++ b/app/controllers/prompts_controller.rb @@ -130,14 +130,12 @@ def create @prompt = @challenge_signup.requests.build(prompt_params) end - if !@challenge_signup.valid? - flash[:error] = ts("That prompt would make your overall sign-up invalid, sorry.") - redirect_to edit_collection_signup_path(@collection, @challenge_signup) - elsif @prompt.save + if @challenge_signup.save flash[:notice] = ts("Prompt was successfully added.") redirect_to collection_signup_path(@collection, @challenge_signup) else - render action: :new + flash[:error] = ts("That prompt would make your overall sign-up invalid, sorry.") + redirect_to edit_collection_signup_path(@collection, @challenge_signup) end end diff --git a/app/controllers/related_works_controller.rb b/app/controllers/related_works_controller.rb index e00edd610ae..2116d07bfd9 100644 --- a/app/controllers/related_works_controller.rb +++ b/app/controllers/related_works_controller.rb @@ -65,7 +65,7 @@ def destroy end end @related_work.destroy - redirect_back_or_default(user_related_works_path(current_user)) + redirect_back(fallback_location: user_related_works_path(current_user)) end private diff --git a/app/controllers/series_controller.rb b/app/controllers/series_controller.rb index 26f96716089..620996800cf 100644 --- a/app/controllers/series_controller.rb +++ b/app/controllers/series_controller.rb @@ -22,20 +22,19 @@ def index end @user = User.find_by!(login: params[:user_id]) @page_subtitle = ts("%{username} - Series", username: @user.login) - pseuds = @user.pseuds + + @series = if current_user.nil? + Series.visible_to_all + else + Series.visible_to_registered_user + end + if params[:pseud_id] - @pseud = @user.pseuds.find_by!(name: params[:pseud_id]) + @pseud = @user.pseuds.find_by!(name: params[:pseud_id]) @page_subtitle = ts("by ") + @pseud.byline - pseuds = [@pseud] - end - - if current_user.nil? - @series = Series.visible_to_all + @series = @series.exclude_anonymous.for_pseud(@pseud) else - @series = Series.visible_to_registered_user - end - if pseuds.present? - @series = @series.exclude_anonymous.for_pseuds(pseuds) + @series = @series.exclude_anonymous.for_user(@user) end @series = @series.paginate(page: params[:page]) end @@ -43,7 +42,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) @@ -120,7 +119,7 @@ def update_positions end end respond_to do |format| - format.html { redirect_to(@series) and return } + format.html { redirect_to series_path(@series) and return } format.json { head :ok } end end diff --git a/app/controllers/stats_controller.rb b/app/controllers/stats_controller.rb index e2ea2576189..0bee2e524bf 100644 --- a/app/controllers/stats_controller.rb +++ b/app/controllers/stats_controller.rb @@ -24,7 +24,7 @@ def index # sort # NOTE: Because we are going to be eval'ing the @sort variable later we MUST make sure that its content is - # checked against the whitelist of valid options + # checked against the allowlist of valid options sort_options = %w(hits date kudos.count comment_thread_count bookmarks.count subscriptions.count word_count) @sort = sort_options.include?(params[:sort_column]) ? params[:sort_column] : "hits" diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index c741f88159c..b4429d20adf 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -35,6 +35,7 @@ def show # GET /users/1/edit def edit + @page_subtitle = t(".browser_title") authorize @user.profile if logged_in_as_admin? end @@ -127,7 +128,6 @@ def activate def update authorize @user.profile if logged_in_as_admin? - if @user.profile.update(profile_params) if logged_in_as_admin? && @user.profile.ticket_url.present? link = view_context.link_to("Ticket ##{@user.profile.ticket_number}", @user.profile.ticket_url) diff --git a/app/decorators/homepage.rb b/app/decorators/homepage.rb index 705d5f3767a..c986c30bb9a 100644 --- a/app/decorators/homepage.rb +++ b/app/decorators/homepage.rb @@ -1,4 +1,6 @@ class Homepage + include NumberHelper + def initialize(user) @user = user end @@ -62,11 +64,4 @@ def inbox_comments return unless logged_in? @inbox_comments ||= @user.inbox_comments.with_bad_comments_removed.for_homepage end - - def estimate_number(number) - digits = [(Math.log10([number, 1].max).to_i - 3), 0].max - divide = 10**digits - divide * (number / divide).to_i - end - end diff --git a/app/helpers/number_helper.rb b/app/helpers/number_helper.rb new file mode 100644 index 00000000000..76ab70841f6 --- /dev/null +++ b/app/helpers/number_helper.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module NumberHelper + # Converts a precise number to an approximate with no more than 4 digits. + # + # @example + # estimate_number(2) # 2 + # estimate_number(25) # 25 + # estimate_number(308) # 308 + # estimate_number(1234) # 1234 + # estimate_number(120345) # 120300 + def estimate_number(number) + digits = [(Math.log10([number, 1].max).to_i - 3), 0].max + divide = 10**digits + divide * (number / divide).to_i + end +end diff --git a/app/helpers/prompt_restrictions_helper.rb b/app/helpers/prompt_restrictions_helper.rb index 10d0320e842..947e5673e98 100644 --- a/app/helpers/prompt_restrictions_helper.rb +++ b/app/helpers/prompt_restrictions_helper.rb @@ -58,7 +58,7 @@ def required_and_allowed(form, tag_type, hasprompts, allowany) h(ts("Must Be Unique?")) + form.check_box("require_unique_#{tag_type}".to_sym, disabled: (hasprompts ? false : true)) end end - content_tag(:dd, fields.html_safe, title: ts("#{tag_type_label_name(tag_type).pluralize.downcase}")) + "\n".html_safe + content_tag(:dd, fields.html_safe, class: "complex", title: ts(tag_type_label_name(tag_type).pluralize.downcase.to_s)) + "\n".html_safe end # generate the string to use for the labels on sign-up forms diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 40cf0133573..938044a272f 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -82,21 +82,21 @@ def pseud_works_link(pseud) def series_link(user, pseud = nil) return pseud_series_link(pseud) if pseud.present? && !pseud.new_record? - if current_user.nil? - total = Series.visible_to_all.exclude_anonymous.for_pseuds(user.pseuds).length - else - total = Series.visible_to_registered_user.exclude_anonymous.for_pseuds(user.pseuds).length - end - span_if_current ts('Series (%{series_number})', series_number: total.to_s), user_series_index_path(@user) + total = if current_user.nil? + Series.visible_to_all.exclude_anonymous.for_user(user).count.size + else + Series.visible_to_registered_user.exclude_anonymous.for_user(user).count.size + end + span_if_current ts("Series (%{series_number})", series_number: total.to_s), user_series_index_path(user) end def pseud_series_link(pseud) - if current_user.nil? - total = Series.visible_to_all.exclude_anonymous.for_pseuds([pseud]).length - else - total = Series.visible_to_registered_user.exclude_anonymous.for_pseuds([pseud]).length - end - span_if_current ts('Series (%{series_number})', series_number: total.to_s), user_pseud_series_index_path(@user, pseud) + total = if current_user.nil? + Series.visible_to_all.exclude_anonymous.for_pseud(pseud).count.size + else + Series.visible_to_registered_user.exclude_anonymous.for_pseud(pseud).count.size + end + span_if_current ts("Series (%{series_number})", series_number: total.to_s), user_pseud_series_index_path(pseud.user, pseud) end def gifts_link(user) 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/app/mailers/archive_devise_mailer.rb b/app/mailers/archive_devise_mailer.rb index f1879ac1519..91e874e8e27 100644 --- a/app/mailers/archive_devise_mailer.rb +++ b/app/mailers/archive_devise_mailer.rb @@ -17,7 +17,7 @@ def reset_password_instructions(record, token, options = {}) devise_mail(record, :reset_password_instructions, options.merge(subject: subject)) else - I18n.with_locale(Locale.find(@user.preference.preferred_locale).iso) do + I18n.with_locale(@user.preference.locale.iso) do subject = t("users.mailer.reset_password_instructions.subject", app_name: ArchiveConfig.APP_SHORT_NAME) devise_mail(record, :reset_password_instructions, diff --git a/app/mailers/comment_mailer.rb b/app/mailers/comment_mailer.rb index 9900c051b78..c8da2df8d1b 100644 --- a/app/mailers/comment_mailer.rb +++ b/app/mailers/comment_mailer.rb @@ -3,7 +3,7 @@ class CommentMailer < ApplicationMailer def comment_notification(user, comment) @comment = comment @owner = user - I18n.with_locale(Locale.find(user.preference.preferred_locale).iso) do + I18n.with_locale(user.preference.locale.iso) do mail( to: user.email, subject: "[#{ArchiveConfig.APP_SHORT_NAME}] Comment on " + (@comment.ultimate_parent.is_a?(Tag) ? "the tag " : "") + @comment.ultimate_parent.commentable_name.gsub(">", ">").gsub("<", "<") @@ -14,7 +14,7 @@ def comment_notification(user, comment) # Sends email to an owner of the top-level commentable when a comment is edited def edited_comment_notification(user, comment) @comment = comment - I18n.with_locale(Locale.find(user.preference.preferred_locale).iso) do + I18n.with_locale(user.preference.locale.iso) do mail( to: user.email, subject: "[#{ArchiveConfig.APP_SHORT_NAME}] Edited comment on " + (@comment.ultimate_parent.is_a?(Tag) ? "the tag " : "") + @comment.ultimate_parent.commentable_name.gsub(">", ">").gsub("<", "<") diff --git a/app/mailers/kudo_mailer.rb b/app/mailers/kudo_mailer.rb index d56c9128204..5ac847a3304 100644 --- a/app/mailers/kudo_mailer.rb +++ b/app/mailers/kudo_mailer.rb @@ -10,7 +10,7 @@ def batch_kudo_notification(user_id, user_kudos) user = User.find(user_id) kudos_hash = JSON.parse(user_kudos) - I18n.with_locale(Locale.find(user.preference.preferred_locale).iso) do + I18n.with_locale(user.preference.locale.iso) do kudos_hash.each_pair do |commentable_info, kudo_givers_hash| # Parse the key to extract the type and id of the commentable so we can # weed out any commentables that no longer exist. diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index ad45f4c76bd..0e0f9890482 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -58,7 +58,7 @@ def anonymous_or_unrevealed_notification(user_id, work_id, collection_id, option :unrevealed end - I18n.with_locale(Locale.find(@user.preference.preferred_locale).iso) do + I18n.with_locale(@user.preference.locale.iso) do mail( to: @user.email, subject: t(".subject.#{@status}", @@ -95,7 +95,7 @@ def invitation_to_claim(invitation_id, archivist_login) def claim_notification(creator_id, claimed_work_ids, is_user=false) if is_user creator = User.find(creator_id) - locale = Locale.find(creator.preference.preferred_locale).iso + locale = creator.preference.locale.iso else creator = ExternalAuthor.find(creator_id) locale = I18n.default_locale @@ -105,7 +105,7 @@ def claim_notification(creator_id, claimed_work_ids, is_user=false) I18n.with_locale(locale) do mail( to: creator.email, - subject: "[#{ArchiveConfig.APP_SHORT_NAME}] Works uploaded" + subject: t("user_mailer.claim_notification.subject", app_name: ArchiveConfig.APP_SHORT_NAME) ) end end @@ -145,7 +145,7 @@ def batch_subscription_notification(subscription_id, entries) if @creations.count > 1 subject += " and #{@creations.count - 1} more" end - I18n.with_locale(Locale.find(@subscription.user.preference.preferred_locale).iso) do + I18n.with_locale(@subscription.user.preference.locale.iso) do mail( to: @subscription.user.email, subject: "[#{ArchiveConfig.APP_SHORT_NAME}] #{subject}" @@ -157,7 +157,7 @@ def batch_subscription_notification(subscription_id, entries) def invite_increase_notification(user_id, total) @user = User.find(user_id) @total = total - I18n.with_locale(Locale.find(@user.preference.preferred_locale).iso) do + I18n.with_locale(@user.preference.locale.iso) do mail( to: @user.email, subject: "#{t 'user_mailer.invite_increase_notification.subject', app_name: ArchiveConfig.APP_SHORT_NAME}" @@ -170,7 +170,7 @@ def invite_request_declined(user_id, total, reason) @user = User.find(user_id) @total = total @reason = reason - I18n.with_locale(Locale.find(@user.preference.preferred_locale).iso) do + I18n.with_locale(@user.preference.locale.iso) do mail( to: @user.email, subject: t('user_mailer.invite_request_declined.subject', app_name: ArchiveConfig.APP_SHORT_NAME) @@ -213,7 +213,7 @@ def challenge_assignment_notification(collection_id, assigned_user_id, assignmen @assigned_user = User.find(assigned_user_id) assignment = ChallengeAssignment.find(assignment_id) @request = (assignment.request_signup || assignment.pinch_request_signup) - I18n.with_locale(Locale.find(@assigned_user.preference.preferred_locale).iso) do + I18n.with_locale(@assigned_user.preference.locale.iso) do mail( to: @assigned_user.email, # i18n-tasks-use t('user_mailer.challenge_assignment_notification.subject') @@ -225,7 +225,7 @@ def challenge_assignment_notification(collection_id, assigned_user_id, assignmen # Asks a user to validate and activate their new account def signup_notification(user_id) @user = User.find(user_id) - I18n.with_locale(Locale.find(@user.preference.preferred_locale).iso) do + I18n.with_locale(@user.preference.locale.iso) do mail( to: @user.email, subject: t('user_mailer.signup_notification.subject', app_name: ArchiveConfig.APP_SHORT_NAME) @@ -238,7 +238,7 @@ def change_email(user_id, old_email, new_email) @user = User.find(user_id) @old_email = old_email @new_email = new_email - I18n.with_locale(Locale.find(@user.preference.preferred_locale).iso) do + I18n.with_locale(@user.preference.locale.iso) do mail( to: @old_email, subject: t('user_mailer.change_email.subject', app_name: ArchiveConfig.APP_SHORT_NAME) @@ -293,7 +293,7 @@ def related_work_notification(user_id, related_work_id) @related_work = RelatedWork.find(related_work_id) @related_parent_link = url_for(controller: :works, action: :show, id: @related_work.parent) @related_child_link = url_for(controller: :works, action: :show, id: @related_work.work) - I18n.with_locale(Locale.find(@user.preference.preferred_locale).iso) do + I18n.with_locale(@user.preference.locale.iso) do mail( to: @user.email, subject: "[#{ArchiveConfig.APP_SHORT_NAME}] Related work notification" @@ -306,7 +306,7 @@ def recipient_notification(user_id, work_id, collection_id = nil) @user = User.find(user_id) @work = Work.find(work_id) @collection = Collection.find(collection_id) if collection_id - I18n.with_locale(Locale.find(@user.preference.preferred_locale).iso) do + I18n.with_locale(@user.preference.locale.iso) do subject = if @collection t("user_mailer.recipient_notification.subject.collection", app_name: ArchiveConfig.APP_SHORT_NAME, @@ -328,7 +328,7 @@ def prompter_notification(work_id, collection_id=nil) @collection = Collection.find(collection_id) if collection_id @work.challenge_claims.each do |claim| user = User.find(claim.request_signup.pseud.user.id) - I18n.with_locale(Locale.find(user.preference.preferred_locale).iso) do + I18n.with_locale(user.preference.locale.iso) do mail( to: user.email, subject: "[#{ArchiveConfig.APP_SHORT_NAME}] A response to your prompt" @@ -349,7 +349,7 @@ def delete_work_notification(user, work) attachments["#{download.file_name}.html"] = { content: html, encoding: "base64" } attachments["#{download.file_name}.txt"] = { content: html, encoding: "base64" } - I18n.with_locale(Locale.find(@user.preference.preferred_locale).iso) do + I18n.with_locale(@user.preference.locale.iso) do mail( to: user.email, subject: t('user_mailer.delete_work_notification.subject', app_name: ArchiveConfig.APP_SHORT_NAME) @@ -369,7 +369,7 @@ def admin_deleted_work_notification(user, work) attachments["#{download.file_name}.html"] = { content: html, encoding: "base64" } attachments["#{download.file_name}.txt"] = { content: html, encoding: "base64" } - I18n.with_locale(Locale.find(@user.preference.preferred_locale).iso) do + I18n.with_locale(@user.preference.locale.iso) do mail( to: user.email, subject: t('user_mailer.admin_deleted_work_notification.subject', app_name: ArchiveConfig.APP_SHORT_NAME) @@ -382,7 +382,7 @@ def admin_hidden_work_notification(creation_id, user_id) @user = User.find_by(id: user_id) @work = Work.find_by(id: creation_id) - I18n.with_locale(Locale.find(@user.preference.preferred_locale).iso) do + I18n.with_locale(@user.preference.locale.iso) do mail( to: @user.email, # i18n-tasks-use t('user_mailer.admin_hidden_work_notification.subject') diff --git a/app/models/admin.rb b/app/models/admin.rb index e91b31a0493..e57d07fb6b6 100644 --- a/app/models/admin.rb +++ b/app/models/admin.rb @@ -1,5 +1,5 @@ class Admin < ApplicationRecord - VALID_ROLES = %w[superadmin board communications elections translation tag_wrangling docs support policy_and_abuse open_doors].freeze + VALID_ROLES = %w[superadmin board board_assistants_team communications development_and_membership docs elections translation tag_wrangling support policy_and_abuse open_doors].freeze serialize :roles, Array diff --git a/app/models/bookmark.rb b/app/models/bookmark.rb index 1a5c57eaba5..2aac480e454 100644 --- a/app/models/bookmark.rb +++ b/app/models/bookmark.rb @@ -32,14 +32,13 @@ def check_new_external_work end end - default_scope -> { order("bookmarks.id DESC") } # id's stand in for creation date - # renaming scope :public -> :is_public because otherwise it overlaps with the "public" keyword scope :is_public, -> { where(private: false, hidden_by_admin: false) } scope :not_public, -> { where(private: true) } scope :not_private, -> { where(private: false) } scope :since, lambda { |*args| where("bookmarks.created_at > ?", (args.first || 1.week.ago)) } scope :recs, -> { where(rec: true) } + scope :order_by_created_at, -> { order("created_at DESC") } scope :join_work, -> { joins("LEFT JOIN works ON (bookmarks.bookmarkable_id = works.id AND bookmarks.bookmarkable_type = 'Work')"). @@ -100,7 +99,7 @@ def check_new_external_work scope :visible_to_admin, -> { not_private } - scope :latest, -> { is_public.limit(ArchiveConfig.ITEMS_PER_PAGE).join_work } + scope :latest, -> { is_public.order_by_created_at.limit(ArchiveConfig.ITEMS_PER_PAGE).join_work } scope :for_blurb, -> { includes(:bookmarkable, :pseud, :tags, :collections) } diff --git a/app/models/challenge_models/gift_exchange.rb b/app/models/challenge_models/gift_exchange.rb index e2a028959c3..06853e34786 100644 --- a/app/models/challenge_models/gift_exchange.rb +++ b/app/models/challenge_models/gift_exchange.rb @@ -1,5 +1,5 @@ class GiftExchange < ApplicationRecord - PROMPT_TYPES = %w(requests offers) + PROMPT_TYPES = %w[requests offers].freeze include ChallengeCore override_datetime_setters @@ -17,26 +17,28 @@ class GiftExchange < ApplicationRecord belongs_to :potential_match_settings, dependent: :destroy accepts_nested_attributes_for :potential_match_settings - validates_length_of :signup_instructions_general, :signup_instructions_requests, :signup_instructions_offers, { + validates :signup_instructions_general, :signup_instructions_requests, :signup_instructions_offers, length: { allow_blank: true, maximum: ArchiveConfig.INFO_MAX, too_long: ts("must be less than %{max} letters long.", max: ArchiveConfig.INFO_MAX) } PROMPT_TYPES.each do |type| - %w(required allowed).each do |setting| + %w[required allowed].each do |setting| prompt_limit_field = "#{type}_num_#{setting}" - validates_numericality_of prompt_limit_field, only_integer: true, less_than_or_equal_to: ArchiveConfig.PROMPTS_MAX, greater_than_or_equal_to: 0 + validates prompt_limit_field, numericality: { + only_integer: true, + less_than_or_equal_to: ArchiveConfig.PROMPTS_MAX, + greater_than_or_equal_to: 1 + } end end before_validation :update_allowed_values def update_allowed_values - %w(request offer).each do |prompt_type| - required = eval("#{prompt_type}s_num_required") - allowed = eval("#{prompt_type}s_num_allowed") - if required > allowed - eval("#{prompt_type}s_num_allowed = required") - end + %w[request offer].each do |prompt_type| + required = send("#{prompt_type}s_num_required") + allowed = send("#{prompt_type}s_num_allowed") + send("#{prompt_type}s_num_allowed=", required) if required > allowed end end @@ -48,16 +50,16 @@ def update_allowed_values after_save :copy_tag_set_from_offer_to_request def copy_tag_set_from_offer_to_request - if self.offer_restriction - self.request_restriction.set_owned_tag_sets(self.offer_restriction.owned_tag_sets) - - # copy the tag-set-based restriction settings - self.request_restriction.character_restrict_to_fandom = self.offer_restriction.character_restrict_to_fandom - self.request_restriction.relationship_restrict_to_fandom = self.offer_restriction.relationship_restrict_to_fandom - self.request_restriction.character_restrict_to_tag_set = self.offer_restriction.character_restrict_to_tag_set - self.request_restriction.relationship_restrict_to_tag_set = self.offer_restriction.relationship_restrict_to_tag_set - self.request_restriction.save - end + return unless self.offer_restriction + + self.request_restriction.set_owned_tag_sets(self.offer_restriction.owned_tag_sets) + + # copy the tag-set-based restriction settings + self.request_restriction.character_restrict_to_fandom = self.offer_restriction.character_restrict_to_fandom + self.request_restriction.relationship_restrict_to_fandom = self.offer_restriction.relationship_restrict_to_fandom + self.request_restriction.character_restrict_to_tag_set = self.offer_restriction.character_restrict_to_tag_set + self.request_restriction.relationship_restrict_to_tag_set = self.offer_restriction.relationship_restrict_to_tag_set + self.request_restriction.save end # override core diff --git a/app/models/collection.rb b/app/models/collection.rb index dc9d5a7bfc0..d1d71b2466f 100755 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -8,8 +8,6 @@ class Collection < ApplicationRecord path: %w(staging production).include?(Rails.env) ? ":class/:attachment/:id/:style.:extension" : ":rails_root/public:url", storage: %w(staging production).include?(Rails.env) ? :s3 : :filesystem, s3_protocol: "https", - s3_credentials: "#{Rails.root}/config/s3.yml", - bucket: %w(staging production).include?(Rails.env) ? YAML.load_file("#{Rails.root}/config/s3.yml")['bucket'] : "", default_url: "/images/skins/iconsets/default/icon_collection.png" validates_attachment_content_type :icon, content_type: /image\/\S+/, allow_nil: true 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/creatorship.rb b/app/models/creatorship.rb index f9771961f68..e58db701d89 100644 --- a/app/models/creatorship.rb +++ b/app/models/creatorship.rb @@ -17,8 +17,8 @@ class Creatorship < ApplicationRecord validates_uniqueness_of :pseud, scope: [:creation_type, :creation_id], on: :create validate :check_invalid, on: :create - validate :check_disallowed, on: :create validate :check_banned, on: :create + validate :check_disallowed, on: :create validate :check_approved_becoming_false, on: :update # Update approval status if this creatorship should be automatically approved. @@ -41,24 +41,24 @@ def check_invalid end end + # Make sure that the user isn't banned or suspended. + def check_banned + return unless pseud&.user&.banned || pseud&.user&.suspended + + errors.add(:base, ts("%{name} cannot be listed as a co-creator.", + name: pseud.byline)) + throw :abort + end + # Make sure that if this is an invitation, we're not inviting someone who has # disabled invitations. def check_disallowed return if approved? || pseud.nil? return if pseud&.user&.preference&.allow_cocreator - errors.add(:base, ts("%{name} does not allow others to invite them to be a co-creator.", name: pseud.byline)) end - # Make sure that the user isn't banned or suspended. - def check_banned - return unless pseud&.user&.banned || pseud&.user&.suspended - - errors.add(:base, ts("%{name} is currently banned and cannot be listed as a co-creator.", - name: pseud.byline)) - end - # Make sure that we're not trying to set approved to false, since that could # potentially violate some rules about co-creators. (e.g. Having a user # listed as a chapter co-creator, but not a work co-creator.) @@ -149,7 +149,7 @@ def notify_creator pseud.user != User.current_user && pseud.user != User.orphan_account - I18n.with_locale(Locale.find(pseud.user.preference.preferred_locale).iso) do + I18n.with_locale(pseud.user.preference.locale.iso) do if approved? if User.current_user.try(:is_archivist?) UserMailer.creatorship_notification_archivist(id, User.current_user.id).deliver_later diff --git a/app/models/download_writer.rb b/app/models/download_writer.rb index ffaf219e777..7dffe7ca3ee 100644 --- a/app/models/download_writer.rb +++ b/app/models/download_writer.rb @@ -74,15 +74,18 @@ def get_calibre_command ### Format-specific options # epub: don't generate a cover image epub = download.file_type == "epub" ? ["--no-default-epub-cover"] : [] - # pdf: decrease margins from 72pt default + pdf = [] if download.file_type == "pdf" pdf = [ + # pdf: decrease margins from 72pt default "--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" + "--pdf-default-font-size", "17", + # pdf: only include necessary characters when embedding fonts + "--subset-embedded-fonts" ] 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/models/kudo.rb b/app/models/kudo.rb index f04e055fcdc..68a24a89950 100644 --- a/app/models/kudo.rb +++ b/app/models/kudo.rb @@ -71,7 +71,7 @@ def expire_caches end # Expire the cached kudos section under the work. - ActionController::Base.new.expire_fragment("#{commentable.cache_key}/kudos-v3") + ActionController::Base.new.expire_fragment("#{commentable.cache_key}/kudos-v4") end def notify_user_by_email?(user) diff --git a/app/models/preference.rb b/app/models/preference.rb index 9ba6786718d..2cf571fd503 100644 --- a/app/models/preference.rb +++ b/app/models/preference.rb @@ -1,6 +1,7 @@ class Preference < ApplicationRecord belongs_to :user belongs_to :skin + belongs_to :locale, foreign_key: "preferred_locale" validates :work_title_format, format: { @@ -29,4 +30,8 @@ def can_use_skin errors.add(:base, "You don't have permission to use that skin!") end + + def locale + $rollout.active?(:set_locale_preference, user) ? super : Locale.default + end end diff --git a/app/models/prompt.rb b/app/models/prompt.rb index 61fa1ff8284..acf5b2963c5 100755 --- a/app/models/prompt.rb +++ b/app/models/prompt.rb @@ -115,28 +115,44 @@ def correct_number_of_tags validate :allowed_tags def allowed_tags restriction = prompt_restriction - if restriction && tag_set - TagSet::TAG_TYPES.each do |tag_type| - # if we have a specified set of tags of this type, make sure that all the - # tags in the prompt are in the set. - if TagSet::TAG_TYPES_RESTRICTED_TO_FANDOM.include?(tag_type) && restriction.send("#{tag_type}_restrict_to_fandom") - # skip the check, these will be tested in restricted_tags below - elsif restriction.has_tags?(tag_type) - disallowed_taglist = tag_set.send("#{tag_type}_taglist") - restriction.tags(tag_type) - unless disallowed_taglist.empty? - errors.add(:base, ts("^These %{tag_label} tags in your %{prompt_type} are not allowed in this challenge: %{taglist}", + + return unless restriction && tag_set + + TagSet::TAG_TYPES.each do |tag_type| + # if we have a specified set of tags of this type, make sure that all the + # tags in the prompt are in the set. + + # skip the check, these will be tested in restricted_tags below + next if TagSet::TAG_TYPES_RESTRICTED_TO_FANDOM.include?(tag_type) && restriction.send("#{tag_type}_restrict_to_fandom") + + taglist = tag_set.send("#{tag_type}_taglist") + next if taglist.empty? + + if restriction.has_tags?(tag_type) + disallowed_taglist = taglist - restriction.tags(tag_type) + unless disallowed_taglist.empty? + errors.add( + :base, + ts( + "^These %{tag_label} tags in your %{prompt_type} are not allowed in this challenge: %{taglist}", tag_label: tag_type_label_name(tag_type).downcase, prompt_type: self.class.name.downcase, - taglist: disallowed_taglist.collect(&:name).join(ArchiveConfig.DELIMITER_FOR_OUTPUT))) - end - else - noncanonical_taglist = tag_set.send("#{tag_type}_taglist").reject {|t| t.canonical} - unless noncanonical_taglist.empty? - errors.add(:base, ts("^These %{tag_label} tags in your %{prompt_type} are not canonical and cannot be used in this challenge: %{taglist}. To fix this, please ask your challenge moderator to set up a tag set for the challenge. New tags can be added to the tag set manually by the moderator or through open nominations.", + taglist: disallowed_taglist.collect(&:name).join(ArchiveConfig.DELIMITER_FOR_OUTPUT) + ) + ) + end + else + noncanonical_taglist = taglist.reject(&:canonical) + unless noncanonical_taglist.empty? + errors.add( + :base, + ts( + "^These %{tag_label} tags in your %{prompt_type} are not canonical and cannot be used in this challenge: %{taglist}. To fix this, please ask your challenge moderator to set up a tag set for the challenge. New tags can be added to the tag set manually by the moderator or through open nominations.", tag_label: tag_type_label_name(tag_type).downcase, prompt_type: self.class.name.downcase, - taglist: noncanonical_taglist.collect(&:name).join(ArchiveConfig.DELIMITER_FOR_OUTPUT))) - end + taglist: noncanonical_taglist.collect(&:name).join(ArchiveConfig.DELIMITER_FOR_OUTPUT) + ) + ) end end end diff --git a/app/models/pseud.rb b/app/models/pseud.rb index eab42fd1994..ac298bc5fee 100644 --- a/app/models/pseud.rb +++ b/app/models/pseud.rb @@ -14,8 +14,6 @@ class Pseud < ApplicationRecord end, storage: %w(staging production).include?(Rails.env) ? :s3 : :filesystem, s3_protocol: "https", - s3_credentials: "#{Rails.root}/config/s3.yml", - bucket: %w(staging production).include?(Rails.env) ? YAML.load_file("#{Rails.root}/config/s3.yml")['bucket'] : "", default_url: "/images/skins/iconsets/default/icon_user.png" validates_attachment_content_type :icon, @@ -420,7 +418,10 @@ def delete_icon alias_method :delete_icon?, :delete_icon def clear_icon - self.icon = nil if delete_icon? && !icon.dirty? + return unless delete_icon? + + self.icon = nil unless icon.dirty? + self.icon_alt_text = nil end ################################# diff --git a/app/models/search/work_query.rb b/app/models/search/work_query.rb index 75fa7cd7c06..dd6d0ecb4ce 100644 --- a/app/models/search/work_query.rb +++ b/app/models/search/work_query.rb @@ -305,6 +305,20 @@ def aggregations { aggs: aggs } end + def works_per_language(languages_count) + response = $elasticsearch.search(index: index_name, body: { + size: 0, + query: filtered_query, + aggregations: { + languages: { + terms: { field: "language_id.keyword", size: languages_count } + } + } + }) + language_counts = response.dig("aggregations", "languages", "buckets") || [] + language_counts.map(&:values).to_h + end + #################### # HELPERS #################### diff --git a/app/models/series.rb b/app/models/series.rb index 5daecb15bcd..8b9ab830f3d 100644 --- a/app/models/series.rb +++ b/app/models/series.rb @@ -38,8 +38,7 @@ def title too_long: ts("must be less than %{max} letters long.", max: ArchiveConfig.NOTES_MAX) after_save :adjust_restricted - after_update :expire_caches - after_update_commit :update_work_index + after_update_commit :expire_caches, :update_work_index scope :visible_to_registered_user, -> { where(hidden_by_admin: false).order('series.updated_at DESC') } scope :visible_to_all, -> { where(hidden_by_admin: false, restricted: false).order('series.updated_at DESC') } @@ -51,9 +50,12 @@ def title having("MAX(works.in_anon_collection) = 0 AND MAX(works.in_unrevealed_collection) = 0") } - scope :for_pseuds, lambda {|pseuds| - joins(:approved_creatorships). - where("creatorships.pseud_id IN (?)", pseuds.collect(&:id)) + scope :for_pseud, lambda { |pseud| + joins(:approved_creatorships).where(creatorships: { pseud: pseud }) + } + + scope :for_user, lambda { |user| + joins(approved_creatorships: :pseud).where(pseuds: { user: user }) } scope :for_blurb, -> { includes(:work_tags, :pseuds) } @@ -146,8 +148,7 @@ def should_reindex_pseuds? end def expire_caches - # Expire cached work blurbs and metas if series title changes - self.works.each(&:touch) if saved_change_to_title? + self.works.touch_all end def expire_byline_cache diff --git a/app/models/skin.rb b/app/models/skin.rb index 2c9559b3380..610f307f269 100755 --- a/app/models/skin.rb +++ b/app/models/skin.rb @@ -54,8 +54,6 @@ class Skin < ApplicationRecord path: %w(staging production).include?(Rails.env) ? ":class/:attachment/:id/:style.:extension" : ":rails_root/public:url", storage: %w(staging production).include?(Rails.env) ? :s3 : :filesystem, s3_protocol: "https", - s3_credentials: "#{Rails.root}/config/s3.yml", - bucket: %w(staging production).include?(Rails.env) ? YAML.load_file("#{Rails.root}/config/s3.yml")['bucket'] : "", default_url: "/images/skins/iconsets/default/icon_skins.png" after_save :skin_invalidate_cache diff --git a/app/models/user.rb b/app/models/user.rb index e27cc3dccf1..3d4e2dd89bb 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -168,7 +168,6 @@ def expire_caches end def remove_user_from_kudos - # TODO: AO3-5054 Expire kudos cache when deleting a user. # TODO: AO3-2195 Display orphaned kudos (no users; no IPs so not counted as guest kudos). Kudo.where(user: self).update_all(user_id: nil) end @@ -321,7 +320,7 @@ def activate def create_default_associateds self.pseuds << Pseud.new(name: self.login, is_default: true) self.profile = Profile.new - self.preference = Preference.new(preferred_locale: Locale.default.id) + self.preference = Preference.new(locale: Locale.default) end def prevent_password_resets? diff --git a/app/models/work.rb b/app/models/work.rb index f47a8d0574d..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 @@ -266,6 +290,8 @@ def expire_caches tag.update_tag_cache end + series.each(&:expire_caches) + Work.expire_work_blurb_version(id) Work.flush_find_by_url_cache unless imported_from_url.blank? end @@ -292,7 +318,7 @@ def update_tag_index end def self.work_blurb_version_key(id) - "/v3/work_blurb_tag_cache_key/#{id}" + "/v4/work_blurb_tag_cache_key/#{id}" end def self.work_blurb_version(id) @@ -328,9 +354,8 @@ def self.successful_reindex(ids) Collection.expire_ids(collection_ids) end - # TODO: AO3-6085 We can use touch_all once we update to Rails 6. def touch_series - series.each(&:touch) if saved_change_to_in_anon_collection? + series.touch_all if saved_change_to_in_anon_collection? end after_destroy :destroy_chapters_in_reverse @@ -1257,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/policies/admin_banner_policy.rb b/app/policies/admin_banner_policy.rb index 66c29fc575d..7f1ccffa36f 100644 --- a/app/policies/admin_banner_policy.rb +++ b/app/policies/admin_banner_policy.rb @@ -1,10 +1,16 @@ class AdminBannerPolicy < ApplicationPolicy + ACCESS_AND_EDIT_ROLES = %w[superadmin board board_assistants_team communications development_and_membership support].freeze + CREATE_AND_DESTROY_ROLES = %w[superadmin board board_assistants_team communications support].freeze + def index? - user_has_roles?(%w[superadmin board communications support]) + user_has_roles?(ACCESS_AND_EDIT_ROLES) + end + + def create? + user_has_roles?(CREATE_AND_DESTROY_ROLES) end alias show? index? - alias create? index? alias update? index? - alias destroy? index? + alias destroy? create? end diff --git a/app/policies/admin_post_policy.rb b/app/policies/admin_post_policy.rb index 0269a0dd32f..957e1c46e53 100644 --- a/app/policies/admin_post_policy.rb +++ b/app/policies/admin_post_policy.rb @@ -1,5 +1,5 @@ class AdminPostPolicy < ApplicationPolicy - POSTING_ROLES = %w[superadmin board communications support translation].freeze + POSTING_ROLES = %w[superadmin board board_assistants_team communications support translation].freeze def can_post? user_has_roles?(POSTING_ROLES) diff --git a/app/policies/comment_policy.rb b/app/policies/comment_policy.rb index c6c1fdbb840..52745bc7ccc 100644 --- a/app/policies/comment_policy.rb +++ b/app/policies/comment_policy.rb @@ -1,11 +1,12 @@ class CommentPolicy < ApplicationPolicy 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 + DESTROY_ADMIN_POST_COMMENT_ROLES = %w[superadmin board board_assistants_team 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 communications elections policy_and_abuse support].freeze + SPAM_ADMIN_POST_COMMENT_ROLES = %w[superadmin board board_assistants_team communications elections policy_and_abuse support].freeze + SPAM_COMMENT_ROLES = %w[superadmin board policy_and_abuse support].freeze def can_destroy_comment? case record.ultimate_parent @@ -39,7 +40,12 @@ def can_hide_comment? end def can_mark_comment_spam? - user_has_roles?(SPAM_ROLES) + case record.ultimate_parent + when AdminPost + user_has_roles?(SPAM_ADMIN_POST_COMMENT_ROLES) + else + user_has_roles?(SPAM_COMMENT_ROLES) + end end alias destroy? can_destroy_comment? diff --git a/app/views/admin/_header.html.erb b/app/views/admin/_header.html.erb index 0f319e124dc..e2e16dca705 100644 --- a/app/views/admin/_header.html.erb +++ b/app/views/admin/_header.html.erb @@ -1,72 +1,73 @@ -

<%= ts("Admin Navigation", key: "header") %>

- <% 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/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/app/views/comments/_single_comment.html.erb b/app/views/comments/_single_comment.html.erb index ddad289e77c..72085c022c2 100644 --- a/app/views/comments/_single_comment.html.erb +++ b/app/views/comments/_single_comment.html.erb @@ -14,9 +14,9 @@ <% elsif !can_see_hidden_comment?(single_comment) %>

    <%= ts("(This comment is under review by an admin and is currently unavailable.)") %>

    <% else %> - <% cache([single_comment, single_comment.comment_owner], expires_in: 1.week) do %> - <% # FRONT END, update this if a.user comes in %> -

    + <%# FRONT END, update this if a.user comes in %> +

    + <% cache([single_comment, single_comment.comment_owner, "heading"], expires_in: 1.week) do %> <% if single_comment.by_anonymous_creator? %> <%= ts("Anonymous Creator") %> <% else %> @@ -32,10 +32,12 @@ <%= ts("(Unreviewed)") %> <% end %> - - <%= time_in_zone(single_comment.created_at) %> - -

    + <% end %> + + <%= time_in_zone(single_comment.created_at) %> + + + <% cache([single_comment, single_comment.comment_owner, "body"], expires_in: 1.week) do %>
    <% if !single_comment.pseud.nil? %> <% if single_comment.by_anonymous_creator? %> @@ -53,13 +55,15 @@ <% if single_comment.hidden_by_admin? %>

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

    <% end %> -
    <%=raw sanitize_field(single_comment, :comment_content) %>
    - <% unless single_comment.edited_at.blank? %> -

    - <%= ts("Last Edited") %> <%= time_in_zone(single_comment.edited_at) %> -

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

    + <%= ts("Last Edited") %> <%= time_in_zone(single_comment.edited_at) %> +

    + <% end %> <% if policy(single_comment).show_email? && single_comment.email.present? %>

    <%= ts("Email: %{email}", email: single_comment.email) %>

    diff --git a/app/views/inbox/_inbox_comment_contents.html.erb b/app/views/inbox/_inbox_comment_contents.html.erb index 6fa2eb974fc..0cb14c5a21d 100644 --- a/app/views/inbox/_inbox_comment_contents.html.erb +++ b/app/views/inbox/_inbox_comment_contents.html.erb @@ -26,7 +26,6 @@ <% end %>
    -<% # This feedback_comment used to be inbox_comment... not sure why %>
    - <%= raw sanitize_field(feedback_comment, :comment_content) %> + <%= raw sanitize_field(feedback_comment, :comment_content, strip_images: feedback_comment.ultimate_parent.is_a?(AdminPost)) %>
    diff --git a/app/views/kudos/_kudos.html.erb b/app/views/kudos/_kudos.html.erb index c1506507ea5..e9cb66a2c7b 100644 --- a/app/views/kudos/_kudos.html.erb +++ b/app/views/kudos/_kudos.html.erb @@ -3,7 +3,7 @@
    <% has_user_kudos = kudos.exists? %> <% if has_user_kudos || guest_kudos_count > 0 %> - <% cache "#{commentable.cache_key}/kudos-v3", skip_digest: true do %> + <% cache "#{commentable.cache_key}/kudos-v4", expires_in: ArchiveConfig.MINUTES_UNTIL_COMMENTABLE_KUDOS_LISTS_EXPIRE.minutes, race_condition_ttl: 10.seconds, skip_digest: true do %>

    <% if has_user_kudos %> <%= kudos_user_links(commentable, kudos, showing_more: false) %> diff --git a/app/views/languages/index.html.erb b/app/views/languages/index.html.erb index 158f94ead3f..157a111c9bd 100644 --- a/app/views/languages/index.html.erb +++ b/app/views/languages/index.html.erb @@ -1,38 +1,37 @@ -

    <%= ts('The Archive supports these languages') %>

    +

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

    - <% for language in @languages %> - + <% @languages.each do |language| %> + <% works_count = estimate_number(@works_counts[language.short] || 0) %> <% if language == Language.default %>
    <%= language.name %>
    - <%= link_to ts("%{count} works", count: language.works.count), works_path %> + <%= link_to t(".works_count", count: works_count, formatted_count: number_with_delimiter(works_count)), works_path %>
    <% else %>
    <%= link_to language.name, language, lang: language.short %>
    - <%= link_to ts("%{count} works", count: language.works.count), language_works_path(language) %> + <%= link_to t(".works_count", count: works_count, formatted_count: number_with_delimiter(works_count)), language_works_path(language) %> <% if policy(Language).edit? %>

    - <%= link_to ts("Edit"), edit_language_path(language) %> + <%= link_to t(".navigation.edit"), edit_language_path(language) %>

    <% end %>
    <% end %> - <% end %>
    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/layouts/_footer.html.erb b/app/views/layouts/_footer.html.erb index 3653544b3ac..3f6e73ca1ce 100644 --- a/app/views/layouts/_footer.html.erb +++ b/app/views/layouts/_footer.html.erb @@ -1,14 +1,14 @@ -<% 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/series/_series_order.html.erb b/app/views/series/_series_order.html.erb index 0297a5aab9d..c3e3b32baee 100644 --- a/app/views/series/_series_order.html.erb +++ b/app/views/series/_series_order.html.erb @@ -1,5 +1,5 @@
    - <%= form_tag url_for(:action => 'update_positions') do %> + <%= form_tag update_positions_series_path, method: :post do %> diff --git a/public/445.html b/public/445.html index 02da65239ab..ebaad8261fa 100644 --- a/public/445.html +++ b/public/445.html @@ -29,75 +29,76 @@
    - +
    -
    +

    Error 445

    The Archive is currently experiencing heavy traffic. Please wait a few minutes and try again.

    @@ -124,7 +125,7 @@

    Contact Us

    Development

    diff --git a/public/500.html b/public/500.html index 7e8b9b34aba..183ea3e0131 100644 --- a/public/500.html +++ b/public/500.html @@ -29,75 +29,76 @@
    - +
    -
    +

    Error 500

    We're sorry, but something went wrong.

    If you are receiving this error repeatedly, please contact Support. In the form, please include a link to the page you're trying to reach and how you're trying to reach this page.

    @@ -125,7 +126,7 @@

    Contact Us

    Development

    diff --git a/public/502.html b/public/502.html index 7288583a395..d511d8d980f 100644 --- a/public/502.html +++ b/public/502.html @@ -29,75 +29,76 @@
    - +
    -
    +

    Error 502

    The page was responding too slowly.

    @@ -127,7 +128,7 @@

    Contact Us

    Development

    diff --git a/public/503.html b/public/503.html index d37bbaa37ef..80a5f28b6e2 100644 --- a/public/503.html +++ b/public/503.html @@ -29,76 +29,76 @@
    - +
    -
    +

    Error 503

    The page was responding too slowly.

    @@ -127,7 +127,7 @@

    Contact Us

    Development

    diff --git a/public/507.html b/public/507.html index 4d6542e966f..24ea3f9b4c4 100644 --- a/public/507.html +++ b/public/507.html @@ -29,76 +29,76 @@
    - +
    -
    +

    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.

    @@ -127,7 +127,7 @@

    Contact Us

    Development

    diff --git a/public/help/skins-creating.html b/public/help/skins-creating.html index 1255cbf7617..f20a539a843 100644 --- a/public/help/skins-creating.html +++ b/public/help/skins-creating.html @@ -10,9 +10,9 @@

    - background, border, column, cue, font, layer-background, layout-grid, - list-style, margin, marker, outline, overflow, padding, page-break, pause, - scrollbar, text, transform, transition + background, border, column, cue, flex, font, layer-background, + layout-grid, list-style, margin, marker, outline, overflow, padding, + page-break, pause, scrollbar, text, transform, transition

    @@ -20,39 +20,42 @@

    - -replace, -use-link-source, accelerator, alignment-adjust, - alignment-baseline, appearance, azimuth, baseline-shift, behavior, - binding, bookmark-label, bookmark-level, bookmark-target, bottom, - box-align, box-direction, box-flex, box-flex-group, box-lines, box-orient, - box-pack, box-shadow, box-sizing, caption-side, clear, clip, color, - color-profile, content, counter-increment, counter-reset, crop, cue, - cue-after, cue-before, cursor, direction, display, dominant-baseline, + -replace, -use-link-source, accelerator, align-content, align-items, + align-self, alignment-adjust, alignment-baseline, appearance, azimuth, + baseline-shift, behavior, binding, bookmark-label, bookmark-level, + bookmark-target, bottom, box-align, box-direction, box-flex, + box-flex-group, box-lines, box-orient, box-pack, box-shadow, box-sizing, + caption-side, clear, clip, color, color-profile, content, + counter-increment, counter-reset, crop, cue, cue-after, cue-before, + cursor, direction, display, dominant-baseline, drop-initial-after-adjust, drop-initial-after-align, - drop-initial-before-adjust, drop-initial-before-align, drop-initial-size, - drop-initial-value, elevation, empty-cells, filter, fit, fit-position, - float, float-offset, font, font-effect, font-emphasize, - font-emphasize-position, font-emphasize-style, font-family, font-size, - font-size-adjust, font-smooth, font-stretch, font-style, font-variant, - font-weight, grid-columns, grid-rows, hanging-punctuation, height, - hyphenate-after, hyphenate-before, hyphenate-character, hyphenate-lines, - hyphenate-resource, hyphens, icon, image-orientation, image-resolution, - ime-mode, include-source, inline-box-align, layout-flow, left, - letter-spacing, line-break, line-height, line-stacking, - line-stacking-ruby, line-stacking-shift, line-stacking-strategy, mark, - mark-after, mark-before, marks, marquee-direction, marquee-play-count, + drop-initial-before-adjust, drop-initial-before-align, + drop-initial-size, drop-initial-value, elevation, empty-cells, filter, + fit, fit-position, float, float-offset, font, font-effect, + font-emphasize, font-emphasize-position, font-emphasize-style, + font-family, font-size, font-size-adjust, font-smooth, font-stretch, + font-style, font-variant, font-weight, grid-columns, grid-rows, + hanging-punctuation, height, hyphenate-after, hyphenate-before, + hyphenate-character, hyphenate-lines, hyphenate-resource, hyphens, icon, + image-orientation, image-resolution, ime-mode, include-source, + inline-box-align, justify-content, layout-flow, left, letter-spacing, + line-break, line-height, line-stacking, line-stacking-ruby, + line-stacking-shift, line-stacking-strategy, mark, mark-after, + mark-before, marks, marquee-direction, marquee-play-count, marquee-speed, marquee-style, max-height, max-width, min-height, min-width, move-to, nav-down, nav-index, nav-left, nav-right, nav-up, - opacity, orphans, page, page-policy, phonemes, pitch, pitch-range, - play-during, position, presentation-level, punctuation-trim, quotes, - rendering-intent, resize, rest, rest-after, rest-before, richness, right, - rotation, rotation-point, ruby-align, ruby-overhang, ruby-position, - ruby-span, size, speak, speak-header, speak-numeral, speak-punctuation, - speech-rate, stress, string-set, tab-side, table-layout, target, - target-name, target-new, target-position, top, unicode-bibi, unicode-bidi, - user-select, vertical-align, visibility, voice-balance, voice-duration, - voice-family, voice-pitch, voice-pitch-range, voice-rate, voice-stress, - voice-volume, volume, white-space, white-space-collapse, widows, width, - word-break, word-spacing, word-wrap, writing-mode, z-index + opacity, order, orphans, page, page-policy, phonemes, pitch, + pitch-range, play-during, position, presentation-level, + punctuation-trim, quotes, rendering-intent, resize, rest, rest-after, + rest-before, richness, right, rotation, rotation-point, ruby-align, + ruby-overhang, ruby-position, ruby-span, size, speak, speak-header, + speak-numeral, speak-punctuation, speech-rate, stress, string-set, + tab-side, table-layout, target, target-name, target-new, + target-position, top, unicode-bibi, unicode-bidi, user-select, + vertical-align, visibility, voice-balance, voice-duration, oice-family, + voice-pitch, voice-pitch-range, voice-rate, voice-stress, voice-volume, + volume, white-space, white-space-collapse, widows, width, word-break, + word-spacing, word-wrap, writing-mode, z-index

    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/javascripts/application.js b/public/javascripts/application.js index 9d5d9066e8c..84aa957879a 100644 --- a/public/javascripts/application.js +++ b/public/javascripts/application.js @@ -361,7 +361,6 @@ function setupDropdown(){ 'data-target': '#' }); $j('.dropdown').find('.menu').addClass("dropdown-menu"); - $j('.dropdown').find('.menu').children('li').attr("role", "menuitem"); } // Accordion-style collapsible widgets diff --git a/public/javascripts/bootstrap/bootstrap-dropdown.js b/public/javascripts/bootstrap/bootstrap-dropdown.js index 716b39924d6..c8c92ab2e7e 100644 --- a/public/javascripts/bootstrap/bootstrap-dropdown.js +++ b/public/javascripts/bootstrap/bootstrap-dropdown.js @@ -19,9 +19,10 @@ * OTWARCHIVE DEVS: * * When updating to the newest version, make sure to include the - * customizations from LINES 61-69 AND 177-183 and UPDATE THIS - * MESSAGE with the new line numbers. These lines ensure proper - * behavior when both JS and CSS hover are used for menus + * customizations from LINES 62-70, 103, AND 177-184 and UPDATE THIS + * MESSAGE with the new line numbers. These lines ensure the code works + * without the ARIA menu role and ensure proper behavior when both JS and + * CSS hover are used for menus. * ========================================================== */ @@ -99,7 +100,7 @@ return $this.click() } - $items = $('[role=menu] li:not(.divider):visible a', $parent) + $items = $('ul.menu li:not(.divider):visible a', $parent) if (!$items.length) return @@ -173,13 +174,13 @@ .on('click.dropdown.data-api', '.dropdown form', function (e) { e.stopPropagation() }) .on('click.dropdown-menu', function (e) { e.stopPropagation() }) .on('click.dropdown.data-api' , toggle, Dropdown.prototype.toggle) - .on('keydown.dropdown.data-api', toggle + ', [role=menu]' , Dropdown.prototype.keydown) + .on('keydown.dropdown.data-api', toggle + ', ul.menu' , Dropdown.prototype.keydown) .on('mouseenter', '.dropdown', function (e) { var $parent = $(this) if ($parent.siblings('.open').length) { $parent.children('ul').hide() - } + } }) .on('mouseleave', '.dropdown', function (e) { $(this).children('ul').removeAttr('') }) -}(window.jQuery); \ No newline at end of file +}(window.jQuery); diff --git a/public/javascripts/bootstrap/bootstrap-dropdown.min.js b/public/javascripts/bootstrap/bootstrap-dropdown.min.js index 45ab63130c2..e017f9598ae 100644 --- a/public/javascripts/bootstrap/bootstrap-dropdown.min.js +++ b/public/javascripts/bootstrap/bootstrap-dropdown.min.js @@ -1 +1 @@ -!function(e){"use strict";function r(){e(t).each(function(){i(e(this)).removeClass("open")})}function i(t){var n=t.attr("data-target"),r;if(!n){n=t.attr("href");n=n&&/#/.test(n)&&n.replace(/.*(?=#[^\s]*$)/,"")}r=n&&e(n);if(!r||!r.length)r=t.parent();return r}var t="[data-toggle=dropdown]",n=function(t){var n=e(t).on("click.dropdown.data-api",this.toggle);e("html").on("click.dropdown.data-api",function(){n.parent().removeClass("open")})};n.prototype={constructor:n,toggle:function(t){var n=e(this),s,o;if(n.is(".disabled, :disabled"))return;s=i(n);o=s.hasClass("open");r();if(o){s.children("ul").hide();n.blur()}else{s.toggleClass("open").children("ul").removeAttr("style");n.focus()}n.focus();return false},keydown:function(n){var r,s,o,u,a,f;if(!/(38|40|27)/.test(n.keyCode))return;r=e(this);n.preventDefault();n.stopPropagation();if(r.is(".disabled, :disabled"))return;u=i(r);a=u.hasClass("open");if(!a||a&&n.keyCode==27){if(n.which==27)u.find(t).focus();return r.click()}s=e("[role=menu] li:not(.divider):visible a",u);if(!s.length)return;f=s.index(s.filter(":focus"));if(n.keyCode==38&&f>0)f--;if(n.keyCode==40&&f0&&s--,40==t.keyCode&&s - +
    -
    +

    Error 503 - Service unavailable

    The Archive is down for maintenance.

    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.

    @@ -125,7 +126,7 @@

    Contact Us

    Development

    diff --git a/public/status/index.html b/public/status/index.html index f2abe4e72a4..0888bd84206 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,90 +37,92 @@ - - + + - + - +