diff --git a/.github/workflows/automated-tests.yml b/.github/workflows/automated-tests.yml index 494718290b4..f53bb9e01f7 100644 --- a/.github/workflows/automated-tests.yml +++ b/.github/workflows/automated-tests.yml @@ -34,7 +34,7 @@ jobs: - 3306:3306 elasticsearch: - image: docker.elastic.co/elasticsearch/elasticsearch:7.17.5 + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.26 ports: - 9200:9200 options: >- @@ -58,12 +58,15 @@ jobs: arguments: spec/models - command: rspec arguments: --exclude-pattern 'spec/{controllers,models}/**/*.rb' + libvips: true - command: cucumber arguments: features/admins + libvips: true - command: cucumber arguments: features/bookmarks - command: cucumber arguments: features/collections + libvips: true - command: cucumber arguments: features/comments_and_kudos - command: cucumber @@ -73,8 +76,10 @@ jobs: vcr: true - command: cucumber arguments: features/other_a + libvips: true - command: cucumber arguments: features/other_b + libvips: true - command: cucumber arguments: features/prompt_memes_a - command: cucumber @@ -129,6 +134,10 @@ jobs: restore-keys: | cassette-library-${{ hashFiles(matrix.tests.arguments) }}- + - name: Install libvips for image processing + if: ${{ matrix.tests.libvips }} + run: sudo apt-get install -y libvips-dev + - name: Set up Ruby and run bundle install uses: ruby/setup-ruby@v1 with: @@ -150,7 +159,7 @@ jobs: run: bundle exec ${{ matrix.tests.command }} ${{ matrix.tests.arguments }} - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: # Optional for public repos. However, individual forks can set this # secret to reduce the chance of being rate-limited by GitHub. diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml index 80d48f1e2a3..cdd3f0a9e7a 100644 --- a/.github/workflows/reviewdog.yml +++ b/.github/workflows/reviewdog.yml @@ -23,12 +23,12 @@ jobs: bundler-cache: true - name: rubocop - uses: reviewdog/action-rubocop@a162a8e8976d8b3b7141c2147d7d79eed7cc8c4c + uses: reviewdog/action-rubocop@cd8c2943f425b54f97095777109ae9f1f2d79a61 with: use_bundler: true reporter: github-pr-check skip_install: true - fail_on_error: true + fail_level: any erb-lint: name: ERB Lint runner diff --git a/.gitignore b/.gitignore index 53da1b09dbc..df5aa242f87 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,9 @@ public/system/test # /tmp/ /tmp/* +# ActiveRecord storage path +storage/ + # /vendor/ /vendor/gems diff --git a/.rubocop.yml b/.rubocop.yml index 405532e4c50..98ae8d56834 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -122,6 +122,11 @@ Migration/LargeTableSchemaUpdate: - users - works +Naming/VariableNumber: + AllowedIdentifiers: + - age_over_13 + - no_age_over_13 + Rails/DefaultScope: Enabled: true @@ -149,6 +154,8 @@ Rails/OutputSafety: Rails/Output: Exclude: + # Allow patches to print warnings to console: + - 'config/initializers/monkeypatches/*.rb' # Allow migrations to print pt-osc comments to console: - 'db/migrate/*.rb' diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f9fecea70f..b9aa8fd30ca 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,13 +2,13 @@ ## Reporting bugs -We maintain a [Jira issue tracker](https://otwarchive.atlassian.net) for developers, +We maintain a [Jira issue tracker](https://otwarchive.atlassian.net/projects/AO3/issues) for developers, and a [list of Known Issues](https://archiveofourown.org/known_issues) for [Archive of Our Own](https://archiveofourown.org) users, neither of which are publicly editable. If you need help using the site, or want to report an issue you have found, -please [contact the AO3 Support team](https://archiveofourown.org/support). +please [contact the AO3 Support team](https://archiveofourown.org/support). Our Support team is staffed by volunteers, so please wait for a response before submitting another ticket. Duplicate submissions will not make things happen faster. ## Reporting security issues @@ -25,39 +25,31 @@ official OTW volunteers, please feel free to make changes! ## Suggesting new features -Please [contact the AO3 Support team](https://archiveofourown.org/support). +Please [contact the AO3 Support team](https://archiveofourown.org/support). Our Support team is staffed by volunteers, so please wait for a response before submitting another ticket. Duplicate submissions will not make things happen faster. ## Contributing code **We only accept pull requests for issues we have already added to [Jira](https://otwarchive.atlassian.net)**, with the exception of spelling corrections and documentation improvements -(e.g. any Markdown files). - -If you'd like the ability to comment on, assign, and transition issues, -you're welcome to create a Jira account! (It makes things a bit easier for us -on the organizational side if the Full Name on your Jira account either closely -matches the name you'd like us to credit in the release notes or includes it in -parentheses, e.g. "Nickname (CREDIT NAME).") We'll give you permissions when -you create your first pull request. +(e.g. any Markdown files). We also do not accept code generated by AI tools; for more information, +please refer to [our commit policy](https://github.com/otwcode/otwarchive/wiki/Commit-Policy#scary-legal-stuff). Please check out our development wiki for more information on: - [how to set up a development environment](https://github.com/otwcode/otwarchive/wiki) - [code conventions](https://github.com/otwcode/otwarchive/wiki/Commit-policy) -Please follow the checklist on [our template](https://github.com/otwcode/otwarchive/blob/master/.github/PULL_REQUEST_TEMPLATE.md) when submitting pull requests. - -Please be patient with us! Due to our workload, it may take some time before we -can review and eventually merge your pull request. - -Once your pull request is merged, it will be deployed to our internal testing site -and our QA team will check that everything is working as intended. If not, we may -set the issue to ["Broken on Test"](https://github.com/otwcode/otwarchive/wiki/Issue-Tracking-with-Jira) -and ask you to make further changes in new pull requests. +### Workflow -If all is well, your contribution will be deployed to the [Archive of Our Own](https://archiveofourown.org) -and you will be credited in the [release notes](https://archiveofourown.org/admin_posts?tag=1)! +1. If you're a new contributor, find a task on the [issues reserved for first timers](https://otwarchive.atlassian.net/issues/?filter=13119). Otherwise, or if you're up for a challenge, pick a task from the general [open and unassigned issues](https://otwarchive.atlassian.net/issues/?filter=10800). (If you a new contributor, don't worry about claiming the issue for now. If you make a Jira account, you'll get permissions for claiming issues in step 5.) +2. Write code to address the issue. +3. Optional: Create a Jira account if you'd like the ability to comment on, assign, and transition issues. Please make sure the Full Name on your Jira account either closely matches the name you'd like us to credit in the release notes or includes it in parentheses, e.g. "Nickname (CREDIT NAME)." +4. Submit the code with a pull request following the checklist on [our template](https://github.com/otwcode/otwarchive/blob/master/.github/PULL_REQUEST_TEMPLATE.md). +5. Once you've submitted a pull request, we'll review your code and give you permissions on Jira. Please be patient with us! Due to our workload, it may take some time before we can review and eventually merge your pull request. +6. Once your pull request is merged, we will deploy it to our internal testing site and our QA team will check that everything is working as intended. +7. If something is not working as intended, we may set the issue to ["Broken on Test"](https://github.com/otwcode/otwarchive/wiki/Issue-Tracking-with-Jira) and ask you to make further changes in new pull requests. +8. If all is well, your contribution will be deployed to the [Archive of Our Own](https://archiveofourown.org) and you will be credited in the [release notes](https://archiveofourown.org/admin_posts?tag=1)! ## Volunteering for the OTW diff --git a/Gemfile b/Gemfile index 3f14f2a23ae..72596c99adb 100644 --- a/Gemfile +++ b/Gemfile @@ -56,7 +56,6 @@ gem "aws-sdk-s3" gem 'css_parser' gem "terrapin" -gem "kt-paperclip", ">= 5.2.0" # for looking up image dimensions quickly gem 'fastimage' @@ -115,7 +114,7 @@ gem "departure", "~> 6.5" gem "mail", ">= 2.8" group :test do - gem "rspec-rails", "~> 4.0.1" + gem "rspec-rails", "~> 6.0" gem 'pickle' gem 'shoulda' gem "capybara" @@ -172,6 +171,8 @@ gem 'rvm-capistrano' # Use unicorn as the web server gem 'unicorn', '~> 5.5', require: false +# Install puma so we can migrate to it +gem "puma", "~> 6.5.0" # Use god as the monitor gem 'god', '~> 0.13.7' @@ -181,3 +182,5 @@ group :staging, :production do gem "sentry-rails" gem "sentry-resque" end + +gem "image_processing", "~> 1.12" diff --git a/Gemfile.lock b/Gemfile.lock index ffd1507ece5..71edb8fe7fb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -24,75 +24,75 @@ GEM remote: https://rubygems.org/ specs: aaronh-chronic (0.3.9) - actioncable (7.0.8.4) - actionpack (= 7.0.8.4) - activesupport (= 7.0.8.4) + actioncable (7.0.8.7) + actionpack (= 7.0.8.7) + activesupport (= 7.0.8.7) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (7.0.8.4) - actionpack (= 7.0.8.4) - activejob (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + actionmailbox (7.0.8.7) + actionpack (= 7.0.8.7) + activejob (= 7.0.8.7) + activerecord (= 7.0.8.7) + activestorage (= 7.0.8.7) + activesupport (= 7.0.8.7) mail (>= 2.7.1) net-imap net-pop net-smtp - actionmailer (7.0.8.4) - actionpack (= 7.0.8.4) - actionview (= 7.0.8.4) - activejob (= 7.0.8.4) - activesupport (= 7.0.8.4) + actionmailer (7.0.8.7) + actionpack (= 7.0.8.7) + actionview (= 7.0.8.7) + activejob (= 7.0.8.7) + activesupport (= 7.0.8.7) mail (~> 2.5, >= 2.5.4) net-imap net-pop net-smtp rails-dom-testing (~> 2.0) - actionpack (7.0.8.4) - actionview (= 7.0.8.4) - activesupport (= 7.0.8.4) + actionpack (7.0.8.7) + actionview (= 7.0.8.7) + activesupport (= 7.0.8.7) rack (~> 2.0, >= 2.2.4) 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 (7.0.8.4) - actionpack (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + actiontext (7.0.8.7) + actionpack (= 7.0.8.7) + activerecord (= 7.0.8.7) + activestorage (= 7.0.8.7) + activesupport (= 7.0.8.7) globalid (>= 0.6.0) nokogiri (>= 1.8.5) - actionview (7.0.8.4) - activesupport (= 7.0.8.4) + actionview (7.0.8.7) + activesupport (= 7.0.8.7) 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.2) activerecord (>= 6.0.0) - activejob (7.0.8.4) - activesupport (= 7.0.8.4) + activejob (7.0.8.7) + activesupport (= 7.0.8.7) globalid (>= 0.3.6) - activemodel (7.0.8.4) - activesupport (= 7.0.8.4) + activemodel (7.0.8.7) + activesupport (= 7.0.8.7) activemodel-serializers-xml (1.0.2) activemodel (> 5.x) activesupport (> 5.x) builder (~> 3.1) - activerecord (7.0.8.4) - activemodel (= 7.0.8.4) - activesupport (= 7.0.8.4) - activestorage (7.0.8.4) - actionpack (= 7.0.8.4) - activejob (= 7.0.8.4) - activerecord (= 7.0.8.4) - activesupport (= 7.0.8.4) + activerecord (7.0.8.7) + activemodel (= 7.0.8.7) + activesupport (= 7.0.8.7) + activestorage (7.0.8.7) + actionpack (= 7.0.8.7) + activejob (= 7.0.8.7) + activerecord (= 7.0.8.7) + activesupport (= 7.0.8.7) marcel (~> 1.0) mini_mime (>= 1.1.0) - activesupport (7.0.8.4) + activesupport (7.0.8.7) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -139,7 +139,7 @@ GEM bigdecimal (3.1.6) brakeman (6.1.2) racc - builder (3.2.4) + builder (3.3.0) bullet (7.1.6) activesupport (>= 3.0.0) uniform_notifier (~> 1.11) @@ -257,7 +257,7 @@ GEM rainbow rubocop smart_properties - erubi (1.12.0) + erubi (1.13.1) escape_utils (1.2.1) et-orbi (1.2.11) tzinfo @@ -308,7 +308,7 @@ GEM httparty (0.21.0) mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) - i18n (1.14.5) + i18n (1.14.6) concurrent-ruby (~> 1.0) i18n-tasks (1.0.14) activesupport (>= 4.0.2) @@ -320,15 +320,12 @@ GEM rails-i18n rainbow (>= 2.2.2, < 4.0) terminal-table (>= 1.5.1) + image_processing (1.12.2) + mini_magick (>= 4.9.5, < 5) + ruby-vips (>= 2.0.17, < 3) jmespath (1.6.2) json (2.7.1) kgio (2.10.0) - kt-paperclip (7.2.2) - activemodel (>= 4.2.0) - activesupport (>= 4.2.0) - marcel (~> 1.0.1) - mime-types - terrapin (>= 0.6.0, < 2.0) launchy (2.5.2) addressable (~> 2.8) lograge (0.14.0) @@ -336,7 +333,7 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) - loofah (2.22.0) + loofah (2.23.1) crass (~> 1.0.2) nokogiri (>= 1.12.0) mail (2.8.1) @@ -346,12 +343,12 @@ GEM net-smtp marcel (1.0.2) matrix (0.4.2) - mechanize (2.10.0) + mechanize (2.12.2) addressable (~> 2.8) base64 domain_name (~> 0.5, >= 0.5.20190701) http-cookie (~> 1.0, >= 1.0.3) - mime-types (~> 3.0) + mime-types (~> 3.3) net-http-digest_auth (~> 1.4, >= 1.4.1) net-http-persistent (>= 2.5.2, < 5.0.dev) nkf @@ -359,13 +356,14 @@ GEM rubyntlm (~> 0.6, >= 0.6.3) webrick (~> 1.7) webrobots (~> 0.1.2) - method_source (1.0.0) + method_source (1.1.0) mime-types (3.5.2) mime-types-data (~> 3.2015) mime-types-data (3.2024.0206) + mini_magick (4.12.0) mini_mime (1.1.2) - mini_portile2 (2.8.7) - minitest (5.22.2) + mini_portile2 (2.8.8) + minitest (5.25.1) mono_logger (1.1.2) multi_json (1.15.0) multi_test (1.1.0) @@ -397,7 +395,7 @@ GEM netrc (0.11.0) nio4r (2.7.0) nkf (0.2.0) - nokogiri (1.16.5) + nokogiri (1.16.8) mini_portile2 (~> 2.8.2) racc (~> 1.4) orm_adapter (0.5.0) @@ -423,11 +421,13 @@ GEM byebug (~> 11.0) pry (>= 0.13, < 0.15) public_suffix (5.0.4) + puma (6.5.0) + nio4r (~> 2.0) pundit (2.3.1) activesupport (>= 3.0.0) raabro (1.4.0) - racc (1.8.0) - rack (2.2.8.1) + racc (1.8.1) + rack (2.2.9) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-dev-mark (0.8.0) @@ -435,22 +435,22 @@ GEM rack-protection (3.2.0) base64 (>= 0.1.0) rack (~> 2.2, >= 2.2.4) - rack-test (2.1.0) + rack-test (2.2.0) rack (>= 1.3) - rails (7.0.8.4) - actioncable (= 7.0.8.4) - actionmailbox (= 7.0.8.4) - actionmailer (= 7.0.8.4) - actionpack (= 7.0.8.4) - actiontext (= 7.0.8.4) - actionview (= 7.0.8.4) - activejob (= 7.0.8.4) - activemodel (= 7.0.8.4) - activerecord (= 7.0.8.4) - activestorage (= 7.0.8.4) - activesupport (= 7.0.8.4) + rails (7.0.8.7) + actioncable (= 7.0.8.7) + actionmailbox (= 7.0.8.7) + actionmailer (= 7.0.8.7) + actionpack (= 7.0.8.7) + actiontext (= 7.0.8.7) + actionview (= 7.0.8.7) + activejob (= 7.0.8.7) + activemodel (= 7.0.8.7) + activerecord (= 7.0.8.7) + activestorage (= 7.0.8.7) + activesupport (= 7.0.8.7) bundler (>= 1.15.0) - railties (= 7.0.8.4) + railties (= 7.0.8.7) rails-controller-testing (1.0.5) actionpack (>= 5.0.1.rc1) actionview (>= 5.0.1.rc1) @@ -459,22 +459,22 @@ GEM activesupport (>= 5.0.0) minitest nokogiri (>= 1.6) - rails-html-sanitizer (1.6.0) + rails-html-sanitizer (1.6.2) loofah (~> 2.21) - nokogiri (~> 1.14) + nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0) rails-i18n (7.0.8) i18n (>= 0.7, < 2) railties (>= 6.0.0, < 8) - railties (7.0.8.4) - actionpack (= 7.0.8.4) - activesupport (= 7.0.8.4) + railties (7.0.8.7) + actionpack (= 7.0.8.7) + activesupport (= 7.0.8.7) method_source rake (>= 12.2) thor (~> 1.0) zeitwerk (~> 2.5) rainbow (3.1.1) raindrops (0.20.1) - rake (13.1.0) + rake (13.2.1) redis (3.3.5) redis-namespace (1.8.2) redis (>= 3.0.4) @@ -499,25 +499,24 @@ GEM http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) - rexml (3.3.6) - strscan + rexml (3.3.9) rollout (2.4.3) rspec-core (3.13.0) rspec-support (~> 3.13.0) - rspec-expectations (3.13.0) + rspec-expectations (3.13.2) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-mocks (3.13.0) + rspec-mocks (3.13.1) diff-lcs (>= 1.2.0, < 2.0) rspec-support (~> 3.13.0) - rspec-rails (4.0.2) - actionpack (>= 4.2) - activesupport (>= 4.2) - railties (>= 4.2) - rspec-core (~> 3.10) - rspec-expectations (~> 3.10) - rspec-mocks (~> 3.10) - rspec-support (~> 3.10) + rspec-rails (6.1.5) + actionpack (>= 6.1) + activesupport (>= 6.1) + railties (>= 6.1) + rspec-core (~> 3.13) + rspec-expectations (~> 3.13) + rspec-mocks (~> 3.13) + rspec-support (~> 3.13) rspec-support (3.13.1) rubocop (1.22.3) parallel (~> 1.10) @@ -537,6 +536,8 @@ GEM rubocop-rspec (2.6.0) rubocop (~> 1.19) ruby-progressbar (1.13.0) + ruby-vips (2.2.1) + ffi (~> 1.12) ruby2_keywords (0.0.5) rubyntlm (0.6.3) rubyzip (2.3.2) @@ -587,7 +588,6 @@ GEM rack (> 1, < 3) stackprof (0.2.26) stringex (2.8.6) - strscan (3.1.0) sys-uname (1.2.3) ffi (~> 1.1) terminal-table (3.0.2) @@ -596,7 +596,7 @@ GEM climate_control test-unit (3.6.2) power_assert - thor (1.3.1) + thor (1.3.2) tilt (2.3.0) timecop (0.9.8) timeliness (0.4.5) @@ -618,7 +618,7 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webrick (1.8.1) + webrick (1.8.2) webrobots (0.1.2) websocket (1.2.10) websocket-driver (0.7.6) @@ -633,7 +633,7 @@ GEM will_paginate (4.0.0) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.6.13) + zeitwerk (2.6.18) PLATFORMS ruby @@ -683,8 +683,8 @@ DEPENDENCIES htmlentities httparty i18n-tasks + image_processing (~> 1.12) kgio (= 2.10.0) - kt-paperclip (>= 5.2.0) launchy lograge mail (>= 2.8) @@ -698,6 +698,7 @@ DEPENDENCIES phraseapp-in-context-editor-ruby (>= 1.0.6) pickle pry-byebug + puma (~> 6.5.0) pundit rack (~> 2.2) rack-attack @@ -712,7 +713,7 @@ DEPENDENCIES resque-scheduler rest-client (~> 2.1.0) rollout - rspec-rails (~> 4.0.1) + rspec-rails (~> 6.0) rubocop (= 1.22.3) rubocop-rails (= 2.12.4) rubocop-rspec (= 2.6.0) diff --git a/README.md b/README.md index 8ad346597c9..8724d779bc5 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,10 @@ We welcome pull requests for bugs described in our issue tracker. Please see our * [Developer Documentation](https://github.com/otwcode/otwarchive/wiki) * [Commit Policy](https://github.com/otwcode/otwarchive/wiki/Commit-policy) +We do not have a public chat, but you are welcome to contact us at otw-coders@transformativeworks.org if you have any questions. + +We grant your Jira account permissions for commenting on, assigning, and transitioning issues [after you create your first pull request](https://github.com/otwcode/otwarchive/blob/master/CONTRIBUTING.md#workflow). + API ---------- There is currently no API for the OTW-Archive software. While it is something we're considering for the future, we ask that contributors instead focus on issues already in our [Jira issue tracker](https://otwarchive.atlassian.net/). diff --git a/SECURITY.md b/SECURITY.md index 63258de0691..1a646472cdc 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,7 +5,7 @@ with details and reproduction steps. We will get back to you as soon as possible and we may ask for additional information or guidance. Please avoid testing for security issues on the Archive of Our Own itself, -as you risk disrupting other users and violating the [Terms of Service](https://archiveofourown.org/tos#IV.C.). +as you risk disrupting other users and violating the [Terms of Service](https://archiveofourown.org/content#II.K.1). Please give us a reasonable amount of time to address the issue before any disclosure to the public or a third-party. diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 53f5b0b37f0..89a98aa6b0c 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,4 +1,5 @@ class ApplicationController < ActionController::Base + include ActiveStorage::SetCurrent include Pundit::Authorization protect_from_forgery with: :exception, prepend: true rescue_from ActionController::InvalidAuthenticityToken, with: :display_auth_error @@ -176,7 +177,7 @@ def load_admin_banner before_action :load_tos_popup def load_tos_popup # Integers only, YYYY-MM-DD format of date Board approved TOS - @current_tos_version = 20180523 + @current_tos_version = 2024_11_19 # rubocop:disable Style/NumericLiterals end # store previous page in session to make redirecting back possible @@ -269,13 +270,17 @@ def access_denied(options ={}) def admin_only_access_denied respond_to do |format| format.html do - flash[:error] = ts("Sorry, only an authorized admin can access the page you were trying to reach.") + flash[:error] = t("admin.access.page_access_denied") redirect_to root_path end format.json do - errors = [ts("Sorry, only an authorized admin can do that.")] + errors = [t("admin.access.action_access_denied")] render json: { errors: errors }, status: :forbidden end + format.js do + flash[:error] = t("admin.access.page_access_denied") + render js: "window.location.href = '#{root_path}';" + end end end @@ -389,11 +394,20 @@ def use_caching? # Prevents banned and suspended users from adding/editing content def check_user_status if current_user.is_a?(User) && (current_user.suspended? || current_user.banned?) - flash[:error] = if current_user.suspended? - t("users.status.suspension_notice_html", contact_abuse_link: view_context.link_to(t("users.status.contact_abuse"), new_abuse_report_path), suspended_until: localize(current_user.suspended_until)) - else - t("users.status.ban_notice_html", contact_abuse_link: view_context.link_to(t("users.status.contact_abuse"), new_abuse_report_path)) - end + if current_user.suspended? + suspension_end = current_user.suspended_until + + # Unban threshold is 6:51pm, 12 hours after the unsuspend_users rake task located in schedule.rb is run at 6:51am + unban_theshold = DateTime.new(suspension_end.year, suspension_end.month, suspension_end.day, 18, 51, 0, "+00:00") + + # If the stated suspension end date is after the unban threshold we need to advance a day + suspension_end = suspension_end.next_day(1) if suspension_end > unban_theshold + localized_suspension_end = view_context.date_in_zone(suspension_end) + flash[:error] = t("users.status.suspension_notice_html", suspended_until: localized_suspension_end, contact_abuse_link: view_context.link_to(t("users.contact_abuse"), new_abuse_report_path)) + + else + flash[:error] = t("users.status.ban_notice_html", contact_abuse_link: view_context.link_to(t("users.contact_abuse"), new_abuse_report_path)) + end redirect_to current_user end end @@ -401,8 +415,18 @@ def check_user_status # Prevents temporarily suspended users from deleting content def check_user_not_suspended return unless current_user.is_a?(User) && current_user.suspended? + + suspension_end = current_user.suspended_until + + # Unban threshold is 6:51pm, 12 hours after the unsuspend_users rake task located in schedule.rb is run at 6:51am + unban_theshold = DateTime.new(suspension_end.year, suspension_end.month, suspension_end.day, 18, 51, 0, "+00:00") + + # If the stated suspension end date is after the unban threshold we need to advance a day + suspension_end = suspension_end.next_day(1) if suspension_end > unban_theshold + localized_suspension_end = view_context.date_in_zone(suspension_end) - flash[:error] = t("users.status.suspension_notice_html", contact_abuse_link: view_context.link_to(t("users.status.contact_abuse"), new_abuse_report_path), suspended_until: localize(current_user.suspended_until)) + flash[:error] = t("users.status.suspension_notice_html", suspended_until: localized_suspension_end, contact_abuse_link: view_context.link_to(t("users.contact_abuse"), new_abuse_report_path)) + redirect_to current_user end diff --git a/app/controllers/archive_faqs_controller.rb b/app/controllers/archive_faqs_controller.rb index b97130558f8..8a8a8817ba2 100644 --- a/app/controllers/archive_faqs_controller.rb +++ b/app/controllers/archive_faqs_controller.rb @@ -1,14 +1,14 @@ class ArchiveFaqsController < ApplicationController - before_action :admin_only, except: [:index, :show] before_action :set_locale before_action :validate_locale, if: :logged_in_as_admin? before_action :require_language_id + before_action :default_locale_only, only: [:new, :create, :manage, :update_positions, :confirm_delete, :destroy] around_action :with_locale # GET /archive_faqs def index - @archive_faqs = ArchiveFaq.order('position ASC') + @archive_faqs = ArchiveFaq.order("position ASC") unless logged_in_as_admin? @archive_faqs = @archive_faqs.with_translations(I18n.locale) end @@ -40,6 +40,7 @@ def show end protected + def build_questions notice = "" num_to_build = params["num_questions"] ? params["num_questions"].to_i : @archive_faq.questions.count @@ -59,9 +60,10 @@ def build_questions end public + # GET /archive_faqs/new def new - @archive_faq = ArchiveFaq.new + @archive_faq = authorize ArchiveFaq.new 1.times { @archive_faq.questions.build(attributes: { question: "This is a temporary question", content: "This is temporary content", anchor: "ThisIsATemporaryAnchor"})} respond_to do |format| format.html # new.html.erb @@ -70,32 +72,34 @@ def new # GET /archive_faqs/1/edit def edit - @archive_faq = ArchiveFaq.find_by(slug: params[:id]) + @archive_faq = authorize ArchiveFaq.find_by(slug: params[:id]) + authorize :archive_faq, :full_access? if default_locale? build_questions end # GET /archive_faqs/manage def manage - @archive_faqs = ArchiveFaq.order('position ASC') + @archive_faqs = authorize ArchiveFaq.order("position ASC") end # POST /archive_faqs def create - @archive_faq = ArchiveFaq.new(archive_faq_params) - if @archive_faq.save - flash[:notice] = 'ArchiveFaq was successfully created.' - redirect_to(@archive_faq) - else - render action: "new" - end + @archive_faq = authorize ArchiveFaq.new(archive_faq_params) + if @archive_faq.save + flash[:notice] = t(".success") + redirect_to(@archive_faq) + else + render action: "new" + end end # PUT /archive_faqs/1 def update - @archive_faq = ArchiveFaq.find_by(slug: params[:id]) + @archive_faq = authorize ArchiveFaq.find_by(slug: params[:id]) + authorize :archive_faq, :full_access? if default_locale? if @archive_faq.update(archive_faq_params) - flash[:notice] = 'ArchiveFaq was successfully updated.' + flash[:notice] = t(".success") redirect_to(@archive_faq) else render action: "edit" @@ -104,9 +108,10 @@ def update # reorder FAQs def update_positions + authorize :archive_faq if params[:archive_faqs] @archive_faqs = ArchiveFaq.reorder_list(params[:archive_faqs]) - flash[:notice] = ts("Archive FAQs order was successfully updated.") + flash[:notice] = t(".success") elsif params[:archive_faq] params[:archive_faq].each_with_index do |id, position| ArchiveFaq.update(id, position: position + 1) @@ -149,6 +154,13 @@ def require_language_id redirect_to url_for(request.query_parameters.merge(language_id: @i18n_locale.to_s)) end + def default_locale_only + return if default_locale? + + flash[:error] = t("archive_faqs.default_locale_only") + redirect_to archive_faqs_path + end + # Setting I18n.locale directly is not thread safe def with_locale I18n.with_locale(@i18n_locale) { yield } @@ -156,18 +168,22 @@ def with_locale # GET /archive_faqs/1/confirm_delete def confirm_delete - @archive_faq = ArchiveFaq.find_by(slug: params[:id]) + @archive_faq = authorize ArchiveFaq.find_by(slug: params[:id]) end # DELETE /archive_faqs/1 def destroy - @archive_faq = ArchiveFaq.find_by(slug: params[:id]) + @archive_faq = authorize ArchiveFaq.find_by(slug: params[:id]) @archive_faq.destroy redirect_to(archive_faqs_path) end private + def default_locale? + @i18n_locale.to_s == I18n.default_locale.to_s + end + def archive_faq_params params.require(:archive_faq).permit( :title, diff --git a/app/controllers/blocked/users_controller.rb b/app/controllers/blocked/users_controller.rb index 9c2c232a099..bf93a58ac1a 100644 --- a/app/controllers/blocked/users_controller.rb +++ b/app/controllers/blocked/users_controller.rb @@ -12,7 +12,8 @@ class UsersController < ApplicationController # GET /users/:user_id/blocked/users def index @blocks = @user.blocks_as_blocker - .joins(:blocked).includes(blocked: :default_pseud) + .joins(:blocked) + .includes(blocked: [:pseuds, { default_pseud: { icon_attachment: :blob } }]) .order(created_at: :desc).order(id: :desc).page(params[:page]) @pseuds = @blocks.map { |b| b.blocked.default_pseud } diff --git a/app/controllers/challenge_assignments_controller.rb b/app/controllers/challenge_assignments_controller.rb index 55555e4705e..1e90132adde 100644 --- a/app/controllers/challenge_assignments_controller.rb +++ b/app/controllers/challenge_assignments_controller.rb @@ -219,9 +219,11 @@ def default_all def default @challenge_assignment.defaulted_at = Time.now @challenge_assignment.save - @challenge_assignment.collection.notify_maintainers("Challenge default by #{@challenge_assignment.offer_byline}", - "Signed-up participant #{@challenge_assignment.offer_byline} has defaulted on their assignment for #{@challenge_assignment.request_byline}. " + - "You may want to assign a pinch hitter on the collection assignments page: #{collection_assignments_url(@challenge_assignment.collection)}") + + assignments_page_url = collection_assignments_url(@challenge_assignment.collection) + + @challenge_assignment.collection.notify_maintainers_challenge_default(@challenge_assignment, assignments_page_url) + flash[:notice] = "We have notified the collection maintainers that you had to default on your assignment." redirect_to user_assignments_path(current_user) end diff --git a/app/controllers/chapters_controller.rb b/app/controllers/chapters_controller.rb index 3fe5a4478ad..bdab981c760 100644 --- a/app/controllers/chapters_controller.rb +++ b/app/controllers/chapters_controller.rb @@ -1,6 +1,6 @@ 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 :users_only, except: [:index, :show, :destroy, :confirm_delete] 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 @@ -28,53 +28,55 @@ def manage # GET /work/:work_id/chapters/:id.xml def show @tag_groups = @work.tag_groups - if params[:view_adult] - cookies[:view_adult] = "true" - elsif @work.adult? && !see_adult? - render "works/_adult", layout: "application" and return - end - if params[:selected_id] - redirect_to url_for(controller: :chapters, action: :show, work_id: @work.id, id: params[:selected_id]) and return - end + redirect_to url_for(controller: :chapters, action: :show, work_id: @work.id, id: params[:selected_id]) and return if params[:selected_id] + @chapters = @work.chapters_in_order( include_content: false, include_drafts: (logged_in_as_admin? || @work.user_is_owner_or_invited?(current_user)) ) - if !@chapters.include?(@chapter) + + unless @chapters.include?(@chapter) access_denied + return + end + + chapter_position = @chapters.index(@chapter) + if @chapters.length > 1 + @previous_chapter = @chapters[chapter_position - 1] unless chapter_position.zero? + @next_chapter = @chapters[chapter_position + 1] + end + + if @work.unrevealed? + @page_title = t(".unrevealed") + t(".chapter_position", position: @chapter.position.to_s) else - chapter_position = @chapters.index(@chapter) - if @chapters.length > 1 - @previous_chapter = @chapters[chapter_position-1] unless chapter_position == 0 - @next_chapter = @chapters[chapter_position+1] - end + fandoms = @tag_groups["Fandom"] + fandom = fandoms.empty? ? t(".unspecified_fandom") : fandoms[0].name + title_fandom = fandoms.size > 3 ? t(".multifandom") : fandom + author = @work.anonymous? ? t(".anonymous") : @work.pseuds.sort.collect(&:byline).join(", ") + @page_title = get_page_title(title_fandom, author, @work.title + t(".chapter_position", position: @chapter.position.to_s)) + end - if @work.unrevealed? - @page_title = t(".unrevealed") + t(".chapter_position", position: @chapter.position.to_s) - else - fandoms = @tag_groups["Fandom"] - fandom = fandoms.empty? ? t(".unspecified_fandom") : fandoms[0].name - title_fandom = fandoms.size > 3 ? t(".multifandom") : fandom - author = @work.anonymous? ? t(".anonymous") : @work.pseuds.sort.collect(&:byline).join(", ") - @page_title = get_page_title(title_fandom, author, @work.title + t(".chapter_position", position: @chapter.position.to_s)) - end + if params[:view_adult] + cookies[:view_adult] = "true" + elsif @work.adult? && !see_adult? + render "works/_adult", layout: "application" and return + end - @kudos = @work.kudos.with_user.includes(:user) + @kudos = @work.kudos.with_user.includes(:user) - if current_user.respond_to?(:subscriptions) - @subscription = current_user.subscriptions.where(subscribable_id: @work.id, - subscribable_type: 'Work').first || - current_user.subscriptions.build(subscribable: @work) - end - # update the history. - Reading.update_or_create(@work, current_user) if current_user + if current_user.respond_to?(:subscriptions) + @subscription = current_user.subscriptions.where(subscribable_id: @work.id, + subscribable_type: "Work").first || + current_user.subscriptions.build(subscribable: @work) + end + # update the history. + Reading.update_or_create(@work, current_user) if current_user - respond_to do |format| - format.html - format.js - end + respond_to do |format| + format.html + format.js end end @@ -86,14 +88,14 @@ def new # GET /work/:work_id/chapters/1/edit def edit - if params["remove"] == "me" - @chapter.creatorships.for_user(current_user).destroy_all - if @work.chapters.any? { |c| current_user.is_author_of?(c) } - flash[:notice] = ts("You have been removed as a creator from the chapter.") - redirect_to @work - else # remove from work if no longer co-creator on any chapter - redirect_to edit_work_path(@work, remove: "me") - end + return unless params["remove"] == "me" + + @chapter.creatorships.for_user(current_user).destroy_all + if @work.chapters.any? { |c| current_user.is_author_of?(c) } + flash[:notice] = ts("You have been removed as a creator from the chapter.") + redirect_to @work + else # remove from work if no longer co-creator on any chapter + redirect_to edit_work_path(@work, remove: "me") end end @@ -250,7 +252,7 @@ def chapter_cannot_be_saved? # fetch work these chapters belong to from db def load_work @work = params[:work_id] ? Work.find_by(id: params[:work_id]) : Chapter.find_by(id: params[:id]).try(:work) - unless @work.present? + if @work.blank? flash[:error] = ts("Sorry, we couldn't find the work you were looking for.") redirect_to root_path and return end @@ -263,18 +265,15 @@ def load_work def load_chapter @chapter = @work.chapters.find_by(id: params[:id]) - unless @chapter - flash[:error] = ts("Sorry, we couldn't find the chapter you were looking for.") - redirect_to work_path(@work) - end - end + return if @chapter + flash[:error] = ts("Sorry, we couldn't find the chapter you were looking for.") + redirect_to work_path(@work) + end def post_chapter - if !@work.posted - @work.update_attribute(:posted, true) - end - flash[:notice] = ts('Chapter has been posted!') + @work.update_attribute(:posted, true) unless @work.posted + flash[:notice] = ts("Chapter has been posted!") end private @@ -284,6 +283,5 @@ def chapter_params :"published_at(2i)", :"published_at(1i)", :summary, :notes, :endnotes, :content, :published_at, author_attributes: [:byline, ids: [], coauthors: []]) - end end diff --git a/app/controllers/collection_items_controller.rb b/app/controllers/collection_items_controller.rb index f3cc41724c0..00156750a1b 100644 --- a/app/controllers/collection_items_controller.rb +++ b/app/controllers/collection_items_controller.rb @@ -23,7 +23,7 @@ def index @collection_items.unreviewed_by_collection end elsif params[:user_id] && (@user = User.find_by(login: params[:user_id])) && @user == current_user - @collection_items = CollectionItem.for_user(@user).includes(:collection) + @collection_items = CollectionItem.for_user(@user).includes(:collection).merge(Collection.with_attached_icon) @collection_items = case params[:status] when "approved" @collection_items.approved_by_both diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb index 0a0c7861c0f..06074e58831 100644 --- a/app/controllers/collections_controller.rb +++ b/app/controllers/collections_controller.rb @@ -25,11 +25,20 @@ def load_collection_from_id def index if params[:work_id] && (@work = Work.find_by(id: params[:work_id])) - @collections = @work.approved_collections.by_title.includes(:parent, :moderators, :children, :collection_preference, owners: [:user]).paginate(page: params[:page]) + @collections = @work.approved_collections + .by_title + .for_blurb + .paginate(page: params[:page]) elsif params[:collection_id] && (@collection = Collection.find_by(name: params[:collection_id])) - @collections = @collection.children.by_title.includes(:parent, :moderators, :children, :collection_preference, owners: [:user]).paginate(page: params[:page]) + @collections = @collection.children + .by_title + .for_blurb + .paginate(page: params[:page]) elsif params[:user_id] && (@user = User.find_by(login: params[:user_id])) - @collections = @user.maintained_collections.by_title.includes(:parent, :moderators, :children, :collection_preference, owners: [:user]).paginate(page: params[:page]) + @collections = @user.maintained_collections + .by_title + .for_blurb + .paginate(page: params[:page]) @page_subtitle = ts("%{username} - Collections", username: @user.login) else if params[:user_id] diff --git a/app/controllers/comments_controller.rb b/app/controllers/comments_controller.rb index da5c9d84d95..6570e6e2013 100644 --- a/app/controllers/comments_controller.rb +++ b/app/controllers/comments_controller.rb @@ -666,16 +666,13 @@ def redirect_to_all_comments(commentable, options = {}) if commentable.is_a?(Chapter) && (options[:view_full_work] || current_user.try(:preference).try(:view_full_works)) commentable = commentable.work end - redirect_to controller: commentable.class.to_s.underscore.pluralize, - action: :show, - id: commentable.id, - show_comments: options[:show_comments], - add_comment_reply_id: options[:add_comment_reply_id], - delete_comment_id: options[:delete_comment_id], - view_full_work: options[:view_full_work], - anchor: options[:anchor], - page: options[:page], - only_path: true + redirect_to polymorphic_path(commentable, + options.slice(:show_comments, + :add_comment_reply_id, + :delete_comment_id, + :view_full_work, + :anchor, + :page)) end end diff --git a/app/controllers/feedbacks_controller.rb b/app/controllers/feedbacks_controller.rb index 7dfdeb7f087..9f3acab63aa 100644 --- a/app/controllers/feedbacks_controller.rb +++ b/app/controllers/feedbacks_controller.rb @@ -3,8 +3,9 @@ class FeedbacksController < ApplicationController before_action :load_support_languages def new - @feedback = Feedback.new @admin_setting = AdminSetting.current + @feedback = Feedback.new + @feedback.referer = request.referer if logged_in_as_admin? @feedback.email = current_admin.email elsif is_registered_user? @@ -19,6 +20,8 @@ def create @feedback.rollout = @feedback.rollout_string @feedback.user_agent = request.env["HTTP_USER_AGENT"] @feedback.ip_address = request.remote_ip + @feedback.referer = nil unless @feedback.referer && ArchiveConfig.PERMITTED_HOSTS.include?(URI(@feedback.referer).host) + @feedback.site_skin = helpers.current_skin if @feedback.save @feedback.email_and_send flash[:notice] = t("successfully_sent", @@ -39,8 +42,7 @@ def load_support_languages def feedback_params params.require(:feedback).permit( - :comment, :email, :summary, :username, :language + :comment, :email, :summary, :username, :language, :referer ) end - end diff --git a/app/controllers/home_controller.rb b/app/controllers/home_controller.rb index 96ce257414d..86e83630c1e 100644 --- a/app/controllers/home_controller.rb +++ b/app/controllers/home_controller.rb @@ -7,13 +7,25 @@ class HomeController < ApplicationController def unicorn_test end + def content + @page_subtitle = t(".page_title") + render action: "content", layout: "application" + end + + def privacy + @page_subtitle = t(".page_title") + render action: "privacy", layout: "application" + end + # terms of service def tos + @page_subtitle = t(".page_title") render action: "tos", layout: "application" end # terms of service faq def tos_faq + @page_subtitle = t(".page_title") render action: "tos_faq", layout: "application" end diff --git a/app/controllers/inbox_controller.rb b/app/controllers/inbox_controller.rb index fb7a710f7e6..4191691060a 100644 --- a/app/controllers/inbox_controller.rb +++ b/app/controllers/inbox_controller.rb @@ -2,7 +2,7 @@ class InboxController < ApplicationController include BlockHelper before_action :load_user - before_action :check_ownership + before_action :check_ownership_or_admin before_action :load_commentable, only: :reply before_action :check_blocked, only: :reply @@ -13,6 +13,7 @@ def load_user end def show + authorize InboxComment if logged_in_as_admin? @inbox_total = @user.inbox_comments.with_bad_comments_removed.count @unread = @user.inbox_comments.with_bad_comments_removed.count_unread @filters = filter_params[:filters] || {} @@ -30,6 +31,7 @@ def reply end def update + authorize InboxComment if logged_in_as_admin? begin @inbox_comments = InboxComment.find(params[:inbox_comments]) if params[:read] diff --git a/app/controllers/invite_requests_controller.rb b/app/controllers/invite_requests_controller.rb index ba1c69166f3..e58c027059f 100644 --- a/app/controllers/invite_requests_controller.rb +++ b/app/controllers/invite_requests_controller.rb @@ -2,8 +2,10 @@ class InviteRequestsController < ApplicationController before_action :admin_only, only: [:manage, :destroy] # GET /invite_requests + # Set browser page title to Invitation Requests def index @invite_request = InviteRequest.new + @page_subtitle = t(".page_title") end # GET /invite_requests/1 diff --git a/app/controllers/known_issues_controller.rb b/app/controllers/known_issues_controller.rb index 4f6bbe6cd30..793daff088e 100644 --- a/app/controllers/known_issues_controller.rb +++ b/app/controllers/known_issues_controller.rb @@ -1,5 +1,4 @@ class KnownIssuesController < ApplicationController - before_action :admin_only, except: [:index] # GET /known_issues @@ -9,25 +8,24 @@ def index # GET /known_issues/1 def show - @known_issue = KnownIssue.find(params[:id]) + @known_issue = authorize KnownIssue.find(params[:id]) end # GET /known_issues/new def new - @known_issue = KnownIssue.new + @known_issue = authorize KnownIssue.new end # GET /known_issues/1/edit def edit - @known_issue = KnownIssue.find(params[:id]) + @known_issue = authorize KnownIssue.find(params[:id]) end # POST /known_issues def create - @known_issue = KnownIssue.new(known_issue_params) - + @known_issue = authorize KnownIssue.new(known_issue_params) if @known_issue.save - flash[:notice] = 'Known issue was successfully created.' + flash[:notice] = "Known issue was successfully created." redirect_to(@known_issue) else render action: "new" @@ -36,10 +34,9 @@ def create # PUT /known_issues/1 def update - @known_issue = KnownIssue.find(params[:id]) - + @known_issue = authorize KnownIssue.find(params[:id]) if @known_issue.update(known_issue_params) - flash[:notice] = 'Known issue was successfully updated.' + flash[:notice] = "Known issue was successfully updated." redirect_to(@known_issue) else render action: "edit" @@ -48,7 +45,7 @@ def update # DELETE /known_issues/1 def destroy - @known_issue = KnownIssue.find(params[:id]) + @known_issue = authorize KnownIssue.find(params[:id]) @known_issue.destroy redirect_to(known_issues_path) end diff --git a/app/controllers/languages_controller.rb b/app/controllers/languages_controller.rb index 96892070d76..ecaa6209b9a 100644 --- a/app/controllers/languages_controller.rb +++ b/app/controllers/languages_controller.rb @@ -1,5 +1,4 @@ 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 @@ -16,7 +15,7 @@ def create @language = Language.new(language_params) authorize @language if @language.save - flash[:notice] = t('successfully_added', default: 'Language was successfully added.') + flash[:notice] = t("languages.successfully_added") redirect_to languages_path else render action: "new" @@ -31,8 +30,9 @@ def edit def update @language = Language.find_by(short: params[:id]) authorize @language - if @language.update(language_params) - flash[:notice] = t('successfully_updated', default: 'Language was successfully updated.') + + if @language.update(permitted_attributes(@language)) + flash[:notice] = t("languages.successfully_updated") redirect_to languages_path else render action: "new" @@ -40,6 +40,7 @@ def update end private + def language_params params.require(:language).permit( :name, :short, :support_available, :abuse_support_available, :sortable_name diff --git a/app/controllers/muted/users_controller.rb b/app/controllers/muted/users_controller.rb index 209fbc8b21b..ad29263d71f 100644 --- a/app/controllers/muted/users_controller.rb +++ b/app/controllers/muted/users_controller.rb @@ -12,7 +12,8 @@ class UsersController < ApplicationController # GET /users/:user_id/muted/users def index @mutes = @user.mutes_as_muter - .joins(:muted).includes(muted: :default_pseud) + .joins(:muted) + .includes(muted: [:pseuds, { default_pseud: { icon_attachment: :blob } }]) .order(created_at: :desc).order(id: :desc).page(params[:page]) @pseuds = @mutes.map { |b| b.muted.default_pseud } diff --git a/app/controllers/people_controller.rb b/app/controllers/people_controller.rb index 34a641a7fdf..96e3833cd68 100644 --- a/app/controllers/people_controller.rb +++ b/app/controllers/people_controller.rb @@ -8,14 +8,14 @@ def search else options = people_search_params.merge(page: params[:page]) @search = PseudSearchForm.new(options) - @people = @search.search_results + @people = @search.search_results.scope(:for_search) flash_search_warnings(@people) end end def index if @collection.present? - @people = @collection.participants.order(:name).page(params[:page]) + @people = @collection.participants.with_attached_icon.includes(:user).order(:name).page(params[:page]) @rec_counts = Pseud.rec_counts_for_pseuds(@people) @work_counts = Pseud.work_counts_for_pseuds(@people) else diff --git a/app/controllers/pseuds_controller.rb b/app/controllers/pseuds_controller.rb index f71c4ddf8e3..331600fe3f1 100644 --- a/app/controllers/pseuds_controller.rb +++ b/app/controllers/pseuds_controller.rb @@ -15,7 +15,7 @@ def load_user # GET /pseuds.xml def index if @user - @pseuds = @user.pseuds.alphabetical.paginate(page: params[:page]) + @pseuds = @user.pseuds.with_attached_icon.alphabetical.paginate(page: params[:page]) @rec_counts = Pseud.rec_counts_for_pseuds(@pseuds) @work_counts = Pseud.work_counts_for_pseuds(@pseuds) @page_subtitle = @user.login diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index a00ef266972..4c82934805f 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -1,33 +1,34 @@ class QuestionsController < ApplicationController - before_action :load_archive_faq, except: [:index, :update_positions] + before_action :load_archive_faq, except: :update_positions # GET /archive_faq/:archive_faq_id/questions/manage def manage - @questions = @archive_faq.questions.order('position') + authorize :archive_faq, :full_access? + @questions = @archive_faq.questions.order("position") end # fetch archive_faq these questions belong to from db def load_archive_faq - @archive_faq = ArchiveFaq.find_by_slug(params[:archive_faq_id]) + @archive_faq = ArchiveFaq.find_by(slug: params[:archive_faq_id]) unless @archive_faq.present? - flash[:error] = ts("Sorry, we couldn't find the FAQ you were looking for." - ) + flash[:error] = t("questions.not_found") redirect_to root_path and return end end # Update the position number of questions within a archive_faq def update_positions + authorize :archive_faq, :full_access? if params[:questions] - @archive_faq = ArchiveFaq.find_by_slug(params[:archive_faq_id]) + @archive_faq = ArchiveFaq.find_by(slug: params[:archive_faq_id]) @archive_faq.reorder_list(params[:questions]) - flash[:notice] = ts("Question order has been successfully updated.") + flash[:notice] = t(".success") elsif params[:question] params[:question].each_with_index do |id, position| Question.update(id, position: position + 1) (@questions ||= []) << Question.find(id) end - flash[:notice] = ts("Question order has been successfully updated.") + flash[:notice] = t(".success") end respond_to do |format| format.html { redirect_to(@archive_faq) and return } diff --git a/app/controllers/skins_controller.rb b/app/controllers/skins_controller.rb index a3131bcfb16..16a2d1fcc03 100644 --- a/app/controllers/skins_controller.rb +++ b/app/controllers/skins_controller.rb @@ -1,5 +1,4 @@ class SkinsController < ApplicationController - before_action :users_only, only: [:new, :create, :destroy] before_action :load_skin, except: [:index, :new, :create, :unset] before_action :check_title, only: [:create, :update] @@ -23,22 +22,22 @@ def index redirect_to skins_path and return end if is_work_skin - @skins = @user.work_skins.sort_by_recent + @skins = @user.work_skins.sort_by_recent.includes(:author).with_attached_icon @title = ts('My Work Skins') else - @skins = @user.skins.site_skins.sort_by_recent + @skins = @user.skins.site_skins.sort_by_recent.includes(:author).with_attached_icon @title = ts('My Site Skins') end else if is_work_skin - @skins = WorkSkin.approved_skins.sort_by_recent_featured + @skins = WorkSkin.approved_skins.sort_by_recent_featured.includes(:author).with_attached_icon @title = ts('Public Work Skins') else - if logged_in? - @skins = Skin.approved_skins.usable.site_skins.sort_by_recent_featured - else - @skins = Skin.approved_skins.usable.site_skins.cached.sort_by_recent_featured - end + @skins = if logged_in? + Skin.approved_skins.usable.site_skins.sort_by_recent_featured.with_attached_icon + else + Skin.approved_skins.usable.site_skins.cached.sort_by_recent_featured.with_attached_icon + end @title = ts('Public Site Skins') end end diff --git a/app/controllers/tag_set_nominations_controller.rb b/app/controllers/tag_set_nominations_controller.rb index 8b554768d54..02e8e060e45 100644 --- a/app/controllers/tag_set_nominations_controller.rb +++ b/app/controllers/tag_set_nominations_controller.rb @@ -139,6 +139,10 @@ def base_nom_query(tag_type) # set up various variables for reviewing nominations def setup_for_review set_limit + + # Only this amount of tag nominations is shown on the review page. + # If there are more (more_noms == true), moderators have to approve/reject from the shown noms to see more noms. + # TODO: AO3-3764 Show all tag set nominations @nom_limit = 30 @nominations = HashWithIndifferentAccess.new @nominations_count = HashWithIndifferentAccess.new @@ -147,7 +151,8 @@ def setup_for_review if @tag_set.includes_fandoms? # all char and rel tags happen under fandom noms @nominations_count[:fandom] = @tag_set.fandom_nominations.unreviewed.count - more_noms = true if @nominations_count[:fandom] > @nom_limit + more_noms = true if @nominations_count[:fandom] > @nom_limit + # Show a random selection of nominations if there are more noms than can be shown at once @nominations[:fandom] = more_noms ? base_nom_query("fandom").random_order : base_nom_query("fandom").order(:tagname) if (@limit[:character] > 0 || @limit[:relationship] > 0) @nominations[:cast] = base_nom_query(%w(character relationship)). @@ -162,7 +167,7 @@ def setup_for_review more_noms = true if (@tag_set.character_nominations.unreviewed.count > @nom_limit || @tag_set.relationship_nominations.unreviewed.count > @nom_limit) @nominations[:character] = base_nom_query("character") if @limit[:character] > 0 @nominations[:relationship] = base_nom_query("relationship") if @limit[:relationship] > 0 - if more_noms + if more_noms # Show a random selection of nominations if there are more noms than can be shown at once parent_tagnames = TagNomination.for_tag_set(@tag_set).unreviewed.random_order.limit(100).pluck(:parent_tagname).uniq.first(30) @nominations[:character] = @nominations[:character].where(parent_tagname: parent_tagnames) if @limit[:character] > 0 @nominations[:relationship] = @nominations[:relationship].where(parent_tagname: parent_tagnames) if @limit[:relationship] > 0 @@ -172,6 +177,7 @@ def setup_for_review end @nominations_count[:freeform] = @tag_set.freeform_nominations.unreviewed.count more_noms = true if @nominations_count[:freeform] > @nom_limit + # Show a random selection of nominations if there are more noms than can be shown at once @nominations[:freeform] = (more_noms ? base_nom_query("freeform").random_order : base_nom_query("freeform").order(:tagname)) unless @limit[:freeform].zero? if more_noms diff --git a/app/controllers/tag_wranglings_controller.rb b/app/controllers/tag_wranglings_controller.rb index 8bbd2dd6830..c3068d93a3e 100644 --- a/app/controllers/tag_wranglings_controller.rb +++ b/app/controllers/tag_wranglings_controller.rb @@ -8,32 +8,36 @@ class TagWranglingsController < ApplicationController def index @counts = tag_counts_per_category - unless params[:show].blank? - raise "Redshirt: Attempted to constantize invalid class initialize tag_wranglings_controller_index #{params[:show].classify}" unless Tag::USER_DEFINED.include?(params[:show].classify) + authorize :wrangling, :read_access? if logged_in_as_admin? + return if params[:show].blank? - params[:sort_column] = 'created_at' if !valid_sort_column(params[:sort_column], 'tag') - params[:sort_direction] = 'ASC' if !valid_sort_direction(params[:sort_direction]) + raise "Redshirt: Attempted to constantize invalid class initialize tag_wranglings_controller_index #{params[:show].classify}" unless Tag::USER_DEFINED.include?(params[:show].classify) - if params[:show] == "fandoms" - @media_names = Media.by_name.pluck(:name) - @page_subtitle = ts("fandoms") - end + params[:sort_column] = "created_at" unless valid_sort_column(params[:sort_column], "tag") + params[:sort_direction] = "ASC" unless valid_sort_direction(params[:sort_direction]) - type = params[:show].singularize.capitalize - @tags = TagQuery.new({ - type: type, - in_use: true, - unwrangleable: false, - unwrangled: true, - sort_column: params[:sort_column], - sort_direction: params[:sort_direction], - page: params[:page], - per_page: ArchiveConfig.ITEMS_PER_PAGE - }).search_results + if params[:show] == "fandoms" + @media_names = Media.by_name.pluck(:name) + @page_subtitle = t(".page_subtitle") end + + type = params[:show].singularize.capitalize + @tags = TagQuery.new({ + type: type, + in_use: true, + unwrangleable: false, + unwrangled: true, + has_posted_works: true, + sort_column: params[:sort_column], + sort_direction: params[:sort_direction], + page: params[:page], + per_page: ArchiveConfig.ITEMS_PER_PAGE + }).search_results end def wrangle + authorize :wrangling, :full_access? if logged_in_as_admin? + params[:page] = '1' if params[:page].blank? params[:sort_column] = 'name' if !valid_sort_column(params[:sort_column], 'tag') params[:sort_direction] = 'ASC' if !valid_sort_direction(params[:sort_direction]) diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb index f7ae2d2b6e9..cdc5932ba12 100644 --- a/app/controllers/tags_controller.rb +++ b/app/controllers/tags_controller.rb @@ -56,12 +56,14 @@ def search flash_search_warnings(@tags) end - # if user is Admin or Tag Wrangler, show them details about the tag + # if user is admin with view access or Tag Wrangler, show them details about the tag # if user is not logged in or a regular user, show them # 1. the works, if the tag had been wrangled and we can redirect them to works using it or its canonical merger # 2. the tag, the works and the bookmarks using it, if the tag is unwrangled (because we can't redirect them # to the works controller) def show + authorize :wrangling, :read_access? if logged_in_as_admin? + @page_subtitle = @tag.name if @tag.is_a?(Banned) && !logged_in_as_admin? flash[:error] = ts('Please log in as admin') @@ -166,6 +168,8 @@ def show_hidden # GET /tags/new def new + authorize :wrangling if logged_in_as_admin? + @tag = Tag.new respond_to do |format| @@ -209,6 +213,8 @@ def create end def edit + authorize :wrangling, :read_access? if logged_in_as_admin? + @page_subtitle = ts('%{tag_name} - Edit', tag_name: @tag.name) if @tag.is_a?(Banned) && !logged_in_as_admin? @@ -241,6 +247,8 @@ def edit end def update + authorize :wrangling if logged_in_as_admin? + # update everything except for the synonym, # so that the associations are there to move when the synonym is created syn_string = params[:tag].delete(:syn_string) @@ -272,6 +280,8 @@ def update end def wrangle + authorize :wrangling, :read_access? if logged_in_as_admin? + @page_subtitle = ts('%{tag_name} - Wrangle', tag_name: @tag.name) @counts = {} @tag.child_types.map { |t| t.underscore.pluralize.to_sym }.each do |tag_type| @@ -303,6 +313,8 @@ def wrangle end def mass_update + authorize :wrangling if logged_in_as_admin? + params[:page] = '1' if params[:page].blank? params[:sort_column] = 'name' unless valid_sort_column(params[:sort_column], 'tag') params[:sort_direction] = 'ASC' unless valid_sort_direction(params[:sort_direction]) diff --git a/app/controllers/unsorted_tags_controller.rb b/app/controllers/unsorted_tags_controller.rb index fc6c8fd6c68..eef8ef17277 100644 --- a/app/controllers/unsorted_tags_controller.rb +++ b/app/controllers/unsorted_tags_controller.rb @@ -5,13 +5,17 @@ class UnsortedTagsController < ApplicationController before_action :check_permission_to_wrangle def index + authorize :wrangling, :read_access? if logged_in_as_admin? + @tags = UnsortedTag.page(params[:page]) @counts = tag_counts_per_category end def mass_update - unless params[:tags].blank? - params[:tags].delete_if {|tag_id, tag_type| tag_type.blank? } + authorize :wrangling if logged_in_as_admin? + + if params[:tags].present? + params[:tags].delete_if { |_, tag_type| tag_type.blank? } tags = UnsortedTag.where(id: params[:tags].keys) tags.each do |tag| new_type = params[:tags][tag.id.to_s] diff --git a/app/controllers/user_invite_requests_controller.rb b/app/controllers/user_invite_requests_controller.rb index daad6c7c58d..48bc411f4ee 100644 --- a/app/controllers/user_invite_requests_controller.rb +++ b/app/controllers/user_invite_requests_controller.rb @@ -55,10 +55,13 @@ def update params[:requests].each_pair do |id, quantity| unless quantity.blank? request = UserInviteRequest.find(id) + user = User.find(request.user_id) requested_total = request.quantity.to_i request.quantity = 0 request.save! - UserMailer.invite_request_declined(request.user_id, requested_total, request.reason).deliver_later + I18n.with_locale(user.preference.locale.iso) do + UserMailer.invite_request_declined(request.user_id, requested_total, request.reason).deliver_later + end end end flash[:notice] = 'All Requests were declined.' diff --git a/app/controllers/users/passwords_controller.rb b/app/controllers/users/passwords_controller.rb index a96166674d6..98eb170146a 100644 --- a/app/controllers/users/passwords_controller.rb +++ b/app/controllers/users/passwords_controller.rb @@ -14,14 +14,14 @@ def create end 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 + flash[:error] = t(".reset_blocked_html", contact_abuse_link: view_context.link_to(t(".contact_abuse"), new_abuse_report_path)) 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 + flash[:error] = t(".reset_cooldown_html", reset_available_time: available_time) redirect_to root_path and return end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 5f11db3c066..590392f2fe5 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -50,7 +50,7 @@ def configure_permitted_parameters devise_parameter_sanitizer.permit( :sign_up, keys: [ - :password_confirmation, :email, :age_over_13, :terms_of_service, :accepted_tos_version + :password_confirmation, :email, :age_over_13, :data_processing, :terms_of_service, :accepted_tos_version ] ) end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index b4429d20adf..b99c8581c55 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -1,7 +1,7 @@ class UsersController < ApplicationController cache_sweeper :pseud_sweeper - before_action :check_user_status, only: [:edit, :update] + before_action :check_user_status, only: [:edit, :update, :change_username, :changed_username] before_action :load_user, except: [:activate, :delete_confirmation, :index] before_action :check_ownership, except: [:activate, :delete_confirmation, :edit, :index, :show, :update] before_action :check_ownership_or_admin, only: [:edit, :update] @@ -39,6 +39,18 @@ def edit authorize @user.profile if logged_in_as_admin? end + def change_email + @page_subtitle = t(".browser_title") + end + + def change_password + @page_subtitle = t(".browser_title") + end + + def change_username + @page_subtitle = t(".browser_title") + end + def changed_password unless params[:password] && reauthenticate render(:change_password) && return @@ -162,7 +174,9 @@ def changed_email if @user.save flash.now[:notice] = ts("Your email has been successfully updated") - UserMailer.change_email(@user.id, old_email, new_email).deliver_later + I18n.with_locale(@user.preference.locale.iso) do + UserMailer.change_email(@user.id, old_email, new_email).deliver_later + end else # Make sure that on failure, the form still shows the old email as the "current" one. @user.email = old_email @@ -315,7 +329,7 @@ def destroy_author use_default = params[:use_default] == 'true' || params[:sole_author] == 'orphan_pseud' Creatorship.orphan(pseuds, works, use_default) - Collection.orphan(pseuds, @sole_owned_collections, use_default) + Collection.orphan(pseuds, @sole_owned_collections, default: use_default) elsif params[:sole_author] == 'delete' # Deletes works where user is sole author @sole_authored_works.each(&:destroy) diff --git a/app/controllers/works_controller.rb b/app/controllers/works_controller.rb index 60a733be368..d87f5fb0596 100755 --- a/app/controllers/works_controller.rb +++ b/app/controllers/works_controller.rb @@ -56,10 +56,8 @@ def index options = params[:work_search].present? ? clean_work_search_params : {} - if params[:fandom_id] || (@collection.present? && @tag.present?) - if params[:fandom_id].present? - @fandom = Fandom.find(params[:fandom_id]) - end + if params[:fandom_id].present? || (@collection.present? && @tag.present?) + @fandom = Fandom.find(params[:fandom_id]) if params[:fandom_id] tag = @fandom || @tag @@ -161,6 +159,8 @@ def drafts redirect_to logged_in? ? user_path(current_user) : new_user_session_path return end + + @page_subtitle = t(".page_title", username: @user.login) if params[:pseud_id] @pseud = @user.pseuds.find_by(name: params[:pseud_id]) diff --git a/app/controllers/wrangling_guidelines_controller.rb b/app/controllers/wrangling_guidelines_controller.rb index 230de7034eb..f015a185d68 100644 --- a/app/controllers/wrangling_guidelines_controller.rb +++ b/app/controllers/wrangling_guidelines_controller.rb @@ -3,7 +3,7 @@ class WranglingGuidelinesController < ApplicationController # GET /wrangling_guidelines def index - @wrangling_guidelines = WranglingGuideline.order('position ASC') + @wrangling_guidelines = WranglingGuideline.order("position ASC") end # GET /wrangling_guidelines/1 @@ -13,57 +13,64 @@ def show # GET /wrangling_guidelines/new def new + authorize :wrangling @wrangling_guideline = WranglingGuideline.new end # GET /wrangling_guidelines/1/edit def edit + authorize :wrangling @wrangling_guideline = WranglingGuideline.find(params[:id]) end # GET /wrangling_guidelines/manage def manage - @wrangling_guidelines = WranglingGuideline.order('position ASC') + authorize :wrangling + @wrangling_guidelines = WranglingGuideline.order("position ASC") end # POST /wrangling_guidelines def create + authorize :wrangling @wrangling_guideline = WranglingGuideline.new(wrangling_guideline_params) if @wrangling_guideline.save - flash[:notice] = ts('Wrangling Guideline was successfully created.') + flash[:notice] = t("wrangling_guidelines.create") redirect_to(@wrangling_guideline) else - render action: 'new' + render action: "new" end end # PUT /wrangling_guidelines/1 def update + authorize :wrangling @wrangling_guideline = WranglingGuideline.find(params[:id]) if @wrangling_guideline.update(wrangling_guideline_params) - flash[:notice] = ts('Wrangling Guideline was successfully updated.') + flash[:notice] = t("wrangling_guidelines.update") redirect_to(@wrangling_guideline) else - render action: 'edit' + render action: "edit" end end # reorder FAQs def update_positions + authorize :wrangling if params[:wrangling_guidelines] @wrangling_guidelines = WranglingGuideline.reorder_list(params[:wrangling_guidelines]) - flash[:notice] = ts('Wrangling Guidelines order was successfully updated.') + flash[:notice] = t("wrangling_guidelines.reorder") end redirect_to(wrangling_guidelines_path) end # DELETE /wrangling_guidelines/1 def destroy + authorize :wrangling @wrangling_guideline = WranglingGuideline.find(params[:id]) @wrangling_guideline.destroy - flash[:notice] = ts('Wrangling Guideline was successfully deleted.') + flash[:notice] = t("wrangling_guidelines.delete") redirect_to(wrangling_guidelines_path) end @@ -72,5 +79,4 @@ def destroy def wrangling_guideline_params params.require(:wrangling_guideline).permit(:title, :content) end - end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 01cfe944a73..2a2cf4b6f0f 100755 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -22,24 +22,23 @@ def classes_for_main show_sidebar = ((@user || @admin_posts || @collection || show_wrangling_dashboard) && !@hide_dashboard) class_names += " dashboard" if show_sidebar - if page_has_filters? - class_names += " filtered" - end - - if %w(abuse_reports feedbacks known_issues).include?(controller.controller_name) - class_names = "system support " + controller.controller_name + ' ' + controller.action_name - end - if controller.controller_name == "archive_faqs" - class_names = "system docs support faq " + controller.action_name - end - if controller.controller_name == "wrangling_guidelines" - class_names = "system docs guideline " + controller.action_name - end - if controller.controller_name == "home" - class_names = "system docs " + controller.action_name - end - if controller.controller_name == "errors" - class_names = "system " + controller.controller_name + " error-" + controller.action_name + class_names += " filtered" if page_has_filters? + + case controller.controller_name + when "abuse_reports", "feedbacks", "known_issues" + class_names = "system support #{controller.controller_name} #{controller.action_name}" + when "archive_faqs" + class_names = "system docs support faq #{controller.action_name}" + when "wrangling_guidelines" + class_names = "system docs guideline #{controller.action_name}" + when "home" + class_names = if %w(content privacy).include?(controller.action_name) + "system docs tos tos-#{controller.action_name}" + else + "system docs #{controller.action_name}" + end + when "errors" + class_names = "system #{controller.controller_name} error-#{controller.action_name}" end class_names @@ -631,4 +630,17 @@ def disallow_robots?(item) item.users.all? { |u| u&.preference&.minimize_search_engines? } end end -end # end of ApplicationHelper + + # Determines if the page (controller and action combination) does not need + # to show the ToS (Terms of Service) popup. + def tos_exempt_page? + case params[:controller] + when "home" + %w[index content dmca privacy tos tos_faq].include?(params[:action]) + when "abuse_reports", "feedbacks", "users/sessions" + %w[new create].include?(params[:action]) + when "archive_faqs" + %w[index show].include?(params[:action]) + end + end +end diff --git a/app/helpers/bookmarks_helper.rb b/app/helpers/bookmarks_helper.rb index a9c09e3b2f4..c16587a105b 100644 --- a/app/helpers/bookmarks_helper.rb +++ b/app/helpers/bookmarks_helper.rb @@ -24,11 +24,6 @@ def get_new_bookmark_path(bookmarkable) end end - # tag_bookmarks_path was behaving badly for tags with slashes - def link_to_tag_bookmarks(tag) - {controller: 'bookmarks', action: 'index', tag_id: tag} - end - def link_to_bookmarkable_bookmarks(bookmarkable, link_text='') if link_text.blank? link_text = number_with_delimiter(Bookmark.count_visible_bookmarks(bookmarkable, current_user)) diff --git a/app/helpers/collections_helper.rb b/app/helpers/collections_helper.rb index 59fcc53ef66..d1c8af19837 100644 --- a/app/helpers/collections_helper.rb +++ b/app/helpers/collections_helper.rb @@ -123,4 +123,16 @@ def collection_item_approval_options(actor:, item_type:) [t("#{key}.rejected"), :rejected] ] end + + # Fetches the icon URL for the given collection, using the standard (100x100) variant. + def standard_icon_url(collection) + return "/images/skins/iconsets/default/icon_collection.png" unless collection.icon.attached? + + rails_blob_url(collection.icon.variant(:standard)) + end + + # Wraps the collection's standard_icon_url in an image tag + def collection_icon_display(collection) + image_tag(standard_icon_url(collection), size: "100x100", alt: collection.icon_alt_text, class: "icon", skip_pipeline: true) + end end diff --git a/app/helpers/comments_helper.rb b/app/helpers/comments_helper.rb index fd87f3e7946..0285291d0bb 100644 --- a/app/helpers/comments_helper.rb +++ b/app/helpers/comments_helper.rb @@ -210,6 +210,10 @@ def can_review_comment?(comment) is_author_of?(comment.ultimate_parent) || policy(comment).can_review_comment? end + def can_review_all_comments?(commentable) + commentable.is_a?(AdminPost) || is_author_of?(commentable) + end + #### HELPERS FOR REPLYING TO COMMENTS ##### # return link to add new reply to a comment diff --git a/app/helpers/home_helper.rb b/app/helpers/home_helper.rb index 0fe46c4376d..55e91cdb8cc 100644 --- a/app/helpers/home_helper.rb +++ b/app/helpers/home_helper.rb @@ -9,4 +9,106 @@ def html_to_text(string) end return string end + + # A TOC section has an h4 header, p with intro link, and ol of subsections. + def tos_table_of_contents_section(action) + return unless %w[content privacy tos].include?(action) + + content = tos_section_header(action) + tos_section_intro_link(action) + tos_subsection_list(action) + # If we're on /tos, /content, or /privacy, use the details tag to make + # sections expandable and collapsable. + if controller.controller_name == "home" + # Use the open attribute to make the page's corresponding section expanded + # by default. + content_tag(:details, content, open: controller.action_name == action) + else + content + end + end + + private + + def tos_section_header(action) + # If we're on /tos, /content, or /privacy, the corresponding section header + # gets extra text indicating it is the current section. + text = if controller.controller_name == "home" && controller.action_name == action + t("home.tos_toc.#{action}.header_current") + else + t("home.tos_toc.#{action}.header") + end + heading = content_tag(:h4, text, class: "heading") + # If we're on /tos, /content, or /privacy, use a summary tag around the h4 + # so it serves as the toggle to expand or collapse its section. + if controller.controller_name == "home" + content_tag(:summary, heading) + else + heading + end + end + + def tos_section_intro_link(action) + content_tag(:p, link_to(t("home.tos_toc.#{action}.intro"), tos_anchor_url(action, action))) + end + + def tos_subsection_list(action) + items = case action + when "content" + content_policy_subsection_items + when "privacy" + privacy_policy_subsection_items + when "tos" + tos_subsection_items + end + content_tag(:ol, items.html_safe, style: "list-style-type: upper-alpha;") + end + + # When we are on the /signup page, the entire TOS is displayed. This lets us + # make sure that page only uses plain anchors in its TOC while the /tos, + # /content, nad /privacy pages (found in the home controller) sometimes + # point to other pages. + def tos_anchor_url(action, anchor) + if controller.controller_name == "home" + url_for(only_path: true, action: action, anchor: anchor) + else + "##{anchor}" + end + end + + def content_policy_subsection_items + content_tag(:li, link_to(t("home.tos_toc.content.offensive_content"), tos_anchor_url("content", "II.A"))) + + content_tag(:li, link_to(t("home.tos_toc.content.fanworks"), tos_anchor_url("content", "II.B"))) + + content_tag(:li, link_to(t("home.tos_toc.content.commercial_promotion"), tos_anchor_url("content", "II.C"))) + + content_tag(:li, link_to(t("home.tos_toc.content.copyright_infringement"), tos_anchor_url("content", "II.D"))) + + content_tag(:li, link_to(t("home.tos_toc.content.plagiarism"), tos_anchor_url("content", "II.E"))) + + content_tag(:li, link_to(t("home.tos_toc.content.personal_information_and_fannish_identities"), tos_anchor_url("content", "II.F"))) + + content_tag(:li, link_to(t("home.tos_toc.content.impersonation"), tos_anchor_url("content", "II.G"))) + + content_tag(:li, link_to(t("home.tos_toc.content.harassment"), tos_anchor_url("content", "II.H"))) + + content_tag(:li, link_to(t("home.tos_toc.content.user_icons"), tos_anchor_url("content", "II.I"))) + + content_tag(:li, link_to(t("home.tos_toc.content.mandatory_tags"), tos_anchor_url("content", "II.J"))) + + content_tag(:li, link_to(t("home.tos_toc.content.illegal_and_inappropriate_content"), tos_anchor_url("content", "II.K"))) + end + + def privacy_policy_subsection_items + content_tag(:li, link_to(t("home.tos_toc.privacy.applicability"), tos_anchor_url("privacy", "III.A"))) + + content_tag(:li, link_to(t("home.tos_toc.privacy.scope_of_personal_information_we_process"), tos_anchor_url("privacy", "III.B"))) + + content_tag(:li, link_to(t("home.tos_toc.privacy.types_of_personal_information_we_collect_and_process"), tos_anchor_url("privacy", "III.C"))) + + content_tag(:li, link_to(t("home.tos_toc.privacy.aggregate_and_anonymous_information"), tos_anchor_url("privacy", "III.D"))) + + content_tag(:li, link_to(t("home.tos_toc.privacy.your_rights_under_applicable_data_privacy_laws"), tos_anchor_url("privacy", "III.E"))) + + content_tag(:li, link_to(t("home.tos_toc.privacy.information_shared_with_third_parties"), tos_anchor_url("privacy", "III.F"))) + + content_tag(:li, link_to(t("home.tos_toc.privacy.termination_of_account"), tos_anchor_url("privacy", "III.G"))) + + content_tag(:li, link_to(t("home.tos_toc.privacy.retention_of_personal_information"), tos_anchor_url("privacy", "III.H"))) + + content_tag(:li, link_to(t("home.tos_toc.privacy.contact_us"), tos_anchor_url("privacy", "III.I"))) + end + + def tos_subsection_items + content_tag(:li, link_to(t("home.tos_toc.tos.general_terms"), tos_anchor_url("tos", "I.A"))) + + content_tag(:li, link_to(t("home.tos_toc.tos.updates_to_the_tos"), tos_anchor_url("tos", "I.B"))) + + content_tag(:li, link_to(t("home.tos_toc.tos.potential_problems"), tos_anchor_url("tos", "I.C"))) + + content_tag(:li, link_to(t("home.tos_toc.tos.content_you_access"), tos_anchor_url("tos", "I.D"))) + + content_tag(:li, link_to(t("home.tos_toc.tos.what_we_do_with_content"), tos_anchor_url("tos", "I.E"))) + + content_tag(:li, link_to(t("home.tos_toc.tos.what_you_cant_do"), tos_anchor_url("tos", "I.F"))) + + content_tag(:li, link_to(t("home.tos_toc.tos.registration_and_email_addresses"), tos_anchor_url("tos", "I.G"))) + + content_tag(:li, link_to(t("home.tos_toc.tos.age_policy"), tos_anchor_url("tos", "I.H"))) + + content_tag(:li, link_to(t("home.tos_toc.tos.abuse_policy"), tos_anchor_url("tos", "I.I"))) + end end diff --git a/app/helpers/skins_helper.rb b/app/helpers/skins_helper.rb index e0768992e7f..73bd2af9876 100644 --- a/app/helpers/skins_helper.rb +++ b/app/helpers/skins_helper.rb @@ -9,37 +9,38 @@ def skin_author_link(skin) # we only actually display an image if there's a file def skin_preview_display(skin) - if skin && skin.icon_file_name - link_to image_tag(skin.icon.url(:standard), alt: skin.icon_alt_text, class: "icon", skip_pipeline: true), skin.icon.url(:original) - end - end - - def skin_tag - skin = nil - - if params[:site_skin] - skin ||= Skin.approved_or_owned_by.usable.find_by(id: params[:site_skin]) - end + return unless skin&.icon&.attached? - if (logged_in? || logged_in_as_admin?) && session[:site_skin] - skin ||= Skin.approved_or_owned_by.usable.find_by(id: session[:site_skin]) - end - - skin_id = if skin.nil? - current_user&.preference&.skin_id || AdminSetting.default_skin_id - else - skin.id - end + link_to image_tag(rails_blob_url(skin.icon.variant(:standard)), + alt: skin.icon_alt_text, + class: "icon", + skip_pipeline: true), + rails_blob_url(skin.icon) + end - return "" if skin_id.nil? + # Fetches the current skin. This is determined by the following + # 1. Skin ID set by request parameter + # 2. Skin ID set in the current session (if someone, a user or admin, is logged in) + # 3. Current user's skin preference + # 4. The default skin (as set by the active AdminSetting) + def current_skin + skin = Skin.approved_or_owned_by.usable.find_by(id: params[:site_skin]) if params[:site_skin] + skin ||= Skin.approved_or_owned_by.usable.find_by(id: session[:site_skin]) if (logged_in? || logged_in_as_admin?) && session[:site_skin] + skin ||= current_user&.preference&.skin + skin || AdminSetting.default_skin + end + def skin_tag roles = if logged_in_as_admin? Skin::DEFAULT_ROLES_TO_INCLUDE + ["admin"] else Skin::DEFAULT_ROLES_TO_INCLUDE end - # We include the version information for both the skin_id and the + skin = current_skin + return "" unless skin + + # We include the version information for both the skin's id and the # AdminSetting.default_skin_id because the default skin is used in skins of # type "user", so we need to regenerate the cache block when it's modified. # @@ -47,12 +48,11 @@ def skin_tag # regenerate the cache block when an admin updates the current default # skin. Rails.cache.fetch( - [:v1, :site_skin, skin_id, logged_in_as_admin?], - version: [skin_cache_version(skin_id), + [:v1, :site_skin, skin.id, logged_in_as_admin?], + version: [skin_cache_version(skin.id), AdminSetting.default_skin_id, skin_cache_version(AdminSetting.default_skin_id)] ) do - skin ||= Skin.find(skin_id) skin.get_style(roles) end end diff --git a/app/helpers/tags_helper.rb b/app/helpers/tags_helper.rb index 2669e1699ef..56ac370dbb4 100644 --- a/app/helpers/tags_helper.rb +++ b/app/helpers/tags_helper.rb @@ -240,7 +240,9 @@ def show_hidden_tag_link_list_item(item, category, options = {}) def get_title_string(tags, category_name = "") if tags && tags.size > 0 - tags.collect(&:name).join(", ") + # We don't use .to_sentence because these aren't links and we risk making any + # connector word (e.g., "and") look like part of the final tag. + tags.pluck(:name).join(t("support.array.words_connector")) elsif tags.blank? && category_name.blank? "Choose Not To Use Archive Warnings" else diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 938044a272f..f51e9e0aa2e 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -21,21 +21,17 @@ def print_pseuds(user) end # Determine which icon to show on user pages - def standard_icon(user = nil, pseud = nil) - if pseud && pseud.icon - pseud.icon.url(:standard).gsub(/^http:/, "https:") - elsif user && user.default_pseud && user.default_pseud.icon - user.default_pseud.icon.url(:standard).gsub(/^http:/, "https:") - else - '/images/skins/iconsets/default/icon_user.png' - end + def standard_icon(pseud = nil) + return "/images/skins/iconsets/default/icon_user.png" unless pseud&.icon&.attached? + + rails_blob_url(pseud.icon.variant(:standard)) end # no alt text if there isn't specific alt text def icon_display(user = nil, pseud = nil) path = user ? (pseud ? user_pseud_path(pseud.user, pseud) : user_path(user)) : nil pseud ||= user.default_pseud if user - icon = standard_icon(user, pseud) + icon = standard_icon(pseud) alt_text = pseud.try(:icon_alt_text) || nil if path diff --git a/app/helpers/wrangling_helper.rb b/app/helpers/wrangling_helper.rb index 8ef592ed4b9..6b69f6beb88 100644 --- a/app/helpers/wrangling_helper.rb +++ b/app/helpers/wrangling_helper.rb @@ -3,7 +3,7 @@ def tag_counts_per_category counts = {} [Fandom, Character, Relationship, Freeform].each do |klass| counts[klass.to_s.downcase.pluralize.to_sym] = Rails.cache.fetch("/wrangler/counts/sidebar/#{klass}", race_condition_ttl: 10, expires_in: 1.hour) do - TagQuery.new(type: klass.to_s, in_use: true, unwrangleable: false, unwrangled: true).count + TagQuery.new(type: klass.to_s, in_use: true, unwrangleable: false, unwrangled: true, has_posted_works: true).count end end counts[:UnsortedTag] = Rails.cache.fetch("/wrangler/counts/sidebar/UnsortedTag", race_condition_ttl: 10, expires_in: 1.hour) do diff --git a/app/mailers/tos_update_mailer.rb b/app/mailers/tos_update_mailer.rb new file mode 100644 index 00000000000..2c694c9822b --- /dev/null +++ b/app/mailers/tos_update_mailer.rb @@ -0,0 +1,11 @@ +class TosUpdateMailer < ApplicationMailer + # Sent by notifications:send_tos_update + def tos_update_notification(user, admin_post_id) + @username = user.login + @admin_post = admin_post_id + mail( + to: user.email, + subject: "[#{ArchiveConfig.APP_SHORT_NAME}] Updates to #{ArchiveConfig.APP_SHORT_NAME}'s Terms of Service" + ) + end +end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index a7e4e3b23e3..1eb33ced20f 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -30,9 +30,9 @@ def invited_to_collection_notification(user_id, work_id, collection_id) @work = Work.find(work_id) @collection = Collection.find(collection_id) mail( - to: @user.email, - subject: "[#{ArchiveConfig.APP_SHORT_NAME}]#{'[' + @collection.title + ']'} Request to include work in a collection" - ) + to: @user.email, + subject: "[#{ArchiveConfig.APP_SHORT_NAME}]#{'[' + @collection.title + ']'} Request to include work in a collection" + ) end # We use an options hash here, instead of keyword arguments, to avoid @@ -72,7 +72,7 @@ def anonymous_or_unrevealed_notification(user_id, work_id, collection_id, option # TODO refactor to make it asynchronous def invitation(invitation_id) @invitation = Invitation.find(invitation_id) - @user_name = (@invitation.creator.is_a?(User) ? @invitation.creator.login : '') + @user_name = (@invitation.creator.is_a?(User) ? @invitation.creator.login : "") mail( to: @invitation.invitee_email, subject: t("user_mailer.invitation.subject", app_name: ArchiveConfig.APP_SHORT_NAME) @@ -92,22 +92,14 @@ def invitation_to_claim(invitation_id, archivist_login) end # Notifies a writer that their imported works have been claimed - def claim_notification(creator_id, claimed_work_ids, is_user=false) - if is_user - creator = User.find(creator_id) - locale = creator.preference.locale.iso - else - creator = ExternalAuthor.find(creator_id) - locale = I18n.default_locale - end + def claim_notification(creator_id, claimed_work_ids) + creator = User.find(creator_id) @external_email = creator.email @claimed_works = Work.where(id: claimed_work_ids) - I18n.with_locale(locale) do - mail( - to: creator.email, - subject: t("user_mailer.claim_notification.subject", app_name: ArchiveConfig.APP_SHORT_NAME) - ) - end + mail( + to: creator.email, + subject: default_i18n_subject(app_name: ArchiveConfig.APP_SHORT_NAME) + ) end # Sends a batched subscription notification @@ -116,6 +108,7 @@ def batch_subscription_notification(subscription_id, entries) # then the resque job does not error and we just silently fail. @subscription = Subscription.find_by(id: subscription_id) return if @subscription.nil? + creation_entries = JSON.parse(entries) @creations = [] # look up all the creations that have generated updates for this subscription @@ -123,13 +116,14 @@ def batch_subscription_notification(subscription_id, entries) creation_type, creation_id = creation_info.split("_") creation = creation_type.constantize.where(id: creation_id).first next unless creation && creation.try(:posted) - next if (creation.is_a?(Chapter) && !creation.work.try(:posted)) - next if creation.pseuds.any? {|p| p.user == User.orphan_account} # no notifications for orphan works + next if creation.is_a?(Chapter) && !creation.work.try(:posted) + next if creation.pseuds.any? { |p| p.user == User.orphan_account } # no notifications for orphan works + # TODO: allow subscriptions to orphan_account to receive notifications # If the subscription notification is for a user subscription, we don't # want to send updates about works that have recently become anonymous. - if @subscription.subscribable_type == 'User' + if @subscription.subscribable_type == "User" next if Subscription.anonymous_creation?(creation) end @@ -142,9 +136,7 @@ def batch_subscription_notification(subscription_id, entries) @creations.uniq! subject = @subscription.subject_text(@creations.first) - if @creations.count > 1 - subject += " and #{@creations.count - 1} more" - end + subject += " and #{@creations.count - 1} more" if @creations.count > 1 I18n.with_locale(@subscription.user.preference.locale.iso) do mail( to: @subscription.user.email, @@ -157,12 +149,10 @@ def batch_subscription_notification(subscription_id, entries) def invite_increase_notification(user_id, total) @user = User.find(user_id) @total = total - 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}" - ) - end + mail( + to: @user.email, + subject: default_i18n_subject(app_name: ArchiveConfig.APP_SHORT_NAME) + ) end # Emails a user to say that their request for invitation codes has been declined @@ -170,40 +160,36 @@ def invite_request_declined(user_id, total, reason) @user = User.find(user_id) @total = total @reason = reason - 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) - ) - end + mail( + to: @user.email, + subject: default_i18n_subject(app_name: ArchiveConfig.APP_SHORT_NAME) + ) end - # TODO: This may be sent to multiple users simultaneously. We need to ensure - # each user gets the email for their preferred locale. - def collection_notification(collection_id, subject, message) + def collection_notification(collection_id, subject, message, email) @message = message @collection = Collection.find(collection_id) mail( - to: @collection.get_maintainers_email, + to: email, subject: "[#{ArchiveConfig.APP_SHORT_NAME}][#{@collection.title}] #{subject}" ) end - def invalid_signup_notification(collection_id, invalid_signup_ids) + def invalid_signup_notification(collection_id, invalid_signup_ids, email) @collection = Collection.find(collection_id) @invalid_signups = invalid_signup_ids mail( - to: @collection.get_maintainers_email, + to: email, subject: "[#{ArchiveConfig.APP_SHORT_NAME}][#{@collection.title}] Invalid sign-ups found" ) end # This is sent at the end of matching, i.e., after assignments are generated. # It is also sent when assignments are regenerated. - def potential_match_generation_notification(collection_id) + def potential_match_generation_notification(collection_id, email) @collection = Collection.find(collection_id) mail( - to: @collection.get_maintainers_email, + to: email, subject: "[#{ArchiveConfig.APP_SHORT_NAME}][#{@collection.title}] Potential assignment generation complete" ) end @@ -211,14 +197,12 @@ def potential_match_generation_notification(collection_id) def challenge_assignment_notification(collection_id, assigned_user_id, assignment_id) @collection = Collection.find(collection_id) @assigned_user = User.find(assigned_user_id) - assignment = ChallengeAssignment.find(assignment_id) - @request = (assignment.request_signup || assignment.pinch_request_signup) - I18n.with_locale(@assigned_user.preference.locale.iso) do - mail( - to: @assigned_user.email, - subject: default_i18n_subject(app_name: ArchiveConfig.APP_SHORT_NAME, collection_title: @collection.title) - ) - end + @assignment = ChallengeAssignment.find(assignment_id) + @request = (@assignment.request_signup || @assignment.pinch_request_signup) + mail( + to: @assigned_user.email, + subject: default_i18n_subject(app_name: ArchiveConfig.APP_SHORT_NAME, collection_title: @collection.title) + ) end # Asks a user to validate and activate their new account @@ -227,7 +211,7 @@ def signup_notification(user_id) 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) + subject: t("user_mailer.signup_notification.subject", app_name: ArchiveConfig.APP_SHORT_NAME) ) end end @@ -237,12 +221,10 @@ 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(@user.preference.locale.iso) do - mail( - to: @old_email, - subject: t('user_mailer.change_email.subject', app_name: ArchiveConfig.APP_SHORT_NAME) - ) - end + mail( + to: @old_email, + subject: default_i18n_subject(app_name: ArchiveConfig.APP_SHORT_NAME) + ) end ### WORKS NOTIFICATIONS ### @@ -322,7 +304,7 @@ def recipient_notification(user_id, work_id, collection_id = nil) end # Emails a prompter to say that a response has been posted to their prompt - def prompter_notification(work_id, collection_id=nil) + def prompter_notification(work_id, collection_id = nil) @work = Work.find(work_id) @collection = Collection.find(collection_id) if collection_id @work.challenge_claims.each do |claim| @@ -351,7 +333,7 @@ def delete_work_notification(user, work) 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) + subject: t("user_mailer.delete_work_notification.subject", app_name: ArchiveConfig.APP_SHORT_NAME) ) end end @@ -371,7 +353,7 @@ def admin_deleted_work_notification(user, work) 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) + subject: t("user_mailer.admin_deleted_work_notification.subject", app_name: ArchiveConfig.APP_SHORT_NAME) ) end end @@ -394,8 +376,8 @@ def admin_spam_work_notification(creation_id, user_id) @work = Work.find_by(id: creation_id) mail( - to: @user.email, - subject: "[#{ArchiveConfig.APP_SHORT_NAME}] Your work was hidden as spam" + to: @user.email, + subject: "[#{ArchiveConfig.APP_SHORT_NAME}] Your work was hidden as spam" ) end @@ -405,6 +387,7 @@ def admin_spam_work_notification(creation_id, user_id) def feedback(feedback_id) feedback = Feedback.find(feedback_id) return unless feedback.email + @summary = feedback.summary @comment = feedback.comment @username = feedback.username if feedback.username.present? @@ -428,6 +411,4 @@ def abuse_report(abuse_report_id) ) end - protected - end diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb index 23824aaee0d..7583d66ad4a 100644 --- a/app/models/abuse_report.rb +++ b/app/models/abuse_report.rb @@ -124,8 +124,9 @@ def send_report end def attach_work_download(ticket_id) + is_not_comments = url[%r{/comments/}, 0].nil? work_id = url[%r{/works/(\d+)}, 1] - return unless work_id + return unless work_id && is_not_comments work = Work.find_by(id: work_id) ReportAttachmentJob.perform_later(ticket_id, work) if work diff --git a/app/models/challenge_assignment.rb b/app/models/challenge_assignment.rb index 93986f1eb15..714fecdf537 100755 --- a/app/models/challenge_assignment.rb +++ b/app/models/challenge_assignment.rb @@ -14,29 +14,29 @@ class ChallengeAssignment < ApplicationRecord validate :signups_match, on: :update def signups_match if self.sent_at.nil? && - self.request_signup.present? && - self.offer_signup.present? && - !self.request_signup.request_potential_matches.pluck(:offer_signup_id).include?(self.offer_signup_id) + self.request_signup.present? && + self.offer_signup.present? && + !self.request_signup.request_potential_matches.pluck(:offer_signup_id).include?(self.offer_signup_id) errors.add(:base, ts("does not match. Did you mean to write-in a giver?")) end end - scope :for_request_signup, lambda {|signup| where('request_signup_id = ?', signup.id)} + scope :for_request_signup, ->(signup) { where("request_signup_id = ?", signup.id) } - scope :for_offer_signup, lambda {|signup| where('offer_signup_id = ?', signup.id)} + scope :for_offer_signup, ->(signup) { where("offer_signup_id = ?", signup.id) } - scope :in_collection, lambda {|collection| where('challenge_assignments.collection_id = ?', collection.id) } + scope :in_collection, ->(collection) { where("challenge_assignments.collection_id = ?", collection.id) } - scope :defaulted, -> { where("defaulted_at IS NOT NULL") } + scope :defaulted, -> { where.not(defaulted_at: nil) } scope :undefaulted, -> { where("defaulted_at IS NULL") } scope :uncovered, -> { where("covered_at IS NULL") } - scope :covered, -> { where("covered_at IS NOT NULL") } - scope :sent, -> { where("sent_at IS NOT NULL") } + scope :covered, -> { where.not(covered_at: nil) } + scope :sent, -> { where.not(sent_at: nil) } - scope :with_pinch_hitter, -> { where("pinch_hitter_id IS NOT NULL") } + scope :with_pinch_hitter, -> { where.not(pinch_hitter_id: nil) } scope :with_offer, -> { where("offer_signup_id IS NOT NULL OR pinch_hitter_id IS NOT NULL") } - scope :with_request, -> { where("request_signup_id IS NOT NULL") } + scope :with_request, -> { where.not(request_signup_id: nil) } scope :with_no_request, -> { where("request_signup_id IS NULL") } scope :with_no_offer, -> { where("offer_signup_id IS NULL AND pinch_hitter_id IS NULL") } @@ -53,13 +53,12 @@ def signups_match scope :order_by_offering_pseud, -> { joins(OFFERING_PSEUD_JOIN).order("pseuds.name") } - # Get all of a user's assignments - scope :by_offering_user, lambda {|user| - select("DISTINCT challenge_assignments.*"). - joins(OFFERING_PSEUD_JOIN). - joins("INNER JOIN users ON pseuds.user_id = users.id"). - where('users.id = ?', user.id) + scope :by_offering_user, lambda { |user| + select("DISTINCT challenge_assignments.*") + .joins(OFFERING_PSEUD_JOIN) + .joins("INNER JOIN users ON pseuds.user_id = users.id") + .where("users.id = ?", user.id) } # sorting by fulfilled/posted status @@ -67,26 +66,25 @@ def signups_match collection_items.item_id = challenge_assignments.creation_id AND collection_items.item_type = challenge_assignments.creation_type)" - COLLECTION_ITEMS_LEFT_JOIN = "LEFT JOIN collection_items ON (collection_items.collection_id = challenge_assignments.collection_id AND + COLLECTION_ITEMS_LEFT_JOIN = "LEFT JOIN collection_items ON (collection_items.collection_id = challenge_assignments.collection_id AND collection_items.item_id = challenge_assignments.creation_id AND collection_items.item_type = challenge_assignments.creation_type)" WORKS_JOIN = "INNER JOIN works ON works.id = challenge_assignments.creation_id AND challenge_assignments.creation_type = 'Work'" WORKS_LEFT_JOIN = "LEFT JOIN works ON works.id = challenge_assignments.creation_id AND challenge_assignments.creation_type = 'Work'" - scope :fulfilled, -> { + scope :fulfilled, lambda { joins(COLLECTION_ITEMS_JOIN).joins(WORKS_JOIN) .where("challenge_assignments.creation_id IS NOT NULL AND collection_items.user_approval_status = ? AND collection_items.collection_approval_status = ? AND works.posted = 1", CollectionItem.user_approval_statuses[:approved], CollectionItem.collection_approval_statuses[:approved]) } - scope :posted, -> { joins(WORKS_JOIN).where("challenge_assignments.creation_id IS NOT NULL AND works.posted = 1") } # should be faster than unfulfilled scope because no giant left joins def self.unfulfilled_in_collection(collection) fulfilled_ids = ChallengeAssignment.in_collection(collection).fulfilled.pluck(:id) - fulfilled_ids.empty? ? in_collection(collection) : in_collection(collection).where("challenge_assignments.id NOT IN (?)", fulfilled_ids) + fulfilled_ids.empty? ? in_collection(collection) : in_collection(collection).where.not(challenge_assignments: { id: fulfilled_ids }) end # faster than unposted scope because no left join! @@ -106,7 +104,7 @@ def self.duplicate_recipients(collection) end # has to be a left join to get assignments that don't have a collection item - scope :unfulfilled, -> { + scope :unfulfilled, lambda { joins(COLLECTION_ITEMS_LEFT_JOIN).joins(WORKS_LEFT_JOIN) .where("challenge_assignments.creation_id IS NULL OR collection_items.user_approval_status != ? OR collection_items.collection_approval_status != ? OR works.posted = 0", CollectionItem.user_approval_statuses[:approved], CollectionItem.collection_approval_statuses[:approved]) @@ -117,7 +115,6 @@ def self.duplicate_recipients(collection) scope :unstarted, -> { where("challenge_assignments.creation_id IS NULL") } - before_destroy :clear_assignment def clear_assignment if offer_signup @@ -137,6 +134,7 @@ def request def get_collection_item return nil unless self.creation + CollectionItem.where("collection_id = ? AND item_id = ? AND item_type = ?", self.collection_id, self.creation_id, self.creation_type).first end @@ -153,17 +151,13 @@ def posted? end def defaulted=(value) - if value == "1" - self.defaulted_at = Time.now - else - self.defaulted_at = nil - end + self.defaulted_at = (Time.now if value == "1") end def defaulted !self.defaulted_at.nil? end - alias_method :defaulted?, :defaulted + alias defaulted? defaulted def offer_signup_pseud=(pseud_byline) if pseud_byline.blank? @@ -209,16 +203,27 @@ def offering_pseud end def requesting_pseud - request_signup ? request_signup.pseud : (pinch_request_signup ? pinch_request_signup.pseud : nil) + if request_signup + request_signup.pseud + else + (pinch_request_signup ? pinch_request_signup.pseud : nil) + end end - def offer_byline - offer_signup && offer_signup.pseud ? offer_signup.pseud.byline : (pinch_hitter ? (pinch_hitter.byline + "* (pinch hitter)") : "- none -") + if offer_signup && offer_signup.pseud + offer_signup.pseud.byline + else + (pinch_hitter ? I18n.t("challenge_assignment.offer_byline.pinch_hitter", pinch_hitter_byline: pinch_hitter.byline) : I18n.t("challenge_assignment.offer_byline.none")) + end end def request_byline - request_signup && request_signup.pseud ? request_signup.pseud.byline : (pinch_request_signup ? (pinch_request_byline + "* (pinch recipient)") : "- None -") + if request_signup && request_signup.pseud + request_signup.pseud.byline + else + (pinch_request_signup ? I18n.t("challenge_assignment.request_byline.pinch_recipient", pinch_request_byline: pinch_request_byline) : I18n.t("challenge_assignment.request_byline.none")) + end end def pinch_hitter_byline @@ -260,9 +265,17 @@ def send_out unless self.sent_at self.sent_at = Time.now save - assigned_to = self.offer_signup ? self.offer_signup.pseud.user : (self.pinch_hitter ? self.pinch_hitter.user : nil) + assigned_to = if self.offer_signup + self.offer_signup.pseud.user + else + (self.pinch_hitter ? self.pinch_hitter.user : nil) + end request = self.request_signup || self.pinch_request_signup - UserMailer.challenge_assignment_notification(collection.id, assigned_to.id, self.id).deliver_later if assigned_to && request + if assigned_to && request + I18n.with_locale(assigned_to.preference.locale.iso) do + UserMailer.challenge_assignment_notification(collection.id, assigned_to.id, self.id).deliver_later + end + end end end @@ -289,9 +302,7 @@ def self.delayed_send_out(collection_id) collection.assignments.each do |assignment| assignment.send_out end - subject = I18n.t("user_mailer.collection_notification.assignments_sent.subject") - message = I18n.t("user_mailer.collection_notification.assignments_sent.complete") - collection.notify_maintainers(subject, message) + collection.notify_maintainers_assignments_sent # purge the potential matches! we don't want bazillions of them in our db PotentialMatch.clear!(collection) @@ -332,13 +343,14 @@ def self.delayed_generate(collection_id) @offer_match_buckets = {} @max_match_count = 0 if settings.nil? || settings.no_match_required? - # stuff everyone into the same bucket - @max_match_count = 1 - @request_match_buckets[1] = collection.signups - @offer_match_buckets[1] = collection.signups + # stuff everyone into the same bucket + @max_match_count = 1 + @request_match_buckets[1] = collection.signups + @offer_match_buckets[1] = collection.signups else collection.signups.find_each do |signup| next if signup.nil? + request_match_count = signup.request_potential_matches.count @request_match_buckets[request_match_count] ||= [] @request_match_buckets[request_match_count] << signup @@ -357,24 +369,37 @@ def self.delayed_generate(collection_id) # matches.) 0.upto(@max_match_count) do |count| if @request_match_buckets[count] - @request_match_buckets[count].sort_by {rand}.each do |request_signup| + @request_match_buckets[count].sort_by { rand } + .each do |request_signup| # go through the potential matches in order from best to worst and try and assign request_signup.reload next if request_signup.assigned_as_request + ChallengeAssignment.assign_request!(collection, request_signup) end end - if @offer_match_buckets[count] - @offer_match_buckets[count].sort_by {rand}.each do |offer_signup| - offer_signup.reload - next if offer_signup.assigned_as_offer - ChallengeAssignment.assign_offer!(collection, offer_signup) - end + next unless @offer_match_buckets[count] + + @offer_match_buckets[count].sort_by { rand } + .each do |offer_signup| + offer_signup.reload + next if offer_signup.assigned_as_offer + + ChallengeAssignment.assign_offer!(collection, offer_signup) end end REDIS_GENERAL.del(progress_key(collection)) - UserMailer.potential_match_generation_notification(collection.id).deliver_later + + if collection.collection_email.present? + UserMailer.potential_match_generation_notification(collection.id, collection.collection_email).deliver_later + else + collection.maintainers_list.each do |user| + I18n.with_locale(user.preference.locale.iso) do + UserMailer.potential_match_generation_notification(collection.id, user.email).deliver_later + end + end + end end # go through the request's potential matches in order from best to worst and try and assign @@ -388,7 +413,7 @@ def self.assign_request!(collection, request_signup) # if there's a circular match let's save it as our last choice if potential_match.offer_signup.assigned_as_request && !last_choice && - collection.assignments.for_request_signup(potential_match.offer_signup).first.offer_signup == request_signup + collection.assignments.for_request_signup(potential_match.offer_signup).first.offer_signup == request_signup last_choice = potential_match next end @@ -398,9 +423,7 @@ def self.assign_request!(collection, request_signup) break end - if !assigned && last_choice - ChallengeAssignment.do_assign_request!(assignment, last_choice) - end + ChallengeAssignment.do_assign_request!(assignment, last_choice) if !assigned && last_choice request_signup.assigned_as_request = true request_signup.save! @@ -420,7 +443,7 @@ def self.assign_offer!(collection, offer_signup) # if there's a circular match let's save it as our last choice if potential_match.request_signup.assigned_as_offer && !last_choice && - collection.assignments.for_offer_signup(potential_match.request_signup).first.request_signup == offer_signup + collection.assignments.for_offer_signup(potential_match.request_signup).first.request_signup == offer_signup last_choice = potential_match next end @@ -430,9 +453,7 @@ def self.assign_offer!(collection, offer_signup) break end - if !assigned && last_choice - ChallengeAssignment.do_assign_offer!(assignment, last_choice) - end + ChallengeAssignment.do_assign_offer!(assignment, last_choice) if !assigned && last_choice offer_signup.assigned_as_offer = true offer_signup.save! diff --git a/app/models/collection.rb b/app/models/collection.rb index d1d71b2466f..84c92a4fd99 100755 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -2,16 +2,16 @@ class Collection < ApplicationRecord include Filterable include WorksOwner - has_attached_file :icon, - styles: { standard: "100x100>" }, - url: "/system/:class/:attachment/:id/:style/:basename.:extension", - 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", - default_url: "/images/skins/iconsets/default/icon_collection.png" + has_one_attached :icon do |attachable| + attachable.variant(:standard, resize_to_limit: [100, 100]) + end - validates_attachment_content_type :icon, content_type: /image\/\S+/, allow_nil: true - validates_attachment_size :icon, less_than: 500.kilobytes, allow_nil: true + # i18n-tasks-use t("errors.attributes.icon.invalid_format") + # i18n-tasks-use t("errors.attributes.icon.too_large") + validates :icon, attachment: { + allowed_formats: %r{image/\S+}, + maximum_size: ArchiveConfig.ICON_SIZE_KB_MAX.kilobytes + } belongs_to :parent, class_name: "Collection", inverse_of: :children has_many :children, class_name: "Collection", foreign_key: "parent_id", inverse_of: :parent @@ -22,13 +22,14 @@ class Collection < ApplicationRecord has_one :collection_preference, dependent: :destroy accepts_nested_attributes_for :collection_preference + before_validation :clear_icon + before_validation :cleanup_url before_create :ensure_associated def ensure_associated - self.collection_preference = CollectionPreference.new unless self.collection_preference - self.collection_profile = CollectionProfile.new unless self.collection_profile + self.collection_preference = CollectionPreference.new unless self.collection_preference + self.collection_profile = CollectionProfile.new unless self.collection_profile end - belongs_to :challenge, dependent: :destroy, polymorphic: true has_many :prompts, dependent: :destroy @@ -52,10 +53,10 @@ def clean_up_challenge accepts_nested_attributes_for :collection_items, allow_destroy: true has_many :approved_collection_items, -> { approved_by_both }, class_name: "CollectionItem" - has_many :works, through: :collection_items, source: :item, source_type: 'Work' + has_many :works, through: :collection_items, source: :item, source_type: "Work" has_many :approved_works, -> { posted }, through: :approved_collection_items, source: :item, source_type: "Work" - has_many :bookmarks, through: :collection_items, source: :item, source_type: 'Bookmark' + has_many :bookmarks, through: :collection_items, source: :item, source_type: "Bookmark" has_many :approved_bookmarks, through: :approved_collection_items, source: :item, source_type: "Bookmark" has_many :collection_participants, dependent: :destroy @@ -63,41 +64,34 @@ def clean_up_challenge has_many :participants, through: :collection_participants, source: :pseud has_many :users, through: :participants, source: :user - has_many :invited, -> { where('collection_participants.participant_role = ?', CollectionParticipant::INVITED) }, through: :collection_participants, source: :pseud - has_many :owners, -> { where('collection_participants.participant_role = ?', CollectionParticipant::OWNER) }, through: :collection_participants, source: :pseud - has_many :moderators, -> { where('collection_participants.participant_role = ?', CollectionParticipant::MODERATOR) }, through: :collection_participants, source: :pseud - has_many :members, -> { where('collection_participants.participant_role = ?', CollectionParticipant::MEMBER) }, through: :collection_participants, source: :pseud - has_many :posting_participants, -> { where('collection_participants.participant_role in (?)', [CollectionParticipant::MEMBER,CollectionParticipant::MODERATOR, CollectionParticipant::OWNER ] ) }, through: :collection_participants, source: :pseud - - + has_many :invited, -> { where(collection_participants: { participant_role: CollectionParticipant::INVITED }) }, through: :collection_participants, source: :pseud + has_many :owners, -> { where(collection_participants: { participant_role: CollectionParticipant::OWNER }) }, through: :collection_participants, source: :pseud + has_many :moderators, -> { where(collection_participants: { participant_role: CollectionParticipant::MODERATOR }) }, through: :collection_participants, source: :pseud + has_many :members, -> { where(collection_participants: { participant_role: CollectionParticipant::MEMBER }) }, through: :collection_participants, source: :pseud + has_many :posting_participants, -> { where(collection_participants: { participant_role: [CollectionParticipant::MEMBER, CollectionParticipant::MODERATOR, CollectionParticipant::OWNER] }) }, through: :collection_participants, source: :pseud CHALLENGE_TYPE_OPTIONS = [ - ["", ""], - [ts("Gift Exchange"), "GiftExchange"], - [ts("Prompt Meme"), "PromptMeme"], - ] - - before_validation :clear_icon + ["", ""], + [ts("Gift Exchange"), "GiftExchange"], + [ts("Prompt Meme"), "PromptMeme"] + ].freeze validate :must_have_owners def must_have_owners # we have to use collection participants because the association may not exist until after # the collection is saved - errors.add(:base, ts("Collection has no valid owners.")) if (self.collection_participants + (self.parent ? self.parent.collection_participants : [])).select {|p| p.is_owner?}.empty? + errors.add(:base, ts("Collection has no valid owners.")) if (self.collection_participants + (self.parent ? self.parent.collection_participants : [])).select(&:is_owner?) + .empty? end validate :collection_depth def collection_depth - if (self.parent && self.parent.parent) || (self.parent && !self.children.empty?) || (!self.children.empty? && !self.children.collect(&:children).flatten.empty?) - errors.add(:base, ts("Sorry, but %{name} is a subcollection, so it can't also be a parent collection.", name: parent.name)) - end + errors.add(:base, ts("Sorry, but %{name} is a subcollection, so it can't also be a parent collection.", name: parent.name)) if self.parent&.parent || (self.parent && !self.children.empty?) || (!self.children.empty? && !self.children.collect(&:children).flatten.empty?) end validate :parent_exists def parent_exists - unless parent_name.blank? || Collection.find_by(name: parent_name) - errors.add(:base, ts("We couldn't find a collection with name %{name}.", name: parent_name)) - end + errors.add(:base, ts("We couldn't find a collection with name %{name}.", name: parent_name)) unless parent_name.blank? || Collection.find_by(name: parent_name) end validate :parent_is_allowed @@ -111,111 +105,112 @@ def parent_is_allowed end end - validates_presence_of :name, message: ts("Please enter a name for your collection.") + validates :name, presence: { message: ts("Please enter a name for your collection.") } validates :name, uniqueness: { message: ts("Sorry, that name is already taken. Try again, please!") } - validates_length_of :name, - minimum: ArchiveConfig.TITLE_MIN, - too_short: ts("must be at least %{min} characters long.", min: ArchiveConfig.TITLE_MIN) - validates_length_of :name, - maximum: ArchiveConfig.TITLE_MAX, - too_long: ts("must be less than %{max} characters long.", max: ArchiveConfig.TITLE_MAX) - validates_format_of :name, - message: ts('must begin and end with a letter or number; it may also contain underscores. It may not contain any other characters, including spaces.'), - with: /\A[A-Za-z0-9]\w*[A-Za-z0-9]\Z/ - validates_length_of :icon_alt_text, allow_blank: true, maximum: ArchiveConfig.ICON_ALT_MAX, - too_long: ts("must be less than %{max} characters long.", max: ArchiveConfig.ICON_ALT_MAX) - validates_length_of :icon_comment_text, allow_blank: true, maximum: ArchiveConfig.ICON_COMMENT_MAX, - too_long: ts("must be less than %{max} characters long.", max: ArchiveConfig.ICON_COMMENT_MAX) + validates :name, + length: { minimum: ArchiveConfig.TITLE_MIN, + too_short: ts("must be at least %{min} characters long.", min: ArchiveConfig.TITLE_MIN) } + validates :name, + length: { maximum: ArchiveConfig.TITLE_MAX, + too_long: ts("must be less than %{max} characters long.", max: ArchiveConfig.TITLE_MAX) } + validates :name, + format: { message: ts("must begin and end with a letter or number; it may also contain underscores. It may not contain any other characters, including spaces."), + with: /\A[A-Za-z0-9]\w*[A-Za-z0-9]\Z/ } + validates :icon_alt_text, length: { allow_blank: true, maximum: ArchiveConfig.ICON_ALT_MAX, + too_long: ts("must be less than %{max} characters long.", max: ArchiveConfig.ICON_ALT_MAX) } + validates :icon_comment_text, length: { allow_blank: true, maximum: ArchiveConfig.ICON_COMMENT_MAX, + too_long: ts("must be less than %{max} characters long.", max: ArchiveConfig.ICON_COMMENT_MAX) } validates :email, email_format: { allow_blank: true } - validates_presence_of :title, message: ts("Please enter a title to be displayed for your collection.") - validates_length_of :title, - minimum: ArchiveConfig.TITLE_MIN, - too_short: ts("must be at least %{min} characters long.", min: ArchiveConfig.TITLE_MIN) - validates_length_of :title, - maximum: ArchiveConfig.TITLE_MAX, - too_long: ts("must be less than %{max} characters long.", max: ArchiveConfig.TITLE_MAX) + validates :title, presence: { message: ts("Please enter a title to be displayed for your collection.") } + validates :title, + length: { minimum: ArchiveConfig.TITLE_MIN, + too_short: ts("must be at least %{min} characters long.", min: ArchiveConfig.TITLE_MIN) } + validates :title, + length: { maximum: ArchiveConfig.TITLE_MAX, + too_long: ts("must be less than %{max} characters long.", max: ArchiveConfig.TITLE_MAX) } validate :no_reserved_strings def no_reserved_strings errors.add(:title, ts("^Sorry, the ',' character cannot be in a collection Display Title.")) if - title.match(/\,/) + title.match(/,/) end # return title.html_safe to overcome escaping done by sanitiser def title - read_attribute(:title).try(:html_safe) + self[:title].try(:html_safe) end - validates_length_of :description, - allow_blank: true, - maximum: ArchiveConfig.SUMMARY_MAX, - too_long: ts("must be less than %{max} characters long.", max: ArchiveConfig.SUMMARY_MAX) + validates :description, + length: { allow_blank: true, + maximum: ArchiveConfig.SUMMARY_MAX, + too_long: ts("must be less than %{max} characters long.", max: ArchiveConfig.SUMMARY_MAX) } - validates_format_of :header_image_url, allow_blank: true, with: URI::regexp(%w(http https)), message: ts("is not a valid URL.") - validates_format_of :header_image_url, allow_blank: true, with: /\.(png|gif|jpg)$/, message: ts("can only point to a gif, jpg, or png file."), multiline: true + validates :header_image_url, format: { allow_blank: true, with: URI::DEFAULT_PARSER.make_regexp(%w[http https]), message: ts("is not a valid URL.") } + validates :header_image_url, format: { allow_blank: true, with: /\A\S+\.(png|gif|jpg)\z/, message: ts("can only point to a gif, jpg, or png file.") } validates :tags_after_saving, length: { maximum: ArchiveConfig.COLLECTION_TAGS_MAX, message: "^Sorry, a collection can only have %{count} tags." } scope :top_level, -> { where(parent_id: nil) } - scope :closed, -> { joins(:collection_preference).where("collection_preferences.closed = ?", true) } - scope :not_closed, -> { joins(:collection_preference).where("collection_preferences.closed = ?", false) } - scope :moderated, -> { joins(:collection_preference).where("collection_preferences.moderated = ?", true) } - scope :unmoderated, -> { joins(:collection_preference).where("collection_preferences.moderated = ?", false) } - scope :unrevealed, -> { joins(:collection_preference).where("collection_preferences.unrevealed = ?", true) } - scope :anonymous, -> { joins(:collection_preference).where("collection_preferences.anonymous = ?", true) } + scope :closed, -> { joins(:collection_preference).where(collection_preferences: { closed: true }) } + scope :not_closed, -> { joins(:collection_preference).where(collection_preferences: { closed: false }) } + scope :moderated, -> { joins(:collection_preference).where(collection_preferences: { moderated: true }) } + scope :unmoderated, -> { joins(:collection_preference).where(collection_preferences: { moderated: false }) } + scope :unrevealed, -> { joins(:collection_preference).where(collection_preferences: { unrevealed: true }) } + scope :anonymous, -> { joins(:collection_preference).where(collection_preferences: { anonymous: true }) } scope :no_challenge, -> { where(challenge_type: nil) } - scope :gift_exchange, -> { where(challenge_type: 'GiftExchange') } - scope :prompt_meme, -> { where(challenge_type: 'PromptMeme') } + scope :gift_exchange, -> { where(challenge_type: "GiftExchange") } + scope :prompt_meme, -> { where(challenge_type: "PromptMeme") } scope :name_only, -> { select("collections.name") } scope :by_title, -> { order(:title) } + scope :for_blurb, -> { includes(:parent, :moderators, :children, :collection_preference, owners: [:user]).with_attached_icon } - before_validation :cleanup_url def cleanup_url self.header_image_url = Addressable::URI.heuristic_parse(self.header_image_url) if self.header_image_url end # Get only collections with running challenges def self.signup_open(challenge_type) - if challenge_type == "PromptMeme" - not_closed.where(challenge_type: challenge_type). - joins("INNER JOIN prompt_memes on prompt_memes.id = challenge_id").where("prompt_memes.signup_open = 1"). - where("prompt_memes.signups_close_at > ?", Time.now).order("prompt_memes.signups_close_at DESC") - elsif challenge_type == "GiftExchange" - not_closed.where(challenge_type: challenge_type). - joins("INNER JOIN gift_exchanges on gift_exchanges.id = challenge_id").where("gift_exchanges.signup_open = 1"). - where("gift_exchanges.signups_close_at > ?", Time.now).order("gift_exchanges.signups_close_at DESC") + case challenge_type + when "PromptMeme" + not_closed.where(challenge_type: challenge_type) + .joins("INNER JOIN prompt_memes on prompt_memes.id = challenge_id").where("prompt_memes.signup_open = 1") + .where("prompt_memes.signups_close_at > ?", Time.zone.now).order("prompt_memes.signups_close_at DESC") + when "GiftExchange" + not_closed.where(challenge_type: challenge_type) + .joins("INNER JOIN gift_exchanges on gift_exchanges.id = challenge_id").where("gift_exchanges.signup_open = 1") + .where("gift_exchanges.signups_close_at > ?", Time.zone.now).order("gift_exchanges.signups_close_at DESC") end end - scope :with_name_like, lambda {|name| - where("collections.name LIKE ?", '%' + name + '%'). - limit(10) + scope :with_name_like, lambda { |name| + where("collections.name LIKE ?", "%#{name}%") + .limit(10) } - scope :with_title_like, lambda {|title| - where("collections.title LIKE ?", '%' + title + '%') + scope :with_title_like, lambda { |title| + where("collections.title LIKE ?", "%#{title}%") } - scope :with_item_count, -> { - select("collections.*, count(distinct collection_items.id) as item_count"). - joins("left join collections child_collections on child_collections.parent_id = collections.id + scope :with_item_count, lambda { + select("collections.*, count(distinct collection_items.id) as item_count") + .joins("left join collections child_collections on child_collections.parent_id = collections.id left join collection_items on ( (collection_items.collection_id = child_collections.id OR collection_items.collection_id = collections.id) AND collection_items.user_approval_status = 1 - AND collection_items.collection_approval_status = 1)"). - group("collections.id") + AND collection_items.collection_approval_status = 1)") + .group("collections.id") } def to_param - name_was + name_was end # Change membership of collection(s) from a particular pseud to the orphan account - def self.orphan(pseuds, collections, default=true) - for pseud in pseuds - for collection in collections + def self.orphan(pseuds, collections, default: true) + pseuds.each do |pseud| + collections.each do |collection| if pseud && collection && collection.owners.include?(pseud) orphan_pseud = default ? User.orphan_account.default_pseud : User.orphan_account.pseuds.find_or_create_by(name: pseud.name) pseud.change_membership(collection, orphan_pseud) @@ -237,8 +232,8 @@ def autocomplete_search_string_before_last_save end def autocomplete_prefixes - [ "autocomplete_collection_all", - "autocomplete_collection_#{closed? ? 'closed' : 'open'}" ] + ["autocomplete_collection_all", + "autocomplete_collection_#{closed? ? 'closed' : 'open'}"] end def autocomplete_score @@ -246,7 +241,6 @@ def autocomplete_score end ## END AUTOCOMPLETE - def parent_name=(name) @parent_name = name self.parent = Collection.find_by(name: name) @@ -285,31 +279,32 @@ def maintainers end def user_is_owner?(user) - user && user != :false && !(user.pseuds & self.all_owners).empty? + user && user != false && !(user.pseuds & self.all_owners).empty? end def user_is_moderator?(user) - user && user != :false && !(user.pseuds & self.all_moderators).empty? + user && user != false && !(user.pseuds & self.all_moderators).empty? end def user_is_maintainer?(user) - user && user != :false && !(user.pseuds & (self.all_moderators + self.all_owners)).empty? + user && user != false && !(user.pseuds & (self.all_moderators + self.all_owners)).empty? end def user_is_participant?(user) - user && user != :false && !get_participating_pseuds_for_user(user).empty? + user && user != false && !get_participating_pseuds_for_user(user).empty? end def user_is_posting_participant?(user) - user && user != :false && !(user.pseuds & self.all_posting_participants).empty? + user && user != false && !(user.pseuds & self.all_posting_participants).empty? end def get_participating_pseuds_for_user(user) - (user && user != :false) ? user.pseuds & self.all_participants : [] + (user && user != false) ? user.pseuds & self.all_participants : [] end def get_participants_for_user(user) return [] unless user + CollectionParticipant.in_collection(self).for_user(user) end @@ -321,28 +316,65 @@ def gift_notification self.collection_profile.gift_notification || (parent ? parent.collection_profile.gift_notification : "") end - def moderated? ; self.collection_preference.moderated ; end - def closed? ; self.collection_preference.closed ; end - def unrevealed? ; self.collection_preference.unrevealed ; end - def anonymous? ; self.collection_preference.anonymous ; end - def challenge? ; !self.challenge.nil? ; end + def moderated?() = self.collection_preference.moderated + + def closed?() = self.collection_preference.closed + + def unrevealed?() = self.collection_preference.unrevealed + + def anonymous?() = self.collection_preference.anonymous + + def challenge?() = !self.challenge.nil? def gift_exchange? - return self.challenge_type == "GiftExchange" + self.challenge_type == "GiftExchange" end + def prompt_meme? - return self.challenge_type == "PromptMeme" + self.challenge_type == "PromptMeme" end - def get_maintainers_email - return self.email if !self.email.blank? - return parent.email if parent && !parent.email.blank? - "#{self.maintainers.collect(&:user).flatten.uniq.collect(&:email).join(',')}" + def maintainers_list + self.maintainers.collect(&:user).flatten.uniq end - def notify_maintainers(subject, message) - # send maintainers a notice via email - UserMailer.collection_notification(self.id, subject, message).deliver_later + def collection_email + return self.email if self.email.present? + return parent.email if parent && parent.email.present? + end + + def notify_maintainers_assignments_sent + subject = I18n.t("user_mailer.collection_notification.assignments_sent.subject") + message = I18n.t("user_mailer.collection_notification.assignments_sent.complete") + if self.collection_email.present? + UserMailer.collection_notification(self.id, subject, message, self.collection_email).deliver_later + else + # if collection email is not set and collection parent email is not set, loop through maintainers and send each a notice via email + self.maintainers_list.each do |user| + I18n.with_locale(user.preference.locale.iso) do + translated_subject = I18n.t("user_mailer.collection_notification.assignments_sent.subject") + translated_message = I18n.t("user_mailer.collection_notification.assignments_sent.complete") + UserMailer.collection_notification(self.id, translated_subject, translated_message, user.email).deliver_later + end + end + end + end + + def notify_maintainers_challenge_default(challenge_assignment, assignments_page_url) + if self.collection_email.present? + subject = I18n.t("user_mailer.collection_notification.challenge_default.subject", offer_byline: challenge_assignment.offer_byline) + message = I18n.t("user_mailer.collection_notification.challenge_default.complete", offer_byline: challenge_assignment.offer_byline, request_byline: challenge_assignment.request_byline, assignments_page_url: assignments_page_url) + UserMailer.collection_notification(self.id, subject, message, self.collection_email).deliver_later + else + # if collection email is not set and collection parent email is not set, loop through maintainers and send each a notice via email + self.maintainers_list.each do |user| + I18n.with_locale(user.preference.locale.iso) do + translated_subject = I18n.t("user_mailer.collection_notification.challenge_default.subject", offer_byline: challenge_assignment.offer_byline) + translated_message = I18n.t("user_mailer.collection_notification.challenge_default.complete", offer_byline: challenge_assignment.offer_byline, request_byline: challenge_assignment.request_byline, assignments_page_url: assignments_page_url) + UserMailer.collection_notification(self.id, translated_subject, translated_message, user.email).deliver_later + end + end + end end include AsyncWithResque @@ -366,45 +398,49 @@ def reveal_collection_item_authors end def send_reveal_notifications - approved_collection_items.each {|collection_item| collection_item.notify_of_reveal} + approved_collection_items.each(&:notify_of_reveal) end def self.sorted_and_filtered(sort, filters, page) - pagination_args = {page: page} + pagination_args = { page: page } # build up the query with scopes based on the options the user specifies query = Collection.top_level - if !filters[:title].blank? + if filters[:title].present? # we get the matching collections out of autocomplete and use their ids ids = Collection.autocomplete_lookup(search_param: filters[:title], - autocomplete_prefix: (filters[:closed].blank? ? "autocomplete_collection_all" : (filters[:closed] ? "autocomplete_collection_closed" : "autocomplete_collection_open")) - ).map {|result| Collection.id_from_autocomplete(result)} - query = query.where("collections.id in (?)", ids) - else - query = (filters[:closed] == "true" ? query.closed : query.not_closed) if !filters[:closed].blank? + autocomplete_prefix: (if filters[:closed].blank? + "autocomplete_collection_all" + else + (filters[:closed] ? "autocomplete_collection_closed" : "autocomplete_collection_open") + end)).map { |result| Collection.id_from_autocomplete(result) } + query = query.where(collections: { id: ids }) + elsif filters[:closed].present? + query = (filters[:closed] == "true" ? query.closed : query.not_closed) end - query = (filters[:moderated] == "true" ? query.moderated : query.unmoderated) if !filters[:moderated].blank? + query = (filters[:moderated] == "true" ? query.moderated : query.unmoderated) if filters[:moderated].present? if filters[:challenge_type].present? - if filters[:challenge_type] == "gift_exchange" + case filters[:challenge_type] + when "gift_exchange" query = query.gift_exchange - elsif filters[:challenge_type] == "prompt_meme" + when "prompt_meme" query = query.prompt_meme - elsif filters[:challenge_type] == "no_challenge" + when "no_challenge" query = query.no_challenge end end - query = query.order(sort) + query = query.order(sort).for_blurb - if !filters[:fandom].blank? + if filters[:fandom].blank? + query.paginate(pagination_args) + else fandom = Fandom.find_by_name(filters[:fandom]) if fandom (fandom.approved_collections & query).paginate(pagination_args) else [] end - else - query.paginate(pagination_args) end end @@ -416,9 +452,13 @@ def delete_icon=(value) def delete_icon !!@delete_icon end - alias_method :delete_icon?, :delete_icon + alias delete_icon? delete_icon def clear_icon - self.icon = nil if delete_icon? && !icon.dirty? + return unless delete_icon? + + self.icon.purge + self.icon_alt_text = nil + self.icon_comment_text = nil end end diff --git a/app/models/comment.rb b/app/models/comment.rb index 40d30a3aa8d..4e56da9feef 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -85,7 +85,7 @@ def check_for_spam includes( pseud: { user: [:roles, :block_of_current_user, :block_by_current_user, :preference] }, parent: { work: [:pseuds, :users] } - ) + ).merge(Pseud.with_attached_icon) } # Gets methods and associations from acts_as_commentable plugin diff --git a/app/models/concerns/justifiable.rb b/app/models/concerns/justifiable.rb index dea0c09d80c..c4c4df3e8c5 100644 --- a/app/models/concerns/justifiable.rb +++ b/app/models/concerns/justifiable.rb @@ -5,9 +5,12 @@ module Justifiable attr_accessor :ticket_number attr_reader :ticket_url + before_validation :strip_octothorpe validates :ticket_number, presence: true, - numericality: { only_integer: true }, + # i18n-tasks-use t("activerecord.errors.messages.numeric_with_optional_hash") + numericality: { only_integer: true, + message: :numeric_with_optional_hash }, if: :enabled? validate :ticket_number_exists_in_tracker, if: :enabled? @@ -15,6 +18,12 @@ module Justifiable private + def strip_octothorpe + return if ticket_number.is_a?(Integer) + + self.ticket_number = self.ticket_number.delete_prefix("#") unless self.ticket_number.nil? + end + def enabled? # Only require a ticket if the record has been changed by an admin. User.current_user.is_a?(Admin) && changed? diff --git a/app/models/external_author.rb b/app/models/external_author.rb index 9e10c901c92..e70005a00a9 100644 --- a/app/models/external_author.rb +++ b/app/models/external_author.rb @@ -131,7 +131,9 @@ def block_import def notify_user_of_claim(claimed_work_ids) # send announcement to user of the stories they have been given - UserMailer.claim_notification(self.id, claimed_work_ids).deliver_later + I18n.with_locale(self.user.preference.locale.iso) do + UserMailer.claim_notification(self.user_id, claimed_work_ids).deliver_later + end end def find_or_invite(archivist = nil) diff --git a/app/models/feedback.rb b/app/models/feedback.rb index 66916140c9f..f13582d1d5d 100644 --- a/app/models/feedback.rb +++ b/app/models/feedback.rb @@ -1,8 +1,8 @@ # Class which holds feedback sent to the archive administrators about the archive as a whole class Feedback < ApplicationRecord - attr_accessor :ip_address + attr_accessor :ip_address, :referer, :site_skin - # note -- this has NOTHING to do with the Comment class! + # NOTE: this has NOTHING to do with the Comment class! # This is just the name of the text field in the Feedback # class which holds the user's comments. validates_presence_of :comment @@ -60,7 +60,8 @@ def rollout_string end def send_report - return unless %w(staging production).include?(Rails.env) + return unless zoho_enabled? + reporter = SupportReporter.new( title: summary, description: comment, @@ -69,8 +70,17 @@ def send_report username: username, user_agent: user_agent, site_revision: ArchiveConfig.REVISION.to_s, - rollout: rollout + rollout: rollout, + ip_address: ip_address, + referer: referer, + site_skin: site_skin ) reporter.send_report! end + + private + + def zoho_enabled? + %w[staging production].include?(Rails.env) + end end diff --git a/app/models/feedback_reporters/abuse_reporter.rb b/app/models/feedback_reporters/abuse_reporter.rb index 63196890b06..5a03b7e1bbe 100644 --- a/app/models/feedback_reporters/abuse_reporter.rb +++ b/app/models/feedback_reporters/abuse_reporter.rb @@ -1,6 +1,4 @@ class AbuseReporter < FeedbackReporter - attr_accessor :ip_address - def report_attributes super.deep_merge( "departmentId" => department_id, diff --git a/app/models/feedback_reporters/feedback_reporter.rb b/app/models/feedback_reporters/feedback_reporter.rb index 39a56c92072..c0cce2e20e1 100644 --- a/app/models/feedback_reporters/feedback_reporter.rb +++ b/app/models/feedback_reporters/feedback_reporter.rb @@ -13,7 +13,8 @@ class FeedbackReporter :language, :category, :username, - :url + :url, + :ip_address def initialize(attrs = {}) attrs.each_pair do |key, val| diff --git a/app/models/feedback_reporters/support_reporter.rb b/app/models/feedback_reporters/support_reporter.rb index 572b2170c60..3c50eacf163 100644 --- a/app/models/feedback_reporters/support_reporter.rb +++ b/app/models/feedback_reporters/support_reporter.rb @@ -1,5 +1,5 @@ class SupportReporter < FeedbackReporter - attr_accessor :user_agent, :site_revision, :rollout + attr_accessor :user_agent, :referer, :rollout, :site_revision, :site_skin def report_attributes super.deep_merge( @@ -13,10 +13,17 @@ def report_attributes private def custom_zoho_fields + # To avoid issues where Zoho ticket creation silently fails, only grab the first + # 255 characters of the referer URL. That may miss some complex search queries, + # but still keep enough to be useful most of the time. + truncated_referer = referer.present? ? referer[0..254] : "Unknown URL" { "cf_archive_version" => site_revision.presence || "Unknown site revision", "cf_rollout" => rollout.presence || "Unknown", - "cf_user_agent" => user_agent.presence || "Unknown user agent" + "cf_user_agent" => user_agent.presence || "Unknown user agent", + "cf_ip" => ip_address.presence || "Unknown IP", + "cf_url" => truncated_referer, + "cf_site_skin" => site_skin&.public ? site_skin.title : "Custom skin" } end diff --git a/app/models/invitation.rb b/app/models/invitation.rb index cc7bb464a88..d8437ab8c7e 100644 --- a/app/models/invitation.rb +++ b/app/models/invitation.rb @@ -31,7 +31,9 @@ def self.grant_all(total) total.times do user.invitations.create end - UserMailer.invite_increase_notification(user.id, total).deliver_later + I18n.with_locale(user.preference.locale.iso) do + UserMailer.invite_increase_notification(user.id, total).deliver_later + end end User.out_of_invites.update_all('out_of_invites = 0') end @@ -43,7 +45,9 @@ def self.grant_empty(total) total.times do user.invitations.create end - UserMailer.invite_increase_notification(user.id, total).deliver_later + I18n.with_locale(user.preference.locale.iso) do + UserMailer.invite_increase_notification(user.id, total).deliver_later + end end User.out_of_invites.update_all('out_of_invites = 0') end diff --git a/app/models/potential_match.rb b/app/models/potential_match.rb index 251e2da1503..0927aeb660c 100644 --- a/app/models/potential_match.rb +++ b/app/models/potential_match.rb @@ -1,5 +1,4 @@ class PotentialMatch < ApplicationRecord - # We use "-1" to represent all the requested items matching ALL = -1 @@ -12,26 +11,23 @@ class PotentialMatch < ApplicationRecord belongs_to :offer_signup, class_name: "ChallengeSignup" belongs_to :request_signup, class_name: "ChallengeSignup" -protected - + def self.progress_key(collection) - CACHE_PROGRESS_KEY + "#{collection.id}" + CACHE_PROGRESS_KEY + collection.id.to_s end def self.signup_key(collection) - CACHE_SIGNUP_KEY + "#{collection.id}" + CACHE_SIGNUP_KEY + collection.id.to_s end def self.interrupt_key(collection) - CACHE_INTERRUPT_KEY + "#{collection.id}" + CACHE_INTERRUPT_KEY + collection.id.to_s end def self.invalid_signup_key(collection) - CACHE_INVALID_SIGNUP_KEY + "#{collection.id}" + CACHE_INVALID_SIGNUP_KEY + collection.id.to_s end -public - def self.clear!(collection) # rapidly delete all potential prompt matches and potential matches # WITHOUT CALLBACKS @@ -89,10 +85,20 @@ def self.generate_in_background(collection_id) # check for invalid signups PotentialMatch.clear_invalid_signups(collection) - invalid_signup_ids = collection.signups.select {|s| !s.valid?}.collect(&:id) - unless invalid_signup_ids.empty? - invalid_signup_ids.each {|sid| REDIS_GENERAL.sadd invalid_signup_key(collection), sid} - UserMailer.invalid_signup_notification(collection.id, invalid_signup_ids).deliver_later + invalid_signup_ids = collection.signups.reject(&:valid?) + .collect(&:id) + if invalid_signup_ids.present? + invalid_signup_ids.each { |sid| REDIS_GENERAL.sadd invalid_signup_key(collection), sid } + + if collection.collection_email.present? + UserMailer.invalid_signup_notification(collection.id, invalid_signup_ids, collection.collection_email).deliver_later + else + collection.maintainers_list.each do |user| + I18n.with_locale(user.preference.locale.iso) do + UserMailer.invalid_signup_notification(collection.id, invalid_signup_ids, user.email).deliver_later + end + end + end PotentialMatch.cancel_generation(collection) else @@ -133,7 +139,7 @@ def self.generate_for_signup(collection, signup, settings, collection_tag_sets, potential_match = other_signup.match(signup, settings) end - potential_match.save if potential_match && potential_match.valid? + potential_match.save if potential_match&.valid? end end @@ -150,7 +156,7 @@ def self.matching_signup_ids(collection, signup, collection_tag_sets, required_t signup_tagsets = signup.send(prompt_type.pluralize).pluck(:tag_set_id, :optional_tag_set_id).flatten.compact # get the ids of all the tags of the required types in the signup's tagsets - signup_tags = SetTagging.where(tag_set_id: signup_tagsets).joins(:tag).where("tags.type IN (?)", required_types).pluck(:tag_id) + signup_tags = SetTagging.where(tag_set_id: signup_tagsets).joins(:tag).where(tags: { type: required_types }).pluck(:tag_id) if signup_tags.empty? # a match is required by the settings but the user hasn't put any of the required tags in, meaning they are open to anything @@ -162,9 +168,9 @@ def self.matching_signup_ids(collection, signup, collection_tag_sets, required_t # and now we look up any signups that have one of those tagsets in the opposite position -- ie, # if this signup is a request, we are looking for offers with the same tag; if it's an offer, we're # looking for requests with the same tag. - matching_signup_ids = (prompt_type == "request" ? Offer : Request). - where("tag_set_id IN (?) OR optional_tag_set_id IN (?)", match_tagsets, match_tagsets). - pluck(:challenge_signup_id).compact + matching_signup_ids = (prompt_type == "request" ? Offer : Request) + .where("tag_set_id IN (?) OR optional_tag_set_id IN (?)", match_tagsets, match_tagsets) + .pluck(:challenge_signup_id).compact # now add on "any" matches for the required types condition = case required_types.first.underscore @@ -204,14 +210,14 @@ def self.regenerate_for_signup_in_background(signup_id) # Get all the data settings = collection.challenge.potential_match_settings collection_tag_sets = Prompt.where(collection_id: collection.id).pluck(:tag_set_id, :optional_tag_set_id).flatten.compact - required_types = settings.required_types.map {|t| t.classify} + required_types = settings.required_types.map(&:classify) # clear the existing potential matches for this signup in each direction signup.offer_potential_matches.destroy_all signup.request_potential_matches.destroy_all # We check the signup in both directions -- as a request signup and as an offer signup - %w(request offer).each do |prompt_type| + %w[request offer].each do |prompt_type| PotentialMatch.generate_for_signup(collection, signup, settings, collection_tag_sets, required_types, prompt_type) end end @@ -254,21 +260,25 @@ def <=>(other) # start with seeing how many offers/requests match cmp = compare_all(self.num_prompts_matched, other.num_prompts_matched) - return cmp unless cmp == 0 + return cmp unless cmp.zero? # compare the "quality" of the best prompt match # (i.e. the number of matching tags between the most closely-matching # request prompt/offer prompt pair) cmp = compare_all(max_tags_matched, other.max_tags_matched) - return cmp unless cmp == 0 + return cmp unless cmp.zero? # if we're a match down to here just match on id - return self.id <=> other.id + self.id <=> other.id end -protected + protected + def compare_all(self_value, other_value) - self_value == ALL ? (other_value == ALL ? 0 : 1) : (other_value == ALL ? -1 : self_value <=> other_value) + if self_value == ALL + other_value == ALL ? 0 : 1 + else + (other_value == ALL ? -1 : self_value <=> other_value) + end end - end diff --git a/app/models/pseud.rb b/app/models/pseud.rb index ac298bc5fee..6dca5d7641b 100644 --- a/app/models/pseud.rb +++ b/app/models/pseud.rb @@ -3,24 +3,16 @@ class Pseud < ApplicationRecord include WorksOwner include Justifiable - has_attached_file :icon, - styles: { standard: "100x100>" }, - path: if Rails.env.production? - ":attachment/:id/:style.:extension" - elsif Rails.env.staging? - ":rails_env/:attachment/:id/:style.:extension" - else - ":rails_root/public/system/:rails_env/:class/:attachment/:id_partition/:style/:filename" - end, - storage: %w(staging production).include?(Rails.env) ? :s3 : :filesystem, - s3_protocol: "https", - default_url: "/images/skins/iconsets/default/icon_user.png" - - validates_attachment_content_type :icon, - content_type: %w[image/gif image/jpeg image/png], - allow_nil: true + has_one_attached :icon do |attachable| + attachable.variant(:standard, resize_to_limit: [100, 100]) + end - validates_attachment_size :icon, less_than: 500.kilobytes, allow_nil: true + # i18n-tasks-use t("errors.attributes.icon.invalid_format") + # i18n-tasks-use t("errors.attributes.icon.too_large") + validates :icon, attachment: { + allowed_formats: %w[image/gif image/jpeg image/png], + maximum_size: ArchiveConfig.ICON_SIZE_KB_MAX.kilobytes + } NAME_LENGTH_MIN = 1 NAME_LENGTH_MAX = 40 @@ -88,6 +80,7 @@ class Pseud < ApplicationRecord scope :alphabetical, -> { order(:name) } scope :default_alphabetical, -> { order(is_default: :desc).alphabetical } scope :abbreviated_list, -> { default_alphabetical.limit(ArchiveConfig.ITEMS_PER_PAGE) } + scope :for_search, -> { includes(:user).with_attached_icon } def self.not_orphaned where("user_id != ?", User.orphan_account) @@ -419,9 +412,10 @@ def delete_icon def clear_icon return unless delete_icon? - - self.icon = nil unless icon.dirty? + + self.icon.purge self.icon_alt_text = nil + self.icon_comment_text = nil end ################################# diff --git a/app/models/skin.rb b/app/models/skin.rb index 610f307f269..6b100ab9123 100755 --- a/app/models/skin.rb +++ b/app/models/skin.rb @@ -48,13 +48,16 @@ class Skin < ApplicationRecord accepts_nested_attributes_for :skin_parents, allow_destroy: true, reject_if: proc { |attrs| attrs[:position].blank? || (attrs[:parent_skin_title].blank? && attrs[:parent_skin_id].blank?) } - has_attached_file :icon, - styles: { standard: "100x100>" }, - url: "/system/:class/:attachment/:id/:style/:basename.:extension", - 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", - default_url: "/images/skins/iconsets/default/icon_skins.png" + has_one_attached :icon do |attachable| + attachable.variant(:standard, resize_to_limit: [100, 100]) + end + + # i18n-tasks-use t("errors.attributes.icon.invalid_format") + # i18n-tasks-use t("errors.attributes.icon.too_large") + validates :icon, attachment: { + allowed_formats: %r{image/\S+}, + maximum_size: ArchiveConfig.ICON_SIZE_KB_MAX.kilobytes + } after_save :skin_invalidate_cache def skin_invalidate_cache @@ -70,8 +73,6 @@ def skin_invalidate_cache end end - validates_attachment_content_type :icon, content_type: /image\/\S+/, allow_nil: true - validates_attachment_size :icon, less_than: 500.kilobytes, allow_nil: true validates_length_of :icon_alt_text, allow_blank: true, maximum: ArchiveConfig.ICON_ALT_MAX, too_long: ts("must be less than %{max} characters long.", max: ArchiveConfig.ICON_ALT_MAX) @@ -102,7 +103,8 @@ def valid_media validate :valid_public_preview def valid_public_preview - return true if (self.official? || !self.public? || self.icon_file_name) + return true if self.official? || !self.public? || self.icon.attached? + errors.add(:base, ts("You need to upload a screencap if you want to share your skin.")) end @@ -476,7 +478,7 @@ def self.load_site_css skin.ie_condition = skin_ie skin.unusable = true skin.official = true - File.open(version_dir + 'preview.png', 'rb') {|preview_file| skin.icon = preview_file} + skin.icon.attach(io: File.open("#{version_dir}preview.png", "rb"), content_type: "image/png", filename: "preview.png") skin.save! skins << skin end @@ -490,7 +492,7 @@ def self.load_site_css top_skin = Skin.new(title: "Archive #{version}", css: "", description: "Version #{version} of the default Archive style.", public: true, role: "site", media: ["screen"]) end - File.open(version_dir + 'preview.png', 'rb') {|preview_file| top_skin.icon = preview_file} + top_skin.icon.attach(io: File.open("#{version_dir}preview.png", "rb"), content_type: "image/png", filename: "preview.png") top_skin.official = true top_skin.save! skins.each_with_index do |skin, index| @@ -579,8 +581,6 @@ def set_thumbnail_from_current_version self.class.site_skins_dir + "preview.png" end - File.open(icon_path) do |icon_file| - self.icon = icon_file - end + self.icon.attach(io: File.open(icon_path), content_type: "image/png", filename: "preview.png") end end diff --git a/app/models/story_parser.rb b/app/models/story_parser.rb index 96477074800..2c2e9e69d9e 100644 --- a/app/models/story_parser.rb +++ b/app/models/story_parser.rb @@ -799,6 +799,8 @@ def download_with_timeout(location, limit = 10) # we do a little cleanup here in case the user hasn't included the 'http://' # or if they've used capital letters or an underscore in the hostname uri = UrlFormatter.new(location).standardized + raise Error, I18n.t("story_parser.on_archive") if ArchiveConfig.PERMITTED_HOSTS.include?(uri.host) + response = Net::HTTP.get_response(uri) case response when Net::HTTPSuccess diff --git a/app/models/tag.rb b/app/models/tag.rb index ebbf35987a9..b8b694a5438 100644 --- a/app/models/tag.rb +++ b/app/models/tag.rb @@ -227,6 +227,21 @@ def flush_work_cache end end + # queue_flush_work_cache will update the cached work (bookmarkable) info for + # bookmarks, but we still need to expire the portion of bookmark blurbs that + # contains the bookmarker's tags. + after_update :queue_flush_bookmark_cache + def queue_flush_bookmark_cache + async_after_commit(:flush_bookmark_cache) if saved_change_to_name? + end + + def flush_bookmark_cache + self.bookmarks.each do |bookmark| + ActionController::Base.new.expire_fragment("bookmark-owner-blurb-#{bookmark.cache_key}-v2") + ActionController::Base.new.expire_fragment("bookmark-blurb-#{bookmark.cache_key}-v2") + end + end + before_save :set_last_wrangler def set_last_wrangler unless User.current_user.nil? @@ -1050,14 +1065,15 @@ def unwrangled? ################################# def unwrangled_query(tag_type, options = {}) - self_type = %w(Character Fandom Media).include?(self.type) ? self.type.downcase : "fandom" + self_type = %w[Character Fandom Media].include?(self.type) ? self.type.downcase : "fandom" TagQuery.new(options.merge( - type: tag_type, - unwrangleable: false, - wrangled: false, - "pre_#{self_type}_ids": [self.id], - per_page: Tag.per_page - )) + type: tag_type, + unwrangleable: false, + wrangled: false, + has_posted_works: true, + "pre_#{self_type}_ids": [self.id], + per_page: Tag.per_page + )) end def unwrangled_tags(tag_type, options = {}) @@ -1188,6 +1204,16 @@ def update_tag_nominations parent_names << "" if parent_names.present? TagNomination.where(tagname: name, parent_tagname: parent_names).update_all(parented: true) + + return unless saved_change_to_name? && name_before_last_save.present? + + # Act as if the tag with the previous name was deleted and mirror clear_tag_nominations + TagNomination.where(tagname: name_before_last_save).update_all( + canonical: false, + exists: false, + parented: false, + synonym: nil + ) end before_destroy :clear_tag_nominations diff --git a/app/models/user.rb b/app/models/user.rb index d2144d14905..47c6c6b3d04 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -210,20 +210,12 @@ def unread_inbox_comments_count validates :email, email_format: true, uniqueness: true - # Virtual attribute for age check and terms of service - attr_accessor :age_over_13 - attr_accessor :terms_of_service - # attr_accessible :age_over_13, :terms_of_service - - 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."), - if: :first_save? - - validates_acceptance_of :age_over_13, - allow_nil: false, - message: ts("^Sorry, you have to be over 13!"), - if: :first_save? + # Virtual attribute for age check, data processing agreement, and terms of service + attr_accessor :age_over_13, :data_processing, :terms_of_service + + validates :data_processing, acceptance: { allow_nil: false, if: :first_save? } + validates :age_over_13, acceptance: { allow_nil: false, if: :first_save? } + validates :terms_of_service, acceptance: { allow_nil: false, if: :first_save? } def to_param login diff --git a/app/models/user_invite_request.rb b/app/models/user_invite_request.rb index 549fbb58955..0095bb24770 100644 --- a/app/models/user_invite_request.rb +++ b/app/models/user_invite_request.rb @@ -26,7 +26,9 @@ def grant_request self.quantity.times do self.user.invitations.create end - UserMailer.invite_increase_notification(self.user.id, self.quantity).deliver_after_commit + I18n.with_locale(self.user.preference.locale.iso) do + UserMailer.invite_increase_notification(self.user.id, self.quantity).deliver_after_commit + end end end end diff --git a/app/models/work_skin.rb b/app/models/work_skin.rb index cb9dc0b3f4c..ff4e56b3a00 100644 --- a/app/models/work_skin.rb +++ b/app/models/work_skin.rb @@ -30,7 +30,11 @@ def self.basic_formatting def self.import_basic_formatting css = File.read(File.join(Rails.public_path, "/stylesheets/work_skins/basic_formatting.css")) skin = WorkSkin.find_or_create_by(title: "Basic Formatting", css: css, role: "user", public: true, official: true) - File.open(File.join(Rails.public_path, '/images/skins/previews/basic_formatting.png'), 'rb') {|preview_file| skin.icon = preview_file} + skin.icon.attach( + io: File.open(File.join(Rails.public_path, "/images/skins/previews/basic_formatting.png"), "rb"), + filename: "basic_formatting.png", + content_type: "image/png" + ) skin.official = true skin.save! skin diff --git a/app/policies/archive_faq_policy.rb b/app/policies/archive_faq_policy.rb new file mode 100644 index 00000000000..87c8a7e9e3d --- /dev/null +++ b/app/policies/archive_faq_policy.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +class ArchiveFaqPolicy < ApplicationPolicy + TRANSLATION_ACCESS_ROLES = %w[superadmin docs support translation].freeze + # a subset of TRANSLATION_ACCESS_ROLES + FULL_ACCESS_ROLES = %w[superadmin docs support].freeze + + def translation_access? + user_has_roles?(TRANSLATION_ACCESS_ROLES) + end + + def full_access? + user_has_roles?(FULL_ACCESS_ROLES) + end + + alias edit? translation_access? + alias update? translation_access? + alias new? full_access? + alias create? full_access? + alias manage? full_access? + alias update_positions? full_access? + alias confirm_delete? full_access? + alias destroy? full_access? +end diff --git a/app/policies/inbox_comment_policy.rb b/app/policies/inbox_comment_policy.rb new file mode 100644 index 00000000000..8ec7d755b38 --- /dev/null +++ b/app/policies/inbox_comment_policy.rb @@ -0,0 +1,7 @@ +class InboxCommentPolicy < ApplicationPolicy + VIEW_INBOX_ROLES = %w[superadmin policy_and_abuse].freeze + + def show? + user_has_roles?(VIEW_INBOX_ROLES) + end +end diff --git a/app/policies/known_issue_policy.rb b/app/policies/known_issue_policy.rb new file mode 100644 index 00000000000..a74de07e65b --- /dev/null +++ b/app/policies/known_issue_policy.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +class KnownIssuePolicy < ApplicationPolicy + MANAGE_ROLES = %w[superadmin support].freeze + + def admin_index? + user_has_roles?(MANAGE_ROLES) + end + + alias destroy? admin_index? + alias edit? admin_index? + alias create? admin_index? + alias new? admin_index? + alias show? admin_index? + alias update? admin_index? +end diff --git a/app/policies/language_policy.rb b/app/policies/language_policy.rb index a7e73ba94e0..37810ec1863 100644 --- a/app/policies/language_policy.rb +++ b/app/policies/language_policy.rb @@ -1,11 +1,35 @@ class LanguagePolicy < ApplicationPolicy - MANAGE_LANGUAGES = %w[superadmin translation].freeze + LANGUAGE_EDIT_ACCESS = %w[superadmin translation support policy_and_abuse].freeze + LANGUAGE_CREATE_ACCESS = %w[superadmin translation].freeze def new? - user_has_roles?(MANAGE_LANGUAGES) + user_has_roles?(LANGUAGE_CREATE_ACCESS) + end + + def edit? + user_has_roles?(LANGUAGE_EDIT_ACCESS) + end + + # Define which roles can update which attributes + ALLOWED_ATTRIBUTES_BY_ROLES = { + "superadmin" => %i[name short support_available abuse_support_available sortable_name], + "translation" => %i[name short support_available abuse_support_available sortable_name], + "support" => %i[name short support_available sortable_name], + "policy_and_abuse" => %i[abuse_support_available] + }.freeze + + def permitted_attributes + ALLOWED_ATTRIBUTES_BY_ROLES.values_at(*user.roles).compact.flatten + end + + def can_edit_abuse_fields? + user_has_roles?(%w[superadmin translation policy_and_abuse]) + end + + def can_edit_non_abuse_fields? + user_has_roles?(%w[superadmin translation support]) end alias create? new? - alias edit? new? - alias update? new? + alias update? edit? end diff --git a/app/policies/wrangling_policy.rb b/app/policies/wrangling_policy.rb index 40acf51c97a..ba17ced729c 100644 --- a/app/policies/wrangling_policy.rb +++ b/app/policies/wrangling_policy.rb @@ -2,13 +2,24 @@ class WranglingPolicy < ApplicationPolicy FULL_ACCESS_ROLES = %w[superadmin tag_wrangling].freeze + READ_ACCESS_ROLES = (FULL_ACCESS_ROLES + %w[policy_and_abuse]).freeze def full_access? user_has_roles?(FULL_ACCESS_ROLES) end + def read_access? + user_has_roles?(READ_ACCESS_ROLES) + end + alias create? full_access? alias destroy? full_access? + alias mass_update? full_access? alias show? full_access? alias report_csv? full_access? + alias new? full_access? + alias edit? full_access? + alias manage? full_access? + alias update? full_access? + alias update_positions? full_access? end diff --git a/app/validators/attachment_validator.rb b/app/validators/attachment_validator.rb new file mode 100644 index 00000000000..79fa02601d5 --- /dev/null +++ b/app/validators/attachment_validator.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# Custom validator to ensure that a field using ActiveStorage +# * matches the given formats, specified with regex or by a list (leave empty to allow any) +# * is less than the given maximum (if none is given, the default is 500kb) +class AttachmentValidator < ActiveModel::EachValidator + def validate_each(record, attribute, value) + return unless value&.attached? + + allowed_formats = options[:allowed_formats] + maximum_size = options[:maximum_size] || 500.kilobytes + + case allowed_formats + when Regexp + record.errors.add(attribute, :invalid_format) unless allowed_formats.match?(value.content_type) + when Array + record.errors.add(attribute, :invalid_format) unless allowed_formats.include?(value.content_type) + end + + record.errors.add(attribute, :too_large, maximum_size: maximum_size.to_fs(:human_size)) unless value.blob.byte_size < maximum_size + + value.purge if record.errors[attribute].any? + end +end diff --git a/app/views/admin/_admin_nav.html.erb b/app/views/admin/_admin_nav.html.erb index 8ee535467a1..e705ea49bb8 100644 --- a/app/views/admin/_admin_nav.html.erb +++ b/app/views/admin/_admin_nav.html.erb @@ -1,15 +1,16 @@ -

<%= ts("Admin Navigation") %>

+

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

diff --git a/app/views/admin/_header.html.erb b/app/views/admin/_header.html.erb index 419db932728..9c7ae29136f 100644 --- a/app/views/admin/_header.html.erb +++ b/app/views/admin/_header.html.erb @@ -27,15 +27,21 @@ <% if policy(AdminPost).can_post? %>
  • <%= link_to t(".nav.posts.post_news"), new_admin_post_path %>
  • <% end %> -
  • <%= link_to t(".nav.posts.faqs"), archive_faqs_path %>
  • -
  • <%= link_to t(".nav.posts.known_issues"), known_issues_path %>
  • -
  • <%= link_to t(".nav.posts.wrangling_guidelines"), wrangling_guidelines_path %>
  • + <% if policy(ArchiveFaq).translation_access? %> +
  • <%= link_to t(".nav.posts.faqs"), archive_faqs_path %>
  • + <% end %> + <% if policy(KnownIssue).admin_index? %> +
  • <%= link_to t(".nav.posts.known_issues"), known_issues_path %>
  • + <% end %> + <% if policy(:wrangling).new? %> +
  • <%= link_to t(".nav.posts.wrangling_guidelines"), wrangling_guidelines_path %>
  • + <% end %> <% if policy(AdminBlacklistedEmail).index? %>
  • <%= link_to t(".nav.banned_emails"), admin_blacklisted_emails_path %>
  • <% end %> - + <% if policy(ModeratedWork).index? %>
  • <%= link_to t(".nav.spam"), admin_spam_index_path %>
  • <% end %> @@ -58,8 +64,9 @@ <% end %> -
  • <%= link_to t(".nav.wrangling"), tag_wranglings_path %>
  • - + <% if policy(:wrangling).full_access? %> +
  • <%= link_to t(".nav.wrangling"), tag_wranglings_path %>
  • + <% end %> <% if policy(Locale).index? %>
  • <%= link_to t(".nav.locales"), locales_path %>
  • <% end %> diff --git a/app/views/archive_faqs/_admin_index.html.erb b/app/views/archive_faqs/_admin_index.html.erb index cdb3395e593..d9cdc0bc9d1 100644 --- a/app/views/archive_faqs/_admin_index.html.erb +++ b/app/views/archive_faqs/_admin_index.html.erb @@ -1,31 +1,33 @@
    -

    <%= ts("Archive FAQ") %>

    +

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

    -<%= render 'archive_faqs/filters' %> -<%= render 'admin/admin_nav' %> +<%= render "archive_faqs/filters" %> +<%= render "admin/admin_nav" %> -

    <%= ts("Manage Archive FAQs") %>

    +

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

    <% @archive_faqs.each do |archive_faq| %>
    <%= link_to archive_faq.title, archive_faq %>
    -

    <%= ts("Created at %{date_created} and updated at %{date_updated}", :date_created => archive_faq.created_at, :date_updated => archive_faq.updated_at) %>

    +

    <%= t(".created_updated_date", date_created: l(archive_faq.created_at), date_updated: l(archive_faq.updated_at)) %>

    diff --git a/app/views/archive_faqs/index.html.erb b/app/views/archive_faqs/index.html.erb index 13f9c7afd9b..d59fc41fc74 100644 --- a/app/views/archive_faqs/index.html.erb +++ b/app/views/archive_faqs/index.html.erb @@ -1,4 +1,4 @@ -<% if logged_in_as_admin? %> +<% if policy(ArchiveFaq).translation_access? %> <%= render "admin_index" %> <% else %> <%= render "faq_index" %> diff --git a/app/views/archive_faqs/show.html.erb b/app/views/archive_faqs/show.html.erb index e91dfa09324..f99fa8f6f6f 100644 --- a/app/views/archive_faqs/show.html.erb +++ b/app/views/archive_faqs/show.html.erb @@ -1,5 +1,5 @@ -

    <%= link_to ts("Archive FAQ"), archive_faqs_path %> > <%= @archive_faq.title %>

    +

    <%= link_to t(".page_heading"), archive_faqs_path %> > <%= @archive_faq.title %>

    @@ -8,34 +8,37 @@ <% if @archive_faq.slug == "search-and-browse" %>

    - <%= ts("Our search engine has recently been updated, and this FAQ is based on our old version. We're working on bringing you more up-to-date information, but in the meantime, you can find out more in our %{elasticsearch_post}!", elasticsearch_post: link_to(ts("news post announcing the search and filter updates"), admin_post_path(10575))).html_safe %> + <%= t(".elasticsearch_update_notice_html", elasticsearch_news_link: link_to(t(".elasticsearch_news"), admin_post_path(10_575))) %>

    <% end %>
    - <% if logged_in_as_admin? %> + <% if policy(ArchiveFaq).translation_access? %>

    - Updated: <%=h @archive_faq.updated_at %> | <%= link_to 'Edit', edit_archive_faq_path(@archive_faq) %> + Updated: <%= h @archive_faq.updated_at %> + <% if Globalize.locale.to_s != "en" || policy(ArchiveFaq).full_access? -%> + | <%= link_to t(".edit"), edit_archive_faq_path(@archive_faq) %> + <% end %>

    <% end %> - <% if @archive_faq.questions.blank? %> -

    <%= ts("We're sorry, there are currently no entries in this category.") %>

    + <% if @archive_faq.questions.blank? %> +

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

    <% else %>
    dir="rtl"<% end %> class="userstuff"> - +
    <% for q in @questions %> @@ -44,7 +47,7 @@ <% unless q.screencast.to_s == "" %>

    - <%= ts("Screencast") %>: <%= link_to q.question, q.screencast.to_s %> + <%= t(".screencast") %> <%= link_to q.question, q.screencast.to_s %>

    <% end %> <%= raw sanitize_field(q, :content) %> diff --git a/app/views/blocked/users/index.html.erb b/app/views/blocked/users/index.html.erb index 4d00f2877b4..55389bc142a 100644 --- a/app/views/blocked/users/index.html.erb +++ b/app/views/blocked/users/index.html.erb @@ -1,8 +1,8 @@

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

    -
    diff --git a/app/views/collections/_collection_blurb.html.erb b/app/views/collections/_collection_blurb.html.erb index 055927b3be7..5fc67b1df88 100644 --- a/app/views/collections/_collection_blurb.html.erb +++ b/app/views/collections/_collection_blurb.html.erb @@ -12,7 +12,7 @@
    - <%= image_tag(collection.icon.url(:standard), :size => "100x100", :alt => collection.icon_alt_text, :class => "icon", skip_pipeline: true) %> + <%= collection_icon_display(collection) %>

    <%= set_format_for_date(collection.updated_at) %>

    <% if collection.all_moderators.length > 0%> diff --git a/app/views/collections/_form.html.erb b/app/views/collections/_form.html.erb index 35b79c0546e..07055c3a424 100644 --- a/app/views/collections/_form.html.erb +++ b/app/views/collections/_form.html.erb @@ -66,7 +66,7 @@
      <% unless @collection.new_record? %>
    • - <%= image_tag(@collection.icon.url(:standard), size: "100x100", alt: @collection.icon_alt_text, skip_pipeline: true) %> + <%= collection_icon_display(@collection) %> <%= ts("This is the collection's icon.") %>
    • <% end %> @@ -74,9 +74,9 @@
    • <%= ts("Icons can be in png, jpeg or gif form") %>
    • <%= ts("Icons should be sized 100x100 pixels for best results") %>
    - <% if @collection.icon_file_name %> + <% if @collection.icon.attached? %> <%= collection_form.check_box :delete_icon, {:checked => false} %> - <%= collection_form.label :delete_icon, ts("Delete collection icon and revert to our default") %> + <%= collection_form.label :delete_icon, t(".icon.delete") %> <% end %> diff --git a/app/views/collections/_header.html.erb b/app/views/collections/_header.html.erb index b6cbad579ab..087ffbdc2c9 100644 --- a/app/views/collections/_header.html.erb +++ b/app/views/collections/_header.html.erb @@ -2,7 +2,7 @@

    <%= link_to_unless_current(@collection.title, @collection) %>

    - <%= image_tag(@collection.icon.url(:standard), size: "100x100", alt: collection.icon_alt_text, skip_pipeline: true) %> + <%= collection_icon_display(@collection) %>
    diff --git a/app/views/comments/_comment_actions.html.erb b/app/views/comments/_comment_actions.html.erb index 52b3815440d..aecccb028da 100644 --- a/app/views/comments/_comment_actions.html.erb +++ b/app/views/comments/_comment_actions.html.erb @@ -13,11 +13,9 @@ <% end %> <% unless comment.unreviewed? %>
  • <%= link_to ts("Thread"), comment %>
  • - <% if comment.depth > 0 %> -
  • - <%= link_to ts("Parent Thread"), comment_path(id: comment.thread) %> -
  • - <% end %> + <% end %> + <% if comment.depth > 0 %> +
  • <%= link_to ts("Parent Thread"), comment_path(id: comment.thread) %>
  • <% end %> <% if can_freeze_comment?(comment) %>
  • <%= freeze_comment_button(comment) %>
  • diff --git a/app/views/comments/unreviewed.html.erb b/app/views/comments/unreviewed.html.erb index 51ad90d82e6..f70d57605ec 100644 --- a/app/views/comments/unreviewed.html.erb +++ b/app/views/comments/unreviewed.html.erb @@ -3,10 +3,12 @@ - +<% if can_review_all_comments?(@commentable) %> + +<% end %> diff --git a/app/views/feedbacks/new.html.erb b/app/views/feedbacks/new.html.erb index aecfa301a66..43929090def 100644 --- a/app/views/feedbacks/new.html.erb +++ b/app/views/feedbacks/new.html.erb @@ -42,9 +42,6 @@ <%= form_for(@feedback, html: {class: "post feedback"}) do |f| %>
    <%= t(".form.legend.contact_info") %> -

    - <%= t(".form.ip") %> -

    <%= f.label :username, t(".form.name.label") %>
    @@ -59,6 +56,7 @@
    <%= t(".form.legend.feedback") %> + <%= f.hidden_field :referer %>
    <%= f.label :language, t(".form.language.label") %> diff --git a/app/views/home/_content.html.erb b/app/views/home/_content.html.erb new file mode 100644 index 00000000000..f6eec5ca58b --- /dev/null +++ b/app/views/home/_content.html.erb @@ -0,0 +1,322 @@ +<%# IMPORTANT: Also update current_tos_version in application_controller %> +<%# To ensure proper formatting, this must always be rendered inside an element + with the userstuff class. The userstuff element must be inside an element with + the classes "docs system". %> +

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

    +

    + <%= t(".intro.our_goal_html", + maximum_inclusiveness_link: link_to(t(".intro.maximum_inclusiveness"), tos_faq_path(anchor: "max_inclusiveness"))) %> +

    +

    + <%= t(".intro.review_before_posting_html", + all_content_must_comply_bold: tag.strong(t(".intro.all_content_must_comply_html", + content_link: link_to(t(".intro.content"), tos_faq_path(anchor: "define_content")))), + tos_faq_link: link_to(t(".intro.tos_faq"), tos_faq_path(anchor: "content_faq"))) %> +

    +

    + <%= t(".intro.you_can_report_html", + report_it_to_us_link: link_to(t(".intro.report_it_to_us"), new_abuse_report_path), + we_do_not_prescreen_bold: tag.strong(t(".intro.we_do_not_prescreen"))) %> +

    + +<% unless local_assigns[:suppress_toc] %> + +<% end %> + +

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

    +

    <%= t(".offensive_content.heading") %>

    +

    <%= t(".offensive_content.removal_not_just_offensiveness") %>

    +

    + + <%= t(".offensive_content.tos_faq_in_parens_html", + tos_faq_link: link_to( + t(".offensive_content.tos_faq"), + tos_faq_path(anchor: "offensive_content_faq"), + aria: { + label: t(".offensive_content.tos_faq_link_label") + } + )) %> + +

    + +

    <%= t(".fanworks.heading") %>

    +

    + <%= t(".fanworks.must_be_fanworks_html", + non_fanwork_content_link: link_to(t(".fanworks.non_fanwork_content"), tos_faq_path(anchor: "non_fanwork_examples"))) %> +

    +

    + <%= t(".fanworks.bookmarks_only_fanworks_html", + bookmarks_link: link_to(t(".fanworks.bookmarks"), archive_faq_path("bookmarks")), + external_bookmarks_link: link_to(t(".fanworks.external_bookmarks"), archive_faq_path("bookmarks", anchor: "externalbookmark"))) %> +

    +

    + + <%= t(".fanworks.tos_faq_in_parens_html", + tos_faq_link: link_to( + t(".fanworks.tos_faq"), + tos_faq_path(anchor: "non_fanwork_faq"), + aria: { + label: t(".fanworks.tos_faq_link_label") + } + )) %> + +

    + +

    <%= t(".commercial_promotion.heading") %>

    +

    <%= t(".commercial_promotion.not_allowed") %>

    +

    + + <%= t(".commercial_promotion.tos_faq_in_parens_html", + tos_faq_link: link_to( + t(".commercial_promotion.tos_faq"), + tos_faq_path(anchor: "commercial_promotion_faq"), + aria: { + label: t(".commercial_promotion.tos_faq_link_label") + } + )) %> + +

    + +

    <%= t(".copyright_infringement.heading") %>

    +

    <%= t(".copyright_infringement.not_allowed") %>

    +

    <%= t(".copyright_infringement.epigraphs_small_quotations_allowed") %>

    +

    + <%= t(".copyright_infringement.transformative_works_legal_html", + transformative_fanworks_link: link_to(t(".copyright_infringement.transformative_fanworks"), tos_faq_path(anchor: "define_transformative"))) %> +

    +

    + + <%= t(".copyright_infringement.tos_faq_in_parens_html", + tos_faq_link: link_to( + t(".copyright_infringement.tos_faq"), + tos_faq_path(anchor: "copyright_plagiarism_faq"), + aria: { + label: t(".copyright_infringement.tos_faq_link_label") + } + )) %> + +

    + +

    <%= t(".plagiarism.heading") %>

    +

    + <%= t(".plagiarism.html", + their_expressions_of_their_ideas_link: link_to(t(".plagiarism.their_expressions_of_their_ideas"), tos_faq_path(anchor: "define_transformative"))) %> +

    +

    + + <%= t(".plagiarism.tos_faq_in_parens_html", + tos_faq_link: link_to( + t(".plagiarism.tos_faq"), + tos_faq_path(anchor: "copyright_plagiarism_faq"), + aria: { + label: t(".plagiarism.tos_faq_link_label") + } + )) %> + +

    + +

    <%= t(".personal_information.heading") %>

    +

    <%= t(".personal_information.not_allowed") %>

    +
      +
    1. + <%= t(".personal_information.revealing_orphaned_creator_html", + orphaned_link: link_to(t(".personal_information.orphaned"), archive_faq_path("glossary", anchor: "orphandef"))) %> +
    2. +
    3. <%= t(".personal_information.linking_fannish_identity") %>
    4. +
    5. <%= t(".personal_information.sharing_sufficient_information") %>
    6. +
    7. + <%= t(".personal_information.disclosing_personal_data_html", + special_categories_of_personal_data_link: link_to(t(".personal_information.special_categories_of_personal_data"), "https://gdpr-info.eu/art-9-gdpr/")) %> +
        +
      1. <%= t(".personal_information.rpf_exception") %>
      2. +
      +
    8. +
    +

    <%= t(".personal_information.right_to_hide_delete") %>

    +

    + + <%= t(".personal_information.tos_faq_in_parens_html", + tos_faq_link: link_to( + t(".personal_information.tos_faq"), + tos_faq_path(anchor: "identity_impersonation_faq"), + aria: { + label: t(".personal_information.tos_faq_link_label") + } + )) %> + +

    + +

    <%= t(".impersonation.heading") %>

    +

    + <%= t(".impersonation.html", + function_link: link_to(t(".impersonation.function"), tos_faq_path(anchor: "impersonate_function"))) %> +

    +

    + + <%= t(".impersonation.tos_faq_in_parens_html", + tos_faq_link: link_to( + t(".impersonation.tos_faq"), + tos_faq_path(anchor: "identity_impersonation_faq"), + aria: { + label: t(".impersonation.tos_faq_link_label") + } + )) %> + +

    + +

    <%= t(".harassment.heading") %>

    +

    <%= t(".harassment.definition") %>

    +

    <%= t(".harassment.not_allowed_and_context") %>

    +

    + <%= t(".harassment.threatening_versus_annoying_html", + blocking_link: link_to(t(".harassment.blocking"), tos_faq_path(anchor: "blocking")), + muting_link: link_to(t(".harassment.muting"), tos_faq_path(anchor: "muting")), + filtering_link: link_to(t(".harassment.filtering"), tos_faq_path(anchor: "filters"))) %> +

    +

    + <%= t(".harassment.policy_applicability_html", + applies_to_all_link: link_to(t(".harassment.applies_to_all"), tos_faq_path(anchor: "harassment_scope")), + otw_abbreviation: tag.abbr(t(".harassment.otw.abbreviated"), title: t(".harassment.otw.full"))) %> +

    +
    <%= t(".harassment.rpf.heading") %>
    +

    <%= t(".harassment.rpf.text") %> +

    <%= t(".harassment.advocating_harm.heading") %>
    +

    <%= t(".harassment.advocating_harm.text") %> +

    + + <%= t(".harassment.tos_faq_in_parens_html", + tos_faq_link: link_to( + t(".harassment.tos_faq"), + tos_faq_path(anchor: "harassment_faq"), + aria: { + label: t(".harassment.tos_faq_link_label") + } + )) %> + +

    + +

    <%= t(".user_icons.heading") %>

    +

    <%= t(".user_icons.text") %> +

    + + <%= t(".user_icons.tos_faq_in_parens_html", + tos_faq_link: link_to( + t(".user_icons.tos_faq"), + tos_faq_path(anchor: "username_icon_faq"), + aria: { + label: t(".user_icons.tos_faq_link_label") + } + )) %> + +

    + +

    <%= t(".mandatory_tags.heading") %>

    +
      +
    1. +

      + <%= t(".mandatory_tags.ao3_may_designate_html", + minimum_criteria_link: link_to(t(".mandatory_tags.minimum_criteria"), tos_faq_path(anchor: "minimum_tags"))) %> +

      +
    2. +
    3. +

      + <%= t(".mandatory_tags.choose_no_warnings_html", + rating_link: link_to(t(".mandatory_tags.rating"), tos_faq_path(anchor: "ratings_list")), + archive_warning_link: link_to(t(".mandatory_tags.archive_warning"), tos_faq_path(anchor: "warnings_list")), + non_specific_tags_link: link_to(t(".mandatory_tags.non_specific_tags"), tos_faq_path(anchor: "nonspecific_tags"))) %> +

      +
    4. +
    5. +

      + <%= t(".mandatory_tags.applying_nonspecific_tag_html", + any_archive_warning_link: link_to(t(".mandatory_tags.any_archive_warning"), tos_faq_path(anchor: "warnings_list"))) %> +

      +
    6. +
    7. +

      + <%= t(".mandatory_tags.tags_applied_automatically_html", + not_available_link: link_to(t(".mandatory_tags.not_available"), tos_faq_path(anchor: "no_language_tag_exists")), + tos_faq_link: link_to(t(".mandatory_tags.tos_faq"), tos_faq_path(anchor: "ratings_warnings_faq"))) %> +

      +
    8. +
    +

    + + <%= t(".mandatory_tags.tos_faq_in_parens_html", + tos_faq_link: link_to( + t(".mandatory_tags.tos_faq_endnote"), + tos_faq_path(anchor: "ratings_warnings_faq"), + aria: { + label: t(".mandatory_tags.tos_faq_link_label") + } + )) %> + +

    + +

    <%= t(".illegal_inappropriate_content.heading") %>

    +
      +
    1. +

      + <%= t(".illegal_inappropriate_content.no_illegal_content_html", + images_of_real_children_link: link_to(t(".illegal_inappropriate_content.images_of_real_children"), + tos_faq_path(anchor: "underage_images"))) %> +

      +

      + <%= t(".illegal_inappropriate_content.conduct_threatening_technical_integrity_html", + technical_integrity_link: link_to(t(".illegal_inappropriate_content.technical_integrity"), tos_faq_path(anchor: "technical_integrity_faq"))) %> +

      +
    2. +
    3. +

      + <%= t(".illegal_inappropriate_content.spamming_behavior") %> +

      +

      + <%= t(".illegal_inappropriate_content.automated_spam_check_html", + contact_ao3_administrators_link: link_to(t(".illegal_inappropriate_content.contact_ao3_administrators"), + new_abuse_report_path)) %> +

      +
    4. +
    +

    + <%= t(".illegal_inappropriate_content.violates_us_law_html", + report_it_to_us_link: link_to(t(".illegal_inappropriate_content.report_it_to_us"), new_abuse_report_path)) %> +

    +

    + + <%= t(".illegal_inappropriate_content.tos_faq_in_parens_html", + tos_faq_link: link_to( + t(".illegal_inappropriate_content.tos_faq"), + tos_faq_path(anchor: "offensive_content_faq"), + aria: { + label: t(".illegal_inappropriate_content.tos_faq_link_label") + } + )) %> + +

    + +
    + +<% unless local_assigns[:suppress_footer] %> +

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

    +

    + + <%= t(".license_html", + terms_of_service_link: link_to(t(".terms_of_service"), tos_path), + content_policy_link: link_to(t(".content_policy"), content_path), + privacy_policy_link: link_to(t(".privacy_policy"), privacy_path), + cc_attribution_4_0_international_link: link_to(t(".cc_attribution_4_0_international"), + "https://creativecommons.org/licenses/by/4.0/", + rel: "nofollow")) %> + +

    +<% end %> diff --git a/app/views/home/_dmca.html.erb b/app/views/home/_dmca.html.erb index d21c17b7b79..33905bcdcb7 100644 --- a/app/views/home/_dmca.html.erb +++ b/app/views/home/_dmca.html.erb @@ -1,95 +1,177 @@
    -

    - The Digital Millennium Copyright Act (17 U.S.C. § 512)("DMCA"), creates a safe harbor, or a legal exemption, from copyright infringement liability for Internet service providers (ISPs) and other intermediaries. To encourage and maintain free speech on our sites, the Organization for Transformative Works (OTW) will push back against DMCA takedown requests if we believe that the content in question is actually lawful. The law requires that DMCA requests be made by the holder of a valid copyright in the work or by an agent authorized to act on behalf of the owner, and requires that requests are made in good faith under penalty of perjury. -

    -

    - If you are a copyright owner and you believe that your work is being infringed, in the spirit of open collaboration, we encourage you to seek resolution through the volunteer community. You may contact abuse with an informal request for content removal. Please provide the exact URL (the "address" or "location" of the page as shown by your web browser) of the content and provide enough information to substantiate your claim of copyright ownership. You must support your concern with a link to your publication, a scanned page of the work, and enough publication details for the volunteer community to evaluate your request. -

    -

    - Alternatively, you may initiate a formal DMCA takedown process. Please note that a third party may submit a counternotice challenging your takedown request, at which point the OTW is required to restore the previously-removed content. You may then initiate legal proceedings against the third party who files a DMCA counternotice in order to determine copyright ownership and the legality of the material’s use. You may also be liable for damages, such as costs and attorney fees, if you knowingly and materially misrepresent your claim. -

    -

    - How to initiate a formal DMCA takedown request... -

    -

    - To initiate a formal DMCA takedown request, please send a letter to us at legal (at) transformativeworks.org or to our designated agent: -

    -

    - Organization for Transformative Works
    - 228 Park Ave S #18156
    - New York, New York 10003-1502
    - Attention: Legal -

    -

    - The takedown notice must substantially comply with the provisions of 17 U.S.C. § 512(c)(3)(A). Submission of a takedown notice further requires that you consent to the jurisdiction of a United States court. Statutory requirements of a valid DMCA takedown notice include: +

    The Digital Millennium Copyright Act (DMCA) establishes the procedure the Organization for Transformative Works + (OTW) must follow when users of our sites, including the Archive of Our Own (AO3), reproduce copyrighted material without authorization. + Specifically, 17 U.S.C. § 512 protects the OTW from + potential liability when infringing content is present on our sites.

    +

    The OTW will respond promptly to notices of alleged copyright infringement that substantially comply with all legal requirements. However, we believe that transformative + fanworks are legal, and we reserve the right to review the allegedly infringing content and + independently determine whether it is infringing. If you have any questions about our DMCA Policy, please contact the OTW Legal + committee.

    + +

    My work was posted on AO3 without my permission. What can I do?

    +

    If you encounter content on AO3 that you believe infringes on your copyright, you have the following options: +

      +
    • Submit an Abuse report: Abuse reports are evaluated by the + Policy & Abuse committee for violations of the AO3 Terms of Service, and held to a very + high standard of confidentiality. When submitting an Abuse report + about copyright infringement, you must provide a link to the violating content on AO3 and clearly identify + the original source in your report description. For more information, please refer to the Terms of Service FAQ.
    • +
    • File a DMCA takedown notice: DMCA takedown notices can + only be filed by the copyright owner or someone legally authorized to act on their behalf. DMCA notices must + be sent to the OTW's designated agent and comply with all legal + requirements. DMCA notices are not subject to confidentiality. The OTW reserves the right to make + public all DMCA notices that we receive, though some information may be redacted for privacy.
    • +
    +

    +

    Please do not submit an Abuse report if you intend to file a DMCA notice. Submitting both an + Abuse report and a DMCA notice about the same content may delay the processing of both requests.

    +

    Filing a DMCA takedown notice

    +

    To initiate a formal DMCA takedown request, contact the OTW's designated agent using the Legal committee's contact + form, via email to legal@transformativeworks.org, or by mailing a letter to:

    +
    +

    + Organization for Transformative Works
    + 228 Park Ave S #18156
    + New York, NY 10003-1502
    + Attention: Legal +

    +
    +

    We prefer to receive DMCA requests through the contact form or by email.

    +

    Requirements of a DMCA takedown notice

    +

    The takedown notice must substantially comply with the requirements of 17 U.S.C. § 512(c)(3)(A). You are also + required to consent to the jurisdiction of a United States court. The legal requirements of a valid DMCA + takedown notice include:

      -
    1. your physical or electronic signature;
    2. -
    3. your name, address, and phone number;
    4. -
    5. identification of the material and its location before it was removed;
    6. -
    7. a statement under penalty of perjury that the material was removed by mistake or misidentification;
    8. -
    9. your consent to the jurisdiction of a federal court in the district where you live (if you are in the U.S.), or your consent to the jurisdiction of a federal court in the district where your service provider is located (if you are not in the U.S.); and
    10. -
    11. your consent to accept service of process from the party who submitted the takedown notice.
    12. +
    13. your physical or electronic signature;
    14. +
    15. your contact information;
    16. +
    17. identification of the copyrighted work being infringed upon (for example, a URL or publication listing); +
    18. +
    19. the URL(s) of the specific content on our site that you believe to be infringing;
    20. +
    21. a statement that you have a good-faith belief that the use of the content is not authorized by the copyright + owner(s), their agent, or the law;
    22. +
    23. +
        +
      1. a statement, made under penalty of perjury, that you are authorized to act on + behalf of the copyright owner; and
      2. +
      3. a statement that the information you have provided is accurate.
      4. +
      +
    -

    - What can I expect if I file a DMCA counternotice? -

    -

    - If the OTW receives a statutorily-compliant counternotice, we will make reasonable efforts to notify the original requestor of the takedown that a counternotice was received. The original requestor will have 10 business days from the date the OTW notifies the original requestor of the counternotice to prevent the restoration of the content by filing a lawsuit. If the original requestor has not filed suit, the OTW will restore your content between 10 and 14 business days after receipt of the counternotice. The OTW may also post the counternotice online, with identifying information redacted. -

    -

    - What happens to repeat copyright infringers? -

    -

    - Pursuant to the DMCA, we will terminate, in appropriate circumstances, users and account holders of our system and network who are repeat infringers (see 17 U.S.C. § 512 (i)(1)(A)). -

    -

    - In the event that material is removed due to a DMCA notice, the only recourse for restoring such material is to file a counter-notice with the OTW. If you believe that a take-down notice which has been acted upon by the OTW is without legal basis, please feel free to visit the following sites as a first step in learning about filing a counter-notice: -

    +

    Per 17 U.S.C. § 512(f), you may be subject to liability if you knowingly and materially misrepresent your claim + that the content infringes on your copyright.

    +

    DMCA takedown process

    +

    If the OTW receives a DMCA takedown notice that fulfills all legal + requirements, we will remove the content once we review the validity of the infringement claim.

    +

    When the content is removed from AO3, the information you provided in the DMCA takedown notice will be forwarded + to the owner of the account responsible for posting the content. We may also send a redacted version of the DMCA + takedown notice to Lumen and other third parties at our discretion.

    +

    If the account owner believes that their content does not infringe upon your copyright, they may file a DMCA + counternotice disputing the takedown. The account owner can file a DMCA counternotice at any point after the + content has been removed; there is no time limit.

    +

    If the OTW receives a valid DMCA counternotice, we will make reasonable efforts to notify you. You will have 10 + business days to file a lawsuit to prevent the restoration of the content. If you do not notify us within 10 + business days that you have filed a lawsuit, we will allow the content to be restored in accordance with 17 + U.S.C. § 512(g)(2)(B)-(C).

    +

    I was notified that my work was removed from AO3 due to a DMCA takedown + notice. What can I do?

    +

    Copyright infringement and plagiarism are violations of + Sections II.D and II.E of our Terms of Service, respectively. For more information, please + refer to our Terms of Service FAQ.

    +

    If the OTW receives a valid DMCA takedown notice for content you uploaded to AO3, we will remove the content and notify you by email. We may also send a + redacted version of the DMCA takedown notice to Lumen and other third + parties at our discretion.

    +

    If your content was removed due to a DMCA takedown notice, you cannot reupload or repost the + content unless you have filed a DMCA counternotice to dispute the takedown. If you reupload or + repost the content without filing a DMCA counternotice, we may suspend your + account.

    +

    Filing a DMCA counternotice

    +

    If you believe that your content does not infringe on the copyright owner's rights, you can file a DMCA + counternotice to dispute the takedown. By filing a counternotice, you are indicating that you are + willing to defend your use of the original copyrighted material in court if the party who submitted + the original takedown notice chooses to pursue legal action against you.

    +

    If you are considering whether to file a DMCA counternotice, we recommend that you contact an intellectual + property lawyer licensed to practice in your jurisdiction, so that you are aware of your legal rights and + obligations. You can also review the Lumen DMCA Explanation and + FAQ.

    +

    To initiate a formal DMCA counter-notification request, contact the OTW's designated agent using the Legal committee's contact + form, via email to legal@transformativeworks.org, or by mailing a letter to:

    +
    +

    + Organization for Transformative Works
    + 228 Park Ave S #18156
    + New York, NY 10003-1502
    + Attention: Legal +

    +
    +

    We prefer to receive DMCA requests through the contact form or by email.

    +

    You can file a DMCA counternotice at any point after your content has been removed; there is no time limit.

    +

    Requirements of a DMCA counternotice

    +

    The counternotice must substantially comply with the requirements of 17 U.S.C. § 512(g)(3). You are also required + to consent to the jurisdiction of a United States court. The legal requirements of a valid DMCA counternotice + include:

      -
    1. "Responding to a DMCA Takedown Notice Targeting Your Content" at Digital Media Law Project
    2. -
    3. Lumen FAQ
    4. -
    5. Lumen DMCA Notices FAQ
    6. -
    7. newmediarights.org
    8. -
    -

    - All notices should be sent to the OTW's designated agent. -


    - Adapted from Wikipedia's DMCA policy. -

    - -
    +
  • your physical or electronic signature;
  • +
  • your name, address, and phone number;
  • +
  • the URL(s) of the content that was removed;
  • +
  • a statement, made under penalty of perjury, that the content was removed due to a mistake + or misidentification;
  • +
  • + a statement that you consent to the jurisdiction of a U.S. federal court in either: +
      +
    1. the district where you live, if you are in the U.S.; or
    2. +
    3. the district where the OTW is located (Manhattan, New York), if you are not + in the U.S.; and
    4. +
    +
  • a statement that you will accept service of process from the party who submitted the original takedown + notice.
  • + +

    Per 17 U.S.C. § 512(f), you may be subject to liability if you knowingly and materially misrepresent your claim + that the content was removed due to a mistake or misidentification.

    +

    DMCA counter-notification process

    +

    If the OTW receives a valid DMCA counternotice, we will make reasonable efforts to notify the party who + submitted the original takedown notice.

    +

    The original submitter will have 10 business days to prevent the restoration of your content by filing a + lawsuit against you. If the original submitter does not file a lawsuit within 10 business days, the OTW will + restore your content (or, if your content has already been deleted, allow you to reupload it) between 10 and + 14 business days after we receive the counternotice. We may also send a redacted version of the DMCA + counternotice to Lumen and other third parties at our discretion. +

    +

    Repeat offenses

    +

    As required by 17 U.S.C. § 512(i)(1)(A), we may suspend users who repeatedly + violate others' copyright. The OTW has the discretion to decide what qualifies as a repeated offense.

    +
    +

    AO3's DMCA policy is licensed under the Creative Commons Attribution-ShareAlike 4.0 International License. Material in + this policy has been drawn from Dreamwidth, the Electronic Frontier Foundation, and Wikipedia. +

    +
    diff --git a/app/views/home/_privacy.html.erb b/app/views/home/_privacy.html.erb new file mode 100644 index 00000000000..066d28adff3 --- /dev/null +++ b/app/views/home/_privacy.html.erb @@ -0,0 +1,236 @@ +<%# IMPORTANT: Also update current_tos_version in application_controller %> +<%# To ensure proper formatting, this must always be rendered inside an element + with the userstuff class. The userstuff element must be inside an element with + the classes "docs system". %> +

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

    +

    + <%= t(".intro.ao3_exists_to_host_html", + personal_information_link: link_to(t(".intro.personal_information"), "#III.A.1")) %> +

    +
      +
    • <%= t(".intro.host_your_fanworks") %>
    • +
    • <%= t(".intro.show_you_works") %>
    • +
    • <%= t(".intro.enable_post_information") %>
    • +
    +

    + <%= t(".intro.details_how_and_why_html", + common_questions_bold: tag.strong(t(".intro.answers_common_questions_html", + tos_faq_link: link_to(t(".intro.tos_faq"), tos_faq_path(anchor: "privacy_faq"))))) %> +

    + +<% unless local_assigns[:suppress_toc] %> + +<% end %> + +

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

    +

    <%= t(".applicability.heading") %>

    +
      +
    1. <%= t(".applicability.policy_covers") %>

    2. +
    3. +

      + <%= t(".applicability.global_subprocessors_html", + subprocessors_link: link_to(t(".applicability.subprocessors"), + "https://www.transformativeworks.org/otw_tos/organization-for-transformative-works-subprocessor-list/")) %> +

      +

      + <%= t(".applicability.transfers_necessary_html", consent_to_us_processing_bold: tag.strong(t(".applicability.consent_to_us_processing"))) %> +

      +
    4. +
    + +

    <%= t(".information_scope.heading") %>

    +
      +
    1. +

      + <%= t(".information_scope.information_in_content_html", + content_link: link_to(t(".information_scope.content"), tos_path(anchor: "I.A.1")), + special_categories_link: link_to(t(".information_scope.special_categories"), "https://gdpr-info.eu/art-9-gdpr/")) %> +

      +
    2. +
    3. <%= t(".information_scope.collect_through_use") %>

    4. +
    + +

    <%= t(".types_of_information.heading") %>

    +
      +
    1. +

      <%= t(".types_of_information.emails.heading") %>

      +

      <%= t(".types_of_information.emails.collect_process_retain") %>

      +

      + <%= t(".types_of_information.emails.address_usage_html", + challenge_link: link_to(t(".types_of_information.emails.challenge"), archive_faq_path("glossary", anchor: "challengedef"))) %> +

      +

      <%= t(".types_of_information.emails.unsubscribe") %>

      +
    2. +
    3. +

      + <%= t(".types_of_information.ip_addresses.heading") %> + <%= t(".types_of_information.ip_addresses.text") %> +

      +
    4. +
    5. +

      + <%= t(".types_of_information.logs.heading") %> + <%= t(".types_of_information.logs.text") %> +

      +
    6. +
    7. +

      + <%= t(".types_of_information.cookies.heading") %> + <%= t(".types_of_information.cookies.text") %> +

      +
    8. +
    9. +

      + <%= link_to t(".types_of_information.fnok.heading"), archive_faq_path("fannish-next-of-kin") %> + <%= t(".types_of_information.fnok.text") %> +

      +
    10. +
    11. +

      + <%= t(".types_of_information.other_information.heading") %> +

      +

      <%= t(".types_of_information.other_information.to_maintain_integrity") %>

      +

      + <%= t(".types_of_information.other_information.to_make_content_available_html", + tos_faq_link: link_to(t(".types_of_information.other_information.tos_faq"), tos_faq_path(anchor: "feature_information"))) %> +

      +
    12. +
    + +

    <%= t(".aggregate_anonymous_info.heading") %>

    +
      +
    1. <%= t(".aggregate_anonymous_info.understand_ao3_usage") %>

    2. +
    3. <%= t(".aggregate_anonymous_info.anonymous_non_personal") %>

    4. +
    + +

    <%= t(".your_rights.heading") %>

    +
      +
    1. +

      + <%= t(".your_rights.request_data_html", + applicable_jurisdiction_link: link_to(t(".your_rights.applicable_jurisdiction"), tos_faq_path(anchor: "privacy_rights"))) %> +

      +
    2. +
    3. +

      + <%= t(".your_rights.potential_other_rights_html", + other_rights_link: link_to(t(".your_rights.other_rights"), tos_faq_path(anchor: "privacy_rights_faq"))) %> +

      +
    4. +
    5. <%= t(".your_rights.require_user_specific_proof") %>

    6. +
    + +

    <%= t(".third_parties.heading") %>

    +
      +
    1. <%= t(".third_parties.do_not_sell_information") %>

    2. +
    3. <%= t(".third_parties.third_party_tools") %>

    4. +
    5. +

      <%= t(".third_parties.sharing_exceptions.intro") %>

      +
        +
      1. +

        + <%= t(".third_parties.sharing_exceptions.external_processing.heading") %> + <%= t(".third_parties.sharing_exceptions.external_processing.html", + subprocessor_list_link: link_to(t(".third_parties.sharing_exceptions.external_processing.subprocessor_list"), "https://www.transformativeworks.org/otw_tos/organization-for-transformative-works-subprocessor-list/")) %> +

        +
      2. +
      3. +

        + + <%= t(".third_parties.sharing_exceptions.challenge_signup.heading_html", + challenge_link: link_to(t(".third_parties.sharing_exceptions.challenge_signup.challenge"), archive_faq_path("glossary", anchor: "challengedef"))) %> + + <%= t(".third_parties.sharing_exceptions.challenge_signup.text") %> +

        +
      4. +
      5. +

        + + <%= t(".third_parties.sharing_exceptions.open_doors_import.heading_html", + open_doors_link: link_to(t(".third_parties.sharing_exceptions.open_doors_import.open_doors"), "https://opendoors.transformativeworks.org/")) %> + + <%= t(".third_parties.sharing_exceptions.open_doors_import.text") %> +

        +
      6. +
      7. +

        + <%= t(".third_parties.sharing_exceptions.handle_complaints.heading") %> + <%= t(".third_parties.sharing_exceptions.handle_complaints.html", + dmca_notice_link: link_to(t(".third_parties.sharing_exceptions.handle_complaints.dmca_notice"), tos_faq_path(anchor: "dmca_complaint")), + pac_confidentiality_policy_link: link_to(t(".third_parties.sharing_exceptions.handle_complaints.pac_confidentiality_policy"), "https://www.transformativeworks.org/committees/policy-abuse-confidentiality-policy/")) %> +

        +
      8. + +
      9. + <%= t(".third_parties.sharing_exceptions.legal_reasons.heading") %> + <%= t(".third_parties.sharing_exceptions.legal_reasons.intro") %> +
          +
        1. <%= t(".third_parties.sharing_exceptions.legal_reasons.legally_compelled") %>
        2. +
        3. <%= t(".third_parties.sharing_exceptions.legal_reasons.good_faith_comply") %>
        4. +
        5. <%= t(".third_parties.sharing_exceptions.legal_reasons.cooperating_law_enforcement") %>
        6. +
        +

        <%= t(".third_parties.sharing_exceptions.legal_reasons.law_enforcement_cooperation_details") %>

        +

        <%= t(".third_parties.sharing_exceptions.legal_reasons.attempt_to_notify") %>

        +
      10. +
      +
    6. +
    + +

    <%= t(".account_termination.heading") %>

    +
      +
    1. +

      + <%= t(".account_termination.deletion_after_termination_html", + terminate_your_account_link: link_to(t(".account_termination.terminate_your_account"), archive_faq_path("your-account", anchor: "deleteaccount"))) %> +

      +
        +
      1. +

        + <%= t(".account_termination.orphans_excluded_html", + orphan_link: link_to(t(".account_termination.orphan"), archive_faq_path("orphaning")), + pseud_link: link_to(t(".account_termination.pseud"), archive_faq_path("pseuds", anchor: "whatisapseud"))) %> +

        +
      2. +
      3. +

        + <%= t(".account_termination.backup_copies_html", + general_principles_link: link_to(t(".account_termination.general_principles"), tos_path(anchor: "I.E.2"))) %> +

        +
      4. +
      +
    2. +
    3. <%= t(".account_termination.legal_enforcement_retention") %>

    4. +
    + +

    <%= t(".retention_of_information.heading") %>

    +

    <%= t(".retention_of_information.text") %>

    + +

    <%= t(".contact_us.heading") %>

    +

    + <%= t(".contact_us.html", + contact_pac_link: link_to(t(".contact_us.contact_pac"), new_abuse_report_path)) %> +

    + +
    +

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

    +

    + + <%= t(".license_html", + terms_of_service_link: link_to(t(".terms_of_service"), tos_path), + content_policy_link: link_to(t(".content_policy"), content_path), + privacy_policy_link: link_to(t(".privacy_policy"), privacy_path), + cc_attribution_4_0_international_link: link_to(t(".cc_attribution_4_0_international"), + "https://creativecommons.org/licenses/by/4.0/", + rel: "nofollow")) %> + +

    diff --git a/app/views/home/_tos.html.erb b/app/views/home/_tos.html.erb index 629b0dc173c..817776b8fbf 100644 --- a/app/views/home/_tos.html.erb +++ b/app/views/home/_tos.html.erb @@ -1,545 +1,350 @@ -<% # IMPORTANT: Also update current_tos_version in application_controller %> -
    -

    The Archive of Our Own is a place for fanworks, including fan fiction based on books, TV, movies, comics, other media, and real-person fiction (RPF).

    - -

    What We Believe

    -
      -
    1. Our goal is maximum inclusiveness (refer to the ToS FAQ) of content. Our software is open-source and available for others to use if they wish to implement the code elsewhere.
    2. -
    3. We are committed to defending fanworks against legal challenges. Our position on transformative fanworks is detailed in the OTW FAQ. We have legal resources and alliances on which we can draw. However, that is not a guarantee that the organization can or will fight each battle. The Board will take into account a variety of factors, both legal and otherwise, in responding to a legal challenge. Further information is available on the OTW site.
    4. -
    5. We do not sell the data that you post on, submit to or share on "OTW Sites" (transformativeworks.org, archiveofourown.org and fanlore.org) to third parties, and we do not include or accept paid advertisements from third parties. Each of the OTW Sites has Terms of Service and a Privacy Policy that explains what information we collect, how we use it and who we share it with; your use of any of the OTW Sites is governed by that site's Terms and Policy.
    6. -
    7. We strive to make each OTW Site's Terms of Service (ToS) and Privacy Policy readable. We have tried to provide explanations for the more unusual legal terms. If you have terminology questions not covered here, Law.com's glossary may help, though we cannot vouch for the accuracy of outside sources, including those referenced in the ToS.
    8. -
    - -

    There are five parts to the Terms of Service (ToS):

    - - -

    I. General Principles

    - -
    A. The legalese: including where a lawsuit would be filed, when, and under what law
    -

    The ToS constitute the entire agreement between you and the Organization for Transformative Works (OTW) and govern your use of the Archive of Our Own service (hereinafter "Service", "AO3" or "Archive"). It takes the place of all prior agreements between you and the OTW concerning your use of the Service. It does not govern your use of other OTW sites and/or projects including the OTW site itself, Fanlore, or Transformative Works and Cultures, or donations to or membership in the OTW, or OTW elections, all of which are covered under separate agreements hosted on each of the OTW Sites.

    -

    The AO3 ToS, the relationship between you and the OTW, and all disputes arising out of or related to it, shall be governed by the laws of the United States and specifically the State of New York (refer to the ToS FAQ), without regard to its conflict of law provisions.

    - -

    You and the OTW agree to submit to the personal and exclusive jurisdiction of the courts located within New York County (Manhattan), New York, and to waive any objection to the laying of venue there.

    -

    The OTW's failure to enforce any part of the ToS will not waive the OTW's ability to enforce it, and any waiver with regard to a specific instance shall not constitute a waiver of any other breaches of the ToS, even with regard to the same user.

    -

    If any provision of the ToS is found by a court of competent jurisdiction to be invalid, you agree that the court should give effect to the party's intentions as reflected in the provision, and that the other provisions of the ToS remain in full force and effect.

    -

    You agree that, regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to use of the Archive or the ToS must be filed within one (1) year after such claim or cause of action arose or be forever barred.

    - -
    B. You agree to the Terms of Service
    -
      -
    1. -

      Archive Of Our Own hosts and shares content created by fans, for fans. Our Privacy Policy is a part of these Terms of Service and is contained in Section III of these Terms of Service. By submitting a work, comment, image, tag, item of information including personally identifying information like an email address, User Name, link, embedded image, audio file or video, or any other form of content ("Content") to the Archive, or by creating a User Account and/or by viewing any Content on the Archive, you affirm, confirm and state that you comply with and assent to the ToS, which incorporates the AO3 Privacy Policy.

      -
    2. -
    3. -

      We may update the ToS as necessary. If the ToS change at any time after May 25, 2018, this is how the process will work: Changes in the ToS may be proposed at any time by or to the OTW Board. Proposed changes will be prominently disclosed on the Service, and we will offer at least a two-week comment period for proposed changes. At the end of the comment period, proposed changes will be voted on by the Board. If the Board votes in favor, the changes will become effective at that time. You can learn about changes in the ToS by visiting the Service.

      -

      Subject to amendments of the update process by the OTW Board, this is the only means by which the Terms of Service may be altered. The Terms of Service cannot be changed by, e.g., emails or oral communications with you.

      -
    4. -
    - -
    C. Potential problems with the Service
    -
      -
    1. -

      The OTW provides services, including the Archive of Our Own, on an "as is" and "as available" basis. The OTW does not warrant (that is, does not make a legally binding promise) that our services will meet your requirements; that our services will be uninterrupted, timely, secure, or error-free; or that the results you get from using the services will be accurate, reliable, or satisfactory to you. We will endeavor to provide the best possible service to users of the Service, but many things (e.g., possible outages, hackings, etc.) are not within our control and we cannot provide for all eventualities. In the event we learn of a breach of personally identifying information that users have submitted to the Service, we will notify affected users as soon as practicable.

      -
    2. -
    3. -

      Any material you download, view, or otherwise access through the Service is at your own risk. You will be solely responsible for any damage or loss of data that results from the download of any such material.

      -
    4. -
    5. -

      The OTW expressly disclaims all warranties of any kind, whether express or implied, including, but not limited to, the implied warranties of merchantability, fitness for a particular purpose and non-infringement. The ToS govern your use of the Service, and therefore no communication from anyone associated with the OTW will create any warranty that isn't expressly stated in the ToS.

      -
    6. -
    7. -

      You expressly agree that the OTW shall not be liable to you for any damages of any kind (even if the OTW has been advised of the possibility of such damages) resulting from the Service, including but not limited to your use of or inability to use the Service; unauthorized access to or changes in Content or information you submit; and the acts and statements of third parties who use the Service.

      -
    8. -
    9. -

      You agree that the OTW shall not be liable to you or any third party for any termination of or limitation on your access to AO3. The OTW may change, end, or put on hiatus the Service, or parts of its Services, at any time.

      -
    10. -
    11. -

      You agree that the OTW shall not be liable to you for any claim arising out of Content you make available, your use of the Service, your connection to the Service, your use of the ToS, or your violation of any rights of another.

      -

      In other words, the OTW is not liable to you for allowing you to post Content, download Content, use the Service, or interact with AO3. The OTW does not assume whatever legal risks you face by posting, viewing, or doing other things with Content.

      -
    12. -
    - -
    D. What you can't do:
    -

    You agree not to use the Service (as well as the e-mail addresses and URLs of OTW sites):

    -
      -
    1. -

      to make available any Content or work that violates the Content Policy;

      -
    2. -
    3. -

      to impersonate any person or entity, including, but not limited to, an AO3 or OTW representative or volunteer, or falsely state or otherwise misrepresent your affiliation with a person or entity (fiction marked as such, including real-person fiction in first-person format, is not subject to this policy);

      -
    4. -
    5. -

      to forge headers or otherwise manipulate identifiers in order to disguise the origin of any Content transmitted to or through the Service or any OTW Content, sites, servers, networks, or services (headers and identifiers are defined as information actually used or intended to be used to route or authenticate Content, and do not include Content that simulates identifiers as part of a story, such as a fictional e-mail exchange);

      -
    6. -
    7. -

      to make available any Content that a court has ruled constitutes patent, trademark, trade secret or copyright infringement (please be aware of the OTW's position on fanwork legality);

      -
    8. -
    9. -

      to make available any unsolicited or unauthorized advertising (defined as solicitations for direct or indirect commercial advantage), junk mail, spam, chain letters, pyramid schemes, or any other form of solicitation;

      -
    10. -
    11. -

      to make available any material that contains software viruses or any other computer code, files or programs designed to interrupt, destroy or limit the functionality of any computer or hardware or telecommunications equipment;

      -
    12. -
    13. -

      to interfere with or disrupt the Service, any OTW-hosted Content or sites, servers, Services or networks connected to OTW sites;

      -
    14. -
    15. -

      to create an account if you are a resident or national of any country to which the U.S. has prohibited transactions by mandating a trade embargo, as detailed further by the State Department;

      -
    16. -
    17. -

      to create an account if you are under the age of thirteen;

      -
    18. -
    19. -

      to create an account if you are: between the ages of 13 and 16, and a resident or citizen of a country, including a European Economic Area country that states an age that is older than yours is required to consent to the processing of personal data without our obtaining written permission from a parent or legal guardian under the General Data Protection Regulation ("GDPR").

      -

      A list of countries that apply the GDPR can be found here. Each user is responsible for knowing the laws of their own country.

      -
    20. -
    21. -

      to use OTW services to break any law that applies to you, including any rules or regulations having the force of law. Just by way of example, do not use the services to disseminate restricted technologies or violate laws governing the export of technical data. This provision is not intended to deal with matters subject to the Content Policy, but the Content Policy cannot cover every law in every country. As a general matter, the Archive follows US law. Content that is alleged to violate the law of a relevant jurisdiction will be dealt with according to the procedures established in the Abuse Policy.

      -
    22. -
    - -
    E. Content you see through use of the Service:
    -
      -
    1. -

      The OTW or users of its services may provide links to or content via sites that are owned or controlled by third parties, and may use such sites, including Twitter and Tumblr, to communicate information about the OTW and its family of sites. The OTW has no control over such sites or their terms of use or privacy policies, and you agree that the OTW is not responsible for and does not endorse their content, terms or availability.

      -
    2. -
    3. -

      While we limit the types of embedded content visible or accessible via Archive-hosted Content, some of the content and/or works that you see displayed at the Archive are not hosted on the Archive or by the OTW. Such embedded content can include videos, tweets, images that are hosted by third-party sites, or audio files ("User-Embedded Content"). If you access a page that includes User-Embedded Content, the content file may share data with the hosted site as if you were on or at the hosted site. Although all visible Content, including embedded images or other works, must comply with our Content Policy, User-Embedded Content is not otherwise governed by these Terms of Use or the Archive Privacy Policy, and instead is covered by the Terms of Use and/or Privacy Policy of the service that hosts the User-Embedded Content. The Archive reserves the right to provide an indicator to users that User-Embedded Content is present on or visible via your Content.

      -
    4. -
    5. -

      You understand that the OTW does not prescreen Content or review it for purposes of compliance with the ToS. This includes but is not limited to work information, a work's content, text, graphics, comments, or any other material. Content, including User-Embedded Content, is the sole responsibility of the submitter. You understand that using the Archive may expose you to material that is offensive, triggering, erroneous, sexually explicit, indecent, blasphemous, objectionable, grammatically incorrect, or badly spelled.

      -
    6. -
    7. -

      You recognize that the OTW does not endorse Content on the Archive in any way, except when material appears as an official statement of the OTW. No committee members, officers, or directors of the OTW are acting in an official capacity when they post fanworks, commentary, or other Content of the type generally provided by site users.

      -
    8. -
    9. -

      The OTW is not liable to you for any Content to which you are exposed on or because of the Archive.

      -
    10. -
    - -
    F. Valid e-mail address:
    -
      -
    1. -

      As part of your registration, you agree to provide an accurate and current e-mail address and to update the address as necessary. If your e-mail address is inaccurate or not current, the Archive may suspend your account. Content provided by suspended accounts before the suspension will not be removed unless the OTW Board reasonably believes that presence/inclusion of said Content exposes the Archive and/or the OTW to liability.

      -
    2. -
    3. -

      We reserve the right to share your e-mail address with the owners and/or moderators of any Challenges that you sign up for, and during Open Doors importation where the email address you provided on your AO3 account matches an email address that you used in connection with a non-AO3 collection that is being imported into AO3, your email address may be shared with the entity that manages the work(s) that is/are being imported onto AO3 through an Open Doors process.

      -
    4. -
    5. -

      By registering or otherwise using an e-mail address in connection with the Archive, you assert that the e-mail address is not listed on any child protection registry. (There are child protection registries in Michigan and Utah, and our policy applies to any others that exist or may be adopted, whether local, state, or national, including any outside the U.S.)

      -
    6. -
    - -
    G. What we do with Content:
    -

    The OTW does not claim any ownership or copyright in your Content. Repeat: we do not own your Content. Nothing in this agreement changes that in any way. Running the Archive, however, requires us to make copies, and backup copies, on servers that may be located anywhere around the world.

    -
      -
    1. -

      You agree that we can make those copies and show your Content to other people, subject to your privacy settings. Specifically, by submitting Content, you grant the OTW a world-wide, royalty-free, nonexclusive license to make your Content available. "Making available" includes distributing, reproducing, performing, displaying, compiling, and modifying or adapting (refer to the ToS FAQ).

      -

      Modifying and adapting here refer strictly to how your work is displayed—not how it is written, drawn, or otherwise created. But because your Content may be transmitted over various networks, we may have to make changes to the formatting or display of your Content in order to adapt to the technical requirements of different networks or devices. Multimedia content may not display properly on all devices. In some circumstances, we may make changes to improve accessibility. For example, we may automatically convert html tags to our standard forms (e.g., changing "bold" html to "strong"). Or we may make special provisions for accessibility, such as allowing you to use nonstandard fonts but also providing an alternate format for those who cannot read such fonts. We may use an internal search engine whose results display relevant snippets from your Content.

      -

      User-provided tags are subject to organization, which is a process we call tag wrangling; for a full explanation of tag wrangling, refer to the ToS FAQ.

      -
    2. -
    3. -

      Subject to the next paragraph of this policy, this license exists only for as long as you choose to continue to include such Content on the Archive and will terminate within a reasonable time after you remove or the OTW removes such Content from the Archive. We will always strive to make your Content unavailable to users as soon as possible should you choose to remove it. Though removed Content will not be publicly available, for legal and disaster recovery purposes we may retain backup copies for longer periods.

      -
    4. -
    5. -

      You may provide Content to a part of the Archive that you do not completely control. For example, you may decide to participate in a challenge run by another user. Or you may provide Content to a part of the Archive where your Content can be deleted by other users, for example a Comments page. Where this is the case, by submitting Content to those parts of the Service, you agree to the rules for removing and retaining such Content on those parts of the Service.

      -
    6. -
    7. -

      You acknowledge and agree that the OTW may preserve Content and may disclose Content if required to do so by law or in the good faith belief that such preservation or disclosure is reasonably necessary to: comply with legal process; enforce the ToS; respond to claims that any Content violates the rights of third parties; or protect the rights, property, or personal safety of the OTW services' users and the public.

      -
    8. -
    - -
    H. Data and Content Processing
    - -

    AO3 and the OTW exist to host Content by creative people like you. In order to host or share your works, as well as comments, Kudos and whatever you put into your User Profile, we have to process certain data and information including personally identifying information that we collect from Users, and that each User inputs. In order to operate the site, host OTW and user Content, and prevent technical issues and breaches, we need to process (e.g. collect, store, retrieve, disseminate, make available, and delete) certain data and information including personally identifying information, also known as "Personal Data". Personal Data includes your username, your email address, your IP information and any personally identifying information you enter on the Archive, including information that you put into your profile, a work's notes or tags, into the body of a work or other Content itself.

    - -

    By using the site, you consent to our collection, processing, retention and display of your Content as set forth and explained in these Terms of Use so we can operate, manage, display and share the creativity on AO3; if we believe that using, retaining and/or sharing that information is necessary to preserve the integrity of the Archive and the Content that we host; for legitimate legal and/or accounting audit purposes; when we have a good faith belief it is required by law, such as pursuant to a subpoena or other legal process; or when we have a good faith belief that doing so will help prevent imminent harm to someone.

    - -

    II. Archive Age Policy

    -

    This Age Policy covers the Archive's treatment of (a) users who are residents or citizens of the European Union and of the age where consent of a parent or legal guardian is required for the processing of personal data of children including email addresses and IP addresses, as well as certain uses of cookies; and (b) users who are under the age of thirteen (13) and residents or citizens of any other country.

    -

    In compliance with United States regulations regarding online privacy for children, the Archive does not knowingly solicit or collect information from children under the age of thirteen (13). Children under the age of thirteen (13) are therefore not permitted to have an account or upload Content of any type to the Archive. By submitting Content to the Archive, you thereby confirm that you are thirteen (13) years old or older (refer to the ToS FAQ).

    -

    Asking a parent or legal guardian to upload Content does not constitute submitting Content under this policy. If you are under the age of thirteen (13) and not an Age-Barred Individual (as defined below), your parent or legal guardian may upload your Content through their account.

    -

    We as an organization have opted to protect teen users' privacy. As a result, we cannot receive or host Content from individuals that we know are under the age of sixteen (16) and residents/citizens of the European Union, unless they are residents/citizens of the EU countries that allow the collection of Special Categories of Personal Data from those at a younger age and are old enough to consent to the collection of Special Categories of Personal Data in their own country. Any individual who is between the ages of 13 and 16 and a resident or citizen of a country, including a European Economic Area country, that states an age that is older than theirs is required to consent to the processing of personal data without our obtaining written permission from a parent or legal guardian under the GDPR ("Age-Barred Individual") may not maintain accounts or submit Content. AO3 Personnel may, in their reasonable discretion, hide and/or delete accounts held by and/or Content submitted by Age-Barred Individuals.

    - -

    III. Archive Privacy Policy

    -
    A. Who runs the Archive
    -

    The Archive is a project of the Organization for Transformative Works (OTW), which is committed to fan privacy. For more information about how you can support the OTW, please see the OTW Website. This Privacy Policy governs the Archive.

    - -
    B. This TOS, including the Privacy Policy, applies only to the Archive
    -

    This Privacy Policy covers the Archive's treatment of personally identifying information submitted to us, and which we collect when you use our services in the course of ordinary communications. Each site or Service hosted by the OTW has its own Terms of Service and Privacy Policy. The OTW or users of its Services may provide links to or content via sites that are owned or controlled by third parties, and may use such sites, including Twitter and Tumblr, to communicate information about the OTW and its family of sites including the Archive. The OTW has no control over such sites or their terms of use or privacy policies, and you agree that the OTW is not responsible for and does not endorse their content, terms or availability. If you follow links off the Archive, you should review those sites' privacy policies, which may be different, and for which the Archive takes no responsibility.

    - -
    C. About possible changes:
    -

    If these Terms of Service and/or this Privacy Policy change at any point in the future, we will post the policy changes to the Archive of Our Own. Such changes will be used only for information provided by those who have visited, used, or accessed the Service after the effective date of such policy changes. If you are concerned about how your information is used, you should check back at the Archive site periodically to review the policies.

    - -
    D. Archive adherence to the TOS:
    -

    If you feel that this Service is not following its stated information policy, please contact Archive Personnel.

    - - -
    E. What we will do:
    -
      -
    1. -

      Any information you include in your work, comment, profile, bookmark, summary, or other Content, including information about your religious views, political views or your sexual identity, or any personally identifying information such as your email address, location, or account User Name for other sites will be accessible by the general public if the Content is marked public, and by Archive users and personnel if the Content is marked accessible to Archive users only. If you save the Content in Draft form, it will be accessible by certain Archive personnel.

      -
    2. -
    3. -

      We may collect personally identifying information such as your IP address and e-mail address when you request an Archive invitation, register for a user account with the Archive, visit any of the sites and services that are part of the OTW family of sites, or use any of the Archive's Services. We may use third-party services to store, process, or transmit data, or perform other technical functions related to operating the Service. These services may include spam detectors, backup services, icon hosting, and e-mail services; a list of third-party services is provided in our Subprocessor List. We cannot guarantee other services' performance. We or the services we use may store or process your personally identifying information in data centers which may be located in the United States or other countries.

      -
    4. -
    5. -

      We will use your email address internally for purposes of managing AO3 and maintaining site integrity; if you agree to allow your email address to be public, it will be public and you can change that setting any time. You can choose to leave it private, or make it public on the Site; if you opt to make it public, everyone has the ability to access it and use it for any purpose. We may occasionally send emails to you from the Archive about your Content and account, the Archive and our fundraisers, as well as news that we reasonably believe to be of interest to our registered users; we will send you your invitation to register for the Archive via email as well. By creating and maintaining a User Account on and with AO3, you consent to receiving such emails, including fundraising emails. We reserve the right to send you notice of complaints raised against you, or alleged violations by you of the Terms of Service, as well as to reply to any email message you send to the Archive and/or its personnel. If you choose to participate in a Challenge, the Challenge maintainer(s) may have access to your e-mail address for purposes of communicating with you (See Section V.I.a. below for discussion of Challenges).

      -
    6. -
    7. -

      We collect, process and retain the following data for the following reasons:

      -
        -
      1. -

        E-mail Addresses: We collect email addresses of and from those who communicate with us via email and any Content or Personal Data included in emails to us. We need this information so we can respond to you, and so we can handle complaints about the Service and any users who may have violated the Terms of Use or other policies, and for other legal and accounting/audit reasons including maintaining the integrity of the Archive and the Content that we host;

        -
      2. -
      3. -

        User-Specific Information: We collect information about what pages users access or visit including your interactions with integral Archive features such as Kudos, Comments, Bookmarks and the tags that you "favorite", as well as referral information (i.e. data about what site you are coming to the Archive from) and whether there are errors in displaying Content to you. We need this information to maintain the integrity of the site, the Service and the Content that we host; to provide you with the Content that you are seeking; to minimize "gaming" Kudos counts and spam; and for other legal and accounting/audit reasons.

        -
          -
        1. -

          You consent to our collection, processing, retention and display of your Content, including personal data, when you upload a work in a manner that is available to all Archive users, or the general public, depending on which you choose. We need such consent because your purpose in and reason for uploading Content to the Archive is for that Content to be visible to users, and if you opt to, for it to be visible to the general public.

          -
        2. -
        3. -

          You consent to our collection, processing and retention of your Content, and personal information connected therewith, including display to you and certain Archive personnel when you upload a work in "Draft" form. We need such consent because your purpose in and reason for uploading such Draft Content to the Archive is for that Content to be accessible to you at a later date. In order for that Content to be available to you, it also needs to be accessible to those on the Archive team who have access to our servers, and to the Board, Policy & Abuse and Legal committees if necessary to enforce these Terms of Service and maintain the integrity of the Service.

          -
        4. -
        5. -

          You consent to our collection, processing, retention and display of personal information and Content you submit for inclusion on your Profile Page. We need such consent because your purpose in and reason for including information in your Profile is for that Content to be visible to the general public.

          -
        6. -
        7. -

          You consent to our collection, processing, retention and display of text that you submit as Comments, as well as your personal information associated therewith. We need such consent because your purpose in and reason for submitting a Comment is for that Content to be visible to the general public.

          -
        8. -
        9. -

          When you are logged in, you consent to our collection, processing, retention, display and use of your User Name as well as our association of it with Kudos that you submit. We need such consent because your purpose in and reason for submitting Kudos is for that Content to be visible to the general public.

          -
        10. -
        11. -

          Whether you are logged in or not, you consent to our collection, processing, retention and use of your IP address for a limited time, including when you give Kudos. We need this consent because your purpose in and reason for submitting Kudos is to engage publicly with the Content that you enjoyed without publicly associating your Kudos with a User Name, and you wish for such Kudos to be publicly visible and disassociated from your Archive Account, if you have one; and because we endeavour to count only one view of a URL per cookie session on the "Hit" count for a work. Temporarily collecting, processing, and retaining IP addresses permits us to conduct internal management of Kudos and Hits.

          -
        12. -
        -
      4. -
      5. -

        IP Addresses: we collect and process the IP addresses of Archive visitors, including registered users. We need this information so we can provide you with the Content that you are requesting, to allow you to submit Comments, Content and Profile information, and leave Kudos, and for other legal and accounting audit reasons including maintaining the integrity of the Archive and the Content that we host. Certain IP information may be collected by the server for log purposes and used for limited technical assessments of the Service.

        -
      6. -
      7. -

        Logs: We collect and process Logs of server interactions, as well as event logs. We need this information for legal and accounting/audit reasons, including maintaining the integrity of the Archive and the Content that we host.

        -
      8. -
      9. -

        Cookies: We use cookies to collect and store visitors' preferences; customize web pages' content based on visitors' browser type or other information that the visitor sends; and record activity at the Archive in order to provide better service when visitors return to our site. Cookies must be enabled for the Archive to function properly with your computer. The Archive has no access to cookies set by other sites.

        -
      10. -
      11. -

        If for any reason you terminate your user account with us, we will destroy active records containing your Personal Data as soon as reasonably possible. "Reasonably" here means no more than thirty business days from the termination of the account; however, we may have to retain some information for a longer period as legal records or for auditing purposes. If we terminate your service, we may retain enough information to prevent you from signing up for the service in the future. If you delete Content from the Archive, it will not be recoverable.

        -
      12. -
      13. -

        If we receive a complaint about a violation of someone's intellectual property rights, we reserve the right to disclose the information provided by the rightsholder or the rightsholder's representative to the subject of the complaint. If we receive a complaint about other matters, such as plagiarism or harassment, we will follow the Abuse Policy, which explains what information provided by the complainant may be disclosed to the subject of the complaint.

        -
      14. -
      -
    8. -
    - -
    F. What we will not do:
    -
      -
    1. -

      The OTW will not provide any third party with any of your personal information or any of the information we collect, except as provided under our Terms of Service. We will not use your personally identifying information to market third party products and services to you via the Archive.

      -
    2. -
    3. -

      We will not sell, trade, or rent your personally identifying information. Except as provided under this policy, we will not disclose your personally identifying information to any third party without your prior consent unless we (1) are legally compelled to do so, (2) have a good-faith belief that such action is necessary to comply with a current judicial proceeding, a court order, or legal process served on the OTW, or (3) are cooperating with law enforcement authorities. As to (3), we will cooperate with all investigations conducted by law enforcement authorities within the United States of America when legally required to do so. Cooperation with law enforcement authorities from other countries and cooperation when it is not legally required are at our sole discretion. Our discretion looks favorably on freedom and justice, and unfavorably on oppression and violence.

      -
    4. -
    5. -

      Unless legally prohibited, we will attempt to notify you any time we disclose your personally identifying information to a third party. In some cases, the information we have, such as an IP address, may be insufficient for us to notify you.

      -
    6. -
    - -

    IV. Content and Abuse Policies

    -

    We recognize that there is no such thing as a popular abuse policy. By their nature, abuse complaints are unpleasant at best. And policy needs to be applied by people, which always complicates matters. We have tried to set out clear procedures to minimize and channel the inevitable conflicts.

    -

    The Content and Abuse Policy covers procedures, spam and commercial promotion, threatening the technical integrity of the site, copyright, plagiarism, personal information and fannish identities, harassment, illegal and non-fanwork content, and ratings and warnings. We have developed a FAQ page to answer additional questions about Content and Abuse (refer to the ToS FAQ).

    -

    The Archive does not prescreen for content. Complaints are investigated only when they are submitted through the appropriate channels and with the appropriate information.

    - -
    A. Procedures
    -
      -
    1. Submitting a complaint -

      Complaints may be submitted to our Policy & Abuse team. Except in the case of copyright complaints, a complainant may submit a complaint via the Policy & Abuse web form. Depending on the nature of the complaint, anonymity of the complainant may hinder our ability to examine and/or verify the complaint. In order for personnel to follow up on any allegation, the exact location (URL) and nature of the alleged violation must be supplied in the original complaint. Repeated unverified complaints from the same source may be subject to summary rejection.

      -
    2. -
    3. Treatment and investigation of complaints -

      Only people who need to know about a complaint will be informed about it. The details of any individual complaint are confidential and must be used only in resolving that complaint.

      -

      The subject of a complaint may also be among those who need to know about it. Only information provided in the complaint will be passed on. The complainant has complete control over what information is submitted to Policy & Abuse, and can submit a complaint pseudonymously. (Legal names and other information sufficient to identify a person in the physical world will never be disclosed as part of a standard abuse complaint. For further clarification, please refer to our privacy policy.)

      -

      In general, the Policy & Abuse team will only communicate with the subject of a complaint if there appears to be a violation of site policy, or if personnel need more information to resolve the issue.

      -

      The Policy & Abuse team records the IP address from which each complaint is submitted, to prevent misuse of the system and/or Service.

      -

      Subject to obligations of confidentiality about specific complaints, the Policy & Abuse team may release statistics about general trends, such as the number of plagiarism complaints made and the actions taken by the team, in public or to other OTW committees, to facilitate discussions of policies, procedures, and trends in complaints.

      -

      When Policy & Abuse personnel determine that Content needs to be removed for reasons other than violation of the Age Policy by Age-Barred Individuals, the team will identify the nature of the problem with the Content, and set a deadline for voluntary removal of the Content. An administrator may also hide Content from other users where appropriate. If the original poster does not remove the Content within the deadline, the Archive will remove the Content. In addition, we may remove Content immediately, without waiting for a response, if we are contacted by a legitimate law enforcement agency or if we determine that the Content is threatening to an individual or reveals an individual's personal information without consent. In such cases we will inform the original poster as soon as possible. The original poster will then have the option to resubmit with the violating Content removed.

      -

      The Policy & Abuse team may also determine that tags need to be added to or edited in an item of Content. For more information, please see the ratings/warnings section.

      -

      If Content violating the ToS is posted with invalid contact information, it will be removed without prior notice.

      -

      If the complainant requests notification of the resolution of the complaint and provides contact information, we will notify them.

      -
    4. -
    5. Submitting an appeal -

      The complainant or the original poster may appeal a decision to the Policy & Abuse team as a whole. During the appeal, the original decision will remain in effect. We will attempt to resolve appeals as speedily as possible, but please remember that the Policy & Abuse team is entirely comprised of volunteers. The team's decisions are final unless overturned by the Board at the Board's sole discretion. There is no right of appeal to the Board. The team, however, may consult with the Board or with committee members authorized by the Board if the team decides that consultation would help resolve an issue.

      -
    6. -
    7. Account statuses -

      "Account status" refers to the existence of warnings on an account and whether that account has been suspended, temporarily or permanently (refer to the ToS FAQ).

      -

      The Policy & Abuse team may issue warnings when it determines that a violation of the ToS was minor or unintentional. More serious, intentional, or repeated violations of the ToS will trigger suspensions. A suspension will generally be for a defined period of time, such as a month. The team may also permanently suspend users when it determines that such action is justified. Permanent suspensions for violations other than spam, violation of the Archive's Age Policy by Age-Barred Individuals, or threatening the technical integrity of the site require a majority vote of the team. The team's discretion will be informed by the nature of the violation and the response of the user, including the user's decision to voluntarily remove, modify or edit the Content that violates the ToS.

      -

      A user whose account has been permanently suspended for reasons other than age may not rejoin under another identity.

      -

      Where possible, we encourage users to try mediating disputes before contacting Policy & Abuse. We provide tools for registered users to control Content that is uploaded in their own spaces. For example, users are able to delete comments on their own stories. If you are unable to resolve the problem on your own, you can file a complaint with the Policy & Abuse team.

      -

      In some cases, objectionable Content may have already been deleted before the Policy & Abuse team acts. We appreciate good faith attempts to resolve disputes, and in most such cases will close the complaint with no further action. However, we reserve the right to consider individual circumstances, including whether the poster has engaged in a pattern of such conduct. In such cases, if we verify that the original Content violated the ToS, we may still decide to warn or suspend the original poster.

      -

      Penalties apply to users, not to screen names/pseudonyms. Penalties are not retroactive: a suspended user's nonobjectionable Content will not be automatically removed unless the user is an Age-Barred Individual. Suspended users retain the right to delete or Orphan their fanworks by contacting Archive administrators.

      -
    8. -
    - -
    B. Spam and commercial promotion
    -

    Promotion of commercial products or activities is not allowed. Repeated identical or nearly identical posts in multiple places, e.g., a large number of identical comments promoting a website, will also be considered spam regardless of commercial content.

    -

    Anything we determine is spam will be removed immediately. Users may be permanently suspended for spam the first time they post spam content.

    -

    In general, unsolicited commercial activity is not permitted on the Archive. The Policy & Abuse team has discretion to decide that a fan-related offer was mistakenly disseminated and issue a warning instead of a suspension.

    -

    Any spam- or commercial activity-related penalties may be appealed using the ordinary appeal process.

    -

    We may use automated means to filter out spam. If you submit Content that is erroneously caught in a spam filter, please notify Archive administrators.

    - -
    C. Threatening the technical integrity of the Service
    -

    Conduct that threatens the technical integrity of the Archive, e.g. attempts to hack the Service or spread viruses through it, will result in an immediate account suspension and deletion of any Content that is hazardous to the operation of the Service or to users' computers. Users may be permanently suspended for threatening the technical integrity of the Archive the first time they do so. Such suspensions may be appealed using the ordinary appeal process.

    -

    Uploading technically misnamed files or Content—e.g., non-image files with an image file extension used to disguise their actual format—constitutes a threat to the technical integrity of the site.

    - -
    D. Copyright and Trademark
    -

    Please be aware that the OTW believes that transformative fanworks are legal; therefore, complaints based merely on the existence of fanwork based on copyrighted content or mentioning trademarks will not be pursued.

    -

    If you believe that your content has been reproduced in whole or in part, without transformative use (transformative use is defined by the OTW as adding something new, with a further purpose or different character, altering the source with new expression, meaning, or message), please follow our procedures for reporting copyright infringement.

    -

    The report must clearly and specifically indicate the exact location (URL), nature, and extent of each instance of allegedly infringing content, as well as the exact copyrighted material that is being infringed, as well as full and complete contact information for the copyright holder and/or an authorized agent.

    -

    Epigraphs and short quotations, including quotations from song lyrics and poetry, are allowed. Content that is set within or draws on an existing work is allowed. Reproductions of entire copyrighted works—whether songs, poems, transcripts, or other material—are not allowed without the consent of the copyright owner.

    - -
    E. Plagiarism
    -

    Plagiarism is an often-contested and fuzzy concept, and no definition will satisfy everyone. Our aim is to be transparent and fair in resolving disputes.

    -

    Plagiarism is the use of someone else's words or concepts without properly attributing those words or concepts to their original source. Simply finding and replacing names, substituting synonyms, or rearranging a few words is not enough to make the work original to you. Deliberately writing a work using the same general idea as another work is not plagiarism, but citation is always appreciated. Generally, quotes from the source material (canon) on which the Content is based will not constitute plagiarism, nor will obvious allusions ("Use the Force, Luke!"). However, when in doubt, cite. Be aware that the Policy & Abuse team may decide that your citation is not sufficient to render the work your own; a mere nod to another author whose work you are presenting as your own may result in a judgment of plagiarism.

    -

    Plagiarism is a violation of the ToS and will incur the penalties described in the Abuse Policy. As with all Content that violates the ToS, plagiarized Content must be removed. Depending on the type and amount of plagiarized Content, this might entail removing an entire piece of Content, removing only the plagiarized portions from a longer work while leaving the original material, or adding citations.

    -

    If you believe a fanwork posted on the Archive plagiarizes another work, please report the work to the Policy & Abuse team. In order to allow us to investigate, please provide a link to the work on the Archive, relevant excerpts, and a specific citation of the original material (for example, a URL or a book edition and page number).

    - -
    F. Personal information and fannish identities
    -
      -
    1. -

      The OTW is committed to protecting the privacy of our users, including the separation many fans choose to keep between their legal, "real life" names and their fannish pseudonyms. Unauthorized disclosure of a fan's personal information and/or data which is included within the definition of Special Categories of Personal Data violates the Terms of Service. For these purposes, "personal information" may include legal names and other information sufficient to identify a person in the physical world that they have not voluntarily shared on the Archive itself. "Personal information" may also include the identity of the creator of an Orphaned fanwork. The Archive reserves the right to delete, hide, mask or otherwise make unavailable to the general public any personal information and/or data which is included within the definition of Special Categories of Personal Data.

      -
    2. -
    3. -

      If you are a resident or citizen of the European Community, you can request that the OTW assemble the data about you that it has in our archive, and provide a copy in electronic format to you; we agree to provide such data to you within a reasonable time.

      -
    4. -
    - -
    G. Harassment
    -

    Harassment is any behavior that produces a generally hostile environment for its target. This includes activities such as bullying and hazing by groups of people as well as personal attacks by individuals. Not everyone agrees about what is offensive and unacceptable. Individual users are encouraged to try to resolve problems on their own before contacting the Policy & Abuse team.

    -

    Harassment is not allowed. Users engaging in this behavior may be warned, suspended, or permanently suspended as described in the general abuse procedure.

    -

    When judging whether a specific incident constitutes harassment, the team will consider factors such as whether the behavior was repeated, whether it was repeated after the offender was asked to stop, whether the behavior was targeted at a specific person, whether that target could have easily avoided encountering the behavior, whether the behavior would be considered unacceptable according to normal community standards, etc. Additionally, making complaints that are both (a) repeated and (b) baseless, particularly those targeting a specific user, can be considered harassing behavior and may be deemed a violation of the ToS.

    -

    While these complaints will be reviewed on a case by case basis, in general, threatening Content will be considered harassment, while Content that is merely annoying will be allowed. Please note that most statements like "X is a terrible actor and should die!" are not death threats. Writing a story where X dies as part of the plot is also not usually a death threat. Content that is harder to avoid (such as comments on the target's fanworks) will be judged more strictly than Content that is easily avoidable (such as stories).

    -

    The behavior of the original poster (the complaint subject) may also affect the team's determination. If the original poster repeatedly contacts the subject of the Content about the Content after being told to stop, harasses the subject, or requests that others harass the subject, the Content may be considered part of a general pattern of harassment and be removed. Please use your best judgment both when producing Content of this type and when reporting it.

    - -
    A special note on RPF (real-person fiction):
    -

    Writing RPF (real-person fiction) never constitutes harassment in and of itself. However, Content that advocates specific, real harmful actions towards real people is not allowed. This includes, but is not limited to, death threats and requests for readers to harass specific people. If you find Content that you believe contains harassing or threatening material, please contact the Policy & Abuse team. As Real-Person Fiction is fictional, generally Archive policy will be that Content in RPF that would be deemed Personal Data and/or Special Categories of Personal Data (e.g. full names, usernames on social media services, city of residence, birth date) will not be considered as such. However, if information that is accurate, non-public and not that of the User is included (i.e. non-public phone numbers, residential addresses, email addresses or hotel room numbers) the work can be removed from public view by the Archive's Policy & Abuse team in its sole discretion.

    - -
    H. Illegal and inappropriate Content
    -

    The Archive of Our Own is a place for fanworks. Content may not be uploaded to OTW's servers if it contains or links to child pornography (images of real children); warez, cracks, hacks or other executable files and their associated utilities; trade secrets, restricted technologies, or classified information; or if it consists entirely of actual instruction manuals, technical data, recipes, or other non-fanwork content, including non-fanwork creative work (refer to the ToS FAQ). Uploading such Content is a violation of the ToS.

    -

    We may determine that we need to remove Content to resolve a threatened or pending lawsuit or mitigate other liability. If so, we will remove the Content. Unless said Content or data otherwise violates the ToS or was submitted by an Age-Barred Individual, removal for such reasons will not lead to a suspension.

    -

    If you believe Content violates a specific law, you may report it to us. Please, however, read our Offensive Content Policy below.

    - -
    I. Offensive Content Policy
    -

    As provided in part I.E.3 of the Terms of Service, the OTW is not liable to you for any Content to which you are exposed on or because of the Service.

    -

    Unless it violates some other policy, we will not remove Content for offensiveness, no matter how awful, repugnant, or badly spelled we may personally find that Content to be. +<%# IMPORTANT: Also update current_tos_version in application_controller %> +<%# To ensure proper formatting, this must always be rendered inside an element + with the userstuff class. The userstuff element must be inside an element with + the classes "docs system". %> +

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

    + +

    <%= t(".what_we_believe.header") %>

    +
      +
    1. + <%= t(".what_we_believe.our_goal_html", + maximum_inclusiveness_link: link_to(t(".what_we_believe.maximum_inclusiveness"), tos_faq_path(anchor: "max_inclusiveness"))) %> +
    2. +
    3. + <%= t(".what_we_believe.ao3_run_by_html", + defending_fanworks_link: link_to(t(".what_we_believe.defending_fanworks"), "https://www.transformativeworks.org/faq/#faq-legalfaq"), + on_the_otw_site_link: link_to(t(".what_we_believe.on_the_otw_site"), "https://www.transformativeworks.org/legal")) %> +
    4. +
    5. + <%= t(".what_we_believe.we_do_not_sell_html", + ao3_link: link_to("archiveofourown.org", root_path), + fanlore_link: link_to("fanlore.org", "https://fanlore.org/"), + transformativeworks_link: link_to("transformativeworks.org", "https://www.transformativeworks.org/")) %> +
    6. +
    7. + <%= t(".what_we_believe.readability_html", + tos_faq_link: link_to(t(".what_we_believe.tos_faq_html", + faq_abbreviation: tag.abbr(t(".what_we_believe.faq.abbreviated"), title: t(".what_we_believe.faq.full"))), + tos_faq_path)) %> +
    8. +
    + + + +

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

    +

    <%= t(".general_terms.heading") %>

    +
      +
    1. +

      + <%= t(".general_terms.agreement.html", + agreement: tag.strong(t(".general_terms.agreement.agreement")), + content_policy_link: link_to(t(".general_terms.agreement.content_policy"), content_path), + privacy_policy_link: link_to(t(".general_terms.agreement.privacy_policy"), privacy_path), + personal_information_link: link_to(t(".general_terms.agreement.personal_information"), privacy_path(anchor: "III.A.1")), + any_other_form_link: link_to(t(".general_terms.agreement.any_other_form"), tos_faq_path(anchor: "define_content"))) %> +

      +
    2. +
    3. +

      + <%= t(".general_terms.entirety_of_agreement.html", + entirety_of_agreement: tag.strong(t(".general_terms.entirety_of_agreement.entirety_of_agreement"))) %> +

      +
    4. +
    5. +

      + <%= t(".general_terms.jurisdiction.html", + jurisdiction: tag.strong(t(".general_terms.jurisdiction.jurisdiction")), + the_state_of_new_york_link: link_to(t(".general_terms.jurisdiction.the_state_of_new_york"), tos_faq_path(anchor: "ny_law"))) %> +

      +
    6. +
    7. +

      + <%= t(".general_terms.non_severability.html", + non_severability: tag.strong(t(".general_terms.non_severability.non_severability"))) %> +

      +
    8. +
    9. +

      + <%= t(".general_terms.limitation_on_claims.html", + limitation_on_claims: tag.strong(t(".general_terms.limitation_on_claims.limitation_on_claims"))) %> +

      +
    10. +
    11. +

      + <%= t(".general_terms.no_assignment.html", + no_assignment: tag.strong(t(".general_terms.no_assignment.no_assignment"))) %> +

      +
    12. +
    + +

    <%= t(".updates_to_the_tos.heading") %>

    +

    + <%= t(".updates_to_the_tos.html", + content_policy_link: link_to(t(".updates_to_the_tos.content_policy"), content_path), + privacy_policy_link: link_to(t(".updates_to_the_tos.privacy_policy"), privacy_path)) %> +

    + +

    <%= t(".potential_problems.heading") %>

    +
      +
    1. <%= t(".potential_problems.service_as_is") %>

    2. +
    3. <%= t(".potential_problems.breach_notification") %>

    4. +
    5. <%= t(".potential_problems.own_risk") %>

    6. +
    7. +

      + + <%= t(".potential_problems.disclaim_warranties_html", + merchantability_link: link_to(t(".potential_problems.merchantability"), tos_faq_path(anchor: "merchantability")), + fitness_for_purpose_link: link_to(t(".potential_problems.fitness_for_purpose"), tos_faq_path(anchor: "fitness"))) %> + +

      +
    8. +
    9. <%= t(".potential_problems.damage_liability") %>

    10. +
    11. <%= t(".potential_problems.account_termination_liability") %>

    12. +
    13. <%= t(".potential_problems.content_access_liability") %>

    14. +
    15. <%= t(".potential_problems.not_personal_storage_html", sole_backup_responsibility: tag.strong(t(".potential_problems.sole_backup_responsibility"))) %>

    16. +
    + +

    <%= t(".content_you_access.heading") %>

    +
      +
    1. <%= t(".content_you_access.external_links_html", here_link: link_to(t(".content_you_access.here"), "https://www.transformativeworks.org/where-find-us/")) %>

    2. +
    3. +

      + <%= t(".content_you_access.third_party_content_html", + hosted_by_third_party_link: link_to(t(".content_you_access.hosted_by_third_party"), tos_faq_path(anchor: "nontextual_fanworks")), + content_policy_link: link_to(t(".content_you_access.content_policy"), content_path), + privacy_policy_link: link_to(t(".content_you_access.privacy_policy"), privacy_path)) %> +

      +
    4. +
    5. <%= t(".content_you_access.no_prescreen") %>

    6. +
    7. +

      + <%= t(".content_you_access.no_otw_endorsement_html", + official_statement_link: link_to(t(".content_you_access.official_statement"), tos_faq_path(anchor: "official_statement"))) %> +

      +
    8. +
    9. <%= t(".content_you_access.otw_not_liable") %>

    10. +
    + +

    <%= t(".what_we_do_with_content.heading") %>

    +

    <%= t(".what_we_do_with_content.no_copyright_ownership_html", we_repeat: tag.strong(t(".what_we_do_with_content.we_repeat"))) %>

    +
      +
    1. +

      + <%= t(".what_we_do_with_content.agree_otw_can_copy_html", + worldwide_royalty_free_nonexclusive_license_link: link_to(t(".what_we_do_with_content.worldwide_royalty_free_nonexclusive_license"), tos_faq_path(anchor: "nonexclusive_license")), + modifying_or_adapting_link: link_to(t(".what_we_do_with_content.modifying_or_adapting"), tos_faq_path(anchor: "modify_adapt")), + tag_wrangling_link: link_to(t(".what_we_do_with_content.tag_wrangling"), archive_faq_path("tags", anchor: "wrangling"))) %> +

      +
    2. +
    3. <%= t(".what_we_do_with_content.license_duration") %>

    4. +
    5. +

      + <%= t(".what_we_do_with_content.content_not_completely_controlled_html", + orphan_link: link_to(t(".what_we_do_with_content.orphan"), archive_faq_path("orphaning")), + challenge_link: link_to(t(".what_we_do_with_content.challenge"), archive_faq_path("glossary", anchor: "challengedef")), + subject_to_moderation_link: link_to(t(".what_we_do_with_content.subject_to_moderation"), "https://www.transformativeworks.org/otw-news-post-moderation-policy/"), + rules_for_removing_such_content_link: link_to(t(".what_we_do_with_content.rules_for_removing_such_content"), tos_faq_path(anchor: "partial_control"))) %> +

      +
    6. +
    7. +

      + <%= t(".what_we_do_with_content.some_content_open_doors_html", + open_doors_link: link_to(t(".what_we_do_with_content.open_doors"), "https://opendoors.transformativeworks.org/en/")) %> +

      +
    8. +
    9. <%= t(".what_we_do_with_content.preserve_for_legal_reasons_html", privacy_policy_link: link_to(t(".what_we_do_with_content.privacy_policy"), privacy_path)) %>

    10. +
    + +

    <%= t(".what_you_cant_do.heading") %>

    +

    <%= t(".what_you_cant_do.you_agree_not_to") %>

    +
      +
    1. +

      + <%= t(".what_you_cant_do.content_violating_policy_html", + content_policy_link: link_to(t(".what_you_cant_do.content_policy"), content_path)) %> +

      +
    2. +
    3. +

      + <%= t(".what_you_cant_do.impersonate_person_or_entity_html", + impersonate_any_person_or_entity_link: link_to(t(".what_you_cant_do.impersonate_any_person_or_entity"), content_path(anchor: "II.G")), + function_link: link_to(t(".what_you_cant_do.function"), tos_faq_path(anchor: "impersonate_function"))) %> +

      +
    4. +
    5. <%= t(".what_you_cant_do.forge_identifiers") %>

    6. +
    7. +

      + <%= t(".what_you_cant_do.copyright_infringement_html", + infringement_of_a_copyright_link: link_to(t(".what_you_cant_do.infringement_of_a_copyright"), content_path(anchor: "II.D")), + position_on_fanwork_legality_link: link_to(t(".what_you_cant_do.position_on_fanwork_legality"), "https://www.transformativeworks.org/faq/#faq-WhydoestheOTWbelievethattransformativeworksarelegal")) %> +

      +
    8. +
    9. +

      + <%= t(".what_you_cant_do.commercial_activity_html", + making_available_any_advertising_link: link_to(t(".what_you_cant_do.making_available_any_advertising"), content_path(anchor: "II.C"))) %> +

      +
    10. +
    11. <%= t(".what_you_cant_do.software_viruses") %>

    12. +
    13. +

      + <%= t(".what_you_cant_do.interfere_disrupt_ao3_html", + interfere_disrupt_ao3_link: link_to(t(".what_you_cant_do.interfere_disrupt_ao3"), content_path(anchor: "technical_integrity"))) %> +

      +
    14. +
    15. +

      + <%= t(".what_you_cant_do.account_if_age_barred_html", + age_barred_individual_link: link_to(t(".what_you_cant_do.age_barred_individual"), "#age")) %> +

      +
    16. +
    17. +

      + <%= t(".what_you_cant_do.resident_embargo_country_html", + comprehensive_trade_embargo_link: link_to(t(".what_you_cant_do.comprehensive_trade_embargo"), tos_faq_path(anchor: "trade_embargo"))) %> +

      +
    18. +
    19. <%= t(".what_you_cant_do.break_applicable_law") %>

    20. +
    + +

    <%= t(".registration_and_email_addresses.heading") %>

    +
      +
    1. +

      + <%= t(".registration_and_email_addresses.agree_current_address_html", + suspend_your_account_link: link_to(t(".registration_and_email_addresses.suspend_your_account"), tos_faq_path(anchor: "invalid_email"))) %> +

      +
    2. +
    3. +

      + <%= t(".registration_and_email_addresses.email_is_yours_html", + lawfully_communicate_with_you_link: link_to(t(".registration_and_email_addresses.lawfully_communicate_with_you"), privacy_path(anchor: "III.C.1"))) %> +

      +
    4. +
    + +

    <%= t(".age_policy.heading") %>

    +

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

    +
      +
    1. <%= t(".age_policy.individuals_under_13") %>
    2. +
    3. + <%= t(".age_policy.individuals_under_16") %> +
        +
      1. + <%= t(".age_policy.eu_country_html", + special_categories_of_personal_data_link: link_to(t(".age_policy.special_categories_of_personal_data"), "https://gdpr-info.eu/art-9-gdpr/")) %> +
      2. +
      3. <%= t(".age_policy.country_disallowing_childrens_data") %>
      4. +
      +
    4. +
    +

    + <%= t(".age_policy.age_barred_not_permitted_html", + not_permitted_account_upload_link: link_to(t(".age_policy.not_permitted_account_upload"), tos_faq_path(anchor: "age_faq"))) %> +

    +

    <%= t(".age_policy.addressing_violations") %>

    +

    <%= t(".age_policy.ask_parent_to_upload") %>

    + +

    <%= t(".abuse_policy.heading") %>

    +

    + <%= t(".abuse_policy.no_prescreen_html", + content_policy_link: link_to(t(".abuse_policy.content_policy"), content_path)) %> +

    +

    + <%= t(".abuse_policy.answers_common_questions_html", + tos_faq_link: link_to(t(".abuse_policy.tos_faq"), tos_faq_path(anchor: "policy_procedures_faq"))) %> +

    +
      +
    1. + <%= t(".abuse_policy.submitting_a_complaint.heading") %> +

      + <%= t(".abuse_policy.submitting_a_complaint.html", + policy_and_abuse_form_link: link_to(t(".abuse_policy.submitting_a_complaint.policy_and_abuse_form"), new_abuse_report_path), + dmca_policy_link: link_to(t(".abuse_policy.submitting_a_complaint.dmca_policy"), dmca_path)) %> +

      +
    2. +
    3. + <%= t(".abuse_policy.treatment_of_complaints.heading") %> +

      + <%= t(".abuse_policy.treatment_of_complaints.html", + privacy_policy_link: link_to(t(".abuse_policy.treatment_of_complaints.privacy_policy"), privacy_path), + pac_confidentiality_policy_link: link_to(t(".abuse_policy.treatment_of_complaints.pac_confidentiality_policy"), + "https://www.transformativeworks.org/committees/policy-abuse-confidentiality-policy/")) %> +

      +
    4. +
    5. + <%= t(".abuse_policy.resolution_of_complaints.heading") %> +

      + <%= t(".abuse_policy.resolution_of_complaints.administrators_determine_content_removal_html", + determine_removal_link: link_to(t(".abuse_policy.resolution_of_complaints.determine_removal"), + tos_faq_path(anchor: "determine_removal"))) %> +

      +

      + <%= t(".abuse_policy.resolution_of_complaints.potentially_legitimate_fanwork_html", + illegal_and_inappropriate_content_policy_link: link_to(t(".abuse_policy.resolution_of_complaints.illegal_and_inappropriate_content_policy"), + content_path(anchor: "II.K"))) %> +

      +

      <%= t(".abuse_policy.resolution_of_complaints.voluntary_removal") %>

      +

      + <%= t(".abuse_policy.resolution_of_complaints.add_or_edit_tags_html", + mandatory_tags_policy_link: link_to(t(".abuse_policy.resolution_of_complaints.mandatory_tags_policy"), + content_path(anchor: "II.J"))) %> +

      +

      <%= t(".abuse_policy.resolution_of_complaints.immediate_removal") %>

      +
    6. +
    7. + <%= t(".abuse_policy.penalties.heading") %> +

      + <%= t(".abuse_policy.penalties.violations_warnings_suspensions_html", + tos_faq_link: link_to(t(".abuse_policy.penalties.tos_faq"), tos_faq_path(anchor: "penalty"))) %> +

      +

      + <%= t(".abuse_policy.penalties.open_doors_removal_html", + content_policy_ii_k_1_link: link_to(t(".abuse_policy.penalties.content_policy_ii_k_1"), content_path(anchor: "II.K.1"))) %> +

      +

      + <%= t(".abuse_policy.penalties.remove_resolve_lawsuit_html", + age_barred_individual_link: link_to(t(".abuse_policy.penalties.age_barred_individual"), "#age")) %> +

      +

      + <%= t(".abuse_policy.penalties.non_violating_content_html", + illegal_inappropriate_content_policy_link: link_to(t(".abuse_policy.penalties.illegal_inappropriate_content_policy"), + content_path(anchor: "II.K"))) %> +

      +

      <%= t(".abuse_policy.penalties.edit_post_while_suspended") %>

      +
    8. +
    9. + <%= t(".abuse_policy.appeals.heading") %> +

      + <%= t(".abuse_policy.appeals.html", + appeal_decision_link: link_to(t(".abuse_policy.appeals.appeal_decision"), tos_faq_path(anchor: "appeal"))) %> +

      +
    10. +
    + +
    + +<% unless local_assigns[:suppress_footer] %> +

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

    +

    + + <%= t(".license_html", + terms_of_service_link: link_to(t(".terms_of_service"), tos_path), + content_policy_link: link_to(t(".content_policy"), content_path), + privacy_policy_link: link_to(t(".privacy_policy"), privacy_path), + cc_attribution_4_0_international_link: link_to(t(".cc_attribution_4_0_international"), + "https://creativecommons.org/licenses/by/4.0/", + rel: "nofollow")) %> +

    - -
    J. User Icons
    -

    User icons should be appropriate for general audiences. They should not contain depictions of genital nudity or explicit sexual activity. For more information, please refer to the ToS FAQ.

    - -
    K. Tags
    -
      -
    1. Introduction -
        -
      1. We will not require specific ratings or warnings. However, a creator who chooses not to use ratings or warnings on a fanwork must signal this choice (refer to the ToS FAQ).
      2. -
      3. By default, all users will see the Archive warnings and tags the creator has selected. Any logged-in user who wishes to avoid Archive warnings and tags may set preferences to hide them by default. Logged-in users who set their preferences to hide information are proceeding at their own risk and may be exposed to Content they would otherwise wish to avoid. Such users may change their preferences, or reveal information for specific stories, at any time.
      4. -
      5. Logged-in users may set their preferences to indicate that they are willing to see mature or explicit content.
      6. -
      7. Other users, including users who are not registered users of the Service, who follow a link to a fanwork rated mature, explicit, or "not rated," will be asked to agree to see mature, explicit, or unrated Content. The Archive software will remember a non-registered user's choice during that user's visit, but will not retain the setting for future visits.
      8. -
      -
    2. -
    3. Ratings -
        -
      1. -

        The Archive uses the following ratings, or the equivalent text as specified on the creator upload form:

        -
          -
        1. General audiences.
        2. -
        3. Teen and up audiences.
        4. -
        5. Mature.
        6. -
        7. Explicit.
        8. -
        9. Not rated.
        10. -
        -
      2. -
      3. -

        As a rule, the creator controls the rating.

        -

        In response to a complaint, the Policy & Abuse team may decide that a "general" or "teen" rating is misleading. In such cases, the creator may be required to change the rating. If the creator declines or fails to respond, the team may hide the work, set the rating at "not rated," or take any other appropriate action, but it will not add any other rating.

        -
      4. -
      5. -

        The meaning of "not rated":

        -

        Fanworks labeled "not rated" may be treated, for purposes of searching, screening, and other Archive functions, like "explicit"-rated fanworks. Thus, users may be asked to agree that they have chosen to access the fanwork before proceeding to the fanwork.

        -
      6. -
      -
    4. -
    5. Warnings and Archive Warnings -
        -
      1. -

        General description:

        -

        There are two components to warnings on the Archive.

        -
          -
        1. Archive warnings: Creators can select from a list of Archive warnings. The list also allows creators to select "choose not to use Archive warnings" and "none of these warnings apply," or equivalent text as specified on the creator upload form.
        2. -
        3. Secondary (optional or additional) tags, including warnings: Creators can define their own tags, as seriously or as humorously as they like. These can include specific content warnings. The warnings policy only covers Archive warnings.
        4. -
        -
      2. -
      3. -

        As a rule, the creator controls the warnings.

        -

        Selecting "choose not to use Archive warnings," or the equivalent text as specified on the creator upload form, satisfies a creator's obligation under the warnings policy. If a fanwork uses this option, we will not sustain any failure-to-warn complaints. If the Policy & Abuse team receives a failure-to-warn complaint in other circumstances, the team may decide the absence of a specific Archive warning is misleading. In such cases, the creator may be asked to add a warning or to select the choose not to warn option. If the creator declines or fails to respond, the team may hide the work, set the warning to indicate that the creator has chosen not to warn, or take any other appropriate action, but it will not select any other warning.

        -
      4. -
      5. -

        The meaning of "choose not to use Archive warnings" or equivalent text:

        -

        The fanwork may or may not contain any of the subject matter on the Archive list. Users who wish to avoid specific elements entirely should not access fanworks marked with "choose not to use Archive warnings." A creator can select both "choose not to use Archive warnings" and one of the Archive warnings in order to warn for some but not all of the Archive warnings.

        -
      6. -
      -
    6. -
    7. Consequence of failure to use an appropriate rating or Archive warning -

      In general, failure to use an appropriate rating or Archive warning is not a violation of the abuse policy.

      -

      It is our policy to defer to creators' categorizations, but we reserve the right to recategorize a fanwork in the situations described above.

      -

      A recategorization decision is appealable through the ordinary appeals process.

      -

      A recategorization of a fanwork will not result in suspension of a user's account, unless it is a repeated pattern for a single user, in which case it may be treated as grounds for a suspension. Moreover, if a creator unilaterally reverses a recategorization, without agreement from the Policy & Abuse team, that will be treated as grounds for a suspension.

      -
    8. -
    9. Fanwork types -

      It is our policy to defer to creators' categorizations, but we reserve the right to recategorize a fanwork type.

      -

      A manual recategorization decision made by the Policy & Abuse team is appealable through the ordinary appeals process.

      -

      A manual recategorization of a fanwork will not result in suspension of a user's account, unless it is a repeated pattern for a single user, in which case it may be treated as grounds for a suspension. Moreover, if a creator unilaterally reverses a manual recategorization, without agreement from the team, that will be treated as grounds for a suspension.

      -
    10. -
    11. General tags -

      Some tags may be automatically applied to a work. It is our policy to defer to creators' categorizations, but we reserve the right to manually recategorize language and other Archive tags, including fandom tags. A manual recategorization or removal of a tag will not result in suspension of a user's account, unless it is a repeated pattern for a single user, in which case it may be treated as grounds for a suspension. Moreover, if a creator unilaterally reverses a manual recategorization or removal, without agreement from the team, that will be treated as grounds for a suspension.

      -
    12. -
    - -

    V. Assorted Policies

    -
    A. Collections, Challenges, and Exchanges
    -
      -
    1. -

      Archive users may create Collections and encourage other users to submit fanworks to those Collections. The Collection maintainer can set any constraints they want on the Collection, including rules about anonymous works (see A.4 below) but must otherwise follow AO3 Content policy (e.g., if the Collection Content is explicit, it should be marked as "explicit" or "choose not to rate"). The Collection maintainer may be able to ask users for suggestions for new fanworks ("prompts"), gather prompts, match participants with prompts (including contacting them via the contact information provided to the Archive or to the Collection maintainer), and show the prompts on the Archive, following the general rules governing works on the Archive. Where Collection rules allow, prompts may be anonymous or limited-visibility, as detailed in A.4 and A.5 below.

      -
        -
      1. -

        A Challenge is a form of AO3 Collection. A Challenge maintainer can communicate with Challenge participants. The Challenge maintainer may have access to participants' email addresses for this purpose.

        -
      2. -
      -
    2. -
    3. -

      To be part of a Collection, created on AO3 the fanwork creator has to affirmatively submit the fanwork to the Collection. The Collection maintainer will be able to remove the fanwork from the Collection, but not from the Archive.

      -
    4. -
    5. -

      If the Collection maintainer has specified in advance in the Collection rules that submissions cannot later be removed from the Collection, the user who submitted the fanwork may not be able to delete it, but will be able to Orphan the Content it so that the user's identity is no longer associated with the Content.

      -
    6. -
    7. -

      In order to implement certain types of Collections, the Archive may allow works to be posted without making the creator generally visible (which we call anonymous works).

      -
        -
      1. Anonymous works are not Orphan works, though they can be Orphaned.
      2. -
      3. The creator's pseudonym will not be publicly associated with the work while the anonymity is in place. For non-Orphaned works, the creator's pseudonym will be visible to administrators (including members of the Policy & Abuse team for purposes of resolving complaints), co-creators (if any), and the maintainers of any Collection of which the work is a part.
      4. -
      5. If the Collection of which a work is a part specifies rules regarding anonymity, such as a designated time for revealing authorship, the Collection maintainer may be able to control the work's anonymity consistent with those rules. In other situations, creators may be able to choose anonymity.
      6. -
      -
    8. -
    9. -

      In order to implement certain types of Collections, the Archive may allow works to be posted which will not be generally visible until a time set by the Collection maintainer.

      -
        -
      1. Once posted, the work will be visible to administrators (including members of the Policy & Abuse team for purposes of resolving complaints), co-creators (if any), and the maintainers of any Collection of which the work is a part.
      2. -
      3. If the collection of which a work is a part specifies rules regarding time of general visibility, the Collection maintainer may be able to control the time at which a work becomes generally visible to Archive users.
      4. -
      -
    10. -
    11. -

      In the absence of an independent violation of the abuse policy, the Archive will not intervene in decisions by the Collection maintainer.

      -
    12. -
    - -
    B. Fannish next-of-kin
    -

    Registered Archive users may designate a fannish next-of-kin. A next-of-kin agreement allows the transfer of Content maintenance in the case of a user's permanent incapacitation or death.

    -

    Both parties to the agreement must be registered users of the Archive.

    -

    The Archive's role in this agreement is only to act as a facilitator. If the person designated as the fannish next-of-kin activates the agreement by sending a message to the Archive, the Archive will not do any independent investigation to confirm the necessity for the transfer.

    -

    A fannish next-of-kin agreement is confidential and accessible only by designated members of the Archive team, who may only use it for purposes of implementing the agreement.

    - -
    C. Orphaning Content and Deleting Data
    -
      -
    1. Definition of Orphaning -

      One of the goals of the OTW and the Archive is to provide a permanent long-term home for fanworks. We also understand that circumstances can arise in which creators wish to remove their stories from the internet or otherwise dissociate themselves from their work. Our Archive software gives creators the ability to anonymize or "Orphan" fanworks along with the option of deleting them from the Archive. For a more detailed description of Orphaning, please see <%= link_to "About Orphaning", archive_faq_path("orphaning") %>.

      -
    2. -
    3. User-controlled Orphaning -

      Users will have the ability to delete or Orphan their Content themselves as long as they have a valid AO3 account, including the account password and/or access to the e-mail address connected with that AO3 account. Users will be able to have passwords e-mailed to them and to change the e-mail addresses associated with accounts. However, a user who has lost a password and has no access to the e-mail associated with the account may be unable to access the account for any purpose, including Orphaning or deletion, unless the user can verify identity in some other way, as described below.

      -
    4. -
    5. Caution: Orphaning may be difficult or impossible to reverse. -

      If a user affirmatively Orphans Content, any connection between the user and the work will be removed. It therefore may be difficult or even impossible to restore the link between an Orphaned Content and a user.

      -
    6. -
    7. Linking an author with an Orphaned work -

      As part of the OTW's commitment to privacy, users are not allowed to use comments or tags to publicly identify the creator of an Orphaned work after the work has been Orphaned. Users who add public identifying tags or comments after a work has been Orphaned violate the Archive's Terms of Service. Additionally, identifying tags or comments will be removed.

      -
    8. -
    9. -

      If the creator has an account, it is the creator's responsibility to delete any comments that personally identify said creator that are associated with the work, prior to Orphaning it, and to inform the Policy & Abuse team of any identifying tags that should be deleted. If the creator does not have an account, it is the creator's responsibility to identify any comments and/or tags that should be deleted as part of a request for Orphaning (see Policy on unverified identities and Orphaning or deletion below).

      -
    10. -
    11. Policy on unverified identities and Orphaning or deletion -

      Our policy is that creators should be able to Orphan or delete Content, and they should also be protected against claims by non-creators. Deleting Content is a user's way of withdrawing consent for AO3 to continue hosting said Content. We provide creators with several alternative methods of confirming source, including using the e-mail address associated with the fanwork; using information from a creator's own site or journal; or using an e-mail address or other form of contact associated with a different copy of the fanwork, including on the Internet Archive. We will also consult with the maintainer of any collection of which the fanwork is a part and take any other actions that seem likely to help with verification.

      -

      If the source of the request is confirmed, we will comply with the request. When the link between the source of the request and the fanwork's creator cannot be confirmed, and attempts to contact the fanwork's creator through any existing contact information receive no response, we will Orphan the fanwork.

      -
    12. -
    13. Deleting Your Account -
        -
      1. -

        Deletion of an account is a way for a user to withdraw their consent for AO3 to continue hosting their Content. -

        -
      2. -
      3. -

        If you delete Content, such Content will be unrecoverable via the Service.

        -
      4. -
      -
    14. -
    - -
    D. Open Doors
    -

    Please note: these terms are designed for agreements between the OTW and owners/maintainers of archives/collections of fanworks that were originally hosted on third-party sites.

    -

    The Open Doors project of the Organization for Transformative Works is dedicated to preserving fanworks for the future.

    -

    We are happy to help maintainers of typical fanfic archives preserve or back up their collections by transferring the contents of their archive into the Archive of Our Own. Other fannish projects that cannot be integrated into the Archive may also be preserved as special collections, resources permitting. Both kinds of projects may be featured on the Open Doors page.

    - -
    ToS for Open Doors projects
    -
      -
    1. Maintainer Consent -

      The OTW will only preserve collections with the full consent of the maintainer of the collection. The current maintainer of the project must agree to the Open Doors ToS, agree to any other conditions of the import, and grant us access to a copy of the current contents of the collection. The maintainer must also transfer ownership of the domain name or follow Open Doors' instructions if they want URL redirects, and if such redirects are possible. (Domain name transfer is not necessary if the maintainer is merely backing up an Archive within the Archive of Our Own.)

      -
    2. -
    3. Transfer of Project -

      When the Open Doors Committee and the current owner of the collection have decided to import an archive or a special collection under the Open Doors project, the current owner will provide a copy of the current contents and take any other steps necessary to carry out the transfer.

      -

      Typically, fanwork archives will be imported into the Archive of Our Own under a collection named after the original archive.

      -
    4. -
    5. URLs for Open Doors Project -

      For collections not hosted on the Archive itself, the special collection or project may be available under subdirectories or subdomains of an OTW site.

      -

      We may also preserve the original project's domain name.

      -

      Archives that have been integrated into the Archive of Our Own will also be listed in the Open Doors gallery.

      -
    6. -
    7. Role of Original Maintainer -

      The original maintainer of an archive that has been imported to the Archive of Our Own will be invited to moderate their archive's collection within the Archive of Our Own with all the powers that a collection owner in the Archive usually has; so, for instance, to decide whether a new work fulfills their collection's rules, or should be removed from the collection.

      -

      If the collection's maintainer no longer wants to work on the collection, they can designate a new maintainer for the collection, or close the collection to new submissions. Should someone volunteer or request to maintain a closed collection, Open Doors will attempt to contact the original maintainer to find out if they are interested in transferring the collection. If the original maintainer does not respond with approval, ownership of the collection will not be transferred.

      -
    8. -
    9. Collection Policy -

      Where possible, the existing policies of the collection will be preserved, even if they differ from the policies of the Archive of Our Own. Specifically, collections (whether integrated into the Archive of Our Own or preserved as special collections) can have limits on fandom, subject matter, sexual content, etc. that do not apply to Archive Content generally. Open Doors collections may be mixed fan and non-fanworks; when we accept a mixed collection, the entire collection can be added to the Archive, and the standard prohibition on non-fanworks will not apply to the collection. However, the OTW retains the right to remove Content from its servers if the Board deems removal necessary for specific legal reasons, or if the Content violates the Content Policy (other than the prohibition on non-fanwork content).

      -

      Control over individual fanworks contained within a collection rests with their creators. If the verified creator of any individual fanwork contained within a collection requests its removal or alteration, the OTW will always comply with such a request in a reasonable period of time. We will also provide mechanisms allowing creators to claim their fanworks from such a collection and if desired to attach them to a new or existing Archive of Our Own account, edit them, Orphan them, or delete them.

      -
    10. -
    11. Parting from the OTW -

      As noted in section 5, control over individual fanworks contained within a collection always rests with their creators. This section applies to collections as a whole. If the collection's original maintainer decides that they no longer want to be affiliated with the OTW, or the OTW board decides they no longer wish to work with the original maintainer, the following procedures for dissolution will apply:

      -
        -
      1. -

        Open Doors will delete the imported archive's collection on AO3. Any works imported to the AO3 already claimed, Orphaned, or anonymized by AO3 users will remain in the Archive under control of the creators' accounts, or the Orphan account. Open Doors will work with the original maintainer to delete any unclaimed works.

        -
      2. -
      3. -

        OTW will give the original maintainer a copy of the archive backup given to the Open Doors Committee, if available.

        -
      4. -
      5. -

        OTW will not be responsible for helping the maintainer set up elsewhere.

        -
      6. -
      7. -

        OTW will place an announcement on the Open Doors page indicating that the original maintainer has moved to a new location, with a link to the new location if provided by the maintainer.

        -
      8. -
      9. -

        These ToS are written assuming a single maintainer for a collection. If there are multiple active maintainers of a collection, they must all agree before the OTW will bring the collection into Open Doors. If some but not all of the maintainers later wish to part from the OTW, those who wish to do so can continue to work with the collection on the OTW servers, while the OTW will follow provisions b. and c. for any maintainers who wish to move the collection elsewhere. However, the OTW will retransfer any domain names only to maintainers who were formerly registered owners of the domain names at issue. For active collections, maintainers can use whatever dispute resolution procedure they work out between themselves, provided that they otherwise comply with OTW policies.

        -
      10. -
      -

      The goal of these rules is to be clear about how special collections and other extant fannish projects might come under the OTW umbrella while still preserving the autonomy both of the original maintainer and of the OTW. We want to provide a permanent home to projects, and preserve the results of our efforts, without the original maintainer feeling like they are giving up all control.

      -
    12. -
    13. General provisions -

      Matters not specifically addressed in this Open Doors Agreement will be governed by the OTW Terms of Service.

      -
    14. -
    -
    -

    Material in this draft has been drawn from Slashcity, NearlyFreeSpeech.Net, Vox Populli, imeem.

    -

    Approved: 23 May 2018

    -
    +<% end %> diff --git a/app/views/home/_tos_navigation.html.erb b/app/views/home/_tos_navigation.html.erb new file mode 100644 index 00000000000..6f325eb2ff5 --- /dev/null +++ b/app/views/home/_tos_navigation.html.erb @@ -0,0 +1,9 @@ + diff --git a/app/views/home/content.html.erb b/app/views/home/content.html.erb new file mode 100644 index 00000000000..86050371429 --- /dev/null +++ b/app/views/home/content.html.erb @@ -0,0 +1,20 @@ + +

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

    + + + +

    <%= t("a11y.navigation") %>

    +<%= render "tos_navigation" %> + + + +

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

    +
    + <%= render "content" %> +
    + + + +

    <%= t("a11y.navigation") %>

    +<%= render "tos_navigation", top_link: true %> + diff --git a/app/views/home/diversity_statement.html.erb b/app/views/home/diversity_statement.html.erb index bbc06fe1d3d..f8cf5e0f9e2 100644 --- a/app/views/home/diversity_statement.html.erb +++ b/app/views/home/diversity_statement.html.erb @@ -1,22 +1,47 @@ -

    <%= ts("You are welcome at the Archive of Our Own.") %>

    +

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

    -

    <%= ts("No matter your appearance, circumstances, configuration or take on the world: if you enjoy consuming, creating or commenting on fanworks, the Archive is for you.") %>

    +

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

    -

    <%= ts("This archive is a permanent, panfandom place for fanworks, built by fans for fans. Whichever way you use the Archive, you're part of this, powering it, shaping it through your use and") %> <%= link_to ts("your feedback"), new_feedback_report_path %>.

    +

    + <%= t(".archive_description_html", + your_feedback_link: link_to(t(".your_feedback"), new_feedback_report_path)) %> +

    -

    <%= ts("We,") %> <%= link_to ts("the Archive team"), admin_posts_path %><%= ts(", know that we won't get everything right on the first try, and we won't be able to make everyone equally happy. But we strive to find a good balance, and we promise to respectfully consider your feedback and to take it seriously.") %>

    +

    + <%= t(".what_we_do_html", + archive_team_link: link_to(t(".archive_team"), admin_posts_path)) %> +

    -

    <%= ts("You are free to express your creativity within the") %> <%= link_to ts("few restrictions"), tos_path %> <%= ts("needed to keep the service viable for other users. The Archive strives to protect your rights to free expression and privacy; you can read about the details in our") %> <%= link_to ts("Terms of Service"), tos_path %>.

    +

    + <%= t(".you_can_html", + few_restrictions_link: link_to(t(".few_restrictions"), content_path), + terms_of_service_link: link_to(t(".terms_of_service"), tos_path)) %> +

    -

    <%= ts("We know that there are") %> <%= link_to ts("some essential parts"), "/admin_posts/295" %> <%= ts("that are still missing to make the Archive truly panfandom: the ability to host fanworks other than text, an interface in languages other than English, and more ways for you to connect with each other, to name just a few. But with your support, we'll get there.") %>

    +

    + <%= t(".still_missing_html", + some_essential_parts_link: link_to(t(".some_essential_parts"), admin_post_path(295))) %> +

    -

    <%= ts("We are building this archive because we believe it's possible for people of all opinions and persuasions to come together and share with each other.") %>

    +

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

    -

    <%= ts("We are building this archive for you. Come be a part of it.") %>

    +

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


    -

    <%= ts("This is a remix of") %> <%= link_to ts("Dreamwidth's"), "http://www.dreamwidth.org" %> <%= link_to ts("Diversity Statement"), "http://www.dreamwidth.org/legal/diversity" %>.

    - -

    Creative Commons License
    This work is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License.

    +

    + <%= t(".dreamwidth_remix_html", + dreamwidth_link: link_to(t(".dreamwidth"), "http://www.dreamwidth.org"), + diversity_statement_link: link_to(t(".diversity_statement"), "http://www.dreamwidth.org/legal/diversity")) %> +

    + +

    + + <%= t(" style="border-width:0" src="http://i.creativecommons.org/l/by-sa/3.0/88x31.png" /> + +
    + <%= t(".license.html", + creative_commons_by_sa_link: link_to(t(".license.creative_commons_by_sa"), + "http://creativecommons.org/licenses/by-sa/3.0/")) %> +

    diff --git a/app/views/home/first_login_help.html.erb b/app/views/home/first_login_help.html.erb index fe9658d4c75..d6f7458f907 100644 --- a/app/views/home/first_login_help.html.erb +++ b/app/views/home/first_login_help.html.erb @@ -1,233 +1,155 @@ -<% # Expects current_user %> +<%# Expects current_user %>

    - <%= ts('Welcome to the %{app_name}!', app_name: ArchiveConfig.APP_NAME) %> + <%= t(".welcome_header", app_name: ArchiveConfig.APP_NAME) %>

    -

    <%= ts('Here are some tips to help you get started.') %>

    +

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

    -

    <%= ts('Table of Contents') %>

    +

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

      -
    • <%= link_to ts('Logging In and Logging Out'), '#logging_in' %>
    • +
    • <%= link_to t(".logging_in_out.header"), "#logging_in" %>
    • +
    • <%= link_to t(".editing_profile.header"), "#edit_profile" %>
    • +
    • <%= link_to t(".pseuds.header"), "#pseuds" %>
    • +
    • <%= link_to t(".posting_works.header"), "#posting" %>
    • +
    • <%= link_to t(".browsing.header"), "#browsing" %>
    • +
    • <%= link_to t(".tags.header"), "#tags" %>
    • +
    • <%= link_to t(".warnings.header"), "#warnings" %>
    • +
    • <%= link_to t(".bookmarking_works.header"), "#bookmarking" %>
    • +
    • <%= link_to t(".preferences.header"), "#preferences" %>
    • - <%= link_to ts('Editing Your Profile, Password, and Preferences'), - '#edit_profile' %> -
    • -
    • <%= link_to ts('Pseuds'), '#pseuds' %>
    • -
    • <%= link_to ts('Posting Works'), '#posting' %>
    • -
    • <%= link_to ts('Browsing'), '#browsing' %>
    • -
    • <%= link_to ts('Tags'), '#tags' %>
    • -
    • <%= link_to ts('Warnings'), '#warnings' %>
    • -
    • <%= link_to ts('Bookmarking Works'), '#bookmarking' %>
    • -
    • <%= link_to ts('Preferences'), '#preferences' %>
    • -
    • - <%= link_to ts('Additional Browsing Information'), '#additional' %> + <%= link_to t(".additional_info.header"), "#additional" %>
        -
      • <%= link_to ts('History and Mark for Later'), '#history' %>
      • -
      • <%= link_to ts('Subscriptions'), '#subscriptions' %>
      • +
      • <%= link_to t(".additional_info.history_mark_later.header"), "#history" %>
      • +
      • <%= link_to t(".additional_info.subscriptions.header"), "#subscriptions" %>
    • -
    • <%= link_to ts('Terms of Service'), '#legal_stuff' %>
    • -
    • <%= link_to ts('Support and Feedback'), '#support' %>
    • +
    • <%= link_to t(".tos.header"), "#legal_stuff" %>
    • +
    • <%= link_to t(".support_and_feedback.header"), "#support" %>
    -

    - <%= ts('Logging In and Logging Out') %> -

    -

    <%= ts('To log in, locate the login link and fill in your Username and - Password. The link will be located at the top right of the screen and as - indicated by your screenreader if you\'re using one. If you forget your - password, then select "%{forgot_password}". To log out, select "Log Out" on - the top right corner in the default browser skin.', - forgot_password: (link_to ts('forgot your password'), new_user_password_path) - ).html_safe %>

    +

    <%= t(".logging_in_out.header") %>

    +

    + <%= t(".logging_in_out.html", + forgot_password_link: link_to(t(".logging_in_out.forgot_password"), new_user_password_path)) %> +

    -

    - <%= ts('Editing Your Profile, Password, and Preferences') %> -

    -

    <%= ts('To add your info, go to %{dashboard}, select the - "%{profile}" tab, and select "%{edit_profile}" from the range of - options at the end of the page. Here, you can enter some basic personal - information. It\'s also where to go to change your password. Refer to the - %{profile_faq} for more information.', - dashboard: (link_to ts('your Dashboard'), user_path(current_user)), - profile: (link_to ts('Profile'), user_profile_path(current_user)), - edit_profile: (link_to ts('Edit My Profile'), edit_user_path(current_user)), - profile_faq: (link_to ts('Profile FAQ'), archive_faqs_path + '/profile') - ).html_safe %>

    +

    <%= t(".editing_profile.header") %>

    +

    + <%= t(".editing_profile.html", + your_dashboard_link: link_to(t(".editing_profile.your_dashboard"), user_path(current_user)), + profile_link: link_to(t(".editing_profile.profile"), user_profile_path(current_user)), + edit_my_profile_link: link_to(t(".editing_profile.edit_my_profile"), edit_user_path(current_user)), + profile_faq_link: link_to(t(".editing_profile.profile_faq"), archive_faq_path("profile"))) %> +

    -

    - <%= ts('Pseuds') %> -

    -

    <%= ts('Pseuds are like pen names linked to your account. You can use - different pseuds to post your works under the appropriate name while still - managing them through the same account. You can %{manage_pseuds} through - your profile page. For more information, please visit the - %{pseuds_faq}.', - manage_pseuds: (link_to ts('manage your pseuds'), - user_pseuds_path(current_user)), - pseuds_faq: (link_to ts('Pseuds FAQ'), archive_faqs_path + '/pseuds'), - ).html_safe %>

    +

    <%= t(".pseuds.header") %>

    +

    + <%= t(".pseuds.html", + manage_your_pseuds_link: link_to(t(".pseuds.manage_your_pseuds"), user_pseuds_path(current_user)), + pseuds_faq_link: link_to(t(".pseuds.pseuds_faq"), archive_faq_path("pseuds"))) %> +

    -

    - <%= ts('Posting Works') %> -

    -

    <%= ts('To open the Post New Work page, just select the %{post} link - from the menu of the Post tab located at the top right of the page in the - default browser skin. Visit the %{posting_faq} or check out our - %{post_tutorial} for more information.', - post: (link_to ts('Post New'), new_work_path), - posting_faq: (link_to ts('Posting and Editing FAQ'), - archive_faqs_path + '/posting-and-editing'), - post_tutorial: (link_to ts('Tutorial: Posting a Work on AO3'), - archive_faqs_path + '/tutorial-posting-a-work-on-ao3') - ).html_safe %>

    +

    <%= t(".posting_works.header") %>

    +

    + <%= t(".posting_works.html", + post_new_link: link_to(t(".posting_works.post_new"), new_work_path), + posting_editing_faq_link: link_to(t(".posting_works.posting_editing_faq"), + archive_faq_path("posting-and-editing")), + tutorial_link: link_to(t(".posting_works.tutorial"), + archive_faq_path("tutorial-posting-a-work-on-ao3"))) %> +

    -

    - <%= ts('Browsing') %> -

    -

    <%= ts('You can start browsing works by going to the "%{fandoms}" tab at - the top of any Archive page in the default skin and selecting either - "%{all_fandoms}" or one of the subsets such as "%{movies}". Alternatively, you - can use the %{search} to look for specific fandoms, works, or users. You can - use the "Sort and Filter" form to narrow down the results in any fandom page. - For more instructions, please visit %{search_faq} and the - %{search_tutorial}.', - fandoms: (link_to ts('Fandoms'), menu_fandoms_path), - all_fandoms: (link_to ts('All Fandoms'), media_path), - movies: Media.find_by_name('Movies') ? (link_to ts('Movies'), - medium_fandoms_path( - Media.find_by_name('Movies'))) : - ts('Movies'), - search: (link_to ts('search feature'), search_works_path), - search_faq: (link_to ts('Search and Browse FAQ'), - archive_faqs_path + '/search-and-browse'), - search_tutorial: (link_to ts('Searching and browsing tutorial'), - admin_posts_path + '/259'), - ).html_safe %>

    +

    <%= t(".browsing.header") %>

    +

    + <% media_movie = Media.find_by_name("Movies") %> + <%= t(".browsing.html", + fandoms_link: link_to(t(".browsing.fandoms"), menu_fandoms_path), + all_fandoms_link: link_to(t(".browsing.all_fandoms"), media_path), + movies_link: if media_movie + link_to(t(".browsing.movies"), medium_fandoms_path(media_movie)) + else + t(".browsing.movies") + end, + search_feature_link: link_to(t(".browsing.search_feature"), search_works_path), + search_browse_faq_link: link_to(t(".browsing.search_browse_faq"), archive_faq_path("search-and-browse")), + search_browse_tutorial_link: link_to(t(".browsing.search_browse_tutorial"), admin_post_path(259))) %> +

    -

    - <%= ts('Tags') %> -

    -

    <%= ts('All tags on the Archive—including those for fandoms, relationships, - and characters—start out user-created. You can always use the existing tags by - selecting from the autocomplete list. If you cannot find the tags you want to - use, feel free to create new tags for your works. Behind the scenes, our tag - wrangling team will match the tags up with their synonyms, so that people can - find your work whether you tag it "AMTDI", "Aliens Made Them Do - It", or "Sex Pollen". Refer to the %{tags_faq} to learn more.', - tags_faq: (link_to ts('Tags FAQ'), archive_faqs_path + '/tags') - ).html_safe %>

    +

    <%= t(".tags.header") %>

    +

    + <%= t(".tags.html", + tags_faq_link: link_to(t(".tags.tags_faq"), archive_faq_path("tags"))) %> +

    -

    - <%= ts('Warnings') %> -

    -

    <%= ts('The Archive defines four "primary" %{warnings_tos}: "Graphic - Depictions Of Violence", "Major Character Death", "Rape/Non-Con", and - "Underage". When posting a work, you have the option to explicitly select - warnings for this content, deny the presence of such content ("No Archive - Warnings Apply"), or choose not to apply warnings regardless of whether or not - these warnings are applicable ("Creator Chose Not To Use Archive Warnings"). - Remember, "No Archive Warnings Apply" only refers to the listed primary - warnings.', - warnings_tos: (link_to ts('Archive-specific warnings'), tos_path + '#IV.K.3') - ).html_safe %>

    -

    <%= ts('When browsing the Archive, warnings will be displayed in the blurb - of each work. Official warning tags are displayed in bold. A four square grid - in the top left corner of each work\'s blurb indicates the work\'s rating, - completion status, pairing category, and any Archive warnings that apply to - it. Refer to the %{symbols_key} for more information.', - symbols_key: (link_to ts('Symbols Key Chart'), '/help/symbols-key.html') - ).html_safe %>

    +

    <%= t(".warnings.header") %>

    +

    + <%= t(".warnings.description_html", + archive_specific_warnings_link: link_to(t(".warnings.archive_specific_warnings"), + tos_faq_path(anchor: "warnings_list"))) %> +

    +

    + <%= t(".warnings.symbols_html", + symbols_key_chart_link: link_to(t(".warnings.symbols_key_chart"), "/help/symbols-key.html")) %> +

    -

    - <%= ts('Bookmarking Works') %> -

    -

    <%= ts('You can bookmark works on the Archive as well as works hosted on - other sites. You can choose to make your bookmarks public or private. You can - also mark a public bookmark as a rec (recommendation). Additionally, you can - add notes and tags to the bookmark, and/or add it to a collection. For more - instructions on managing your Bookmarks, please visit the %{bookmarks_faq}.', - bookmarks_faq: (link_to ts('Bookmarks FAQ'), archive_faqs_path + '/bookmarks') - ).html_safe %>

    +

    <%= t(".bookmarking_works.header") %>

    +

    + <%= t(".bookmarking_works.html", + bookmarks_faq_link: link_to(t(".bookmarking_works.bookmarks_faq"), archive_faq_path("bookmarks"))) %> +

    -

    - <%= ts('Preferences') %>

    -

    <%= ts('"%{set_preferences}" is located next to the "%{edit_profile}" - button, and allows you to adjust certain settings to personalize your - experience on the site. The Preferences area controls both the Archive\'s - behavior (such as whether your history is saved) and the Archive\'s - appearance. Go to the %{preferences_faq} for more details.', - set_preferences: (link_to ts('Set My Preferences'), - user_preferences_path(current_user)), - edit_profile: (link_to ts('Edit My Profile'), edit_user_path(current_user)), - preferences_faq: (link_to ts('Preferences FAQ'), - archive_faqs_path + '/preferences') - ).html_safe %>

    -

    <%= ts('For more information on how to customize the skins and interface of - the Archive, refer to the %{skins_faq} and %{tutorial_list}.', - skins_faq: (link_to ts('Skins and Archive Interface FAQ'), - archive_faqs_path + '/skins-and-archive-interface'), - tutorial_list: (link_to ts('List of Tutorials'), - archive_faqs_path + '/tutorials') - ).html_safe %>

    +

    <%= t(".preferences.header") %>

    +

    + <%= t(".preferences.html", + set_my_preferences_link: link_to(t(".preferences.set_my_preferences"), user_preferences_path(current_user)), + edit_my_profile_link: link_to(t(".preferences.edit_my_profile"), edit_user_path(current_user)), + preferences_faq_link: link_to(t(".preferences.preferences_faq"), archive_faq_path("preferences"))) %> +

    +

    + <%= t(".preferences.skins_detail_html", + skins_faq_link: link_to(t(".preferences.skins_faq"), + archive_faq_path("skins-and-archive-interface")), + tutorials_list_link: link_to(t(".preferences.tutorials_list"), + archive_faq_path("tutorials"))) %> +

    -

    - <%= ts('Additional Browsing Information') %> -

    -

    - <%= ts('History and Mark for Later') %>

    -

    <%= ts('You can access and manage your history by going to %{dashboard} and - selecting the "%{history}" link in the side menu in the default browser skin - or on top of the page in mobile devices. You can clear your history, or delete - individual entries if you don\'t want these entries to show up in your - history. You can also add works to the "Marked for Later" list in your - history. For more information on History, please visit the %{history_faq}.', - dashboard: (link_to ts('your Dashboard'), user_path(current_user)), - history: (link_to ts('History'), user_readings_path(current_user)), - history_faq: (link_to ts('History and Mark for Later FAQ'), - archive_faqs_path + '/History-and-mark-for-later') - ).html_safe %>

    +

    <%= t(".additional_info.header") %>

    +

    <%= t(".additional_info.history_mark_later.header") %>

    +

    + <%= t(".additional_info.history_mark_later.html", + your_dashboard_link: link_to(t(".additional_info.history_mark_later.your_dashboard"), user_path(current_user)), + history_link: link_to(t(".additional_info.history_mark_later.history"), user_readings_path(current_user)), + history_faq_link: link_to(t(".additional_info.history_mark_later.history_faq"), + archive_faq_path("History-and-mark-for-later"))) %>

    -

    - <%= ts('Subscriptions') %> -

    -

    <%= ts('You can subscribe to a work, collection, series of works, or user - by selecting the "Subscribe" link at the top or bottom of a work, or at the - top or bottom of a collection or series page, or the user\'s profile page. You - can check your subscriptions from %{dashboard} by selecting the - "Subscriptions" link. For more information on Subscriptions, please visit the - %{subscription_faq}.', - dashboard: (link_to ts('your Dashboard'), user_path(current_user)), - subscription_faq: (link_to ts('Subscriptions and Feeds FAQ'), - archive_faqs_path + '/subscriptions-and-feeds') - ).html_safe %>

    +

    <%= t(".additional_info.subscriptions.header") %>

    +

    + <%= t(".additional_info.subscriptions.html", + your_dashboard_link: link_to(t(".additional_info.subscriptions.your_dashboard"), user_path(current_user)), + subscriptions_feed_faq_link: link_to(t(".additional_info.subscriptions.subscriptions_feed_faq"), + archive_faq_path("subscriptions-and-feeds"))) %> +

    - -

    <%= ts('You can keep up to date with the policies and procedures for the - site listed in the current %{tos}, and find explanations for some of the more - common questions in the %{tos_faq}. If you still have questions about a - specific work, please %{contact_abuse}. If you have general policy questions, - you can either contact Abuse or %{contact_support}.', - tos: (link_to ts('Terms of Service'), tos_path), - tos_faq:(link_to ts('Terms of Service FAQ'), tos_faq_path), - contact_abuse: (link_to ts('contact Abuse'), new_abuse_report_path), - contact_support: (link_to ts('contact Support'), new_feedback_report_path) - ).html_safe %>

    + +

    + <%= t(".tos.info_html", + tos_link: link_to(t(".tos.tos"), tos_path), + content_policy_link: link_to(t(".tos.content_policy"), content_path), + privacy_policy_link: link_to(t(".tos.privacy_policy"), privacy_path), + tos_faq_link: link_to(t(".tos.tos_faq"), tos_faq_path)) %> +

    +

    + <%= t(".tos.additional_questions_html", + contact_abuse_link: link_to(t(".tos.contact_abuse"), new_abuse_report_path)) %> +

    -

    - <%= ts('Support and Feedback') %> -

    -

    <%= ts('Some frequently asked questions about the Archive are answered in - the broader %{faq}. You may also like to check out our %{known_issues}. If you - need more help, please %{contact_support}. If you want to know more about some - user-created tools that work with the Archive, please visit the - %{tools_faq}.', - faq: (link_to ts('Archive FAQ'), archive_faqs_path), - known_issues: (link_to ts('Known Issues'), known_issues_path), - contact_support: (link_to ts('contact Support'), new_feedback_report_path), - tools_faq: (link_to ts('Unofficial Browser Tools FAQ'), - archive_faqs_path + '/unofficial-browser-tools') - ).html_safe %>

    +

    <%= t(".support_and_feedback.header") %>

    +

    + <%= t(".support_and_feedback.html", + archive_faq_link: link_to(t(".support_and_feedback.archive_faq"), archive_faqs_path), + known_issues_link: link_to(t(".support_and_feedback.known_issues"), known_issues_path), + contact_support_link: link_to(t(".support_and_feedback.contact_support"), new_feedback_report_path), + unofficial_tools_faq_link: link_to(t(".support_and_feedback.unofficial_tools_faq"), + archive_faq_path("unofficial-browser-tools"))) %> +

    diff --git a/app/views/home/index.html.erb b/app/views/home/index.html.erb index 2071c0001ee..7228b2e19e7 100644 --- a/app/views/home/index.html.erb +++ b/app/views/home/index.html.erb @@ -36,11 +36,11 @@

    <%= t(".readings.note") %>

    -
      +
        <% @homepage.readings.each do |reading| %> <%= render "readings/reading_blurb", work: reading.work, reading: reading %> <% end %> -
    +
    <% end %> diff --git a/app/views/home/privacy.html.erb b/app/views/home/privacy.html.erb new file mode 100644 index 00000000000..af20944647f --- /dev/null +++ b/app/views/home/privacy.html.erb @@ -0,0 +1,20 @@ + +

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

    + + + +

    <%= t("a11y.navigation") %>

    +<%= render "tos_navigation" %> + + + +

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

    +
    + <%= render "privacy" %> +
    + + + +

    <%= t("a11y.navigation") %>

    +<%= render "tos_navigation", top_link: true %> + diff --git a/app/views/home/site_map.html.erb b/app/views/home/site_map.html.erb index 6bb292a4758..bb9a2f0f3c1 100644 --- a/app/views/home/site_map.html.erb +++ b/app/views/home/site_map.html.erb @@ -1,57 +1,69 @@ -

    <%= t('.site_map', :default => 'Site Map') %>

    +

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

    -

    Explore

    -
      -
    • <%= link_to t('.homepage', :default => "Homepage"), root_path %>
    • -
    • <%= link_to t('.fandoms', :default => "Fandoms"), media_path %>
    • -
    • <%= link_to t('.recent_works', :default => "Recent Works"), works_path %>
    • -
    • <%= link_to t('.people', :default => "People"), search_people_path %>
    • -
    • <%= link_to t('.bookmarks', :default => "Bookmarks"), bookmarks_path %>
    • -
    • <%= link_to t('.freeform_tag_cloud', :default => "Additional Tags Cloud"), tags_path %>
    • -
    • <%= link_to t('.languages', :default => "Languages"), languages_path %>
    • -
    • <%= link_to t('.collections', :default => "Collections and Challenges"), collections_path %>
    • -
    +

    <%= t(".explore.header") %>

    +
      +
    • <%= link_to t(".explore.homepage"), root_path %>
    • +
    • <%= link_to t(".explore.fandoms"), media_path %>
    • +
    • <%= link_to t(".explore.recent_works"), works_path %>
    • +
    • <%= link_to t(".explore.people"), search_people_path %>
    • +
    • <%= link_to t(".explore.bookmarks"), bookmarks_path %>
    • +
    • <%= link_to t(".explore.additional_tags_cloud"), tags_path %>
    • +
    • <%= link_to t(".explore.languages"), languages_path %>
    • +
    • <%= link_to t(".explore.collections_and_challenges"), collections_path %>
    • +
    -

    About the Archive of Our Own

    -
      -
    • <%= link_to t('.terms_of_service', :default => "Terms of Service"), tos_path %>
    • -
    • <%= link_to t('.archive_faq', :default => "Archive FAQ"), archive_faqs_path %>
    • -
    • <%= link_to t('.ao3_news', :default => "AO3 News"), admin_posts_path %>
    • -
    • <%= link_to t('.known_issues', :default => "Known Issues"), known_issues_path %>
    • -
    • The Archive of Our Own is a project of the OTW
    • -
    +

    <%= t(".about.header") %>

    +
      +
    • <%= link_to t(".about.terms_of_service"), tos_path %>
    • +
    • <%= link_to t(".about.content_policy"), content_path %>
    • +
    • <%= link_to t(".about.privacy_policy"), privacy_path %>
    • +
    • <%= link_to t(".about.tos_faq"), tos_faq_path %>
    • +
    • <%= link_to t(".about.archive_faq"), archive_faqs_path %>
    • +
    • <%= link_to t(".about.ao3_news"), admin_posts_path %>
    • +
    • <%= link_to t(".about.known_issues"), known_issues_path %>
    • +
    • + <%= t(".about.project_of_otw_html", + otw_link: link_to(content_tag(:acronym, t(".otw.abbreviated"), title: t(".otw.full")), + "https://transformativeworks.org")) %> +
    • +
    <% if logged_in? %> -

    Access your account

    -
      -
    • <%= link_to t('.my_home', :default => "My Home"), user_path(current_user) %>
    • -
    • <%= link_to t('.post_new', :default => "Post New"), new_work_path %>
    • -
    • <%= link_to t('.my_works', :default => "My Works"), user_works_path(current_user) %>
    • -
    • <%= link_to t('.my_drafts', :default => "Drafts"), drafts_user_works_path(current_user) %>
    • -
    • <%= link_to t('.my_series', :default => "My Series"), user_series_index_path(current_user) %>
    • -
    • <%= link_to t('.my_bookmarks', :default => "My Bookmarks"), user_bookmarks_path(current_user) %>
    • -
    • <%= link_to t('.my_collections', :default => "My Collections and Challenges"), user_collections_path(current_user) %>
    • -
    • <%= link_to t('.my_inbox', :default => "My Inbox"), user_inbox_path(current_user) %>
    • - <% if current_user.preference.history_enabled? %> -
    • <%= link_to t('.my_history', :default =>"My History"), user_readings_path(current_user) %>
    • - <% end %> -
    • <%= link_to t('.my_subscriptions', :default =>"My Subscriptions"), user_subscriptions_path(current_user) %>
    • -
    • <%= link_to t('.set_preferences', :default => "Set My Preferences"), user_preferences_path(current_user) %> -
    +

    <%= t(".access_your_account.header") %>

    +
      +
    • <%= link_to t(".access_your_account.my_home"), user_path(current_user) %>
    • +
    • <%= link_to t(".access_your_account.post_new"), new_work_path %>
    • +
    • <%= link_to t(".access_your_account.my_works"), user_works_path(current_user) %>
    • +
    • <%= link_to t(".access_your_account.drafts"), drafts_user_works_path(current_user) %>
    • +
    • <%= link_to t(".access_your_account.my_series"), user_series_index_path(current_user) %>
    • +
    • <%= link_to t(".access_your_account.my_bookmarks"), user_bookmarks_path(current_user) %>
    • +
    • <%= link_to t(".access_your_account.my_collections_and_challenges"), user_collections_path(current_user) %>
    • +
    • <%= link_to t(".access_your_account.my_inbox"), user_inbox_path(current_user) %>
    • + <% if current_user.preference.history_enabled? %> +
    • <%= link_to t(".access_your_account.my_history"), user_readings_path(current_user) %>
    • + <% end %> +
    • <%= link_to t(".access_your_account.my_subscriptions"), user_subscriptions_path(current_user) %>
    • +
    • <%= link_to t(".access_your_account.set_my_preferences"), user_preferences_path(current_user) %>
    • +
    -

    Change your account settings

    -
      -
    • <%= link_to t('.edit_user_profile', :default =>"Edit My Profile"), edit_user_path(current_user) %>
    • -
    • <%= link_to t('.my_profile', :default => "My Profile"), user_profile_path(current_user) %>
    • -
    • <%= link_to t('.manage_pseuds', :default => "Manage My Pseuds"), user_pseuds_path(current_user) %>
    • -
    • <%= link_to ts("Delete My Account"), user_path(current_user), data: {confirm: ts('This will permanently delete your account and cannot be undone. Are you sure?')}, :method => :delete %>
    • -
    +

    <%= t(".change_your_account_settings.header") %>

    +
      +
    • <%= link_to t(".change_your_account_settings.edit_my_profile"), edit_user_path(current_user) %>
    • +
    • <%= link_to t(".change_your_account_settings.my_profile"), user_profile_path(current_user) %>
    • +
    • <%= link_to t(".change_your_account_settings.manage_my_pseuds"), user_pseuds_path(current_user) %>
    • +
    • + <%= link_to t(".change_your_account_settings.delete_my_account"), + user_path(current_user), + data: { confirm: t(".change_your_account_settings.delete_account_confirmation") }, + method: :delete %> +
    • +
    <% end %> -

    Contact Us

    -
      -
    • <%= link_to t('.support_and_feedback', :default => "Technical Support & Feedback"), new_feedback_report_path %>
    • -
    • <%= link_to t('.report_abuse', :default => "Policy Questions & Abuse Reports"), new_abuse_report_path %>
    • -
    • <%= link_to t('.donate', :default => "Donations"), donate_path %>
    • -
    +

    <%= t(".contact_us.header") %>

    +
      +
    • <%= link_to t(".contact_us.technical_support_and_feedback"), new_feedback_report_path %>
    • +
    • <%= link_to t(".contact_us.policy_questions_and_abuse_reports"), new_abuse_report_path %>
    • +
    • <%= link_to t(".contact_us.donations"), donate_path %>
    • +
    diff --git a/app/views/home/tos.html.erb b/app/views/home/tos.html.erb index aa6f91069c1..36f288fb26b 100644 --- a/app/views/home/tos.html.erb +++ b/app/views/home/tos.html.erb @@ -1,3 +1,20 @@ -

    <%= ts("Terms of Service") %>

    + +

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

    + -<%= render "home/tos" %> + +

    <%= t("a11y.navigation") %>

    +<%= render "tos_navigation" %> + + + +

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

    +
    + <%= render "tos" %> +
    + + + +

    <%= t("a11y.navigation") %>

    +<%= render "tos_navigation", top_link: true %> + diff --git a/app/views/home/tos_faq.html.erb b/app/views/home/tos_faq.html.erb index e590acf96e0..bb4bfd25f0f 100644 --- a/app/views/home/tos_faq.html.erb +++ b/app/views/home/tos_faq.html.erb @@ -1,498 +1,1247 @@ -

    Terms of Service FAQ

    -
    - -
    -

    General Principles

    -

    Why does the Archive have a goal of maximum inclusiveness?

    -

    There are a number of wonderful specialized archives. Our aim with this Archive is to provide a place to preserve as many fanworks as possible. At the same time, the Archive software can be used by anyone to create their own archives, including archives limited to particular topics, fandoms, or ratings. -

    -

    Why is the agreement between the Archive and Archive users governed by the laws of New York?

    -

    Given the great variation among laws in different places, we want our agreement with you to be governed by predictable and consistent laws. -

    -

    What is an implied warranty of merchantability?

    -

    An implied warranty of merchantability is a legal agreement between a seller and a buyer that goods will be reasonably fit for the general purpose for which they are sold. In the United States this warranty is governed by the Uniform Commercial Code (UCC), which allows sellers to disclaim it, thereby shifting the risk back to the buyer. -

    -

    What is an implied warranty of fitness for a particular purpose?

    -

    An implied warranty of fitness for a particular purpose is a legal agreement that exists when a buyer relies upon the seller to provide goods to fit a specific request. This warranty requires that the seller know or have reason to know of a specific purpose to which the goods are going to be put, and know that the buyer is relying on the seller's expertise or judgment. In the United States this warranty is governed by the Uniform Commercial Code (UCC), which allows sellers to disclaim it, thereby shifting the risk back to the buyer. -

    -

    Under what circumstances would you suspend an account for an out-of-date e-mail address?

    -

    If we need to communicate with you to resolve an abuse complaint, and the e-mail bounces, we will have to resolve it without your participation, which means you won't be able to tell your side of the story. If your e-mail continues to bounce, we will suspend your account because we need to be able to communicate with you if necessary. -

    -

    In that situation, you can get your account reinstated by associating it with a working email address and, if necessary, dealing with whatever problem led to the abuse complaint in the first place. -

    -

    Suspension for bouncing isn't a strike. Suspension for bouncing would never lead to permanent suspension, though a sufficient number of sustained abuse complaints while the email was bouncing could lead to permanent suspension. -

    -

    If we send a routine e-mail about general site policies and it bounces, that will not lead to account suspension, though whatever policies we announce will still apply to all account holders. We will only suspend accounts when individual abuse-related communications bounce. -

    -

    I am a non-US resident. What does the Archive policy on US law mean for me?

    -

    The Archive welcomes fans from all over the globe, but it is set up under US law. We believe that US law governs the Archive, which includes the relationship between the Archive and its users and the definitions of terms in the ToS. Other laws may govern your behavior, however, and you are responsible for knowing them and complying with them. -

    -

    What do you mean by banning "impersonation"? Can I archive first-person real-person fiction? Can I use a celebrity name as a pseudonym?

    -

    Roleplay is permitted when the assumption of such a persona is clearly disclosed (e.g., in a user profile or in another manner appropriate under the circumstances) and it doesn't otherwise violate the Content Policy, including the harassment policy. Fiction marked as such, including real-person fiction in first-person format, is not impersonation. Please consult the - <%= link_to ts("content policy"), tos_path(:anchor => "content") %> for further information. -

    -

    What do you mean by "world-wide, royalty-free, nonexclusive license"?

    -

    This means the Archive can make your content available to other people (subject to any login requirements that apply) without paying you. We will never charge for access to the Archive or otherwise sell your content. You can put your content anywhere else you want, too. -

    -

    Age Policy

    -

    Why are children under the age of 13 not permitted to have an account or upload Content?

    -

    In the U.S., the Children's Online Privacy Protection Act governs the collection of personal information (which can include things like usernames) from children under 13. Requiring users to be over 13 is the easiest way for us to comply with this law. -

    -

    Privacy Policy

    -

    Why do you use cookies?

    -

    Cookies may be required to customize your experience of the site. If you do not accept cookies, you may not be able to use the site. There are many good ways to remove both browser history and cookies between sessions, and we encourage people who are concerned about privacy to investigate more global solutions. For example, Firefox can clear all your private data between sessions. -

    -

    Content Policies and Abuse Procedures

    -

    What sort of things would lead to permanent suspension?

    -

    It's impossible to define everything in advance. We are most concerned with people who are actively and deliberately hostile to the community. Repeated upheld copyright complaints involving nontransformative works may lead to a permanent suspension. Wholesale plagiarism and deliberate disclosure of another person's real-life name or other identifying information readily justify permanent suspension, whereas a personal conflict that gets out of control may justify temporary suspension. Small and honest mistakes, even if they are annoying, are more likely to draw warnings. -

    -

    What constrains the abuse team's discretion?

    -

    Our commitment is to build a community that welcomes anyone with a willingness to learn the rules but defends itself against people who deliberately flout them. Our discretion is aimed at that objective. Procedurally, permanent suspensions for violations other than spam or threatening the technical integrity of the site require a majority vote of the abuse team. Majority rule builds in checks on individual discretion without trying to resolve every possible situation in advance. -

    -

    What do you mean by "only people who have a need to know" about a complaint will be informed of it?

    -

    Abuse and complaint information is kept confidential. The abuse team guards all such information carefully, and all members of the abuse team have agreed to an industry-standard confidentiality policy. On occasion, the abuse team may need to consult with the Systems committee, to request specific technical or log information to aid our investigation, for example, or the Legal committee, to discuss precise legal requirements. We may also contact the subject of a complaint, to request their perspective or to inform them of penalties their actions have incurred. For information about what details we would release to the subject of a complaint, please see our - <%= link_to ts("Privacy Policy"), tos_path(:anchor => "privacy") %> and - <%= link_to ts("other Abuse FAQs"), tos_path(:anchor => "content") %>. -

    -

    What happens if someone who's a friend of someone on the abuse team is involved in a complaint?

    -

    We expect the members of the abuse team to behave professionally, even though OTW is entirely volunteer-staffed. We take the responsibilities of serving on the abuse team seriously, and a member of the team with a personal relationship to either party in an abuse situation is expected to recuse themselves entirely from the case, and, of course, to maintain our standards of confidentiality at all times; failure to do so may be grounds for dismissal from the team. -

    -

    What do the different account statuses mean?

    -

    We define account statuses the following way:

    -
    Warning
    -

    At its discretion, the abuse team may issue a warning, rather than a suspension, in the instance of minor violations of the ToS. A user who has recently received a warning and who violates the ToS again, especially in the same or a similar manner, is likely to incur a suspension. -

    -
    Suspension
    -

    The abuse team may issue a time-limited ban on the uploading of new content and creation of new accounts; suspension occurs as the result of strikes incurred for violating the Archive's ToS. During this time, the suspended user can remove, but not edit, content uploaded prior to the suspension. -

    -
    Permanent Suspension
    -

    The abuse team may issue a permanent ban on the uploading of new content. Permanently suspended users cannot create new accounts or upload content to Archive, though they retain the right to remove, but not edit, content uploaded prior to the permanent suspension. -

    -

    If I complain non-anonymously, will the subject be told who complained?

    -

    Only people who need to know about a complaint will be informed about it. The subject of a complaint may be among those who need to know. No information other than that provided in the complaint will be passed on, and the complainant has complete control over what information is submitted to Abuse. Complaints can be submitted anonymously. Legal names and other information sufficient to identify a person in the physical world will never be disclosed as part of a standard abuse complaint. For further clarification, please see our - <%= link_to ts("Privacy Policy"), tos_path(:anchor => "privacy") %>. -

    -

    Will I be informed of complaints against me?

    -

    In general, the abuse team will only communicate with the subject of a complaint if there appears to be a violation of the abuse policy, or if the abuse team needs more information to resolve the issue. -

    -

    How would the suspended user control their nonobjectionable content without an account?

    -

    Non-objectionable fanworks are not removed from the site when a user is suspended. Suspended users who wish to delete or orphan their fanworks may contact the Abuse team to have this done for them. -

    -

    What information is available about a permanently suspended or deleted account? Can I reuse a userID that belonged to a deleted account?

    -

    Permanent suspension doesn't delete accounts; unless deleted by the user, any existing content that doesn't violate the content policy or other parts of the Terms of Service remains. It is possible that a userID that has been deleted by the user will be available to other people. -

    -

    How do I appeal the resolution of a complaint?

    -

    The person against whom a complaint was resolved can submit an appeal the same way as one would submit a complaint, by contacting Abuse.

    -

    Spam and Commercial Promotion

    -

    How strict is the "no commerce" rule?

    -

    We want the Archive to remain a non-commercial space. That means that it isn't the right place for offering merchandise, even fan-related merchandise. Linking to your personal page (not, for example, an Amazon author page) is fine, even if the personal page includes some items for sale, but the Archive is not advertising space. If the abuse team issues a warning or sustains a complaint about commercial activities, the original poster can always appeal. -

    -

    What about charity drives?

    -

    The Archive will host fanworks of any origin, including fanworks created in response to charity drives or other challenges. A link to a charity drive to explain the origin of a fanwork is appropriate. Solicitation itself, however, should take place outside the Archive. We concluded that this policy was the easiest to apply fairly to everyone, given the wide range of possible solicitation activities. -

    -

    What's this about the spam filter? Can I be permanently suspended if I fail the filter?

    -

    If you're logged in, you shouldn't see a spam filter, so the situation shouldn't arise. If we find that accounts are being created simply to spam other users, we will permanently suspend those accounts. If you're a human being reading this FAQ, you shouldn't worry about the automated spam-control measures. -

    -

    Technical integrity

    -
    What do you mean by "attempting to interfere with the technical integrity of the site"?
    -

    Basically, we mean attempts to hack the site or spread viruses or other unwanted programs through it. If a user deliberately exploits a code vulnerability in order to install unwanted programs, redirect users to spam sites, or other destructive behavior, that's attempting to interfere with the technical integrity of the site. -

    -
    Does that mean I can't have nifty formatting in my story?
    -

    No, this is just a security policy. As for formatting: you will be able to have plenty of nifty formatting, but not everything imaginable. As a practical security matter, we will not be allowing javascript on the Archive, and only a limited subset of HTML. Something elaborately custom-coded would have to go on your own webspace. That is just because there is no secure way to allow people to start uploading unfiltered code. Our limits are designed in part to improve accessibility for all users, but we aren't trying to impose any editorial standard. The basic reason is simply that it's not technically safe to allow unfiltered code. -

    -
    Do you have a policy on bots or scraping? These are ways of extracting information from or indexing websites.
    -

    Using bots or scraping is not against our Terms of Service unless it relates to our guidelines against spam or other activities. However, we do reserve the right to implement robots.txt or other protocols limiting what bots can do, or to notify you and ask you to discontinue if a bot or scraping program is causing problems for the site. -

    -

    Harassment

    -
    Does the harassment policy cover everyone, or just Archive users?
    -

    Both Archive users and non-users might potentially complain about harassment. In today's online environment, the line between non-user and user can be blurry, and so our policy covers both users and non-users. Writing RPF (real-person fiction), however, never constitutes harassment in and of itself, even if the content is objectionable. Please see the - <%= link_to ts("harassment policy"), tos_path(:anchor => "IV.G.") %> for more information. -

    -

    Plagiarism

    -
    Can a person submit a plagiarism complaint anonymously, or without being the author of the plagiarized work?
    -

    Yes. Except in the case of - <%= link_to ts("copyright complaints"), tos_path(:anchor => "IV.D.") %>, a complaining person may submit a complaint via the web form, which does not require identifying information. -

    -

    Pseudonyms

    -
    Why would I want to have different pseudonyms connected to the same account?
    -

    We distinguish between usernames and pseudonyms, allowing multiple pseudonyms for each username. Pseudonyms are useful when more than one person wants to use a particular name; they allow multiple Sarahs or Kittens to coexist but be distinguished. Also, if you have used different fannish names over time or in different fandoms, you can keep them all on the Archive using pseudonyms. -

    -

    User Icons

    -
    Why are the rules for user icons more restrictive than the general Archive rules?
    -

    Right now, user icons appear on pages, such as user profiles, that are entirely unrated. The design provides for only one user icon per pseudonym, rather than multiple icons, which means that whatever icon a user picks will be visible to any browser. If there is substantial interest in changing the system, we may revise the Archive so that a user may have multiple icons and/or may rate their profile just as a fanwork may be rated, in which case we will change the user icon policy. The icon policy is not the general fanart policy. We presently allow embedding various kinds of files hosted elsewhere. We do anticipate eventually hosting images created by fans and treating them like textual fanworks, governed mainly by ratings and warnings rather than by content restrictions. -

    -

    Profiles

    -
    What can be on a user profile?
    -

    User profiles can contain information about the user, including information about the user's preferences and links to other sites on which the user can be found. User profiles must comply with the Archive's policies on harassment, impersonation, plagiarism, commercial promotion, and other conduct that threatens the integrity of the site. Users should not use their profiles to encourage the purchase of their other works (e.g., linking to a Kindle page). Linking to a personal page on another service is fine, even if the personal page offers some items for sale (e.g., the personal page includes links to an Etsy page), but the Archive is not advertising space. -

    -

    Ratings and Warnings

    -
    What kind of content do you allow?
    -

    We will not remove content from the Archive because it contains explicit material, as long as it doesn't violate any other part of the content policy (e.g., the harassment policy). -

    -

    One basic consequence is that users are responsible for reading and heeding the warnings provided by the creator. Risk-averse users should keep in mind that not all content will carry full warnings. If you want to know more, you may also wish to consult the bookmarks that people other than the creator have used to categorize the fanwork. -

    -

    Some creators do not want to put specific ratings or warnings on their works. Our policy aims to enable creators to choose appropriate labels or to opt not to use ratings and warnings, with the understanding that some users will avoid unrated or unwarned content. -

    -

    Though creators are not required to use ratings or warnings, they are often extremely useful to users. Ratings or warnings can attract some readers who are looking for specific content, and they can also warn off readers who are trying to avoid that content. Because fanworks may deal with controversial and painful issues, we encourage creators to choose ratings and warnings that help users make decisions about what to read. The "not rated" and "choose not to use Archive warnings" options will, of course, help users make decisions as well, though without much detail. -

    -
    What sort of information do I need to provide for my fanworks?
    -

    The aim of the Archive is to let fanwork creators and audiences find each other. Choosing at least one fandom is therefore the basic requirement. If we don't have your fandom listed, you can always add it. After that, you can add characters, relationships (if you want), a summary, and other information. -

    -

    The Archive also uses rating and warning tags. Our goal is to provide the maximum amount of control and flexibility for all users of the Archive, both creators and audiences, so that each user can customize their experience. It's always possible for creators to use "not rated" or "choose not to use Archive warnings," but audiences will always be able to avoid unrated or unwarned fanworks if that's how they want to use the Archive. -

    -

    From the user's perspective, users who wish to avoid warnings will be able to hide them. Author-supplied warnings are displayed by default unless and until a user changes their preferences. -

    -

    Users who wish to serve as filters for other users may also use bookmarks and recommendation tags. These will serve as an extra source of information for other users who are trying to determine whether or not to access a work. Users will have to choose to use other users' bookmarks and recommendations. -

    -

    This system is designed to offer numerous different ways to customize the experience on the Archive, which should in general accommodate users' desires for warnings or to avoid warnings, along with authors' ability to choose the appropriate warning or to choose not to provide warnings. In most cases, users can control their experiences by accessing only fanworks that have ratings and warnings that are acceptable to them, and creators can use their artistic judgment about what ratings and/or warnings, if any, ought to be on a fanwork. -

    -
    If I don't choose, what's the default?
    -

    The default is "not rated" and "choose not to use Archive warnings."

    -
    What's the consequence of a violation of the ratings/warnings policy?
    -

    Please see the - <%= link_to ts("Content Policy"), tos_path(:anchor => "content") %> for details. If we sustain a complaint about ratings, the fanwork will remain available on the Archive, but it will have the "not rated" label. If we sustain a complaint about warnings, the fanwork will remain available on the Archive, but it will have the "choose not to use Archive warnings" label. -

    -
    The ratings/warnings policy is really minimal. Why is this?
    -

    We believe that appropriate ratings and warnings are often in the eye of the beholder. Users who feel that a fanwork lacks an appropriate rating/warning are encouraged to try to resolve the issue with the creator. Users may also add tags of their own to on-site bookmarks of a fanwork, which other users can consult for more information. When those tags are present, you can click on the "Bookmarks" link at the top of the work to see them. -

    -
    What's the difference between ratings and warnings?
    -

    Ratings are a measure of the intensity of overall content. Warnings refer to more specific subjects and can be used to complete the sentences: -

    -

    I prefer not to read works that contain X

    -

    I search out and enjoy works that contain X

    -
    Do the Archive maintainers screen works as they're uploaded for compliance with the ratings/warnings policy?
    -

    No.

    -
    How will this work while the Archive is in beta?
    -

    Since many features will not yet be available in the early version of the Archive, the Content Policy only applies to active features. Because we're just getting started, we will emphasize communication and dialogue about policies; everyone, including the Archive maintainers, will be learning how this works. -

    -
    Can chapters of a larger work be rated/warned separately from each other?
    -

    Not in the initial version of the Archive, but we have put it on the roadmap for later addition.

    -
    Can I use "not rated" but not "choose not to use Archive warnings," or vice versa?
    -

    Yes, absolutely. So you could use "not rated" for a story that has a warning for rape, or you could rate a story "explicit" or "general" but choose not to give specific warnings. -

    -
    Do you distinguish between same-sex and opposite-sex relationships or activities for ratings purposes?
    -

    No. Please note that the creator's choice of rating is presumed appropriate. In assessing abuse complaints, we will not treat slash any differently than het. -

    -
    What's the difference between "general" and "teen and up" or "mature" and "explicit"?
    -

    This is left to the creator's judgment. People disagree passionately about the nature and explicitness of content to which younger audiences should be exposed. The creator's discretion to choose between "general" and "teen and up" or between "mature" and "explicit" is absolute: we will not mediate any disputes about those decisions. Instead, we encourage creators to consider community norms, whether fandom-specific or more general (such as how you'd expect a video game or movie with similar content to be rated), in selecting a rating. -

    -
    What's the difference between "teen and up" and "mature"?
    -

    Likewise, this is almost entirely up to the creator's judgment. In response to valid complaints about highly explicit content, the abuse team may redesignate a fanwork marked "general" or "teen and up" to "not rated," as explained in the - <%= link_to ts("abuse policy"), tos_path(:anchor => "content") %>, but our policy is generally to defer to the creator's decision. -

    -
    Suppose I'm searching for explicit fanworks. How will "not rated" Fanworks be treated in my search?
    -

    You can include or exclude "not rated" Fanworks from a ratings-based search. -

    -
    What are "Archive" and "additional" tags?
    -

    In addition to ratings, the Archive provides two separate lists of tags for creators to choose from when uploading a fanwork. These tags allow creators to ensure that their stories are accurately labeled. They are also helpful for many users in finding and categorizing work, or avoiding work which they may not want to see. -

    -

    When uploading a fanwork to the Archive, creators must choose at least one item from the Archive tags list. Along with some specific warnings, the list allows creators to select "choose not to use Archive warnings," and "none of these warnings apply." It is also possible to choose multiple Archive warning combinations, e.g. both "underage" and "graphic description of violence" if a fanwork contains both elements, or "choose not to use Archive warnings" and "underage" if the creator wants to disclose the underage content but doesn't want to say whether the work contains major character death. It's a little messy in that type of rare case, but trying to express that concept in other ways led to significant confusion. -

    -

    Creators may also choose to add additional tags to their work. These tags can be serious or humorous. They can be warnings or promises, or whatever else the creator chooses. Tags may be made synonymous for purposes of filtering by our Tag Wranglers, but your tags will continue to appear in their original form on your work. -

    -

    Please see - <%= link_to ts("our ratings and warnings policy"), tos_path(:anchor => "IV.K.2") %> for more information. -

    -
    What's the purpose of the Archive tags?
    -

    The purpose is to identify subjects that have been the subject of substantial, recurring debate in many sectors of fandom and provide an easy way to warn for those subjects (though a choice not to warn is always acceptable as well). We also decided to limit the Archive tags to a small number of subjects out of concerns for enforcement. Concepts like "dubious consent" vary substantially from person to person, and we decided that we could not reasonably expect fair enforcement of a rule requiring warnings (or a signal that the author chose not to warn) for concepts beyond those listed in the Archive warnings. -

    -
    What do you mean by "underage" in the Archive tags?
    -

    Underage refers to descriptions or depictions of sexual activity by characters under the age of eighteen (18). In general, we rely on authors to use their judgment about the line between reference and description or depiction. Sexual activity does not include dating activity such as kissing, but again, we rely on authors to use their judgment about what is generally understood to be sexual activity. An author may always specify the age of the characters. -

    -
    Why is "underage" defined as "under 18"?
    -

    Though there is no international consensus, there is a trend to focus on 18 as an important age in regulating depictions of sexual activity (as opposed to actual sexual activity/age of consent, which is regulated in many more varied ways). Thus, we decided that 18 would be helpful for the maximum number of users, including audiences as well as creators, though we recognize that no solution is perfect for everyone. We encourage creators and recommenders to be more specific in tags or summaries where this would be useful to potential audiences. -

    -

    Note from the Content Policy committee: With the exception of user icons, the Archive hosts only text, but we do plan to expand over time, and we do allow embeds of certain types of files <%= link_to ts("hosted elsewhere"), archive_faq_path("posting-and-editing", anchor: "hostsites") %>, which are also subject to the Content Policy. Because regulations of sexually explicit content are generally concerned with visual depictions, there is potentially more flexibility for textual depictions. The current rule is for 18 across the board, but we welcome suggestions on alternatives, especially from people with an interest in fan art. -

    -
    What about robots, computer simulations, elves, aliens, vampires who are three hundred years old but were turned into vampires at age 12, etc.?
    -

    The core use of the underage label is to identify fanworks depicting sexual activity by humans under the age of eighteen as measured in Earth years. Please use your judgment for other situations. If the fanwork does not include a depiction of sexual activity with a human under the age of eighteen as measured in Earth years, then we will not generally consider it "underage," though creators may use the tag if they feel it accurately represents their intent. As always, we encourage creators and recommenders to be more specific in tags or summaries where this would be useful to potential audiences. -

    -
    What about when a vignette or other fanwork doesn't specify the characters' ages?
    -

    The presumption is that the characters are of age unless the fanwork's creator indicates otherwise. -

    -
    What if there's only a brief reference to rape in a story–am I required to use either "rape" or "choose not to use Archive warnings," or can I still choose "none of these warnings apply" if I think that's a better description?
    -

    This is the kind of decision that is up to the discretion of authors. In general, we will not recategorize a fanwork in response to a complaint when the content at issue is a reference or is otherwise not graphic. -

    -
    You say that logged-in users can hide warnings. Why is that?
    -

    Some people consider warnings as spoilers and try to avoid them. This is part of our attempt to make the Archive user-customizable. -

    -
    If I'm not logged in, what can I see?
    -

    You can see anything rated "general" or "teen" without logging in or clicking anything else. For the other ratings ("mature," "explicit," and "not rated") you will be asked to agree that you are willing to see such content. -

    -
    Something that I consider really immoral, dangerous, triggering, or outrageous is not on the Archive list for warnings.
    -

    We're very sorry. We encourage you to use the additional tags, summaries, and user-provided bookmarks and recommendations to screen for fanworks you'll enjoy, and you may wish to comment to creators when you feel that further warnings would be desirable. The content policy committee would also like to hear your suggestions for the tag system. -

    -
    How will you apply the ratings and warnings policy to embedded images, videos, etc.?
    -

    In making rating/warning decisions, creators should take into account anything visible, including embedded images and videos. As with all other content, creators' decisions are presumed reasonable, and using "not rated" or "chose not to use Archive warnings" will always be sufficient. -

    -
    How will the ratings/warnings policy apply to fanworks that come in through Open Doors?
    -

    We will import the original ratings, warnings, and other associated information as part of Open Doors as best we can. However, the rating and warning systems used by older archives preserved through Open Doors may differ from our system. Therefore, an Open Doors work that can't be mapped to an Archive rating will be treated as if it were marked "not rated" and "choose not to use Archive warnings" unless the maintainer of the collection (or the original creator, if they "claim" the work) specifically selects other ratings and warnings for it. -

    -
    How explicit or graphic can the summaries and tags on my fanworks be?
    -

    Explicit or graphic content in itself does not violate the content policy. Please use your judgment about what will best identify and describe your fanworks. -

    -
    Someone has added a tag I hate to a bookmark of one of my fanworks!
    -

    We're very sorry. In general, user-provided tags can be positive or negative. Like any other content, tags are subject to the content policy, so if the tag violates the harassment, personal information, or other content policies, please report it. User-provided tags will not automatically be displayed on fanworks, in order to allow you to avoid them. -

    -
    So user-added tags will be displayed differently than creator-added tags? How does that work?
    -

    The tags displayed on the fanwork itself will only be the creator-placed tags. As a site user, you will see user tags in several different situations. These are the current plans: -

    -
      -
    1. If you use Tag Search, you'll be able to find fanworks that anyone, whether creator or user, has labelled with a particular tag.
    2. -
    3. If you use Work Search, you'll see tags from the creator.
    4. -
    5. You will be able to see when other people have publicly bookmarked a work. When you're looking at a given user's bookmark for a particular fanwork, you'll be able to see that user's tags.
    6. -
    7. You will be able to see a list of a user's public bookmarks, with associated tags, and a list of the public bookmarks for a fandom or other canonical tags, with associated tags.
    8. -
    -
    What kinds of fanworks can I post?
    -

    You can post any noncommercial, non-ephemeral fanwork. Here are some examples of allowed content:

    -
      -
    1. An audio performance of a fannish essay about vampire biology across sources, or the same essay in text form.
    2. -
    3. Short clips of footage from existing sources, edited over a song to make an argument or tell a story.
    4. -
    5. A comic telling the romantic adventures of the protagonist of a video game.
    6. -
    7. Photographs of a knitted character.
    8. -
    9. An alternative version of a Jane Austen novel in which there's a zombie apocalypse.
    10. -
    11. The supporting text for an original adventure for a tabletop roleplaying game.
    12. -
    -
    What if what I want to post isn't similar to one of the examples listed in the Terms of Service FAQ?
    -

    In general, you can post any non-ephemeral, transformative content that is fannish in nature. If you have doubts about any particular examples and you don't want to risk posting it, you can always contact our Abuse team to ask, using the Abuse form. -

    -
    Can I archive original fiction?
    -

    Yes and no. Although some users may want a place for all their creative work, our current vision of the Archive is of a place dedicated to fanworks in particular. The Archive was designed to serve the mission of the Organization for Transformative Works (OTW), which was "established by fans to serve the interests of fans by providing access to and preserving the history of fanworks and fan culture in its myriad forms." -

    -

    - Because our long-term plans include hosting fanworks of all kinds, not just fan fiction, we concluded that it was better to draw a line between fanworks and non-fanworks and only host the former, in order to avoid becoming a general repository for all sorts of creative works. In addition, we will enforce the noncommercialization policy strictly, including a ban on works posted to promote the sale of the author's other works, even if those are not hosted on the site. -

    -

    - However, there are a number of varieties of works produced by fans that do not fit comfortably into a narrow definition of fanfiction, fanart, vids, or other types of fanworks. Some of these do fall within our mission. In particular, original fiction that is part of an Open Doors project is allowed, as are types of original fiction and quasi-original fiction produced within a fandom context. Examples include such things as anthropomorfic, original fiction that is produced as part of a fandom challenge, exchange, or charity event, and genres such as Original Slash, Original BL, and Regency romances produced in Jane Austen fandom. -

    -

    - At such time as we are able to host art and vids, we anticipate similar policies will apply to those mediums: Art and vids will be considered fannish even in cases where they do not directly depict characters and elements from or use footage from an existing media source if they were created in a fandom context for a fandom audience. This may include such things as fanart that is intended to be for a particular canon but which does not contain readily identifiable canon characters or elements, art and vids produced as part of a fandom challenge, exchange, or charity event, illustrations of fanfiction, and vids that comment on a particular canon without using any clips from it (e.g., fan-produced videos set in a particular universe). -

    -

    - We presume that, by posting the work to the Archive, the creator is making a statement that they believe it's a fanwork. As such, unless the work doesn't meet some other criterion, it will be allowed to remain. -

    -
    Can I archive nonfiction?
    -

    -Fannish nonfiction, which includes what is called 'meta' by some fans, is allowed. Where we provide a specific function (search, bookmarking, challenges) we will ask you to use the specific methods we provide for those activities rather than create separate works. So, for example, a request for recommendations for particular kinds of fanworks would not be an appropriate work. That search should be carried out by searching works and/or bookmarks. A list of recommended works on a particular topic would also not be an appropriate work. Recommendations should be done by using our bookmarking function. A description of a challenge for other creators would also not be an appropriate work. That should be carried out by using our challenge function. -

    -

    -In addition, as an Archive whose goal is preservation, we want permanent, nonephemeral content. To the extent that your content is designed to be ephemeral, such as liveblogging episode reactions, it should go on a journaling service and not the Archive.

    -
    How will 'ephemeral' be defined?
    -

    'Ephemeral' applies to the nature of the work–designed to be experienced in a particular time period rather than the creator's desire to have a permanent record of their reaction, such as can be found on a journaling or blogging service. Our resources and database structure make it difficult for us to plan to host content of this type. -Please use your best judgment; our general policy is to defer to creators in cases of doubt. Ephemeral content could include, for example, a single short sentence, a single unedited image or .gif with or without a short caption, a short unedited video clip, or a short unedited sound clip. Ephemeral content is generally meant to be read at a particular time: for example, a message about a particular challenge or a reaction meant to be read while or just after a particular episode airs. -

    -

    -Our policies are designed to focus on our mission of preserving fanworks within our resource constraints, including the constraints on our hardworking, all-volunteer Abuse team.

    -
    What falls within the definition of fannish nonfiction?
    -

    Fannish nonfiction can be discussions of fannish tropes, essays designed to entice other people into a fandom, commentary on fandoms, hypothetical casting for alternate versions of works, documentaries, podcasts about fandom, explanations of the creative process behind a fanwork or works, tutorials for creating fanworks, guides for fan-created gaming campaigns, or many other things. -

    -

    -However, the nature of the Archive and the limitations of our resources mean that, while we will endeavor to host as much fannish content as possible, we need to put some limits on allowable works. In particular, the Archive is not a journaling service and it is not designed to host ephemeral content. -

    -

    -In addition, we have a separate project, Fanlore, which is a wiki designed to chronicle fandom history. Some fannish content, especially discussions of specific fandom-related events such as conventions or debates over particular incidents, may be more appropriate for Fanlore than for the Archive. -

    -

    -We will, in general, defer to the creator's characterization of a work as fannish nonfiction as long as it has a reasonably perceptible fannish connection, either to a specific source or to fandom in general, and takes the form of an independent, nonephemeral commentary. For example, an analysis of or commentary on multiple fanworks falls within our definition (and must comply with our other policies, including our harassment policy). An essay on a particular character's narrative arc in canon or of the interaction between film and comics versions of a source is also within our definition. -

    -

    -We understand that, as with many things, there are hard cases at the edges of categories, but we nonetheless need some limits in order to keep the Archive manageable for our hard-working volunteers as well as for other users.

    -
    What isn't fannish nonfiction?
    -

    The examples are potentially limitless, but here are some examples that we believe, based on our experience so far, do not qualify as fandom nonfiction and should not be posted as a work:

    -
      -
    1. episode transcripts and other non-transformative fandom material;
    2. -
    3. primarily autobiographical or non-fandom-related essays (e.g., essays on bike lanes, even if they contain a single reference to a fannish source);
    4. -
    5. general complaints about behavior towards a particular creator (e.g., a post stating that a work was deleted due to lack of feedback);
    6. -
    7. suggestions that other fans contact the creator through email or other social networks;
    8. -
    9. a single word or pairing name repeated hundreds of times;
    10. -
    11. offers and giveaways.
    12. -
    -

    As with all works, we presume good faith on the part of our users, and ask that you do the same for the fans who make up our Support and Abuse teams.

    + +

    Terms of Service FAQ

    + -
    How will you draw the line between fanworks and non-fanworks?
    -

    The presumption is that a work is a fanwork, but if it's clear from context—tags, author's notes, etc.—that it's not, it may be removed for violating the Content Policy. Please note that alternate universes/alternate realities or fanworks set in the distant past/future of a particular canon are still fanworks. Original works that are not based on a specific media source (canon) may also count as fanworks so long as they are fannish in nature. Please see "Can I archive original fiction?" above for more detail. -

    -
    What about nontextual works (like pictures of Daleks I crocheted)?
    -

    We currently don't host nontextual works other than user icons, though <%= link_to ts("you can embed various kinds of files that are hosted elsewhere"), archive_faq_path("posting-and-editing", anchor: "hostsites") %>. If it's a fannish transformative work or part of one, and otherwise complies with the content policy including the rating/warning policy, embeds are fine. Just be aware that we don't allow all kinds of embeds, for technical reasons, and that embeds may break for various reasons including trouble with the host site. -

    + +

    <%= t("a11y.navigation") %>

    +<%= render "tos_navigation" %> + -
    What is a fanwork's 'type'?
    -

    We expect to define a fanwork's type partly by its medium, and partly by its content. Thus, a fanwork can be text, audio, images or video (all different types), and it can be fiction or non-fiction.

    -
    How does tagging apply to work type, such as textual content, and podfics, vids, and other nontextual content? Do I need to use particular tags?
    -

    Please use tags that you think will help people identify works of the type you're posting; this can include fanwork types (e.g., podfic, vid, essay). Anything visible on the Archive should also follow the ratings/warnings policy.

    -

    Later versions of the Archive will have specific work types that can be selected on posting. No one will be penalized for having posted a work that, because of the implementation of work type, becomes technically "mislabeled." However, we may try some automated solutions for detecting work type and/or ask creators to change a work type when they posted before work type was introduced. Part of the transition may thus be to automatically set work type based on the presence or absence of certain tags or other work content, then notify the creators and allow them to change the work type if the automated process made a mistake. Administrators will also have the ability to correct an obvious miscategorization of work type (that is, a case that is not borderline even after deference to the creator) if the creator fails to respond to an inquiry after a reasonable time.

    -
    What kinds of mis-tagging does Abuse handle?
    -

    We encourage direct dialogue with the creator. However, Abuse also handles complaints about work language (e.g., Chinese mistagged as French); Archive warnings; Archive ratings; fandom categorization and work type (once work type is implemented). Tag recategorization policies are about required tags. Abuse will not recategorize any optional tags, though optional tags are subject to relevant principles of the general content policy, including the harassment policy. Unless a tag violates some other policy, such as the harassment policy, Abuse will not mediate disputes about general tagging (e.g., whether a particular relationship tag should or should not be present).

    -
    What do you mean by recategorizing a fanwork type?
    -

    For technical reasons relating to how our database is planned to evolve, we need for Archive administrators to have the ability to change a work type where it is clearly appropriate (e.g., a review essay or fanvid mistakenly or inadvertently categorized as textual fiction). Because work type will be a new addition (and we may create new categories over time), we understand that users won't necessarily go back and change the work type on previously uploaded works. Inaction on already-existing works will not be grounds for any penalty for users, even if we do later ask that the work type be changed to reflect what it is. People will also make mistakes when work type is in place.

    -

    As part of the transition to formal work types, we may automatically set work type based on the presence or absence of certain tags or other work features, then notify the creators and allow them to change the work type if the automated process made a mistake.

    -

    Once work type is in place, our general policy when a recategorization is clearly appropriate will be to ask the user to recategorize the work, and change the work type if we receive no response. In addition, our general policy is to defer to the creator's choices in borderline cases.

    -
    What do you mean by a manual recategorization?
    -

    A manual recategorization is an individualized determination that a specific work has been miscategorized, made as the result of a specific complaint. By contrast, any automatic process we use to detect work type will operate outside the abuse process, not as a manual recategorization.

    -
    When will you change a language tag?
    -

    When Abuse determines that there is no reasonable dispute that the work is not properly categorized, subject to ordinary Abuse procedures.

    -
    When will you remove a fandom tag?
    -

    When Abuse determines that there is no reasonable dispute that the work is not properly categorized, subject to ordinary Abuse procedures. Please note that we will apply this rule restrictively. We will not intervene in cases of disagreement over, for example, whether movie-based works can be tagged using comics/graphical works fandoms when there are both movie and comics versions of a source. This is the kind of decision a creator is best suited to make and falls within our policy of deference to the creator.

    -

    A fandom tag may be removed where there is no relationship between the particular fandom itself and the work. E.g., a fanwork that discusses vampire physiology and uses only examples from Buffy the Vampire Slayer and The Vampire Diaries fanworks should not add in fifteen additional fandoms that also feature vampires (though it can use "various", "fandom", or "fandom-general").

    -
    Will you recategorize or remove other tags, such as relationship tags?
    -

    Because our Abuse and Support resources are limited, and because different people interpret tags in many different ways, we don't think that we can fairly define or enforce specific rules about relationship or other similar tags, including the additional/freeform tags. We encourage users to engage with each other on these issues. However, our general harassment and anti-spam policies apply to tags, as they do to all Archive content.

    - - -
    What should I do about recs/commentary on fanworks?
    -

    <%= link_to ts("Our bookmarking feature"), archive_faq_path("bookmarks") %> is designed for your recommendations or commentary on specific works by other creators. You can bookmark fanworks hosted on the Archive or fanworks hosted elsewhere. Many creators also welcome discussion in the comments to the work, which is another appropriate place for such commentary. As always, while criticism of a fanwork is not itself harassment, content must comply with our other policies, including our harassment policy. -

    -
    I would like to create a list of recommendations or a list of works that use certain tropes.
    -

    Please use our bookmarking and recommending feature for this purpose. Any user with an account can create bookmarks and mark them as recommendations if they so desire. Bookmarks and recommendations can also be tagged, and filtered by those tags, creating groups of bookmarks or recommendations that can be linked to by using the URL of the resulting filtered list.

    -
    What counts as a recommendation that should be in a bookmark versus a more general discussion or analysis of multiple fanworks?
    -

    Please use your judgment on the best way to categorize a commentary. Our general policy is to defer to creators.

    -
    How does the harassment policy apply to reviews?
    -

    The Terms of Service state 'When judging whether a specific incident constitutes harassment, the abuse team will consider factors such as whether the behavior was repeated, whether it was repeated after the offender was asked to stop, whether the behavior was targeted at a specific person, whether that target could have easily avoided encountering the behavior, whether the behavior would be considered unacceptable according to normal community standards, etc.' This policy applies to reviews. Again, criticism of a fanwork, even harsh criticism, is not itself harassment. Calling a creator evil, wishing harm to them, and repeatedly posting negative commentary in a manner designed to be seen by the creator are potential examples of harassment.

    -
    What about 'directors' cut' or 'commentary' versions of my own fanworks?
    -

    We consider those versions of your fanworks, so post them as you would any other fanwork, though we suggest that you distinguish them from non-commentary versions, for example by adding [Directors' Cut] in the title or tagging them to indicate the difference between the original and the 'DVD-style' version. -

    -
    What about 'characters read' or DVD commentary-style/MST3K-style versions of other works?
    -

    If you have permission from the author or if the work is in the public domain, you're fine. In other cases, if you are reproducing a substantial part of the original (rather than just having your characters react to their reading or using occasional quotes) in a way that someone could just ignore your additions and read a substantial, continuous part of the original, then that's beyond what we're prepared to host. -

    - -

    -Please use our search functions for this rather than creating a separate work.

    -
    What about a fanwork prompt?
    -

    Please use our challenge function for this rather than creating a separate work.

    -
    What about a letter to someone I've been anonymously matched with for a challenge?
    -

    Since this content is designed to be ephemeral/nonpermanent and directed at a particular person, please do not create a separate work for it. If the challenge is hosted on the Archive, please put them in the optional details for the challenge. If not, and these are your general preferences, you can put them in your profile.

    -
    May I post the full lyrics of a song or an entire poem that isn't in the public domain without the author's permission?
    -

    No. -

    -
    What about character playlists or fanmixes?
    -

    You may use streaming sites such as Spotify, 8tracks, or YouTube, but you may not provide links that could deliver individual music file downloads, or a single zipped file containing music files, unless you have the right to distribute downloads (for example, a fan song or filk you composed and sang, a work in the public domain, or a Creative Commons-licensed download). Please also keep in mind that you may quote lyrics as part of a transformative work, but you may not reproduce the entire song text unless you have the right to do so.

    -
    May I post someone else's fanworks, giving them credit?
    -

    If you are an archivist seeking to back up your archive of works submitted by other creators, you can do this, but only by using our Open Doors project, which can assist you with importing and/or backing up your archive within the Archive of Our Own. Importing others' works without the involvement of Open Doors risks suspension or termination of your account. If you are not an authorized archivist, you may not post another creator's fanworks without permission. -

    -
    How do I report a violation of the Terms of Service?
    -

    Please use the <%= link_to ts("Report Abuse form") , new_abuse_report_path %> and provide the specific link, as well as any other information required to investigate the violation. You can report a violation both anonymously and under a email address, but take into account you will not be contacted about the case if you report it anonymously.

    -
    Do you have a DMCA (Digital Millennium Copyright Act) notice and takedown policy?
    -

    Yes. Please read <%= link_to ts("our DMCA policy"), dmca_path %> carefully.

    -

    Tags and Warnings

    -

    This is the list of Archive tags that authors will be able to choose from when posting a story. -

    -
    Archive Tags
    -

    Authors must choose at least one of these tags. The default is "choose not to use Archive warnings." -

    + +

    Main Text

    +
    +

    This document addresses common questions about the AO3 Terms of Service and how our policies are implemented. If you have any questions not answered by this FAQ, you can contact the Policy & Abuse committee.

    +
    -
    + +
  • + Privacy Policy FAQ + +
  • + + +

    General Principles FAQ

    +

    Answers to common questions about the General Principles of the AO3 Terms of Service are available below. If you have additional questions that are not covered here, you can contact the Policy & Abuse committee.

    + +

    General Questions

    +
    + +
    Table of Contents
    +
    + +
    +
    Why does the Archive of Our Own (AO3) have a goal of maximum inclusiveness of fanwork content?
    +

    AO3 was founded partly in response to a growing trend of fanworks being removed from websites that had previously allowed them. Commercial entities have frequently permitted fanworks in the early stage of their existence in order to expand their userbase, only to later prohibit certain types of fanworks from being shared on their platform. This pattern has been observed numerous times throughout fandom history. We want AO3 to be a non-commercial space where creators are able to permanently preserve and freely share as many of their fanworks as possible.

    +
    What does it mean for AO3's Terms of Service (TOS) to be governed by the laws of New York? What if I am a resident of a different state or country?
    +

    AO3 operates under the jurisdiction of Manhattan, New York in the United States. As such, the interactions between AO3 and its users, as well as the definitions in the TOS, will comply with U.S. legislative interpretations. If you reside in a different state or country, it is your responsibility – not AO3's – to know about and follow the laws in your local jurisdiction. For example, if certain content on AO3 is restricted under your local laws, it is not AO3's duty to delete that content for you; instead, it is your responsibility to avoid accessing that content.

    +
    What is an "implied warranty of merchantability"?
    +

    An implied warranty of merchantability is a legal agreement between a seller and a buyer that goods will be reasonably fit for the general purpose for which they are sold. For example, if you buy a toaster, it should be able to toast bread. It doesn't have to be perfect, but it should function as a typical toaster would.

    +

    This warranty is governed in the United States by the Uniform Commercial Code (UCC), which allows sellers to "disclaim" it. Disclaiming a warranty cancels the promise, which means the buyer takes on the risk that the product may not work as expected. By disclaiming this warranty about AO3, we are saying that you cannot hold us legally responsible if AO3 does not work as expected (in other words, you can't sue us).

    +
    What is an "implied warranty of fitness for a particular purpose"?
    +

    An implied warranty of fitness for a particular purpose is a legal agreement that exists when a buyer relies upon the seller to provide goods to fit a specific request. For example, if you ask a salesperson for a pair of boots to hike in the snow, and they sell you boots based on that request, they are making a promise (warranty) that those boots will be good for hiking in the snow. The seller has to know two things: 1) the buyer has a specific need, and 2) the buyer is relying on their recommendation. If both of those conditions are met, this warranty applies.

    +

    This warranty is governed in the United States by the Uniform Commercial Code (UCC). Like the implied warranty of merchantability, sellers can disclaim it, thereby shifting the risk back to the buyer. By disclaiming this warranty about AO3, we are saying that you cannot hold us legally responsible if AO3 does not suit the purpose you wanted to use it for, even if we know why you wanted to use it and you are relying on our recommendation to use it. For example, if you want to use AO3 to post your fanworks, but you find that you don't like AO3's posting interface and prefer to use another fanwork site, you can't sue us because our site doesn't meet your needs.

    +
    Why are you talking about buyers and sellers? Are you selling things?
    +

    No, AO3 is and always will be free to use. We don't sell anything. We don't sell products to you, and we also don't sell advertisement space or user data to third parties. AO3 is run by the Organization for Transformative Works (OTW), a non-profit which is funded by donations, not sales or advertisements. However, U.S. law regarding services and contracts generally assumes that one party is a buyer and one party is a seller, even if the service being "sold" is free (as in our case). We use this standard language to make sure that we aren't promising you something that we can't provide.

    +
    What counts as an official statement from the OTW?
    +

    Official statements are communications made by volunteers while they are fulfilling their formal volunteering responsibilities. These can include news posts and comments from official accounts.

    +

    Comments from official accounts are only official OTW statements when the volunteer is both acting in their official OTW role and providing information about the OTW or any of its projects or policies. For example, a news post moderator using an official account to reply to a question about a news post is acting in an official capacity; however, a volunteer who is using an official account to reply to a comment on a "Five Things" post about them (which is about that volunteer's personal opinions) is not acting in an official capacity.

    +
    What do you mean by "a worldwide, royalty-free, nonexclusive license"?
    +

    This means that we can make the content you post on AO3 available to other people who use AO3, without paying you. We will never charge for access to AO3 or otherwise sell your content. You can also put your content on other sites if you want, or remove it from AO3 if you no longer want it here.

    +
    What do you mean by modifying or adapting content?
    +

    This refers to how your work is displayed on the site, not how it is written, drawn, or otherwise created. For example, we may display portions of your content on some pages of the site, such as by showing your work's summary and tags in search results. We may also make changes to the formatting or display of your content in order to adapt to the technical requirements of different networks or devices, or to improve accessibility. For example, we may automatically convert HTML tags to our standard forms (for example, changing <bold> html tags to <strong>) or allow you to use nonstandard fonts and formatting while providing an alternate format for accessibility.

    +
    What are the rules for removing and retaining content on various parts of AO3 that are not fully controlled by the original poster?
    +
      +
    • Orphaning a work: Orphaned works will not be edited or removed unless they contain unauthorized information which may identify the creator or otherwise violate the Terms of Service.
    • +
    • Participation in a Challenge: The maintainer(s) of the challenge may edit or delete the sign-up or prompt, or remove a work from their collection, at any time.
    • +
    • + Comments on someone else's work: +
        +
      • The creator(s) of the work may freeze or delete comments on their work at any time and for any reason. They may also enable comment moderation and choose to leave comments unreviewed, or mark guest comments as spam.
      • +
      • Registered users can delete their own comments at any time. Guest users cannot delete their own comments.
      • +
      • The Policy & Abuse committee may delete comments in situations where a violation of the Terms of Service has occurred.
      • +
      +
    • +
    • Comments on an official AO3 or OTW post: Comments on official AO3 or OTW posts may be frozen, hidden, marked as spam, or deleted in accordance with the OTW News Post Moderation Policy.
    • +
    +
    How can I check if my country is under a comprehensive trade embargo by the US?
    +

    The U.S. Office of Foreign Assets Control (OFAC) provides up-to-date details regarding all current embargoes (including but by no means limited to comprehensive trade embargoes) on their website. Princeton University has a list of countries that are comprehensively sanctioned by OFAC.

    +
    Under what circumstances would you suspend an account for an invalid email address?
    +

    If we need to communicate with you to resolve an Abuse report, we will email you at the address associated with your AO3 account. If you do not check your email, or if you cannot receive emails because your email address is inaccurate or invalid, then we will have to resolve the complaint without your input. A sufficient number of sustained Abuse reports that fail to be delivered ("bounce") or do not receive a response could lead to permanent suspension.

    +

    If our emails to you repeatedly bounce, then we may suspend your account because we need to be able to communicate with you if necessary. In that situation, you can log in and get your account reinstated by associating it with a working email address. Then, if necessary, you can deal with whatever problem led to the Abuse report in the first place.

    +

    Having an invalid email address will not necessarily cause you to be suspended. However, if you violated the Terms of Service in other ways, those violations will not be excused just because you didn't receive our emails. It is your responsibility to ensure your email address is accurate and messages can be delivered to it. Repeated violations of the Terms of Service may result in a temporary suspension or a permanent ban, regardless of whether or not you were able to receive our emails warning you about the violations.

    +

    If we send a routine email about general site policies and it bounces, that will not lead to account suspension, but whatever policies we announce will still apply to all account owners. We will only suspend accounts with invalid email addresses when individual Policy & Abuse–related communications bounce.

    +
    Why did you license the Terms of Service under the Creative Commons Attribution 4.0 International License? What does that mean?
    +

    Creative Commons licenses allow people to use others' works under certain predefined conditions. The Creative Commons Attribution 4.0 International License (CC BY 4.0) permits you to use material from our Terms of Service (including the Content Policy and Privacy Policy) for any purpose, as long as you meet all of the following requirements:

    +
      +
    1. Give appropriate credit: You need to say that the material was created by us and provide a link to the CC license.
    2. +
    3. Indicate if changes were made: If you changed something from our original material, say so when crediting us.
    4. +
    5. Don't suggest we endorse your use: While you're free to adapt our material for your own use, you can't claim that we reviewed or authorized your specific work.
    6. +
    7. Don't impose additional restrictions: You may not apply legal terms or technological measures that restrict others from doing anything that this license permits.
    8. +
    +

    An example of appropriate attribution would be: "This work uses material from AO3's Terms of Service, which was released under a CC BY 4.0 license."

    +
    Does AO3's Terms of Service use material or inspiration from documents by other people?
    +

    Material in AO3's Terms of Service has been drawn from imeem and NearlyFreeSpeech.NET.

    +

    Back to Top | General Questions

    +
    +

    Age Policy

    +
    + +
    Table of Contents
    +
    + +
    +
    Why are children under the age of 13 not permitted to have an AO3 account or upload content?
    +

    In the United States, the Children's Online Privacy Protection Act (COPPA) governs the collection of personal information, including usernames and email addresses, from children under 13. Because the Organization for Transformative Works is a non-profit and does not sell any data, COPPA does not apply to AO3; however, we adhere to the same restriction as a matter of policy.

    +
    Why are children under the age of sixteen (16) who are residents/citizens of certain countries not permitted to have an account or upload content?
    +

    In some countries in Europe, the General Data Protection Regulation (GDPR) governs the collection and processing of personal data, including email addresses and IP addresses, as well as certain uses of cookies. Other countries may have similar data privacy laws. The age at which someone can consent to the collection of personal data without written permission from their parent or legal guardian may be higher than 13, depending on their country of residence. We do not want to store the type of detailed personal information about users that would be required to verify and accept such permission. Therefore, children who wish to create an account or upload content to AO3 must meet their country's minimum age requirements to legally consent to personal data collection without written permission.

    +
    What happens to an account or its content if the account owner is reported for violating the Age Policy?
    +

    If the Policy & Abuse committee determines that a violation of the Age Policy has occurred, the account will be suspended and content on the account may be removed. If the content is not removed, the suspended user or their parent or guardian can contact the Policy & Abuse committee to request deletion of the content associated with the account.

    +

    If you were previously suspended because of your age and you are now old enough to have an account, you may contact the Policy & Abuse committee to regain access to your account.

    +
    Why are children in the EU not allowed to ask their parent or legal guardian to upload content for them? Does this apply to children elsewhere, such as in the UK or the EEA?
    +

    AO3 adheres to the GDPR's requirements for handling the content and information of children within the GDPR's jurisdiction. Accordingly, the parents or legal guardians of children within the European Union are not allowed to upload their child's content under their own (the parent or guardian's) account.

    +

    This restriction does not apply to children in the United Kingdom or the European Economic Area (unless the child is also a resident or citizen of an EU country).

    +

    Back to Top | Age Policy FAQ

    +
    +

    Abuse Policy and Procedures

    +
    + +
    Table of Contents
    +
    + +
    +
    How do I report a violation of the Terms of Service (TOS)?
    +

    Please contact the Policy & Abuse committee. You must provide your email address and a direct link to the specific content you want to report. In the subject and description of your report, please briefly describe the content you are reporting, explain why you are reporting it, and include any additional links or other details that could help us investigate the violation. If you don't provide this information in your report, we may not be able to investigate or act upon your complaint.

    +

    If you are reporting a specific comment or comment thread, you can get the direct link by selecting the "Thread" button on the comment and copying the URL of that page.

    +

    If you are reporting multiple works or comments posted by the same user, please compile all relevant links and other information into a single report, rather than reporting each link individually.

    +

    If you wish to report content posted by multiple unrelated users (such as two different works by different people in the same fandom), please submit separate reports for each user.

    +
    What language should I select when submitting a report?
    +

    In general, if you are not comfortable with reading and writing in English, you should use the language you are most fluent in. If you are fluent in English and you are reporting something written in another language, you can either select English or choose the language of the content you are reporting.

    +
    Why aren't Digital Millennium Copyright Act (DMCA) notices covered by the Abuse Policy? What's the difference between a DMCA notice and an Abuse report?
    +

    A DMCA takedown notice is a legal mechanism that a copyright owner can use to request removal of their copyrighted material from a hosting site. The takedown notice must meet certain legal requirements. For example, you must declare under penalty of perjury that you are the copyright owner or are legally authorized to act on their behalf. As such, DMCA notices are assessed by the Legal committee, and valid notices may be forwarded to the user who posted the work. We reserve the right to make public all DMCA notices that we receive, though some information may be redacted for privacy. DMCA notices are not subject to the procedures described in the Abuse Policy, nor are they governed by the Policy & Abuse confidentiality policy.

    +

    Abuse reports, on the other hand, are held to a very high standard of confidentiality. They can be about any violation of the Terms of Service, including copyright infringement, harassment, commercial promotion, etc. Abuse reports are evaluated by the Policy & Abuse committee, who will never publish the details of a report or reveal any information about who submitted it.

    +

    Please do not submit an Abuse report if you intend to file a DMCA notice. Submitting both an Abuse report and a DMCA notice about the same content may delay the processing of both requests.

    +
    Can I submit an Abuse report even if I don't have an AO3 account?
    +

    Yes, but your report will be screened by our automated spam filters. If your report is rejected as spam, try using a different email address or removing extra links from your report description. Please also enter a valid email address so that we can contact you to request any additional evidence.

    +

    Reports from registered users are not subject to the spam filter, so long as you are logged in and the email address entered into the form is the one associated with your AO3 account (this will be prefilled for you).

    +
    Will I receive notification that my complaint was resolved? How long will it take?
    +

    We will make a reasonable attempt to accommodate a complainant's reply preferences. However, we may choose not to reply to complaints at our discretion, particularly if it is a non-urgent matter or if the complainant submits frequent, duplicate, or baseless reports.

    +

    Whether or not we reply to the complainant, our volunteers do evaluate all reports and act upon them as necessary. Complaints will generally be prioritized based on urgency, but because the Policy & Abuse committee is a small team of volunteers, we cannot guarantee any particular timeframe for the resolution of a complaint.

    +
    Do you monitor content on AO3 for violations of the Terms of Service?
    +

    No. We do not prescreen content on AO3, nor do we review content that has not been reported. If you believe you have encountered content that violates our Terms of Service, you will need to submit an Abuse report.

    +

    Please refrain from seeking out works that are in violation of the Terms of Service for the sole purpose of reporting or mass-reporting them. We investigate every report we receive, so submitting duplicate reports will only serve to delay the processing of the original complaint.

    +
    I'm not sure whether something is against the rules. What happens if I report something that doesn't violate the Terms of Service?
    +

    You can submit a report even if you aren't sure something is a violation. If the Policy & Abuse committee determines that what you reported is not in violation, you will receive an email letting you know and explaining why that type of content is allowed on AO3.

    +

    If you repeatedly submit reports about similar non-violating material, your subsequent reports may not receive a reply.

    +
    How do you determine whether content needs to be removed?
    +

    Abuse reports are reviewed by humans, not algorithms or bots. After a report is submitted, the Policy & Abuse committee reviews the reported content and independently evaluates whether or not it complies with our Terms of Service. If we determine that the content is in violation of the Terms of Service, only then do we take action to resolve the matter.

    +
    Would a work be removed if enough people reported it? What if someone repeatedly or intentionally submits complaints about something that doesn't violate the Terms of Service?
    +

    Multiple complaints about the same issue increases the time it takes for us to investigate, but we don't make decisions based on how many times something is reported. Mass reporting will not change whether or not the reported content is in violation of the Terms of Service, nor will it cause an issue to be addressed faster.

    +

    You don't need to worry that anyone's works will be taken down due to a baseless report or a mass-reporting campaign. We will only contact the subject of a complaint if they have in fact violated the Terms of Service. If we receive a report about something that isn't a violation, we will let the reporter know and close the report. If someone attempts to abuse our reporting system, such as by intentionally submitting baseless complaints, we may consider that harassment and take appropriate action.

    +
    What if the content I reported was deleted or edited before the Policy & Abuse team can investigate?
    +

    We appreciate good-faith attempts to resolve disputes, and in most such cases will close the complaint with no further action. However, we reserve the right to consider individual circumstances, including whether the poster has engaged in a pattern of such conduct. In such cases, if we verify that the original content violated the TOS, we may still decide to warn or suspend the original poster.

    +
    The instructions on the Policy & Abuse form say to include the username of the person I'm reporting. What if I want to report a guest comment or an anonymous or orphaned work?
    +

    You can mention in your report that the content was posted anonymously or orphaned. Content that violates the Terms of Service will be removed regardless of whether the original poster's name is publicly displayed. Penalties may be applied to the accounts of users responsible for posting violating content.

    +
    If I submit an Abuse report, will the subject be told who reported them?
    +

    In general, no. Abuse reports are kept strictly confidential. We do our best not to reveal any information about the identity of a complainant (such as a username), though in some circumstances it may be impossible to keep the source of the report completely anonymous. We do not ever disclose information that would be sufficient to identify a person in the physical world, such as an email address or legal name. For more information, please refer to the Policy & Abuse Confidentiality Policy.

    +

    Complaints can be submitted anonymously, but an email address is required. If you do not provide a valid email address and the complaint requires follow-up, we may be unable to take action.

    +
    Will I be informed if an Abuse report is filed about me or my work?
    +

    In general, the Policy & Abuse committee will only contact the subject of a complaint if there appears to be a violation of the Terms of Service, or if the team needs more information to resolve the issue.

    +

    If someone files an invalid report against you, we will inform them that their complaint has not been upheld. You will not be told about the complaint and no action will be taken against your account or content.

    +

    If we determine that you have in fact violated the Terms of Service, an email will be sent to the address associated with your account.

    +
    How do I find out who reported or complained about me?
    +

    Anonymity and privacy are essential to maintaining a fair reporting system. The Policy & Abuse committee will not disclose the identity of any complainant as part of an Abuse case.

    +

    All users are responsible for following the Terms of Service, and all users have the right to file a complaint if they witness someone violating the Terms of Service.

    +

    The Policy & Abuse committee will not uphold a complaint without investigating and confirming that a violation of the Terms of Service has occurred. This means that if the Policy & Abuse committee contacts you, their investigation has independently concluded that you have violated the Terms of Service.

    +
    What happens if a report is made about me and the Policy & Abuse committee determines that the complaint is valid?
    +

    The Policy & Abuse committee will send an email to the address associated with your AO3 account. That email will explain what the violating content or behavior is, where it occurred, and (if applicable) what you need to do to resolve it. If you cannot locate the email notifying you of your violation (please check your spam folder), you can submit an Abuse report and we will resend the original email to you.

    +

    What happens when a complaint is upheld depends greatly on the severity of the violation. For very minor issues, such as tag miscategorizations, we will simply ask you to fix the problem on your work. Violations of other portions of the Content Policy may result in content being temporarily hidden or permanently deleted, and/or a penalty being applied to your account.

    +
    Will I be notified if my work is hidden or deleted, or if I get suspended?
    +

    If your work is hidden, you'll receive an automatic email informing you that it's been hidden and providing you with a direct link to the work. You must be logged in to your account to access the hidden work.

    +

    If your work is deleted, you'll receive an automatic email informing you that it's been deleted. A copy of the work will be attached to that email.

    +

    In addition, the Policy & Abuse committee will also separately email you to explain why your work was hidden or deleted. If you've received an automatic "your work was hidden" or "your work was deleted" notification without also receiving an explanatory email from the Policy & Abuse committee, please check your spam folder. If you still can't find it, please contact the Policy & Abuse committee to let us know that you didn't receive our explanation.

    +

    If you are suspended, the email from the Policy & Abuse committee will inform you why you are suspended and how long your suspension will last. You will be further reminded automatically by the site if you attempt to post, edit, or delete content during the suspension period. If you were asked to edit or delete violating content on your account, you must wait until your suspension has ended in order to do so. You will not receive an email notification when your suspension is over.

    +
    I received an email from the Policy & Abuse committee, but I don't agree with or understand their decision. How do I appeal?
    +

    If you were contacted about something you did that is in violation of the AO3 Terms of Service, you can appeal the decision or request clarification by replying directly to the original email. If you cannot locate the email notifying you of your violation (please check your spam folder), you can submit an Abuse report, but please do not submit multiple appeals before receiving a response to the first one. Submitting multiple appeals will delay the processing of your appeal, because it creates more paperwork for us to handle before we can respond to you.

    +

    If you submitted a complaint and were told that the subject of your complaint is not in violation of the Terms of Service, then you can appeal the decision by replying directly to that email.

    +

    At least one Policy & Abuse administrator who was not previously involved with the original investigation will evaluate all information provided in an appeal to determine whether or not the appeal should be granted. Additional reviewers may be involved at the discretion of the Policy & Abuse committee. Please note that it may take some time to process your appeal and inform you of the result. The Policy & Abuse committee's decisions are final.

    +
    Are there any appeals that you will not grant?
    +

    In general, we do not email users or respond to complaints until after we have investigated the reported content and determined whether or not a violation of the Terms of Service has occurred. In order to appeal successfully, you will need to provide evidence demonstrating that our original decision was incorrect or did not adhere to the Terms of Service. If you only tell us "I want to appeal", then you haven't provided enough information for us to overturn our original ruling.

    +

    If you are notified that your content was removed in order to resolve a lawsuit or mitigate other liability, an appeal is unlikely to succeed. These cases are extremely rare, and are thoroughly discussed and reviewed at multiple levels before any action is taken.

    +
    If I disagree with the Policy & Abuse committee's decision, can I appeal to someone else, like the Support committee or the OTW Board?
    +

    No. If your appeal to the Policy & Abuse committee was rejected, you cannot appeal to any other committee. The Policy & Abuse committee is the final authority on TOS violations. To protect user privacy, other committees such as the Support committee and the OTW Board of Directors do not have information about Policy & Abuse cases. If you file an appeal with a different committee, they will simply forward the complaint to the Policy & Abuse committee or tell you to contact Policy & Abuse directly.

    +
    I was given a deadline to edit or delete my work, and I have done so. What happens next?
    +

    After the deadline, a member of the Policy & Abuse committee will review your work. The following situations may occur:

    +
      +
    • If you have already deleted your work, then there is no further action you need to take.
    • +
    • If you sufficiently edited your work, we will verify your edits. If your work was hidden, we will unhide the work. You will not receive a notification when your work is unhidden.
    • +
    • If you didn't sufficiently edit your work, and the work was not already hidden, we may hide the work. Please review the original email you received from us and contact us promptly if you don't understand what further edits you need to make. Failure to make all required edits may result in the deletion of your work.
    • +
    • If your work was hidden and you did not edit or delete all violating content, we will delete the work. You will automatically be emailed a copy of the work.
    • +
    +

    As a general rule, we will not review content in advance of any stated deadlines. While we strive to review content promptly after the deadline, the Policy & Abuse committee is composed entirely of volunteers. We therefore cannot guarantee a timeframe in which your content will be reviewed.

    +
    I was given a deadline to edit or delete my work, but I'm not going to make it in time. Can I have an extension?
    +

    If you need an extension on a deadline, please reply to the email the Policy & Abuse committee sent you. The Policy & Abuse committee will accommodate requests for extensions, within reason. Note that any hidden content will usually remain hidden during any extensions.

    +
    My work was removed by the Policy & Abuse committee. Can I repost it?
    +

    Works that have been hidden or deleted due to violations of the Terms of Service may not be reposted as-is. If you don't understand why your work was removed, do not re-upload the work. Instead, contact the Policy & Abuse committee to request clarification.

    +

    If you know what the original problem with the work was, you may be able to edit the work and upload a non-violating version. However, if you have not sufficiently edited your work and the new work is still in violation, you may be reported again. Violating the Terms of Service in a manner similar to a previous violation is grounds for suspension.

    +
    What if the violating content was posted years ago?
    +

    Content that is in violation of the Terms of Service may be reported and removed regardless of how long it has been since it was posted. The user responsible for uploading the violating content may be warned or suspended, subject to the discretion of the Policy & Abuse committee.

    +
    What do the different penalties mean?
    +

    Penalties are issued by the Policy & Abuse committee as a result of violating the Terms of Service. They are defined as follows:

    +
      +
    • +

      Warning: A warning is a formal notification to a user who has posted content that violates the TOS. Warnings do not affect the function of the user's account, but they are a permanent administrative record and a reminder not to repeat the behavior. At the discretion of the Policy & Abuse committee, a warning may be issued as a result of minor or unintentional violations of the TOS. A user who has previously received a warning and who violates the TOS again, especially in the same or a similar manner, is likely to incur a suspension.

      +
    • +
    • +

      Temporary Suspension: A temporary suspension is a time-limited restriction on the uploading of new content and creation of new accounts. During this time, the suspended user cannot upload new content, nor can they edit or delete content uploaded prior to the suspension. The duration of each suspension is subject to the discretion of the Policy & Abuse committee. A user who has previously been suspended and who violates the TOS again, especially in the same or a similar manner, may incur a longer temporary suspension or a permanent ban.

      +
    • +
    • +

      Permanent Suspension: A permanent suspension is a permanent ban on the uploading of new content and creation of new accounts. Permanently suspended users retain the right to remove, but not edit, content uploaded prior to their suspension.

      +
    • +
    +
    What happens to a user's works or other content when they are temporarily suspended?
    +

    In general, non-violating content is not removed from the site when a user is suspended. If a user is temporarily suspended, then they will be able to edit or delete their content after their suspension is over. Otherwise, suspended users who wish to delete their fanworks may contact the Policy & Abuse committee to have this done for them.

    +

    A user who has been temporarily suspended is not permitted to upload new content while they are suspended. Any new content uploaded to AO3 during the suspension would automatically be in violation. Such content may be removed and/or the alternate account(s) may be suspended. The duration of the original suspension may also be extended at the discretion of the Policy & Abuse committee.

    +

    After the suspension has ended, the user will have full access to their account(s) again.

    +
    What happens to a user's works or other content when they are permanently suspended/banned?
    +

    Permanent suspension doesn't delete someone's account or content. In general, any content that doesn't violate the Content Policy or other parts of the Terms of Service will remain on the account unless the user deletes it themselves or requests that the content be deleted by the Policy & Abuse committee.

    +

    A user who has been permanently suspended is not permitted to create a new account or upload new content to AO3. Any new content or accounts created by a permanently suspended user would automatically be in violation. The content may be removed and/or the alternate account(s) may be permanently suspended.

    +
    What sort of things would lead to each type of penalty?
    +

    It's impossible to define everything in advance. We are most concerned with people who are actively and deliberately hostile to the community. Small and honest mistakes are likely to result in warnings, especially on a first offense. More serious or deliberate violations of the Terms of Service may justify temporary suspension on a first or subsequent offense. Repeated and/or particularly severe TOS violations may result in permanent suspension.

    +
    What constrains the Policy & Abuse committee's discretion?
    +

    We are committed to building a community that welcomes anyone with a willingness to learn the rules while also safeguarding against those who intentionally violate them. Our discretion is aimed at that objective. We strive to handle all Abuse reports consistently, no matter which volunteer is doing the work. Procedurally, appeals undergo review by multiple Policy & Abuse committee members, and we require consensus or majority vote for major decisions. Our internal decision-making processes are designed to build in checks on individual discretion without trying to resolve every possible situation in advance.

    +
    What happens if someone who's a friend of someone on the Policy & Abuse committee is involved in a complaint?
    +

    We expect the members of the Policy & Abuse committee to behave professionally, even though the Organization for Transformative Works is an all-volunteer organization. We take the responsibilities of serving on the Policy & Abuse committee seriously, and a member of the team with a personal relationship to any party in a complaint is expected to recuse themselves entirely from the case, and, of course, to maintain our standards of confidentiality at all times. Failure to do so is grounds for dismissal from the Policy & Abuse committee.

    +
    AO3 is still being actively developed. How will the Abuse Policy apply to planned future features?
    +

    Since new features may be added at any time, the Abuse Policy only applies to active features. As we develop features, we will strive to be transparent and communicate with users about our policies as much as possible.

    +

    Back to Top | Abuse Policy and Procedures FAQ

    +
    +

    Content Policy FAQ

    +

    Answers to common questions about the Content Policy are available below. If you have additional questions that are not covered here, you can contact the Policy & Abuse committee.

    + +

    General Questions about the Content Policy

    +
    + +
    Table of Contents
    +
    + +
    +
    What is "content"?
    +

    Content is anything that you post on AO3 or otherwise submit to us. This includes, but is not limited to:

    +
      +
    • works
    • +
    • bookmarks
    • +
    • comments
    • +
    • tags
    • +
    • collections
    • +
    • links
    • +
    • icons
    • +
    • embedded text, image, audio, or video files
    • +
    • usernames and pseuds
    • +
    • profile and pseud descriptions
    • +
    • fannish next-of-kin information
    • +
    • Personal Information, such as an email address
    • +
    • any other item of information or type of content
    • +
    +

    All content on AO3 must comply with our Content Policy.

    +
    What happens if someone posts content that violates the Content Policy?
    +

    We do not prescreen content on AO3, nor do we review content that has not been reported to us. If you believe you have encountered content that violates our Terms of Service, you will need to submit an Abuse report.

    +

    The Policy & Abuse committee will investigate each report and independently determine whether a violation of the Terms of Service has occurred. Penalties may be applied to the accounts of users responsible for posting violating content. For more information, please refer to the Abuse Policy and Procedures FAQ.

    +

    Back to Top | General Questions about the Content Policy

    +
    +

    Offensive Content vs Illegal Content

    +
    + +
    Table of Contents
    +
    + +
    +
    Why doesn't AO3 remove extremely offensive content?
    +

    The Archive of Our Own was created in 2008 as a response to several challenges fandom faced at the time.

    +

    One common challenge was that platforms would unilaterally remove content without warning. The decisions were often shaped by the platform owner's preferences or determined by what was more friendly to advertisers. This meant that platforms frequently banned explicit sexual content.

    +

    Such prohibitions were often disproportionately enforced against minorities and marginalized groups. For example, fanworks featuring LGBT+ characters were (and still are) more likely to be reported and removed for having "sexual content". This is due to societal bias: a story featuring a romantic relationship between two women would be considered more sexual or adult than one with an equivalent relationship between a man and a woman. Such works would either be required to have a higher rating or were often removed entirely.

    +

    Biased enforcement of content rules has been shown to occur even when the purpose of the rule is to push back against discrimination. For example, rules intended to reduce racial hate speech on social media often end up being disproportionately enforced against racial minorities speaking out against racism or discussing their own lived experiences. To date, the problem of unbiased content moderation hasn't been solved by any large internet site.

    +

    Challenges such as these led AO3 to adopt a policy that welcomes all forms of fictional, transformative fanwork content. Our mission is to host transformative fanworks without making judgments based on morality or personal preferences. If it's a fictional fanwork that is legal to post in the United States, then it is welcome on AO3. This approach is intended to reduce the risk that content will be removed as a result of cultural or personal bias against marginalized communities.

    +

    We recognize that there are works on AO3 that contain or depict bigotry and objectionable content. However, we are dedicated to safeguarding all fanworks, without consideration of any work's individual merits or how we personally feel about it. We will not remove works from AO3 simply because someone believes they are offensive or objectionable.

    +

    All users who would like to avoid encountering particular types of content are recommended to make use of our filters and muting features.

    +
    Why does AO3 allow fanworks about things that are illegal in real life?
    +

    AO3's Terms of Service are designed to comply with United States law. It is legal in the U.S. to create and share fictional content about murder, theft, assault, or other such crimes. It is also generally legal in the U.S. to create and share fictional content about topics such as child sexual abuse, rape, incest, or bestiality. AO3 allows users to post and access fiction about all of these topics.

    +

    In accordance with U.S. law, AO3 prohibits Child Sexual Abuse Material (sexually explicit photorealistic images of real children). However, stories and non-photorealistic artwork are allowed, both under U.S. law and on AO3. Fiction about real people is still fiction, and therefore it is allowed on AO3.

    +

    Depending on your country of residence or citizenship, the laws that apply to you may be more restrictive than those of the United States. All users are responsible for following the laws that apply to them. If certain content on AO3 is illegal for you to access, then you should ensure you carefully observe all relevant ratings and warnings, and avoid opening any work that indicates it may contain such content.

    +
    What does banning "sexually explicit or suggestive photographic or photorealistic images of real children" mean, particularly for works featuring sexual content with underage characters?
    +

    Sexually explicit photographs, videos, and other photorealistic images of children (also known as Child Sexual Abuse Material, or CSAM) are prohibited in the United States and on AO3. Users who embed, link to, solicit, distribute, or otherwise provide access to such material will be banned and reported to the appropriate authorities.

    +

    Stories and non-photorealistic artwork (such as drawings or cartoons) that depict sexual activity involving characters under the age of eighteen are allowed, provided that the works are properly rated and carry the appropriate Archive Warning. However, photographic or photorealistic images of humans may not be used to illustrate works featuring underage sexual content. This includes (but is not limited to) photographs of children, porn gifs, photo manipulations, computer-generated or "AI" images, or other linked or embedded images that could potentially be mistaken for photographs of real humans.

    +

    We understand that not all photorealistic images of humans are actually documenting the real-life abuse of a child or derived from illegal material, but we decided to use a guideline that can be uniformly applied without relying on subjective judgment. If the work appears to feature underage sexual content (as indicated by the "Underage Sex" Archive Warning or other contextual markers present in the work's tags, notes, or text), then the Policy & Abuse committee may require all photographic or photorealistic images of humans, regardless of age, to be removed from the work.

    +
    Does AO3 screen for quality?
    +

    No. We welcome creators and fanworks of all skill levels, and we will never remove a work on account of its quality, grammar, spelling, or punctuation.

    +
    How can I avoid works that contain content I don't want to encounter?
    +

    When browsing a tag, you can use the Filters sidebar to filter out works. If you are on a mobile device, select the "Filters" button at the top of the page to bring up this sidebar. Adding tags under the Exclude section will remove all works that use an excluded tag. For example, you can exclude any ratings and Archive Warnings that you don't want to encounter (make sure to also exclude the non-specific Rating and Archive Warning tags). You can also exclude other types of tags that users have chosen to add to their works.

    +

    You can use the Search within results box to filter works using keywords. This searches the metadata (title, summary, tags, and beginning/end notes) of a work, but not the chapter notes or body text. You can also use the following symbols to refine your search:

    +
      +
    • A minus sign (-) in front of a word (or a phrase in quotes) in the "Search within results" box will filter out all works that have the word anywhere in their metadata.
    • +
    • An asterisk (*) before or after your search term will allow you to look for partial matches.
    • +
    • If your search term has multiple words, using straight double quotation marks ("like this") on either side of your search term will allow you to search for the exact phrase. Single and/or curly “smart” quotation marks will not work.
    • +
    +

    For example, if you enter -sex* into the "Search within results" box, your results will exclude any works with metadata that contains words beginning with "sex" (including "sex", "sexual", "sexy", etc).

    +

    Once you have a search setup that works for you, you can bookmark the page in your browser in order to return to it later. If there's content you want to avoid on AO3, we recommend using filter keywords and browser bookmarks to exclude that content from what you encounter while browsing.

    +

    If you need help using filters, please contact the Support committee.

    +
    How can I avoid encountering works or other content by a specific user? What about anonymous or orphaned works?
    +
    Mute a specific user
    +

    If you want to avoid all content by a specific user, you should mute the user. To mute a user, go to their dashboard by following the link in their username. Then select the "Mute" button in the top-right corner, and confirm that you want to mute them on the next page. Muting a user means you will no longer be shown their works, series, bookmarks, or comments while browsing AO3. Please note that muting is a separate function from blocking.

    +
    Mute a specific work
    +

    If you want to hide a specific work (for example, a specific anonymous or orphaned work), you can mute it with a site skin. To do so, create a site skin and add .work-000 { display: none !important; } to it, replacing 000 with the ID of the work you want to mute. The work ID number can be found in the work's URL immediately after /works/. For example, 000 would be the work ID of https://archiveofourown.org/works/000/chapters/123. Make sure to apply your site skin after you've created it. You can contact the Support committee if you have any problems using site skins.

    +
    How can I prevent a user from commenting on my works or interacting with me?
    +
    Block a registered user
    +

    If you want a registered user to stop interacting with you, you can block the user. To block a user, select the "Block" button on any of the user's comments or on their user profile or dashboard, then confirm that you want to block them on the next page. Blocking a user means they cannot comment on or kudos your works, reply to your comments elsewhere, or give you gifts outside of a Challenge. Please note that blocking is a separate function from muting.

    +
    Block guest (anonymous) users
    +

    If you want to prevent a guest user from commenting on your works, you can disable guest comments or only allow registered users to access your works. You may also want to enable comment moderation.

    +

    To prevent guest users from replying to your comments on other users' works, go to your Preferences page and enable "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)". Remember to select the "Update" button at the bottom of the page to save any changes to your preferences.

    +

    Back to Top | Offensive Content vs Illegal Content FAQ

    +
    +

    Fanworks and Non-Fanwork Content

    +
    + +
    Table of Contents
    +
    + +
    +
    What kinds of fanworks can I post?
    +

    AO3 allows a wide range of fanworks other than fanfiction, including but not limited to art, videos, crafts, games, fanmixes, authorized podfics, authorized translations, fannish nonfiction, original fiction, and more. You can post any non-commercial, non-ephemeral fanwork. Here are some examples of allowed fanwork content:

    +
      +
    • A retelling of an existing story from another character's point of view
    • +
    • An original fantasy story about a modern person traveling to medieval times
    • +
    • An alternative version of a published novel in which there's a zombie apocalypse
    • +
    • The supporting text for an original adventure for a tabletop roleplaying game
    • +
    • A fannish essay about vampire biology, or the same essay in audio or video form
    • +
    • Short clips of footage from existing sources, edited over a song to make an argument or tell a story
    • +
    • Artwork (such as a drawing) of an iconic scene from a book, movie, or TV show
    • +
    • A comic about the romantic adventures of a playful thief
    • +
    • Photographs of a crocheted (amigurumi) character you made
    • +
    +
    What if what I want to post isn't similar to one of the examples listed above?
    +

    In general, you can post any non-commercial, non-ephemeral, transformative content you created that is fannish in nature. If you're uncertain if your work can be posted on AO3, you can always contact the Policy & Abuse committee to ask.

    +
    How will "ephemeral" be defined?
    +

    Ephemeral content is material that exists primarily to share someone's impressions, reactions, or feelings about a current event, fandom, or trend. If the content contains limited analytical or interpretive content, or lacks any artistic material, it is likely to be classified as ephemeral. Some examples of ephemeral content include live reactions, announcements about upcoming fanworks, and requests for prompts.

    +

    While it may benefit general fannish history to keep a record of such moments, AO3 is not intended to host all content that is fannish in nature. This type of content is often better suited for social media, personal sites, or blogging services. The Organization for Transformative Works also runs Fanlore, a wiki about fanworks and fan communities, including fandom trends and current events.

    +

    Please use your best judgment. Our general policy is to defer to creators in cases of doubt; however, the Policy & Abuse committee has final discretion in determining ephemerality.

    +
    Can I post original fiction?
    +

    Yes. Original works are allowed, unless the work would be in violation of some other part of the Terms of Service.

    +

    Our vision of AO3 is for all fanworks, including those beyond traditional fanfiction, fanart, and fanvids. Original stories and artwork, including those imported as part of an Open Doors project, are permitted. Some examples of original fiction that we host include original slash, anthropomorphic works, and Regency romance. However, works intended for commercial publication are not suitable for AO3. The Policy & Abuse committee has final discretion in maintaining AO3's focus on non-commercial fannish works.

    +

    We generally presume that, by posting the work to AO3, the creator is making a statement that they believe it's a fanwork. As such, original work will be allowed to remain unless the work is in violation of some other part of the Terms of Service, such as our plagiarism or non-commercialization policies.

    +
    Can I post nonfiction?
    +

    Fannish nonfiction, which includes what is called "meta" by some fans, is allowed. However, it must still be fannish in some way and contain some kind of analytical or creative content. In addition, as an Archive whose goal is preservation, we want permanent, non-ephemeral content. If the content is meant to be ephemeral, such as a liveblog of episode reactions, it should be posted on a social media account rather than on AO3.

    +

    Examples of fannish nonfiction and things that are not fanworks are available below.

    +
    What falls within the definition of fannish nonfiction?
    +

    Examples of fannish nonfiction allowed on AO3 include:

    +
      +
    • Discussions of fannish tropes
    • +
    • Essays designed to entice other people into a fandom
    • +
    • Commentary on fandoms
    • +
    • Documentaries or podcasts about fandom
    • +
    • Explanations of the creative process behind one or more fanworks
    • +
    • Tutorials for creating fanworks
    • +
    • Guides for fan-created gaming campaigns
    • +
    • Detailed analyses of multiple fanworks
    • +
    • Essays on characters' narrative arcs in canon
    • +
    • Comparisons of the film and comics versions of a source
    • +
    +

    This isn't an exhaustive list – fannish nonfiction may take many other forms.

    +

    We will generally defer to the creator's characterization of a work as fannish nonfiction as long as it has a reasonably perceptible fannish connection, either to a specific source or to fandom in general, and takes the form of an independent, non-ephemeral commentary. However, not all nonfiction falls within our mandate. Please consider what isn't fannish nonfiction before posting your work. While we acknowledge the complexity of certain cases that may fall on the boundaries of categories, setting limits is necessary to maintain AO3's manageability for our dedicated volunteers and users.

    +
    What are some examples of non-fanwork content that should not be posted as works on AO3?
    +

    The examples are potentially limitless, but here are some examples that do not fall under AO3's definition of fannish fiction or nonfiction and should not be posted as a work:

    +
      +
    • ephemeral content (including personal journal or diary entries, reactions, or blog posts)
    • +
    • episode transcripts, reposted canon material, and other non-transformative fandom content
    • +
    • primarily autobiographical or non-fandom-related essays (for example, your science, math, or philosophy homework, even if it contains a reference to a fannish source)
    • +
    • lists of names, titles, or statistics (such as information about a character's name, age, pronouns, and personality traits) that contain little to no other analytical, narrative, or descriptive content
    • +
    • discussions of specific fandom-related events (such as conventions or debates over particular incidents), which are considered more appropriate for Fanlore
    • +
    • general statements, questions, or complaints about a person or group
    • +
    • suggestions that other fans contact the creator through email or other social networks
    • +
    • links, lists, or requests for recommendations, whether of fanworks or published works
    • +
    • ads for roleplaying partners, sessions, servers, sites, or games
    • +
    • advertisements, offers, and giveaways
    • +
    • technical instructions (for example, a recipe for making ice cream, or a list of steps explaining how to assemble an ice cream machine)
    • +
    • faceclaims, fancastings, or other reference lists (such as collections of photographs, media, or other resources)
    • +
    • a single word or phrase repeated hundreds or thousands of times
    • +
    • prompts or requests for prompts
    • +
    • announcements, placeholders, or updates about future, existing, or deleted works
    • +
    • an explanation of why a work was removed from AO3
    • +
    +

    Works that incorporate fannish content in clearly bad faith are not fanworks. For example, a work primarily composed of fic search requests is not a fanwork even if there are a few sentences of fandom-specific content.

    +

    In general, we presume good faith on the part of our users, and ask that you do the same for the fans who make up our Support and Policy & Abuse committees. The Policy & Abuse committee will exercise its discretion, which is final, in the service of maintaining AO3 as a place focused on non-ephemeral fanworks.

    +
    How will you draw the line between fanworks and non-fanworks?
    +

    The presumption is that a work is a fanwork, but if it's clear from context (summary, tags, notes, etc.) that it's not, it may be removed for violating the Content Policy. The Policy & Abuse committee will consider many factors when determining if something is a fanwork, such as whether the reported content is transformative, ephemeral, or fannish in nature.

    +

    Additionally, original works that are not based on a specific media source (canon) are considered fanworks. Please see Can I post original fiction? for more detail.

    +
    Can I post a "placeholder" work to tell other fans that I intend to post my story soon?
    +

    Placeholders are not allowed on AO3. This includes works in progress that do not have any story content, works where only the summary and tags have been posted, and lists of ship names, character profiles, or ideas for what you plan to publish. You can create a draft to edit your tags or preview your work before posting it publicly, but please don't post the work unless you have at least one chapter of your fanwork that is ready to be shared with other people.

    +
    Can I post fancastings for my story as a separate work? What about character notes or profiles?
    +

    A work that is simply a collection of actors' photos paired with character names would not be considered a fanwork, nor would a work that only consists of statistics or summaries of canon elements (such as what you might find on a fannish wiki). However, if you include more in-depth analytical, descriptive, or narrative content, then we would likely consider that fannish analysis (meta), which is allowed. An example would be an extended analysis of the character traits that led you to choose that particular fancasting.

    +

    If you would like to post fancastings, statistics, or a summary about your own fanwork, you can include it in the fanwork's notes or post it as an extra chapter inside the fanwork.

    +
    Can I post "incorrect quotes"?
    +

    No. The incorrect quotes meme format involves a collection of quotes where the names have been substituted with the names from a different source. While the amount of quoted text is relatively small, replacing names and minor rewording of quotations is not sufficiently transformative to make the resulting content a fanwork.

    +
    Can I post non-textual works (such as fanart, fanvids, or podfic)?
    +

    We currently don't host multimedia content other than user icons, though you can embed various kinds of files that are hosted elsewhere if it's a fannish transformative work (or part of one) and otherwise complies with the Content Policy. However, for technical and legal reasons we don't allow all kinds of embeds. Please read our policies about embedding images that you did not create, podfics of stories that you did not write, images that depict explicit content, and images in works featuring underage sexual content. In addition, keep in mind that embeds may break for various reasons, including trouble with the hosting site.

    +
    Can I post "directors' cut" or "commentary" versions of my own fanworks?
    +

    We consider those versions of your fanworks, so you may post them as you would any other fanwork. We suggest that you distinguish them from non-commentary versions, for example by adding "[Directors' Cut]" in the title or tagging them to indicate the difference between the original and the "DVD-style" version.

    +
    Can I post announcements or status updates as separate works? What about if I post it as a chapter of my fanwork?
    +

    No, you cannot post announcements or other blog-style updates as separate works. Status updates and other author notes are considered ephemeral content. If you want to discuss a fanwork you've posted or plan to post on AO3, we suggest including such information on your profile page or in the notes or comment sections of your existing fanworks.

    +

    In general, adding several announcement chapters to an existing fanwork will not cause your entire work to become a non-fanwork. However, if you want to talk extensively about your works or personal life, then we recommend linking to a social media site in the notes of your fanworks instead of posting announcement chapters.

    +
    Can I post roleplay ads?
    +

    No. Requests or calls for roleplay partners are not fanworks. We encourage you to seek and advertise for roleplaying partners or servers on your preferred social media platform(s) instead.

    + +

    No. Please use our search functions for this rather than creating a separate work. You can use our Work Search or Bookmark Search, or select a particular tag and then use the Filters sidebar to further refine the results.

    +

    Should you find that this is not sufficient to locate the work you are seeking, your preferred internet search engine (such as Google or DuckDuckGo) may be able to search for distinctive phrases within the work text itself. You can perform an AO3-specific sitewide search by adding the search term site:archiveofourown.org, which will limit your results to pages on AO3.

    +

    If you require assistance from other users, we advise seeking out fandom-specific or pairing-specific fic-finding communities on social media platforms such as Tumblr, Reddit, or Discord, whose members will be more than happy to help you locate the works you are looking for.

    +
    I have an idea that other people might want to write. Can I post my prompt or challenge as a work?
    +

    No. Please create a prompt meme to offer suggestions or challenges to other people, rather than posting a work.

    +
    Does that mean I can't write a short story scene or snippet and suggest that others continue where I left off?
    +

    A short, distinct piece (such as a drabble or vignette) would be considered a fanwork. If you have written a full scene or outlined the plot in enough detail that it could count as a fanwork in and of itself, that would also be allowed.

    +

    However, if you only have a handful of sentences or bullet points and there isn't much plot or characterization, that may be considered a non-fanwork placeholder or prompt. If your primary reason for posting the work is to offer ideas or suggestions for other people, please create a prompt meme instead of posting a work.

    +
    I want other people to give me prompts or requests for fanworks they'd like to see. Can I post a work so that they'll have a place to do that?
    +

    No, you cannot post a work that is only a request for other people to give you prompts. Please create a prompt meme instead. You can share the link to your prompt signup form on social media or in the notes of your fanworks.

    +
    Can I post a letter to someone I've been anonymously matched with for a challenge?
    +

    No. Since this content is designed to be ephemeral (it is directed at a particular person for a particular event), please do not create a separate work for it. If the challenge is hosted on AO3, please put your letter in the optional details for the challenge. If you want to share your general preferences such as your favorite fandoms or tropes, you can put that information in your profile.

    +
    Can I create a list of recommendations or a list of works that use certain tropes? If I want to include commentary on the fanworks I am recommending, would that count as meta?
    +

    Posting a "rec list" (one or more recommendations as a work) may be a violation of our non-fanwork policy. Please use our bookmark feature for this purpose instead. Bookmarks can include commentary, be marked as recommendations, and organized with tags or into collections.

    +

    Criticism of a fanwork is permitted in the tags or notes of a bookmark and will not be considered harassment. However, no matter its location on the site, all commentary must comply with our other policies, including our harassment policy.

    +

    The difference between a recommendation versus a general meta-discussion or analysis of a fanwork is determined through several factors, such as whether the content is ephemeral in nature or if it contains analytical or interpretive content. A work is more likely to be a non-fanwork if it's just a list of titles and summaries (such as a "Top 10 List" or "Recs for Fluff Fics") or if it's similar to a product review (for example, "This is the best slow burn fic in the fandom and here's why you should read it"). If the work contains extended commentary or analysis about the nature of the recommended work, it is more likely to be considered a fanwork and allowed on AO3.

    +

    Please use your judgment on the best way to categorize such commentary.

    +
    Are there any limits to what I can use AO3 bookmarks for?
    +

    On AO3, bookmarks are intended for organizing and recommending fanworks hosted on any site (not just AO3). For example, you can bookmark fanfic from FanFiction.Net, fanart on DeviantArt, fanvids on YouTube, or meta posts on Tumblr. However, you should not create a bookmark for something that isn't a fanwork. Bookmarking a news article, meme, or reaction gif would be in violation of our non-fanwork policy because those aren't fanworks.

    +

    In addition, please keep in mind that bookmarks are also subject to all of our general content rules, including our policies against harassment, commercial promotion, and copyright infringement. For example, you cannot use bookmarks to link to illegally distributed copies of copyrighted material.

    +

    As offsite ("external") bookmarks are described by users, their actual content may differ from the descriptions offered. Please exercise caution when following any links away from AO3, including external bookmarks.

    +

    Back to Top | Fanworks and Non-Fanwork Content FAQ

    +
    +

    Commercial Promotion

    +
    + +
    Table of Contents
    +
    + +
    +
    Why is commercial promotion prohibited on AO3?
    +

    The Organization for Transformative Works is committed to the defense and protection of fans and fanworks from commercial exploitation and legal challenges. AO3 was created to give fan creators a non-commercial space to share their works.

    +

    It is part of AO3's mission to remain a non-commercial space, so all forms of commercial promotion and activities are prohibited. AO3 isn't the right place for offering merchandise or requesting donations, whether for yourself or others. We enforce the non-commercialization policy strictly.

    +
    What kinds of things are considered "promotion, solicitation, and advertisement of commercial products or activities"?
    +

    Some examples of commercial activities include:

    +
      +
    • linking to or referencing the use of a commercial platform or the monetization features of a non-commercial platform
    • +
    • providing a "tip jar", bank information, or other method for people to give you money
    • +
    • offering paid commissions or other content in exchange for money, gift cards, or similar
    • +
    • stating that a fanwork was created as a result of a donation or paid commission
    • +
    • encouraging donations to a person or cause
    • +
    • listing potential benefits of a paid membership or subscription
    • +
    • posting free previews for paywalled content
    • +
    • advertising a paid service or product (linking to an item's product page, suggesting that others purchase an item, etc.)
    • +
    • selling merchandise, even if the merchandise is fannish in nature
    • +
    • discussing the sale of the creator's other works, even if that paid content is not itself hosted on AO3
    • +
    • creating or promoting an app or website that charges money to access works posted on AO3
    • +
    +

    In general, if a financial transaction is involved, you cannot discuss it on AO3. Commercial activity is prohibited regardless of the reason why the commercial activity is occurring.

    +
    What do you mean by "commercial platform"?
    +

    A commercial platform is a site whose primary purpose is to facilitate the exchange of money. This includes online storefronts as well as tipping, patronage, subscription, and crowdfunding services. Linking, discussing, or referencing someone's presence (including your own) on a commercial platform is prohibited.

    +

    Examples of commercial platforms include, but are by no means limited to:

    +
      +
    • Amazon, Etsy, Redbubble, and other online storefronts
    • +
    • Patreon, Ko-fi, and other patronage, tip-jar, or subscription services
    • +
    • Kickstarter, GoFundMe, and other crowdfunding services
    • +
    • PayPal, Venmo, and other money transfer services
    • +
    +

    As stated, this list is non-exhaustive. If the primary function of the platform involves allowing someone to give someone else money, then you should not advertise your presence on it in any way.

    +
    What do you mean by "monetization features of a non-commercial platform"?
    +

    Sometimes sites mainly dedicated to some other purpose (such as social media or image/audio/video hosting) will also have features or subsections of their platform dedicated to monetization. For example, DeviantArt is primarily a free-to-use art gallery and social media website, but it does have some specific sections of its platform that are commercial in nature, such as DeviantArt's Shop, Commissions, Premium Galleries, and Core Memberships. In such cases, you are allowed to link to content on the "free" portion of the website, so long as the page you are linking to is non-commercial in nature. However, linking to or mentioning the monetized content is not allowed.

    +
    Can I link to my Tumblr, Discord, Linktree, Wordpress, or other social media account or personal website? What if my profile or pinned post on that site has a link to a commercial platform?
    +

    In general, linking to your social media accounts or personal website is fine, even if you sometimes post about commercial activities on that site. However, you may not link to accounts, posts, sites, or pages that reference commercial activity in the URL, or that are primarily commercial in nature (such as a Carrd that lists your published novels and explains where to buy them). In addition, you may not provide instructions anywhere on AO3 for finding your commercial content elsewhere (for example, stating that information about your paid commissions is available on a specific webpage even if you don't link directly to that page).

    +
    I am a published author. Can I let people know what my pen name is or what my books' titles are?
    +

    Stating in general terms that you have written a book or providing your pen name is fine, even if you use that pen name for commercial works. However, you may not use AO3 to promote your commercial works, tell people where they can find or buy those works, or otherwise advertise your commercial works in a manner that could encourage others to seek out and purchase the works.

    +
    Can I ask for donations or tips on my account profile or my original works?
    +

    No. Our non-commercialization policy applies everywhere on AO3, including user profiles, comments, fanworks, and works in the "Original Work" fandom. Asking for donations or tips is considered engaging in commercial activities and is not allowed anywhere on the site.

    +
    Can I post the first chapter of my published original novel on AO3?
    +

    Posting a preview or advertisement for a commercial work is not allowed. This includes uploading only a "snippet" to promote a larger paid work, as well as removing significant portions of a fanwork that has been "pulled to publish" professionally. Even if it contains some fannish content, sharing excerpts intended to promote or sell paid content is prohibited.

    +
    I post all of my fanworks on AO3 for free, but I also have paid supporters on another site who get new chapters a week early. Can I let readers know that these "early access" chapters exist, if I don't explain how or where they can subscribe?
    +

    No. If you provide an "early access" service in exchange for money, you cannot reference it on AO3. This includes stating that additional "bonus content" is available elsewhere, regardless of whether you plan to make the content available to the public in the future. Advertising paywalled content is not allowed even if you don't provide instructions, include links, or name a specific commercial platform.

    +
    The stories posted on my Patreon are free for anyone to read, even if they're not one of my Patrons. Can I link to one of these stories from AO3?
    +

    No. Sites like Patreon are considered commercial platforms because their primary purpose is to enable creators to receive funding from their fans. Linking to commercial platforms is not allowed, even if some content on the platform is free.

    +
    I would like to thank one of my clients or patrons (someone who supported me monetarily). Can I acknowledge them in the notes of my work?
    +

    You may not indicate anywhere on AO3 that other people are financially supporting you due to your fanworks. This includes stating that they paid for a commission, donated money to you or others, or are a patron or paid subscriber. However, you are allowed to name someone in a non-commercial manner, such as by crediting them for a prompt or by using the Gift feature to dedicate your work to them.

    +
    This creator's work is amazing! Can I leave a comment telling them they should set up a tipping or subscription service, publish their work commercially, or otherwise get paid for it?
    +

    No. Our non-commercialization policy applies to the entirety of AO3, including the comments section of other users' works. Encouraging other users to engage in commercial activity is prohibited.

    +
    I paid someone else to create a story, artwork, or podfic. Can I post or link to their work, or suggest that other people commission them too?
    +

    You may not encourage commercial activity on behalf of someone else, which includes encouraging people to purchase a commission. If you are embedding commissioned images, audio, or videos, you must ensure that there are no ads, links, or watermarks for any commercial platforms. In addition, you may only upload someone else's work if you have their explicit permission to do so.

    +
    I take paid commissions on another site. Can I post the fanworks I create on AO3 and say that they were commissioned? Can I invite other people to commission me?
    +

    Offering paid commissions is not allowed. This includes posting links to pricelists or payment request forms. However, you are allowed to post fanworks that were created upon request and credit the person who made the request. If you do so, you must not indicate that you received payment for the commission or that you are available to create other paid commissions. Because not all commissioned fanworks were created for pay, we do permit usage of the word "commission" as long as there is no indication that a monetary transaction was involved in the creation of the work.

    +
    Can I post a work that was created for a charity drive or auction?
    +

    Yes. AO3 will host fanworks of any origin, including fanworks created in response to charity events or other challenges. A link to a charity to explain the origin of a fanwork is appropriate, but please do not link directly to any fundraising sites or pages. You may state that a work was produced for a particular charity event, project, or other entity, as long as you do not mention donating, bidding, or any specific contribution amounts or donation platforms.

    +
    Does that mean I can't ask people to support a charity or non-profit organization?
    +

    You are allowed to link to a charity's website, encourage people to learn more about the charity, or explain why you believe in its mission. However, you may not link directly to the charity's donation form, promote their fundraisers, or request that people donate to them.

    +
    Can I post a work that was originally part of a for-sale or charity zine, and if so, can I name the zine?
    +

    Yes. However, you may not encourage users to buy the zine or its merchandise, such as by linking to an advertisement or to a sales or orders page.

    +
    What if a zine or other merchandise is available on an optional "pay what you want" basis? On sites like Gumroad, it's possible to access or download the content completely for free.
    +

    Links to product purchase pages are not allowed, even if payment is optional. If the content is hosted on a non-commercial site that doesn't offer payment options, you can link to that site instead.

    +
    I created merch for one of my fanworks. I'd like to hold a giveaway and send it to one of my readers for the cost of shipping. Since I'm not making money off of it, can I advertise this on AO3?
    +

    No. Since this involves an exchange of money, it is considered a commercial activity regardless of whether you personally make a profit.

    +
    I bought some fan merch. Can I post an image of what I bought and talk about it in my work notes or comments?
    +

    In general, yes. However, you cannot encourage other users to go and buy it themselves. This includes providing links or directions to the place where you purchased the merchandise. You may want to take your own photographs in order to avoid linking to the product page.

    +
    I've created a mobile app with features that make AO3 easier to navigate and use. Can I charge people to use my app?
    +

    No. It is a violation of the Terms of Service to charge users money for access to content on AO3. That includes, for example, operating an app that restricts access to works on AO3 behind a paywall, or copying works from AO3 to sell.

    +
    Can I talk about prohibited commercial activities on AO3 if I don't include any direct links or name any commercial platforms?
    +

    No. Both direct and indirect references to commercial platforms or activities are not allowed.

    +
    Do the rules against commercial promotion mean that I can't write fanworks that reference real-world businesses or feature characters engaging in commercial activities?
    +

    No. You are allowed to create fanworks in which the characters engage in or reference commercial activities as part of the fictional story. For example, you could create a fanwork in which one character is an OnlyFans creator and another subscribes to them or buys their merchandise. However, you cannot link to or otherwise promote any real-world subscription, merchandise, or other commercial activity.

    +

    Please keep in mind that all Abuse reports are reviewed by the human volunteers on our Policy & Abuse committee. In general, we presume good faith on the part of our users. However, if we conclude that someone is deliberately trying to circumvent our rules by having fictional characters discuss commercial activities, then the fanwork may be deemed commercial in nature and unacceptable to post on AO3.

    +

    Back to Top | Commercial Promotion FAQ

    +
    + +
    + +
    Table of Contents
    +
    + +
    +
    What makes a fanwork "transformative"? Why is a "transformative work" not a copyright violation?
    +

    Copyright protects an individual's expression of an idea, not the idea itself. "Expression" refers to the work created, such as the wording of a paragraph in a book, while an "idea" covers general plots or tropes. For example, posting a transcript of a movie without permission constitutes copyright infringement, as it replicates a significant part of the original work (the spoken dialogue) exactly. In comparison, a transformative fanwork reframes existing material in a unique manner, such as retelling a superhero movie from the perspective of civilians.

    +

    The Supreme Court of the United States has explained transformative use as "add[ing] something new, with a further purpose or different character, altering the first [work] with new expression, meaning, or message." Essentially, by significantly reinterpreting the original material, the creator of a transformative work makes a new, distinct creation that does not require the copyright owner's permission to create or share.

    + +

    Yes. AO3 maintains that fanworks are transformative and that a fanwork's creator owns the rights to the expressions in their work that are unique to them. A fanwork creator holds the rights to their own content, just the same as any professional author, artist, or other creator.

    +
    May I post someone else's fanworks, giving them credit?
    +

    You may only post someone else's fanworks if they granted you permission to do so. If you do not have the creator's permission, you cannot post their work. Including a disclaimer that the work is not yours or crediting the original creator is not sufficient. Posting and then anonymizing or orphaning the work is also not allowed. Please use our bookmark feature to share other people's fanworks instead of reposting their works.

    +

    If you do have the creator's permission, you can upload their fanwork as long as you provide appropriate credit. For example, you could add the creator's name and include links to the work on the original site and to the place where they gave you permission.

    +

    If you created or moderate a fanwork archive or mailing list, you may be eligible to become an authorized Open Doors archivist and import your archive to AO3. The Open Doors committee will work with approved moderators to contact and fully credit the original creators of the fanworks, giving the creators as much control over their fanworks as possible.

    +
    Can I post a translation or a podfic of someone else's work?
    +

    Translations and podfics can be posted only if you have permission from the copyright owner (usually the creator or publisher) of the original work. If you do not have permission, then you may not post your work.

    +

    Under United States copyright law, translations and audiobooks are considered "derivative" works – not transformative works. Derivative works cannot be posted without the copyright holder's consent. AO3 adheres to U.S. law, so if you want to post a translation or podfic of a fanwork, you need the fanwork creator's permission.

    +

    If the original work is no longer under copyright because it is old enough that it has entered the public domain, refer to How do these rules apply to works that are in the public domain?

    +
    Can I post a conversion or adaptation of someone else's work? This is a type of work where the original content is modified slightly to fit a different fandom, ship, character, or format.
    +

    While you are welcome to create a fanwork that is based on or inspired by another work, you may not take someone else's work and only make minor changes to it (such as swapping out the names, changing the formatting, or rewording the original text). Unless the fanwork creator or copyright owner granted you permission to modify, convert, or adapt their work in this manner, posting this kind of work is a violation of our Terms of Service, even if you provide credit.

    +
    Can I post a sequel, prequel, or continuation of another fanwork? What about a recursive fic that's completely different from the original fanwork?
    +

    Yes. Fanworks based on other fanworks are also transformative, and are allowed. You can use the "Inspired By" feature to link to the original creator's work. Unlike with reposts, conversions, podfics, and translations, you don't need permission to create a recursive fanwork. However, you cannot include large excerpts from the original work unless the original creator gave you permission to do so.

    +
    What if the original creator deleted or orphaned their work, or posted it anonymously?
    +

    Even if someone deletes or orphans their work, or posts it anonymously, they still hold the copyright to their own work. If the original creator has chosen to remove their fanwork from the internet, or cannot be contacted, then please respect their decision. You cannot post, convert, podfic, or translate someone else's work without their explicit permission, even if you credit them or disclaim credit for yourself.

    +
    How can I obtain permission to post, convert, podfic, or translate someone else's work?
    +

    Some fanwork creators may list "blanket permission" statements in their profile or in the tags or notes of their work. An example of a blanket permission statement would be "Anyone can translate my works so long as they provide me credit and link back to the original here on AO3." In such cases, you have permission if and only if you meet the conditions of the permission statement.

    +

    If a creator doesn't have a blanket permission statement, then you can try to contact them directly and ask. This could be by commenting on their work or asking them via social media or email (if they have shared that information publicly). If the creator responds and gives you permission, then you're good to go. If they refuse or do not respond, then you do not have their permission and you may not post the work.

    +
    Can I post a transcript of a movie or a TV show?
    +

    Scripts of movies, TV shows, and plays are subject to copyright, just like published books, songs, poems, photographs, artwork, and other works. Transcribing and posting the content of a copyrighted work is not allowed. However, you may include a limited number of short quotes in your fanwork.

    +
    Can I post "reaction fic" or "MST3K or DVD-commentary–style" versions of other works? This is a type of fanwork where characters read/watch another work (such as the original canon or a popular fanwork) and "live react" to scenes or dialogue.
    +

    If you have permission from the copyright owner or if the work is in the public domain, then your fanwork can include as many quotes from the original work as you like.

    +

    If the work is still under copyright and you don't have permission from the copyright owner, then you can describe or paraphrase scenes to allude to what the characters are reacting to and include timestamps, page numbers, or occasional short quotes from the original. However, you cannot quote or otherwise reproduce large amounts of dialogue, lyrics, text, or other copyrighted material. If someone could access a substantial portion of the original material by skipping over your additions, your work likely violates our Terms of Service.

    +
    May I post the full lyrics of a song or an entire poem that isn't in the public domain without the copyright holder's permission?
    +

    No. Posting large portions of text from a song or poem is not allowed, unless it is in the public domain or you have the copyright owner's permission. You are only allowed to quote a limited amount of material from a copyrighted work, even if the lyrics or stanzas are broken up by your own original text. You can link to a licensed site such as Genius or Poetry Foundation instead.

    +
    Can I post a fanvid that uses a full song without the copyright owner's permission?
    +

    Fanvids where the audio is an entire song are allowed, so long as you have added or remixed a substantial number of visuals to accompany the song such that the video becomes a transformative work. For example, simply posting the text of the song's lyrics to accompany the song would not be considered transformative. A video consisting of an entire song accompanied by two or three long, unedited clips from a movie would also not be considered a transformative work.

    +
    Can I post character playlists or fanmixes?
    +

    A "fanmix" is a thematic compilation of songs curated by a fan to reflect a character, chapter, setting, theme, or other element of a work. If you wish to post character playlists or fanmixes, you may use licensed streaming sites such as Spotify, 8tracks, or YouTube. You may not provide links that could deliver individual music file downloads (such as a single zipped file containing music files) unless you have the right to distribute downloads (for example, a fan song or filk you composed and sang, a work in the public domain, or a Creative Commons-licensed download). Please also keep in mind that you may quote lyrics as part of a transformative work, but you may not reproduce the entire song text unless you have the right to do so. In addition, if you just post a list of song titles without including any way to listen to the songs, then your work may not be considered a transformative fanwork.

    +
    Can I embed someone else's artwork or photos to accompany a work that I wrote, or post someone else's story to accompany the art I created for it?
    +

    If you have the artist's or writer's permission, then you may embed or upload their work alongside your own as long as you credit them appropriately. If you do not have the creator's permission, you cannot repost their work, even if you credit them. We recommend that you instead provide a link to wherever the original creator has chosen to host it.

    +
    How do these rules apply to works that are in the public domain?
    +

    If a work is in the public domain, then it is no longer copyrighted. In this case, you can use as many excerpts from it in your fanwork as you like.

    +

    However, a public domain work is not in and of itself a fanwork. You cannot simply upload a public domain work to AO3 or make minor alterations such as replacing names or synonyms. You need to add your own content in order for your work to be considered a fanwork.

    +

    If you create your own fan translation or podfic of a public domain work, you can post your work on AO3. However, you can't repost someone else's translation or podfic without their explicit permission. Translations and audio recordings have their own copyright separate from the source work's. The translator or podficcer has ownership over their own specific translation or recording, even if the underlying work is in the public domain.

    +
    If I want to file a plagiarism or copyright infringement complaint, is there anything in particular I should include in my report?
    +

    Yes – please make sure you provide a link to the original work as well as a link to the infringing work in your report description. If you don't tell us what the original source is and which work is infringing upon it, then we may not be able to uphold your complaint.

    +

    If you want to report multiple instances of plagiarism or copyright infringement by a single user, please submit only one report rather than reporting each link individually. We'll be able to process your complaint faster if you take the time to compile all relevant links, excerpts, and other information into a single report and correctly pair each infringing work with the original source.

    +

    If you are reporting multiple works by different users, please submit a separate report for each user.

    +
    Can I submit a plagiarism complaint even if I am not the original creator whose work was stolen?
    +

    In general, yes, as long as you include a link to the alleged source work. If we cannot compare the two works, we will not be able to uphold your complaint. However, in some cases, we may require a report or other evidence from the original creator.

    +
    Can I submit a plagiarism or copyright infringement complaint even if the original work is not hosted on AO3?
    +

    Yes. However, such reports must include a link to the alleged source work. If we cannot compare the two works, we will not be able to uphold a complaint of plagiarism or copyright infringement.

    +
    Do you have a Digital Millennium Copyright Act (DMCA) notice and takedown policy?
    +

    Yes. Before filing a DMCA takedown notice, please read our DMCA Policy carefully. If you have any questions, contact the Legal committee.

    +

    DMCA takedowns and counternotices are not handled by the Policy & Abuse committee. Please do not submit both an Abuse report and a DMCA notice about the same content, as this may delay the processing of both requests.

    +
    What happens if someone reposts my fanwork to another site without my permission?
    +

    The Organization for Transformative Works does not hold the copyright to your fanworks, so we cannot contact other sites or organizations on your behalf. Because you are the copyright owner, you will have to contact the other site yourself to request that they remove the unauthorized copy of your work.

    +

    Back to Top | Copyright Infringement and Plagiarism FAQ

    +
    +

    Fannish Identities and Impersonation

    +
    + +
    Table of Contents
    +
    + +
    +
    I posted a guest comment that contained information about me. Can you delete it?
    +

    If the comment contained personal information sufficient to identify you in the real world (such as a full legal name, an email address, or a phone number) then you can contact the Policy & Abuse committee to request its removal. However, we will not remove guest comments simply because they contain a first name, username, or online handle.

    +
    I orphaned a work, and later realized that it contained my personal information. Can you delete it?
    +

    We will not delete orphaned works that don't violate our Terms of Service. However, if you left identifying information on an orphaned work (such as your name, email address, or social media account) and you would no longer like that information to be public, then you can contact the Support committee to request the removal of that specific information from the work.

    +
    What does banning "impersonation" mean? Does this mean I can't post first-person real-person fiction (RPF)?
    +

    We do not permit users to misrepresent themselves as another person or entity (such as a corporation or government agency), particularly in order to deceive others, violate our Terms of Service, or otherwise cause harm.

    +

    Roleplay is permitted when the assumption of such a persona is clearly disclosed (such as in a user profile or in another manner appropriate under the circumstances) and it doesn't otherwise violate the Terms of Service, including the harassment policy. Fiction (including RPF in first-person format) clearly marked as such will not be considered impersonation. Please consult our RPF policy for further information.

    +
    Can I use a celebrity name as a pseudonym, or is that impersonation?
    +

    In general, you can use a joke celebrity pseudonym, or roleplay as a celebrity, so long as you clearly disclaim that you are not actually that person.

    +
    What do you mean by banning impersonation of a "function"? Does this mean I can't have fake Tumblr or TikTok messages in my fanwork?
    +

    You're allowed to mimic functions inside fictional content, so long as the content is clearly fictional – fiction is not impersonation. However, if you're posting content outside of a fictional context, then you can't do so in an impersonating manner. For example, using the username "orphaned_account" is not allowed, as that could cause users to confuse your user page with AO3's official orphan_account.

    +

    Back to Top | Fannish Identities and Impersonation FAQ

    +
    +

    Harassment

    +
    + +
    Table of Contents
    +
    + +
    +
    Does the harassment policy cover everyone, or just AO3 users?
    +

    Both AO3 users and non-users can complain about harassment. The line between user and non-user can be blurry, so our policy covers both. However, writing RPF (real-person fiction) never constitutes harassment in and of itself, even if the content is objectionable. Please refer to our RPF policy for more information.

    +
    Does the harassment policy apply to every part of AO3?
    +

    Yes. This includes works, tags, comments, usernames, pseuds, profiles, icons, and every other type of content that can be submitted to, hosted on, or embedded on AO3, now or in the future. The harassment policy applies to everything a user does on AO3 and all communications with AO3 volunteers.

    +

    The use of any tool or feature could constitute harassment if it's being used to create a hostile environment. When investigating harassment, we will consider relevant context. For example, someone who has a username, pseud, or icon that is negative towards an individual or group could harass those people by leaving comments on their works, even if that same username, pseud, or icon would not be harassing in other contexts.

    +
    Is criticizing fanworks allowed? Is criticism a violation of the harassment policy?
    +

    Criticism is allowed on AO3 and is not considered harassment. This includes negative commentary in comments, bookmark notes, bookmark tags, and other locations. Criticizing a work is not a personal attack against the person who created that work, nor against people who enjoy that work. Issuing personal attacks or threats against other users for creating or enjoying a work is harassment and is not allowed.

    +
    How does the harassment policy apply to comments?
    +

    Criticism of a fanwork, even harsh criticism, is not by itself harassment. All creators have control over the comments on their works, and can delete comments or block users for any reason.

    +

    Calling a creator evil, wishing them harm, and repeatedly posting negative commentary in a manner designed to be seen by the creator (such as by posting multiple negative comments on their work) are potential examples of harassment.

    +
    Is it harassment if someone deletes my comment? My comment wasn't criticizing or attacking them.
    +

    No. Fanwork creators have control over the comments posted on their work. They can remove, freeze, moderate, or restrict comments as they please. This is never considered to be harassment.

    +
    Is it harassment if someone blocks me? They had no reason to do it.
    +

    No. Being blocked by someone is never harassment. If you are blocked by someone or if they told you to leave them alone, you are expected to stop interacting with them. Attempting to interact with someone after they have blocked you may be considered harassment.

    +
    Somebody bookmarked my fanwork and added a note to it that is really negative about my work. Is that harassment?
    +

    Criticism of a fanwork, even harsh criticism, is not by itself harassment. We do not consider criticism of a fanwork to be a personal attack against its creator. However, if the bookmark note includes harassing elements such as personal attacks or threats towards the creator of the work, then that would be considered harassment.

    +
    There's a person in my fandom who is harassing other people. Can I post a work, chapter, comment, or author's note to inform other people in my fandom who they are and that they should stay away?
    +

    No. Creating or sharing a "callout post" about another individual, or encouraging other users to shun them, is harassing behavior. We encourage you to mute and block anyone you don't want to interact with. If you witness or experience harassment on AO3, you should contact the Policy & Abuse committee. If you witness or experience harassment on a different platform, you should contact the moderators or Trust & Safety team for that platform.

    +
    Someone in my fandom is posting really disgusting content that is against the boundaries of the canon creators. Can I comment on their work and ask them to stop?
    +

    We recommend that you avoid engaging with content that you do not like. If you encounter content on AO3 that you find upsetting or disturbing, you should navigate away from the content and use filters or muting to avoid encountering it again. Users are allowed to post fanworks about any topic, regardless of how offensive it is to other users or if the canon creators would approve. If the content doesn't violate a specific clause of our Terms of Service, the fanwork is allowed – we do not remove content for offensiveness. In addition, be aware that joining in on group bullying to force someone to remove their works is likely to be deemed harassment.

    +
    There are some people in my fandom who ship things I think are disgusting or dangerous. I don't want them to comment on my works. Is it harassment if I tell them to stay away?
    +

    If you don't want people to comment on your works, then you should block them. You are allowed to make polite requests that other groups of fans do not interact with you. If you insult those people, or threaten them in any way, you are in violation of our harassment policy.

    +
    What if the tag I want to use is technically threatening, but it's actually a joke? I promise I'm not serious!
    +

    You may not threaten other groups of fans. There is no exception for jokes or memes.

    +
    Is it harassment if someone reports me for violating the Terms of Service?
    +

    As an AO3 user, it is your responsibility to ensure that you are following our Terms of Service. If we receive a report about you, you will only be told about it if our investigation reveals that you did in fact violate our Terms of Service. In such cases, we will let you know about your violation regardless of who reported it.

    +

    While malicious reporting is harassment, such reports are rarely about content that actually violates the Terms of Service. Most reports about violating content are submitted by users who happen to encounter such content during the course of their normal browsing. We do not generally consider a valid report to be harassment of the subject of the report.

    +

    Back to Top | Harassment FAQ

    +
    +

    Usernames, Icons, and Profiles

    +
    + +
    Table of Contents
    +
    + +
    +
    Are there any rules about what I can choose as my username?
    +

    Yes. Aside from technical limitations on your username, all of the regular content rules also apply to usernames. This means that (for example) you can't choose a username that impersonates someone else, or that violates our commercial promotion or harassment policies. Because usernames are highly visible content, we enforce our policies very strictly on usernames.

    +
    Can I reuse a username that belonged to a deleted account?
    +

    Yes, it is possible that a username that is no longer used by its original user will be available to you.

    +
    Someone is using a username on AO3 that I've used on a different site. Can you make them stop or make them give me the username?
    +

    Usernames on AO3 aren't reserved, even if you've used that name before on AO3 or another site. In general, we won't consider the mere existence of a similar or identical username to be impersonation. If another AO3 account is already using your desired username, then you can create a pseud with that name, but you can't make the other person give up their username. However, if someone starts claiming to be you on AO3, or otherwise starts behaving in a harassing manner, then you can submit an Abuse report.

    +
    Why are the rules for user icons more restrictive than the general content rules?
    +

    User icons appear on pages that don't have rating filters, such as user profiles and comments. This means that other users have little ability to avoid encountering them. Therefore, user icons operate under more restrictive rules than rated, tagged, and warned-for content.

    +

    The user icon policy is not the general fanart policy. Although AO3 does not have native media hosting, images and other file types can be embedded in a work. For more information about what is allowed in fanart, please refer to Can I embed explicit images in my fanworks?

    +
    What can be on a user profile?
    +

    User profiles can contain information about the user, such as their fandom preferences and links to other sites on which the user can be found. User profiles must comply with AO3's policies on commercial promotion, harassment, impersonation, copyright, and other general content rules.

    +

    Back to Top | Usernames, Icons, and Profiles FAQ

    +
    +

    Ratings and Archive Warnings

    +
    + +
    Table of Contents
    +
    + +
    +
    What kind of content do you allow?
    +

    AO3 was designed to be a permanent home for all transformative fanworks. We will not remove content from AO3 solely because it contains explicit, offensive, or upsetting material, as long as it doesn't violate any other part of the Content Policy (such as the harassment policy or the non-fanwork policy).

    +

    We allow content of any rating, and all kinds of fictional topics. Users are responsible for reading and heeding the ratings and warnings provided by the creator. If there is a type of content that you don't want to encounter, you should avoid opening any work tagged or rated to indicate that it may contain such content. Risk-averse users should keep in mind that not all content will carry full warnings. If you want to know more about a particular fanwork, you may also want to consult the bookmarks that people other than the creator have used to categorize it.

    +
    What kind of ratings or warnings must be present on works?
    +

    AO3 has a Ratings system and an Archive Warnings system. These provide basic information about the intensity and type of content that may be present in a work. The only rating or warning information AO3 requires is listed within these two systems. Creators may add more information in the summaries, notes, or Additional tags of their works, but they are not required to do so.

    +

    If a creator doesn't want to put a specific Rating and/or Archive Warning on their works, then they can opt out of one or both systems by applying a non-specific Rating and/or Archive Warning.

    +
    What do you mean by non-specific Rating or Archive Warning tags?
    +

    Non-specific tags indicate that the creator has chosen not to use a more specific tag in that field. Any user who wants to avoid a particular rating and/or type of content should also avoid any work labeled with a non-specific Rating and/or Archive Warning. The non-specific Rating tag is "Not Rated", and the non-specific Archive Warning is the "Creator Chose Not To Use Archive Warnings" label. (Tags are also sometimes referred to as labels.)

    +
    What is the Ratings system?
    +

    AO3 has five different rating tags that creators can apply to their works:

    +
      +
    • General Audiences: The content should be suitable for all ages.
    • +
    • Teen and Up Audiences: The content may be inappropriate for audiences under 13.
    • +
    • Mature: The content may contain adult themes (sex, violence, etc.) that aren't as graphic as Explicit-rated content.
    • +
    • Explicit: The content may contain explicit adult themes, such as detailed sex scenes, graphic violence, etc.
    • +
    • Not Rated: This non-specific rating tag means that the creator has chosen not to rate their work. It may contain content that is general, explicit, or anything in between.
    • +
    +

    If a creator doesn't want to apply a specific rating to their work, they are always welcome to use the non-specific "Not Rated" label.

    +
    Are there any rules about how I must rate my work?
    +

    If your work contains graphic or detailed sex, violence, gore, or other adult content, then you may not rate it "General" or "Teen". Whether you choose to rate the work "Mature", "Explicit", or "Not Rated" is up to you.

    +
    If I don't choose a rating, what's the default?
    +

    The default rating is the non-specific "Not Rated" tag.

    +
    What does the "Not Rated" label mean?
    +

    "Not Rated" means that the work may contain content of any rating – the creator has declined to choose a specific rating. A "Not Rated" work could contain anything from a very fluffy coffeeshop meet-cute to extremely graphic sexual content.

    +
    What's the difference between "General" and "Teen"?
    +

    This is left entirely up to the creator's judgment. People disagree passionately about the nature and explicitness of content to which younger audiences should be exposed. Therefore, the creator's discretion to choose between "General" and "Teen" is absolute: we will not mediate any disputes about those decisions. Instead, we encourage creators to consider community norms, whether fandom-specific or more general (such as how you'd expect a video game or movie with similar content to be rated), when selecting a rating.

    +
    What's the difference between "Teen" and "Mature"?
    +

    If the work contains graphic adult content, it should not be rated "General" or "Teen". In response to valid complaints about a misleading rating, the Policy & Abuse committee may redesignate a fanwork marked "General" or "Teen" to "Not Rated", but in other cases, we will defer to the creator's decision. In general, we will not recategorize a fanwork in response to a complaint when the content at issue is a reference or is otherwise not graphic.

    +
    What's the difference between "Mature" and "Explicit"?
    +

    This is left entirely up to the creator's judgment. Both of these ratings require a user to accept the adult content warning and agree to access adult content in order to access the work. The creator's discretion to choose between "Mature" and "Explicit" is absolute: we will not mediate any disputes about those decisions. Instead, we encourage creators to consider community norms, whether fandom-specific or more general (such as how you'd expect a video game or movie with similar content to be rated), when selecting a rating.

    +
    There's an explicit sex scene in one chapter of my work, but all of the other chapters are really fluffy material that would be suitable for general audiences. Which rating should I choose?
    +

    Individual chapters of a work cannot be rated separately, so the rating chosen should be sufficient to cover the highest-intensity content in the work. If you do not want to use a rating of "Mature" or "Explicit" in such a situation, then you can always opt out of the Ratings system by labeling your work as "Not Rated".

    +
    Does the gender or sexual identity of a character matter when determining what rating to use?
    +

    No. The Policy & Abuse committee will not treat works differently on account of the genders or sexual identities of the characters or the types of relationships featured in the work. Please also note that in general, the creator's choice of rating is presumed to be appropriate.

    +
    Do you ever require a rating change on works rated "Mature" or "Explicit"?
    +

    No. Choosing a rating of "Mature" or "Explicit" is always acceptable. We will not require that ratings be lowered, even if there is no intense or explicit content in the work.

    +
    If I want to avoid explicit fanworks, what ratings should I exclude in my search?
    +

    To avoid all fanworks that may contain explicit content, you should exclude or filter out the "Mature", "Explicit", and "Not Rated" labels.

    +
    What is the Archive Warnings system?
    +

    The Archive Warnings system consists of several warning tags, which creators are able to choose from when posting their fanwork. At least one of the following options must be chosen before a work can be posted:

    +
      +
    • Graphic Depictions of Violence: The work may contain detailed descriptions of gore or graphic violence.
    • +
    • Major Character Death: The work may include the death of a character who is prominently featured.
    • +
    • Rape/Non-Con: The work may contain non-consensual sexual activity.
    • +
    • Underage Sex: The work may contain descriptions or depictions of sexual activity involving characters under the age of eighteen.
    • +
    • Creator Chose Not To Use Archive Warnings: This non-specific warning tag means that the work may contain content pertaining to any of the Archive Warnings.
    • +
    • No Archive Warnings Apply: If this warning tag is used in the absence of other Archive Warnings, it means that the work does not contain detailed content pertaining to any of the four specific Archive Warnings. However, the work may briefly reference an Archive Warning topic and/or contain other intense or unpleasant content that is not covered by any of the Archive Warnings.
    • +
    +

    If a creator doesn't want to apply a specific Archive Warning to their work (for example, if they wish to avoid giving spoilers), they are always welcome to use the non-specific "Creator Chose Not To Use Archive Warnings" label.

    +
    Are there any rules about applying Archive Warnings to my work?
    +

    If your work contains graphic depictions of violence, major character death, depictions of rape or non-consensual sex, or depictions of underage sexual activity, then you must use either the relevant specific Archive Warning or the non-specific "Creator Chose Not To Use Archive Warnings" label.

    +
    Why is the number of Archive Warning tags so limited?
    +

    We wished to limit the number and type of Archive Warnings so that they could be easily used by creators from a wide variety of fandoms, and so that each Archive Warning could be fairly and consistently enforced by our all-volunteer Policy & Abuse committee. We decided that we could not reasonably expect fair enforcement of a rule requiring warnings for concepts beyond those listed in the Archive Warnings.

    +
    Something that I consider really upsetting or unpleasant is not on the list of Archive Warnings.
    +

    Because the Archive Warnings policy is deliberately minimal, this may be the case. We encourage you to use the Additional tags, summaries, and user-provided bookmarks and recommendations to screen for fanworks you'll enjoy, and you may wish to comment on a creator's work when you feel that further tags would be desirable. Please be respectful when you do, and keep in mind that they may choose not to add such extra tags.

    +
    My work includes content that I want to warn other users about, but it's something that's not on the list of Archive Warnings. How can I warn them?
    +

    Creators can add additional information about what their works contain in the Additional tags field. You can also add this type of information to the notes or summary of your work, including the notes and summaries on individual chapters. Doing so is entirely voluntary: the Policy & Abuse committee will not enforce the presence of any warnings outside of the Archive Warnings.

    +
    If I don't choose an Archive Warning, what's the default?
    +

    There is no default Archive Warning, so creators have to choose one or more of the Archive Warning options. A creator can opt out of using a specific Archive Warning by selecting "Creator Chose Not To Use Archive Warnings" instead.

    +
    What does the "Creator Chose Not To Use Archive Warnings" label mean?
    +

    "Creator Chose Not To Use Archive Warnings" means that the creator has opted out of using the Archive Warnings system. A work with this warning may or may not contain content covered by any of the specific Archive Warnings. Users who access a work labeled "Creator Chose Not To Use Archive Warnings" proceed entirely at their own risk, regardless of any other tags on the work.

    +
    Can I include multiple Archive Warnings?
    +

    Yes, you can. For example, you could select both "Major Character Death" and "Underage Sex" if a fanwork contains both elements, or "Creator Chose Not To Use Archive Warnings" and "Underage Sex" if you want to disclose the underage sexual content but don't want to say whether the work contains major character death.

    +
    Can a work have both "No Archive Warnings Apply" and one or more other Archive Warnings?
    +

    Yes. The "No Archive Warnings Apply" label can be added to a work regardless of any other Archive Warnings, so it only has meaning when no other Archive Warnings are present on the work.

    +

    If you would like to avoid encountering works with content pertaining to a specific Archive Warning when browsing AO3, then you should exclude the specific Archive Warning as well as the "Creator Chose Not To Use Archive Warnings" label.

    +
    If a story has only a brief reference to an Archive Warning topic, am I required to use either that warning or the "Creator Chose Not To Use Archive Warnings" label? Or can I still choose "No Archive Warnings Apply" if I think that's a better description?
    +

    This is the kind of decision that is up to the creator's discretion. In general, we will not recategorize a fanwork in response to a complaint when the content at issue is a reference or is otherwise not portrayed in explicit detail.

    +
    What does the "Underage Sex" Archive Warning mean?
    +

    Underage Sex refers to descriptions or depictions of sexual activity involving characters under the age of eighteen (18). In general, we rely on creators to use their judgment about the line between reference and description or depiction. Sexual activity does not include dating activities such as kissing; but again, we rely on creators to use their judgment about what is generally understood to be sexual activity. Creators may always specify the age of the characters in their work.

    +
    Why is "underage" defined as "under 18"?
    +

    Though there is no international consensus, there is a trend to focus on 18 as an important age for depictions of sexual activity. Thus, we decided that 18 would be helpful for the maximum number of users, including audiences as well as creators, though we recognize that no solution is perfect for everyone. We encourage creators and bookmarkers to be more specific in tags or notes where this would be useful to potential audiences.

    +
    What if the age of consent in my local jurisdiction is something other than 18, or if the age of majority in the fictional setting is under 18?
    +

    The "Underage Sex" warning on AO3 is used for fanworks depicting sexual activity involving humans under 18 as measured in Earth years, regardless of the fictional setting or users' local laws.

    +
    What about robots, computer simulations, elves, aliens, vampires who are three hundred years old but were turned into vampires at age 12, etc.?
    +

    The primary use of the "Underage Sex" warning is to identify fanworks depicting sexual activity involving humans under the age of 18 as measured in Earth years. Please use your judgment for other situations. If the fanwork does not include a depiction of sexual activity with a human under 18 years old, then we will not generally consider it "underage sex", though creators may use the Archive Warning if they feel it accurately represents their intent. As always, we encourage creators and bookmarkers to be more specific in tags or summaries where this would be useful to potential audiences.

    +
    What about when a fanwork isn't set during the canon timeline and doesn't specify the characters' ages?
    +

    If the characters' ages in the fanwork are ambiguous, then we will assume the characters have been "aged up", even if they are underage in the canon setting.

    +
    Is "Rape/Non-Con" or "Creator Chose Not To Use Archive Warnings" required for works featuring adult/minor relationships? In real life, that would be considered statutory rape in many jurisdictions, regardless of whether the underage participant was willing.
    +

    No. Archive Warnings apply to the fictional content depicted in the work, so a work featuring a sexual relationship between an adult and a minor would need to use the "Underage Sex" Archive Warning. However, unless a character is clearly depicted in the work as unwilling to engage in sexual activity, then the "Rape/Non-Con" warning (or the "Creator Chose Not To Use Archive Warnings" label) is not required, regardless of the age of any of the characters.

    +
    If consent is unclear or dubious, is the "Rape/Non-Con" or "Creator Chose Not To Use Archive Warnings" label needed?
    +

    The primary use of the "Rape/Non-Con" warning is to identify fanworks depicting characters who are clearly unwilling or otherwise forced to engage in sexual activities. Please use your judgment for other situations. When a fanwork features unclear or dubious consent ("dubcon"), we will defer to the creator's decision on how to categorize their work.

    +
    When is the "Major Character Death" warning needed? What makes a character "major"?
    +

    If a character has a significant presence in the fanwork, and they die during the course of the story, then the work would require the "Major Character Death" warning (or the "Creator Chose Not To Use Archive Warnings" label). This warning is also required when the fanwork is focused on the character's death, even if it happened prior to the start of the work. It doesn't matter whether the character is a main character or a side character in canon – it's what is in the fanwork that counts. For example, even if a character has only one line in canon, a fanwork that is primarily about that character's funeral and how much their partner misses them would merit this warning.

    +
    If a major character dies in my fanwork but later returns to life, does the "Major Character Death" warning apply?
    +

    If the character returns to life or is revealed to not be dead within the same fanwork, then you don't need to apply the "Major Character Death" warning (or the "Creator Chose Not To Use Archive Warnings" label) for this character.

    +

    Keep in mind that Archive Warnings apply to the entirety of an individual fanwork's posted content, not to any drafts or sequels. If you appear to have killed off a significant character in a specific work, but plan for them to return in a future chapter or sequel, we suggest using "Creator Chose Not To Use Archive Warnings" as the content currently posted does feature character death.

    +
    What about vampires, zombies, sentient robots, and other characters that aren't "alive"?
    +

    Please use your best judgment for characters who are or become undead, mechanized, or otherwise non-human. If the character is generally still able to think or act in a somewhat human fashion throughout the course of the story, then the "Major Character Death" warning is not needed. When in doubt, we will defer to the fanwork creator's discretion about whether a character has meaningfully died.

    +
    What if a major character's death is ambiguous or unclear?
    +

    This is the kind of decision that is up to the creator's discretion. In general, we will not recategorize a fanwork in response to a complaint when the content at issue is a reference or is otherwise not graphic.

    +
    Will you ever require that an Archive Warning be removed?
    +

    No. The presence of an Archive Warning indicates that the work may contain such content, but it is not a guarantee. This includes works marked with "No Archive Warnings Apply". If this warning is accompanied by another Archive Warning (including the non-specific "Creator Chose Not To Use Archive Warnings" label), then the other warning has precedence.

    +
    What's the difference between ratings and warnings?
    +

    Ratings are a measure of the intensity of overall content. Warnings refer to a specific type of content that is present.

    +

    For example, a work may be rated Explicit because it describes an extended, violent torture scene, or because it has a detailed depiction of consensual sex – or both. The rating tells you only that there may be adult content, and does not inform you what type of adult content it is. A warning indicates the work may contain an "onscreen" depiction of a specific type of content, and does not inform you of the level of detail in which it is described. For example, a work rated "Teen" might also carry the "Major Character Death" warning.

    +
    What sort of rating and warning information am I required to provide for my fanworks?
    +

    Some users may prefer not to read works with particular ratings and/or warnings, while others may search out works with those same ratings and/or warnings. Our goal is to provide the maximum amount of control and flexibility possible for all users of AO3, both creators and audiences alike, so that each user can customize their own experience. Creators can choose to provide a specific rating and/or Archive Warning(s), or they can choose to opt out of providing a specific rating and/or Archive Warning by using "Not Rated" and/or "Creator Chose Not To Use Archive Warnings", respectively.

    +
    Can I use "Not Rated" but not "Creator Chose Not To Use Archive Warnings," or vice versa?
    +

    Yes, absolutely. For example, you could use "Not Rated" and "Rape/Non-Con" for a work that has an explicit rape scene. Or you could rate a work "General" but choose not to warn about the main character dying by using "Creator Chose Not To Use Archive Warnings".

    +
    How do the ratings and warnings policies apply to embedded images, videos, etc.?
    +

    When making rating/warning decisions, creators should take into account any content within the work, including embedded images and videos. As with all other content, creators' decisions are presumed reasonable, and using "Not Rated" or "Creator Chose Not To Use Archive Warnings" will always be sufficient.

    +
    Can I embed explicit images in my fanworks?
    +

    Explicit drawings and other non-photorealistic artwork are generally allowed to be embedded in a work, as long as the work's rating and warnings appropriately describe both the embedded and the textual content. If you are using other people's images to illustrate your fanwork, you must embed from a source authorized by the creator and provide appropriate credit.

    +

    You may not embed sexually explicit or suggestive photographic or photorealistic images in works that contain underage sexual content or any other contextual indication (such as in the tags, notes, or text of the work) that the characters may be under the age of 18. This includes (but is not limited to) porn gifs, photo manipulations, and computer-generated or "AI" images.

    +
    How explicit or graphic can the summaries and tags on my fanworks be?
    +

    Explicit or graphic content in a summary or tag does not violate the Content Policy, as long as the work is appropriately rated and warned. Please use your judgment about what will best identify and describe your fanworks. The summary and tags are part of your work, and your choice of rating and warnings must reflect all parts of your work.

    +
    Do AO3 personnel prescreen works as they're uploaded to ensure that they comply with the ratings and warnings policies?
    +

    No. We will only review a work that has been reported to us for potentially violating the ratings or warnings policies.

    +
    What's the consequence of a violation of the ratings or warnings policies?
    +

    Please see the Mandatory Tags policy for details. If we uphold a complaint, we will ask the creator to amend the rating or warning as necessary. If the creator fails to do so, the Policy & Abuse committee may add the non-specific "Not Rated" and/or "Creator Chose Not To Use Archive Warnings" labels as appropriate. In general, a fanwork will not be removed from AO3 merely for not being correctly labeled. However, repeated or deliberate mislabeling may result in additional consequences.

    +
    What do you mean by "not all works will carry full warnings"?
    +

    Some creators prefer not to assign specific ratings or warnings to their works, or may not be certain if some content in their work "counts" for a required Archive Warning. Our policy aims to enable creators to opt in or out of using specific ratings or warnings while still allowing users to filter out unwanted content. Works for which creators have opted out of providing specific ratings and/or warnings will be labeled "Not Rated" and/or "Creator Chose Not To Use Archive Warnings". These options aid users in decision-making, albeit with more limited detail than a specific rating or warning tag would.

    +
    What type of rating and warning information will be provided when I am browsing fanworks?
    +

    Users who wish to avoid works labeled with specific ratings or Archive Warnings can filter them out or exclude them from searches. Users trying to avoid all possibility of encountering specific ratings or warnings should also avoid works marked as "Not Rated" or "Creator Chose Not To Use Archive Warnings" in the same way, because these works may contain content pertaining to any rating or warning, respectively.

    +

    Beyond the Archive Warnings, Additional tags can also be used to filter and/or search for works. Users who wish to screen works for other users may also add tags, notes, or recommendations to their bookmarks to warn other users about the subject matter contained in the bookmarked works. This can serve as an extra source of information for users who are trying to determine whether or not to access a work. However, please be aware that Additional tags are not mandatory.

    +

    If a user does not want to see Archive Warnings and/or Additional tags (for example, if they wish to avoid potential spoilers), then they can hide those tags in their preferences. Doing so will not hide any works that carry warning labels, only the warning labels themselves.

    +

    All users are responsible for reading and heeding the warnings provided by the creator. Risk-averse users should keep in mind that our ratings and warnings policies are deliberately minimal, and not all content will carry full warnings. If you want to know more about a particular fanwork, you may also wish to consult the bookmarks that people other than the creator have used to organize it.

    +
    If I'm not logged in, what can I see?
    +

    You can see the summary and tags (including Archive Warnings) of any work whose creator has not restricted access to AO3 users only. In addition, you can access any unrestricted work rated "General" or "Teen" without logging in or clicking anything else. For the other ratings ("Mature", "Explicit", and "Not Rated"), you will be asked to agree that you are willing to see adult content before you can access the work.

    +

    Back to Top | Ratings and Archive Warnings FAQ

    +
    +

    Other Tags

    +
    + +
    Table of Contents
    +
    + +
    +
    What are the minimum criteria for tags in mandatory fields?
    +

    The tags on your work must meet the following criteria:

    +
      +
    • Rating: If the work contains graphic adult content, you must rate the work as either "Mature" or "Explicit", or use the "Not Rated" tag.
    • +
    • Archive Warnings: If the work contains depictions of content described by one of the four specific Archive Warnings, you must apply that Archive Warning tag or the "Creator Chose Not To Use Archive Warnings" tag.
    • +
    • Fandoms: The work may only use fandom tags that directly relate to content currently present in the work.
    • +
    • Language: The language tag must indicate a language used in a major portion of the work text, unless no such language tag is available.
    • +
    +

    If you wish to provide other users with more information about your work, you are welcome to do so using the non-mandatory tag fields (such as Additional tags), your work summary, and/or the notes of your work.

    +
    What is a non-specific tag?
    +

    Some mandatory fields may have non-specific tags, such as "Not Rated", "Creator Chose Not To Use Archive Warnings", or "Unspecified Fandom". These tags indicate that the creator has deliberately chosen not to provide more specific information in the mandatory tag field. For example, sometimes creators aren't certain whether a specific tag would apply, or want to avoid using a specific tag because they believe it would reveal spoilers. Using a non-specific tag is always sufficient tagging for that tag field. The Policy & Abuse committee will not require any work that bears a non-specific tag to have more tags added to that field. Users who wish to avoid certain types of content should avoid all works that use non-specific tags.

    +
    What should I do if no language tag exists for the primary language used in my work?
    +

    If the language is one that you or someone else created, you can use the "Uncategorized Constructed Language" tag. If the language is not a constructed language ("conlang"), please contact the Support committee.

    +
    Will you require an incorrect language tag to be changed?
    +

    In general, we will assume good faith from creators. However, if it is clear that the language tag used does not apply to the work, then we may update the work's language tag.

    +

    In order to report a work that uses an incorrect language tag, please contact the Support committee and make sure to include a link to the work in your report.

    +
    In what circumstances will you remove a fandom tag from a work?
    +

    The Policy & Abuse committee may remove a fandom tag when there is no relationship between the particular fandom itself and the work. For example, a fanwork that discusses vampire physiology and uses only examples from Buffy the Vampire Slayer and The Vampire Diaries should not add in fifteen additional fandom tags simply because those fandoms also feature vampires.

    +

    Note that we will apply this rule restrictively. We will not intervene in cases of disagreement over, for example, whether a movie-based work can use the fandom tag for a comic when there are both movie and comics versions of a source. This is the kind of decision a creator is best suited to make and falls within our policy of deference to the creator.

    +

    If you believe a work's tags are misleading, then we encourage direct, polite conversation with the creator. However, if the work has fandom tags that aren't represented in the work, then you can report that to the Policy & Abuse committee.

    +
    This work is about a fandom that the creator hasn't tagged. Will you require them to add the fandom tag to their work?
    +

    No, we will not require that a creator add a specific fandom tag to their work. However, we may apply the non-specific "Unspecified Fandom" tag if the creator has not applied any suitable fandom tags themselves. If you would like to avoid encountering a particular work, then we recommend that you mute the creator or the work.

    +
    Why do some tags have "RPF" on the end? What's the difference between a fandom tag with RPF and without?
    +

    RPF stands for Real Person Fiction. "RPF" is often used to distinguish fandom tags intended for fanworks about a canon's real-life creators (actors, directors, voice actors, authors, etc.), as opposed to fanworks about the fictional characters that appear in any books, movies, or other canons.

    +

    When you are posting your work, you may find that entering the name of a canon into the fandom tag field results in two canonical tags appearing in the autocomplete, one with "RPF" at the end and one without. If your work is about the real people who created the canon, rather than about the fictional canon itself, then you should use the RPF fandom tag for your work. If your work is about the fictional characters or universe, you should use the non-RPF fandom tag instead.

    +

    In some cases, RPF fandoms and works don't have fandom tags with "RPF" at the end. For example, canonical fandom tags about musicians or bands may consist of the names of those individuals or groups. Fandom tags that consist of the name(s) of real people are also considered RPF fandom tags and can be used on works about those people.

    +

    For more information about RPF and non-RPF tags, please refer to the Tags FAQ. The Policy & Abuse committee may remove fandom tags from your work if you've used a non-RPF tag for RPF content, or vice versa.

    +
    Am I allowed to use an RPF fandom tag if my work is about the reader, or if it contains a self-insert character?
    +

    You should not use RPF fandom tags unless your work is about the real people who contributed to the creation of the fictional canon. If the reader or self-insert character is interacting with the fictional characters from the canon and not with the canon's writers, actors, etc., then the work is not RPF and should only be tagged with the non-RPF fandom tag.

    +
    I'm posting a work that will contain content for a particular fandom in a future chapter that I haven't posted yet. Can I use that fandom tag to advertise the upcoming content of my work?
    +

    No. To use a specific fandom tag, the work must currently contain fanwork content pertaining to that fandom. Once you post a chapter featuring characters from or set in the universe of that fandom, you may add the fandom tag to the work. Until then, you can use the notes, summary, or Additional tags to let other users know about your plans.

    +
    A work is appearing in my fandom's tag even though it's not tagged with or about my fandom. I think the tag the creator used was linked to my fandom by mistake. Can I report that?
    +

    Our volunteer tag wranglers connect tags to each other to help users locate fanworks. However, at times this can lead to unexpected search results. If you believe that a tag has been incorrectly wrangled, you can contact the Support committee with a brief explanation of why these tags shouldn't be connected together.

    +
    I want to report a work that has a wrong language tag and inapplicable fandom tags. Who should I report it to?
    +

    If the language tag is the only miscategorization on the work, you may report it to the Support committee. If there is any other type of miscategorization or violation of the Terms of Service (for example, if the work is not a fanwork or is incorrectly rated), you should report it to the Policy & Abuse committee instead, whether or not there is an incorrect language tag on the work.

    +
    What if a work has an incorrect category, relationship, character, or additional tag?
    +

    The Policy & Abuse committee will only evaluate the accuracy of tags in mandatory fields. We will not add, edit, or remove incorrect category, relationship, character, or additional tags. Our resources are limited and it would be challenging to impartially and fairly establish or enforce accuracy rules for these types of tags. However, our general policies against harassment and spam apply to all tags, as they do to any content posted on AO3.

    +
    Can other users add tags to my fanworks? How does that work?
    +

    The only tags displayed on the fanwork itself will be the tags that the creator added to it. If another user bookmarks the work, they may add tags to the bookmark. Users can search for bookmarks with Tag Search or Bookmark Search. Bookmark tags are also visible when viewing a user's public bookmarks. However, bookmark tags will not impact or appear in results when using Work Search or browsing works listings.

    +
    Someone has added a tag I hate to a bookmark of one of my fanworks!
    +

    We recommend that you mute the bookmarker. In general, tags and notes on bookmarks can be positive or negative. Like any other content, tags are subject to the Content Policy, so if the tag violates our harassment, personal information, or other policies, please report it. However, criticism of a fanwork is not considered harassment in and of itself. Bookmark tags and notes will not automatically be displayed on fanworks, in order to allow you to avoid them.

    +
    Where can I find more information about tags?
    +

    Please check out our Tags FAQ for more information about how tags function and how they are generally used.

    +

    Back to Top | Other Tags FAQ

    +
    +

    Spam and Technical Integrity

    +
    + +
    Table of Contents
    +
    + +
    +
    What's this about the spam filter? Can I be permanently suspended if I fail the filter?
    +

    You shouldn't encounter the spam filter if you're logged in. If we discover accounts created solely to post spam, we will permanently suspend those accounts, but such cases are always reviewed by members of the Policy & Abuse committee. If you're a human being reading this FAQ, you shouldn't worry about the automated spam-control measures.

    +
    What do you mean by "conduct that threatens the technical integrity of AO3"?
    +

    Basically, we mean attempts to hack the site or deliberately exploit a code vulnerability in order to engage in destructive behavior on AO3. Spreading viruses or other unwanted programs, redirecting users to spam sites, or trying to undermine or evade compliance with our Terms of Service through technological means are all examples of attempting to interfere with or threaten the technical integrity of the site.

    +
    Does that mean I can't have nifty formatting in my work?
    +

    No, this is just a security policy. You will be able to have plenty of nifty formatting, but not everything imaginable. As a practical security matter, we do not allow JavaScript in works posted on AO3, and only allow a limited subset of HTML and CSS. This is because there is no secure way to allow people to start uploading unfiltered code. For content that uses a lot of custom code, we recommend hosting it on your own website. You can make a bookmark or post a simpler version on AO3 and link to the fancier version in the notes.

    +
    Do you have a policy on bots or scraping? These are ways of extracting information from or indexing websites.
    +

    The use of bots or scraping for spam, commercial promotion, or other purposes that violate our policies is forbidden. This includes scraping AO3 for the purposes of obtaining material for commercial generative AI or creating an app that hosts or paywalls AO3 content.

    +

    The use of bots or scraping for purposes that do not violate our policies is generally allowed. However, we reserve the right to implement robots.txt or other protocols limiting what bots can do, or to notify you and ask you to discontinue if a bot or scraping program is causing problems for the site. At our discretion, we may also ban specific bots or other programs from accessing AO3.

    +

    Back to Top | Spam and Technical Integrity FAQ

    +
    +

    Privacy Policy FAQ

    +

    Answers to common questions about the Privacy Policy are available below. If you have additional questions that are not covered here, you can contact the Policy & Abuse committee.

    + +

    Information Collection and Use

    +
    + +
    Table of Contents
    +
    + +
    +
    Why do you use cookies?
    +

    Cookies may be required to facilitate and customize your site experience, such as by allowing you to log in to your AO3 account. If you do not accept cookies, you may not be able to use the site. There are many ways to remove both browser history and cookies, and we encourage people who are concerned about privacy to investigate broader solutions. For example, most internet browsers can be set to clear your private data automatically between sessions.

    +
    Which AO3 features collect, process, retain, and/or display my content or personal information, and how do they use it?
    +

    Some AO3 features may display your content to the public, to other AO3 users, and/or to yourself and AO3 administrators.

    +
    Display to the public
    +

    We may collect, process, retain, and display your content to the general public, including any personal information you've included in that content, when you use the following AO3 features:

    +
      +
    • Post or edit a work or chapter, when the work is available to the general public
    • +
    • Create or edit a work skin that is applied to a work that is available to the general public
    • +
    • Edit information about a series, when any work in that series is available to the general public
    • +
    • Create or edit a bookmark, when the bookmark is not marked private
    • +
    • Choose a username or pseud
    • +
    • Edit your profile page
    • +
    • Edit the description for one of your pseuds
    • +
    • Upload an icon
    • +
    • Give kudos, both when you are logged in and when you are not logged in
    • +
    • Create or edit information about a Challenge or other type of Collection
    • +
    • Submit a prompt to a Prompt Meme Challenge
    • +
    • Submit a sign-up to a Gift Exchange Challenge (note: the owners and moderators of the Challenge will have access to your email address)
    • +
    • Post or edit a comment, when you comment on a news post or on a work that is available to the general public
    • +
    +

    We also collect, process, and retain personal information that is associated with the content, including your IP address. We may display this information to AO3 administrators for the purposes of managing the site and enforcing the Terms of Service.

    +
    Display to other AO3 users
    +

    We may collect, process, retain, and display your content to other AO3 users, including any personal information you've included in that content, when you use the following AO3 features:

    + +

    We also collect, process, and retain personal information that is associated with the content, including your IP address. We may display this information to AO3 administrators for the purposes of managing the site and enforcing the Terms of Service.

    +
    Display to yourself and/or to AO3 administrators
    +

    We may collect, process, retain, and display your content and preferences to yourself and/or to AO3 administrators, including any personal information you've included in that content, when you use the following AO3 features:

    +
      +
    • Request an account invitation, whether or not you choose to create an account
    • +
    • Create an account
    • +
    • Save a work in draft form
    • +
    • Create or edit a private bookmark
    • +
    • Update your preferences
    • +
    • Mark a work for later
    • +
    • Favorite a tag
    • +
    • Block a user
    • +
    • Mute a user
    • +
    • Create or edit a site skin
    • +
    • Create or edit a work skin, even without applying it to a work
    • +
    +

    We also collect, process, and retain personal information that is associated with the content, including your IP address. We may display this information to AO3 administrators for the purposes of managing the site and enforcing the Terms of Service.

    +
    Why do AO3 features need to collect, process, retain, and/or display my content or information in the ways that they do?
    +
    Because you want your content or information to be available to other people
    +

    AO3 is a site for fans to share fanworks. When you post a work and set it to be available to the general public, you are agreeing to make that content available to everyone. Similarly, the purpose of using other public-facing AO3 features is because you want that content to be available to the public. When you post a work and set it to be available to registered AO3 users only, you are agreeing to make that content available to those users.

    +

    Similarly, other AO3 features or content may be accessible by all registered AO3 users or only accessible to specific registered AO3 users (such as the co-creator(s) of a work or the maintainer(s) of a collection). The purpose of using these features is because you want that content to be available to those people. For example, if you're collaborating with someone else on a draft work where you have added them as a co-creator, then you want your co-creator to be able to access the draft. If you have submitted a work to a collection, then you have chosen to submit content to a part of AO3 that the collection maintainer controls. In all cases, we need to collect, process, and retain certain information about you and your content in order to make it available to those people.

    +
    Because you want your content or information to be associated with your identity
    +

    Usernames and pseudonyms allow you to develop your account as a fannish identity on AO3. When you take actions while using a username or pseud (as opposed to not being logged in, or not using a pseud), it's because you want your username or pseud to be associated with those actions and/or the content that you've uploaded. In order to connect your actions and content to your AO3 account and/or pseud, we need to collect, process, and retain the associated personal information about your account, including the text you entered for your username or pseud.

    +
    Because you want your content or information to be available to yourself
    +

    Some of our features are designed to let you upload content that you want to access later. For example, when you create a draft work or a private bookmark, use the "Mark for Later" feature, or favorite a tag, your purpose is to be able to return to them later. In order to allow you to do so, we need to collect, process, and retain the content that you provided and the associated information.

    +
    To allow you to customize your AO3 experience
    +

    Several of our features are designed to let you customize the way AO3 is displayed to you or the ways in which other users can interact with you. We need the data collected by these features in order to customize your AO3 experience in the ways you've requested. For example, we need access to your blocklist in order to ensure that users you have blocked can't leave comments or kudos on your works or reply to your comments; and we need access to your mutelist to ensure that you won't be shown works, bookmarks, comments, or series by users you've muted. Similarly, when you update your preferences, we need to collect, process, and retain the content and/or information that you provided in order to implement those preferences for you.

    +
    To operate, maintain, and protect AO3
    +

    In order to be able to enforce the Terms of Service, ensure we are compliant with applicable legislation, and handle any legal matters that may arise, AO3 administrators (such as the Policy & Abuse committee, the Legal committee, and the OTW Board of Directors) may need to view content that was uploaded, or the associated data, even if that content is not visible to the general public or to other AO3 users.

    +

    We also need the data collected by each feature to internally manage AO3. For example, we want to count only one view of a URL per cookie session on the "Hit" count for a work. Temporarily collecting, processing, and retaining the IP addresses of users who leave kudos while logged out permits us to conduct internal management of kudos and avoid duplication of kudos, without requiring you to log in and associate your username with the kudos. Our systems and administrators need the data to manage our services, prevent abuse or spam, and maintain the integrity of AO3.

    +
    To assist you with using AO3
    +

    It's possible you may need assistance with your account. In such cases, the Support committee or other AO3 administrators may need to look at your content or associated data in order to troubleshoot a problem you're having, assist you with using a particular AO3 feature, or diagnose or solve a possible bug. For example, if you're having difficulty with not receiving emails about comments on your works, an administrator may need to check whether your Preferences are set correctly.

    +
    What happens if I change the privacy settings on my work?
    +

    When you post or edit a work on AO3, you can choose to restrict access to other logged-in users only. You can change this setting at any time. If the work was originally accessible to AO3 users only, and you (or a co-creator) update the work to make it accessible to the general public, then the work and any content associated with the work (such as bookmarks, comments, or series) will become accessible to the general public at that time. Similarly, if your work was previously set to be accessible to the general public, and you update the work to make it accessible to AO3 users only, then the content associated with the work will no longer be accessible to the general public.

    +

    If you have a series where all works in the series are only accessible to other AO3 users, and you (or a series co-creator) add to the series a work that is accessible to the general public, then the series content will also become accessible to the general public at that time. This applies to the series content only, and not to specific works in the series, which are all controlled separately.

    +

    You (or a co-creator) can add your work to a collection that is maintained by yourself and/or someone else. Adding your work to an anonymous and/or unrevealed collection will limit some information about and/or access to the work to yourself and to the collection maintainers. The collection maintainers can change the collection settings or remove your work from their collection at any time. You can remove your work from a collection at any time.

    +
    What information can a co-creator access? What privacy settings can they change?
    +

    When you invite a co-creator to a work you've uploaded to AO3, you are giving them the ability to edit or delete the work, add or remove the work from a series or collection, or delete any of its comments at any time, regardless of whether the work has been posted or is still in draft form. Any co-creators of a work can change the privacy settings for that work at any time, without notifying you. If the co-creators of a work have different account preferences regarding their works' accessibility or permissions, the work will adhere to the least restrictive preference. For example, if one co-creator allows their works to be invited to collections, and another co-creator does not, it will be possible to invite the co-created work to a collection.

    +

    When you invite a co-creator to a series you've created, you are giving them the ability to edit or delete the series (but not any works in the series, unless they are also a co-creator of the work(s) in question). If all works in the series are only accessible to other AO3 users, and your co-creator adds to the series a work that is accessible to the general public, then the series content will also become accessible to the general public at that time. This applies to the series content only, and not to specific works in the series, which are all controlled separately.

    +

    We recommend that you do not invite anyone as a co-creator of your work unless you are comfortable with giving that person equal access to and control over that work and the content and settings associated with it. You cannot remove a co-creator from your work after you have invited them. Co-creators can remove themselves from a work at any time.

    +
    What information can a challenge or collection maintainer access? What privacy settings can they change?
    +

    If you add your work to a collection, the owners and moderators of the collection ("collection maintainers") will be able to access (but not edit or delete) the content of the work. They will also know the usernames and pseuds of all creators associated with the work, even if the work is in a collection marked as anonymous or unrevealed. You can remove your work from a collection at any time. A collection maintainer can remove a work from their collection at any time, or remove the "anonymous" and/or "unrevealed" status provided by their collection, without notifying you. When this occurs, the creators' usernames and/or the content of the work will become accessible either to other AO3 users or to the general public, depending on the work settings.

    +

    A maintainer of a Gift Exchange challenge can access sign-ups as well as the usernames and email addresses that participants use to sign up. This is in case the maintainer needs to communicate with the exchange's participants. Maintainers of other types of collections do not have access to other users' email addresses.

    +

    Please note that using this information in any way other than to manage the collection or challenge is a violation of our Terms of Service and may result in the permanent suspension of the maintainer's account.

    +
    If a new feature is introduced, how will it handle my content and personal information?
    +

    If the feature is designed to display content to other users or to the general public, then when you choose to use that feature, your content may be displayed to other users or to the general public. Any personal information or other data we collect will be used to operate that feature and other integrated features on AO3, maintain AO3, and prevent spam and abuse of the feature or AO3. Administrators may have access to any content uploaded with the feature, and associated data, for these purposes.

    +

    AO3 is committed to protecting the privacy of our users. We will never sell your personal information, data, or other content. If you have a specific question about how your content and/or information is collected or used, please contact the Policy & Abuse committee.

    +

    Back to Top | Information Collection and Use FAQ

    +
    +

    User Privacy Rights

    +
    + +
    Table of Contents
    +
    + +
    +
    What information do you sell, trade, or rent to third parties?
    +

    None. We do not and will not sell, trade, or rent information, including your personal information.

    +
    What legal rights do I have under data privacy laws?
    +

    If you are located in the European Union, the United Kingdom, or parts of the United States, you may have rights regarding your personal information or personal data that qualifies as such under the laws of your jurisdiction.

    +

    As provided by the laws of your applicable jurisdiction, these rights may include:

    +
      +
    • The right to know what personal information we have collected about you
    • +
    • The right to request that we provide access to, correct, or delete your personal information
    • +
    • The right to request that your personal information be provided in portable form
    • +
    • The right to not be discriminated against for exercising your legal rights
    • +
    • The right to not be subject to a decision that would affect your legal rights based solely on automated processing of your personal information
    • +
    +
    What lawful grounds does AO3 rely upon to process personal information from users in the EU and the UK?
    +

    We rely on the following lawful grounds to process personal information from users in the EU and the UK:

    +
      +
    • It is necessary for the performance of a contract with you
    • +
    • Our or a third party's legitimate business interest
    • +
    • Your consent
    • +
    +

    If we are processing your personal information based on the grounds that you consented to it, you have the right to withdraw your consent for such processing at any time. Withdrawing your consent does not affect the lawfulness of consent-based processing that occurred prior to when your consent was withdrawn.

    +

    These protections are not limited specifically to users from the EU and the UK. We limit our data processing in this way for all users. If you have questions regarding the protection of your personal information, you can contact the Policy & Abuse committee.

    +
    How do I exercise my rights under data privacy laws such as the General Data Protection Regulation (GDPR) or the California Consumer Privacy Act (CCPA)?
    +

    To exercise the rights available to you as a data subject or consumer under applicable data privacy laws, contact the Policy & Abuse committee. In order for us to confirm your request, the email address you enter must be the one associated with your account (if applicable) and you must be able to send and receive emails from that address. In the subject and description of your request, please specify which data privacy law (GDPR, CCPA, etc.) applies to you, and clearly state what kind of data request you are making (for example, you can request a copy of your personal information). We may require you to submit additional personal information necessary to verify your identity and status as a data subject. Repeated requests within a 1-year timeframe may incur a fee.

    +

    Please note that you may be able to exercise some of these rights without our intervention. For example, if you are a registered user, you can access and update certain personal information via your account preferences.

    +
    What is the procedure for requesting details about the information you sell, trade, or rent to third parties for direct marketing purposes under the California Consumer Privacy Act (CCPA)?
    +

    The CCPA only applies to for-profit entities. Because the Organization for Transformative Works is a non-profit, we are not required to have such a procedure. More importantly, we do not have such a procedure because we do not sell, trade, or rent information to third parties for any reason, including for direct marketing purposes.

    +
    Do you recognize and comply with Do Not Track signals or opt-out preference signals?
    +

    Some web browsers, mobile applications, and operating systems allow users to signal their preferences regarding the tracking of their personal information. These are known as Do Not Track (DNT) signals or opt-out preference signals. AO3 does not respond to DNT or opt-out preference signals because we do not use, sell, or share your personal information for targeted advertising purposes. In addition, we collect only the minimum data necessary to operate the site and/or that you have consented to provide to us. Without this minimum data, you can't access the site.

    +

    If a standard for online tracking is adopted that we must follow in the future, we will inform you about that practice in a revised version of the Privacy Policy.

    +

    Back to Top | User Privacy Rights FAQ

    +
    + + + +

    <%= t("a11y.navigation") %>

    +<%= render "tos_navigation", top_link: true %> + diff --git a/app/views/invite_requests/_index_open.html.erb b/app/views/invite_requests/_index_open.html.erb index 5726e4c7a9b..06cd0674d20 100644 --- a/app/views/invite_requests/_index_open.html.erb +++ b/app/views/invite_requests/_index_open.html.erb @@ -1,25 +1,18 @@

    - <%= ts("Invitation Requests") %> + <%= t(".page_heading") %>

    - <%= ts("To get a free Archive of Our Own account, you need an Invitation. - By submitting your email address to our invitation queue, you confirm - that you are at least 13 years old, and if you're in a country whose - residents/citizens have to be of an age older than 13 to consent, - you are old enough to consent to the processing of your personal data - without our obtaining written permission from a parent or legal guardian. - We will use the email address you submit only to send you an Invitation - and to process/manage your account activation. Please don't request an Invitation - unless you've read the %{tos} and agree to abide by those Terms.", - tos: link_to(ts("Terms of Service"), tos_path) - ).html_safe %> + <%= t(".details_html", + tos_link: link_to(t(".tos"), tos_path), + content_policy_link: link_to(t(".content_policy"), content_path), + privacy_policy_link: link_to(t(".privacy_policy"), privacy_path)) %>

    -

    <%= ts("Request an invitation") %>

    +

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

    <%= form_for(@invite_request, html: { class: "simple" }) do |f| %> <%= error_messages_for @invite_request %> @@ -27,17 +20,14 @@

    <%= f.label :email %> <%= f.text_field :email %> - <%= f.submit ts("Add me to the list") %> + <%= f.submit t(".add_to_list") %>

    <% end %> -

    - <%= ts("If you have already requested an invitation, you can %{status}. There are currently %{count} people on the waiting list. We are sending - out %{invites} invitations per day.", - status: link_to("check your position on the waiting list", - status_invite_requests_path), - count: InviteRequest.count, - invites: AdminSetting.current.invite_from_queue_number).html_safe %> +

    + <%= t(".already_requested_html", check_status_link: link_to(t(".check_waitlist_position"), status_invite_requests_path)) %> + <%= t(".waiting_list_count", count: InviteRequest.count) %> + <%= t(".invitations_per_day", count: AdminSetting.current.invite_from_queue_number) %>

    diff --git a/app/views/known_issues/index.html.erb b/app/views/known_issues/index.html.erb index acd5616f914..6d550cdb5e6 100644 --- a/app/views/known_issues/index.html.erb +++ b/app/views/known_issues/index.html.erb @@ -1,5 +1,5 @@ -<% if logged_in_as_admin? %> +<% if policy(KnownIssue).admin_index? %> <%= render :partial => "admin_index" %> <% else %>

    <%= ts("Known Issues") %>

    diff --git a/app/views/languages/_form.html.erb b/app/views/languages/_form.html.erb index 0634721c16b..cdaff60f3fa 100644 --- a/app/views/languages/_form.html.erb +++ b/app/views/languages/_form.html.erb @@ -2,51 +2,50 @@ <%= form_for(@language, html: { class: "post" }) do |f| %> -

    * <%= t('.required_notice', default: "Required information") %>

    +

    * <%= t(".required_notice") %>

    - <%= f.label :name, t('.name', default: "Name") + " *" %> + <%= f.label :name, "#{t('.name')} *" %>
    - <%= f.text_field :name %> + <%= f.text_field :name, disabled: !policy(@language).can_edit_non_abuse_fields? %>
    - <%= f.label :short, t('.short', default: "Abbreviation") + " *" %> + <%= f.label :short, "#{t('.short')} *" %>
    - <%= f.text_field :short %> + <%= f.text_field :short, disabled: !policy(@language).can_edit_non_abuse_fields? %>
    - <%= f.label :sortable_name, t('.sortable_name', default: "Name for alphabetical sorting") %> + <%= f.label :sortable_name, t(".sortable_name") %>
    - <%= f.text_field :sortable_name %> + <%= f.text_field :sortable_name, disabled: !policy(@language).can_edit_non_abuse_fields? %>
    - <%= f.check_box :support_available %> + <%= f.check_box :support_available, disabled: !policy(@language).can_edit_non_abuse_fields? %>
    - <%= f.label :support_available, t('.support_available', default: "Support available") %> + <%= f.label :support_available, t(".support_available") %>
    - <%= f.check_box :abuse_support_available %> + <%= f.check_box :abuse_support_available, disabled: !policy(@language).can_edit_abuse_fields? %>
    - <%= f.label :abuse_support_available, t('.abuse_support_available', default: "Abuse support available") %> + <%= f.label :abuse_support_available, t(".abuse_support_available") %>

    <% if @language.new_record? %> - <%= f.submit t('.create', default: "Create Language") %> + <%= f.submit t(".submit.create") %> <% else %> - <%= f.submit t('.update', default: "Update Language") %> + <%= f.submit t(".submit.update") %> <% end %>

    <% end %> - diff --git a/app/views/layouts/_footer.html.erb b/app/views/layouts/_footer.html.erb index 3f6e73ca1ce..fd64780d04e 100644 --- a/app/views/layouts/_footer.html.erb +++ b/app/views/layouts/_footer.html.erb @@ -1,14 +1,14 @@ +<% end %> diff --git a/app/views/tos_update_mailer/tos_update_notification.html.erb b/app/views/tos_update_mailer/tos_update_notification.html.erb new file mode 100644 index 00000000000..6c84a1b278a --- /dev/null +++ b/app/views/tos_update_mailer/tos_update_notification.html.erb @@ -0,0 +1,16 @@ +<% content_for :message do %> +

    <%= t("mailer.general.greeting.formal_html", name: style_bold(@username)) %>

    + +

    In order to make AO3's rules clearer to our users, we intend to update the AO3 Terms of Service (TOS) later this year. Once this occurs, you will need to agree to the updated TOS in order to continue using AO3.

    + +

    Here are the highlights of the changes in the 2024 version of the TOS:

    + +
    • We've clarified the Content Policy, but we haven't changed what works are or are not allowed. <%= style_bold("If your fanwork was allowed on AO3 before, then it is still allowed.") %>
    • +
    • The TOS has been split into three pages (General Principles, Content Policy, and Privacy Policy). This should make it easier to find what you're looking for when you want to know about a specific part of the TOS.
    • +
    • We've simplified the language throughout the TOS and removed redundant or overly specific phrases and passages. When longer explanations would help to provide clarity, we've added new questions to the TOS FAQ instead.
    • +
    • We've updated the descriptions of how we and our subprocessors collect and process user information (including personal information) in the Privacy Policy.
    • +
    • The Abuse Policy has been generalized to provide the AO3 Policy & Abuse committee with greater flexibility to determine how to address TOS violations, while still providing protections for fanworks in accordance with AO3's mission.
    • +
    • The "Underage" Archive Warning, which is used for works that depict or describe underage sex, will be renamed to "Underage Sex". This does not change the meaning of this warning or how it is enforced. When the TOS update occurs, <%= style_bold('all works with the "Underage" Archive Warning will be recategorized automatically to display the new "Underage Sex" Archive Warning label instead.') %> If you have a work that carries the "Underage" warning and you don't want it to display the "Underage Sex" label, you can replace it with the "Creator Chose Not to Use Archive Warnings" label at any time.
    + +

    You can learn more about the intended changes, access the full draft text, ask questions, and provide public feedback by visiting our <%= style_link(style_bold("news post about the 2024 Terms of Service updates"), admin_post_url(@admin_post)) %>.

    +<% end %> diff --git a/app/views/tos_update_mailer/tos_update_notification.text.erb b/app/views/tos_update_mailer/tos_update_notification.text.erb new file mode 100644 index 00000000000..a9d5452412f --- /dev/null +++ b/app/views/tos_update_mailer/tos_update_notification.text.erb @@ -0,0 +1,16 @@ +<% content_for :message do %> +<%= t("mailer.general.greeting.formal_html", name: @username) %> + +In order to make AO3's rules clearer to our users, we intend to update the AO3 Terms of Service (TOS) later this year. Once this occurs, you will need to agree to the updated TOS in order to continue using AO3. + +Here are the highlights of the changes in the 2024 version of the TOS: + + - We've clarified the Content Policy, but we haven't changed what works are or are not allowed. If your fanwork was allowed on AO3 before, then it is still allowed. + - The TOS has been split into three pages (General Principles, Content Policy, and Privacy Policy). This should make it easier to find what you're looking for when you want to know about a specific part of the TOS. + - We've simplified the language throughout the TOS and removed redundant or overly specific phrases and passages. When longer explanations would help to provide clarity, we've added new questions to the TOS FAQ instead. + - We've updated the descriptions of how we and our subprocessors collect and process user information (including personal information) in the Privacy Policy. + - The Abuse Policy has been generalized to provide the AO3 Policy & Abuse committee with greater flexibility to determine how to address TOS violations, while still providing protections for fanworks in accordance with AO3's mission. + - The "Underage" Archive Warning, which is used for works that depict or describe underage sex, will be renamed to "Underage Sex". This does not change the meaning of this warning or how it is enforced. When the TOS update occurs, all works with the "Underage" Archive Warning will be recategorized automatically to display the new "Underage Sex" Archive Warning label instead. If you have a work that carries the "Underage" warning and you don't want it to display the "Underage Sex" label, you can replace it with the "Creator Chose Not to Use Archive Warnings" label at any time. + +You can learn more about the intended changes, access the full draft text, ask questions, and provide public feedback by visiting our news post about the 2024 Terms of Service updates: <%= admin_post_url(@admin_post) %>. +<% end %> diff --git a/app/views/user_mailer/challenge_assignment_notification.html.erb b/app/views/user_mailer/challenge_assignment_notification.html.erb index 632e0e870f7..9237a13c180 100644 --- a/app/views/user_mailer/challenge_assignment_notification.html.erb +++ b/app/views/user_mailer/challenge_assignment_notification.html.erb @@ -12,7 +12,7 @@ <% def styled_tag_list(tags) %> <% return nil if !tags || tags.empty? %> - <% tags.map { |tag| style_link(tag.name, tag_works_url(tag)) }.to_sentence.html_safe %> + <% to_sentence(tags.map { |tag| style_link(tag.name, tag_works_url(tag)) }) %> <% end %> <% fandoms = prompt.any_fandom ? t(".any") : styled_tag_list(tag_groups["Fandom"]) %> @@ -28,15 +28,51 @@ <%= index + 1 %>. <%= style_bold(prompt.title) %>

    - <% if fandoms %><%= style_bold(Fandom.human_attribute_name("name_with_colon", count: prompt.any_fandom ? 1 : tag_groups["Fandom"].count)) %> <%= fandoms %>
    <% end %> - <% if chars %><%= style_bold(Character.human_attribute_name("name_with_colon", count: prompt.any_character ? 1 : tag_groups["Character"].count)) %> <%= chars %>
    <% end %> - <% if ships %><%= style_bold(Relationship.human_attribute_name("name_with_colon", count: prompt.any_relationship ? 1 : tag_groups["Relationship"].count)) %> <%= ships %>
    <% end %> - <% if ratings %><%= style_bold(Rating.human_attribute_name("name_with_colon")) %> <%= ratings %>
    <% end %> - <% if warnings %><%= style_bold(ArchiveWarning.human_attribute_name("name_with_colon", count: prompt.any_archive_warning ? 1 : tag_groups["ArchiveWarning"].count)) %> <%= warnings %>
    <% end %> - <% if categories %><%= style_bold(Category.human_attribute_name("name_with_colon", count: prompt.any_category ? 1 : tag_groups["Category"].count)) %> <%= categories %>
    <% end %> - <% if atags %><%= style_bold(Freeform.human_attribute_name("name_with_colon", count: prompt.any_freeform ? 1 : tag_groups["Freeform"].count)) %> <%= atags %>
    <% end %> - <% if otags %><%= style_bold("#{t(".optional_tags")}") %> <%= otags %>
    <% end %> - <% if prompt.url && !prompt.url.blank? %><%= style_bold("#{t(".prompt_url")}") %> <%= style_link(prompt.url, prompt.url) %>
    <% end %> + <% if fandoms %> + <%= style_bold(t("activerecord.models.fandom", count: prompt.any_fandom ? 1 : tag_groups["Fandom"].count) + t("mailer.general.metadata_label_indicator")) %> + <%= fandoms %> +
    + <% end %> + <% if chars %> + <%= style_bold(t("activerecord.models.character", count: prompt.any_character ? 1 : tag_groups["Character"].count) + t("mailer.general.metadata_label_indicator")) %> + <%= chars %> +
    + <% end %> + <% if ships %> + <%= style_bold(t("activerecord.models.relationship", count: prompt.any_relationship ? 1 : tag_groups["Relationship"].count) + t("mailer.general.metadata_label_indicator")) %> + <%= ships %> +
    + <% end %> + <% if ratings %> + <%= style_bold(t("activerecord.models.rating", count: 1) + t("mailer.general.metadata_label_indicator")) %> + <%= ratings %> +
    + <% end %> + <% if warnings %> + <%= style_bold(t("activerecord.models.archive_warning", count: prompt.any_archive_warning ? 1 : tag_groups["ArchiveWarning"].count) + t("mailer.general.metadata_label_indicator")) %> + <%= warnings %> +
    + <% end %> + <% if categories %> + <%= style_bold(t("activerecord.models.category", count: prompt.any_category ? 1 : tag_groups["Category"].count) + t("mailer.general.metadata_label_indicator")) %> + <%= categories %> +
    + <% end %> + <% if atags %> + <%= style_bold(t("activerecord.models.freeform", count: prompt.any_freeform ? 1 : tag_groups["Freeform"].count) + t("mailer.general.metadata_label_indicator")) %> + <%= atags %> +
    + <% end %> + <% if otags %> + <%= style_bold(t(".optional_tags")) %> + <%= otags %> +
    + <% end %> + <% if prompt.url && !prompt.url.blank? %> + <%= style_bold(t(".prompt_url")) %> + <%= style_link(prompt.url, prompt.url) %> +
    + <% end %> <% if prompt.description && !prompt.description.blank? %> <%= style_bold(t(".description")) %> <%= style_quote(prompt.description) %> @@ -50,7 +86,7 @@ <%= style_bold(t(".due")) %> <%= time_in_zone(@collection.challenge.assignments_due_at, (@collection.challenge.time_zone || Time.zone.name), @assigned_user) %>.

    -

    <%= t(".html.look_up", link: style_link(t(".html.look_up_link"), user_assignments_url(@assigned_user))).html_safe %>

    +

    <%= t(".look_up.html", your_assignments_link: style_link(t(".look_up.your_assignments"), user_assignments_url(@assigned_user))) %>

    <% if @collection && !@collection.assignment_notification.blank? %>

    <%= escape_html_and_create_linebreaks(@collection.assignment_notification) %>

    @@ -58,9 +94,9 @@ <% end %> <% content_for :footer_note do %> - <%= t(".html.footer", title: style_footer_link(@collection.title, collection_url(@collection)), footer_link: style_footer_link(t(".html.footer_link"), collection_profile_url(@collection))).html_safe %> + <%= t(".footer.html", title: style_footer_link(@collection.title, collection_url(@collection)), challenge_profile_link: style_footer_link(t(".footer.challenge_profile"), collection_profile_url(@collection))) %> <% end %> <% content_for :sent_at do %> - <%= l(Time.now) %> + <%= l(@assignment.sent_at) %> <% end %> diff --git a/app/views/user_mailer/challenge_assignment_notification.text.erb b/app/views/user_mailer/challenge_assignment_notification.text.erb index 8cb06b561f4..f7db169b07d 100644 --- a/app/views/user_mailer/challenge_assignment_notification.text.erb +++ b/app/views/user_mailer/challenge_assignment_notification.text.erb @@ -1,5 +1,5 @@ <% content_for :message do %> -<%= t ".text.assignment", collection_title: @collection.title, collection_url: collection_url(@collection) %> +<%= t ".assignment.text", collection_title: @collection.title, collection_url: collection_url(@collection) %> <%= t ".recipient" %> <%= @request.nil? ? t(".recipient_missing") : text_pseud(@request.pseud) %> @@ -22,26 +22,50 @@ <%= index + 1 %>. <%= prompt.title %> -<% if fandoms %><%= Fandom.human_attribute_name("name_with_colon", count: prompt.any_fandom ? 1 : tag_groups["Fandom"].count) %> <%= fandoms %><% end %><% if chars %> -<%= Character.human_attribute_name("name_with_colon", count: prompt.any_character ? 1 : tag_groups["Character"].count) %> <%= chars %><% end %><% if ships %> -<%= Relationship.human_attribute_name("name_with_colon", count: prompt.any_relationship ? 1 : tag_groups["Relationship"].count) %> <%= ships %><% end %><% if ratings %> -<%= Rating.human_attribute_name("name_with_colon") %> <%= ratings %><% end %><% if warnings %> -<%= ArchiveWarning.human_attribute_name("name_with_colon", count: prompt.any_archive_warning ? 1 : tag_groups["ArchiveWarning"].count) %> <%= warnings %><% end %><% if categories %> -<%= Category.human_attribute_name("name_with_colon", count: prompt.any_category ? 1 : tag_groups["Category"].count) %> <%= categories %><% end %><% if atags %> -<%= Freeform.human_attribute_name("name_with_colon", count: prompt.any_freeform ? 1 : tag_groups["Freeform"].count) %> <%= atags %><% end %><% if otags %> -<%= t ".optional_tags" %> <%= otags %><% end %><% if prompt.url && !prompt.url.blank? %> -<%= t ".prompt_url" %> <%= prompt.url %><% end %><% if prompt.description && !prompt.description.blank? %> -<%= t ".description" %> - <%= to_plain_text(prompt.description) %><% end %> +<% if fandoms %> +<%= t("activerecord.models.fandom", count: prompt.any_fandom ? 1 : tag_groups["Fandom"].count) + t("mailer.general.metadata_label_indicator") %><%= fandoms %> +<% end %> +<% if chars %> +<%= t("activerecord.models.character", count: prompt.any_character ? 1 : tag_groups["Character"].count) + t("mailer.general.metadata_label_indicator") %><%= chars %> +<% end %> +<% if ships %> +<%= t("activerecord.models.relationship", count: prompt.any_relationship ? 1 : tag_groups["Relationship"].count) + t("mailer.general.metadata_label_indicator") %><%= ships %> +<% end %> +<% if ratings %> +<%= t("activerecord.models.rating", count: 1) + t("mailer.general.metadata_label_indicator") %><%= ratings %> +<% end %> +<% if warnings %> +<%= t("activerecord.models.archive_warning", count: prompt.any_archive_warning ? 1 : tag_groups["ArchiveWarning"].count) + t("mailer.general.metadata_label_indicator") %><%= warnings %> +<% end %> +<% if categories %> +<%= t("activerecord.models.category", count: prompt.any_category ? 1 : tag_groups["Category"].count) + t("mailer.general.metadata_label_indicator") %><%= categories %> +<% end %> +<% if atags %> +<%= t("activerecord.models.freeform", count: prompt.any_freeform ? 1 : tag_groups["Freeform"].count) + t("mailer.general.metadata_label_indicator") %><%= atags %> +<% end %> +<% if otags %> +<%= t(".optional_tags") %> <%= otags %> +<% end %> +<% if prompt.url && !prompt.url.blank? %> +<%= t(".prompt_url") %> <%= prompt.url %> +<% end %> +<% if prompt.description && !prompt.description.blank? %> +<%= t(".description") %> + <%= to_plain_text(prompt.description) %> +<% end %> <% end %><%= text_divider %> -<%= t '.due' %> <%= to_plain_text(time_in_zone(@collection.challenge.assignments_due_at, (@collection.challenge.time_zone || Time.zone.name), @assigned_user)).gsub(/\n\s*/, "") %>. +<%= t(".due") %> <%= to_plain_text(time_in_zone(@collection.challenge.assignments_due_at, (@collection.challenge.time_zone || Time.zone.name), @assigned_user)).gsub(/\n\s*/, "") %>. -<%= t ".text.look_up", link: user_assignments_url(@assigned_user) %> +<%= t(".look_up.text", your_assignments_url: user_assignments_url(@assigned_user)) %> <% if @collection && !@collection.assignment_notification.blank? %> <%= @collection.assignment_notification %><% end %><% end %> -<% content_for :footer_note do %><%= t ".text.footer", title: @collection.title, url: collection_url(@collection), profile_url: collection_profile_url(@collection) %><% end %> -<% content_for :sent_at do %><%= l(Time.now) %><% end %> +<% content_for :footer_note do %> +<%= t(".footer.text", title: @collection.title, url: collection_url(@collection), challenge_profile_url: collection_profile_url(@collection)) -%> +<% end %> +<% content_for :sent_at do %> +<%= l(@assignment.sent_at) -%> +<% end %> diff --git a/app/views/user_mailer/invite_increase_notification.html.erb b/app/views/user_mailer/invite_increase_notification.html.erb index 28f3ec28dfb..eb5036e5deb 100644 --- a/app/views/user_mailer/invite_increase_notification.html.erb +++ b/app/views/user_mailer/invite_increase_notification.html.erb @@ -1,7 +1,7 @@ <% content_for :message do %>

    <%= t("mailer.general.greeting.informal.addressed_html", name: style_bold(@user.login)) %>

    -

    <%= t(".html.body", count: @total, invitation_page_link: style_link(t(".invitation_page_link_text"), user_invitations_url(@user))).html_safe %>

    +

    <%= t(".body.html", count: @total, invitation_page_link: style_link(t(".invitation_page_link_text"), user_invitations_url(@user))) %>

    <%= t("mailer.general.closing.informal") %>
    diff --git a/app/views/user_mailer/invite_increase_notification.text.erb b/app/views/user_mailer/invite_increase_notification.text.erb index 71db974dd6d..499d6c02e2c 100644 --- a/app/views/user_mailer/invite_increase_notification.text.erb +++ b/app/views/user_mailer/invite_increase_notification.text.erb @@ -1,7 +1,7 @@ <% content_for :message do %> <%= t("mailer.general.greeting.informal.addressed_html", name: @user.login) %> -<%= t(".text.body", count: @total, invitation_page_url: user_invitations_url(@user)) %> +<%= t(".body.text", count: @total, invitation_page_url: user_invitations_url(@user)) %> <%= t("mailer.general.closing.informal") %> <%= t("mailer.general.signature.app_short_name") %> diff --git a/app/views/users/_sidebar.html.erb b/app/views/users/_sidebar.html.erb index 429c8ebcc95..a6a8a0295d2 100644 --- a/app/views/users/_sidebar.html.erb +++ b/app/views/users/_sidebar.html.erb @@ -1,30 +1,30 @@

    " role="navigation region"> -

    <%= ts("Choices")%>

    +

    <%= t(".landmark.choices") %>

    -

    <%= ts("Pitch")%>

    +

    <%= t(".landmark.pitch") %>

    -<% if @user == current_user %> -

    <%= ts("Catch")%>

    +<% if @user == current_user || policy(:inbox_comment).show? %> +

    <%= t(".landmark.catch") %>

    <% end %> -

    <%= ts("Switch")%>

    +

    <%= t(".landmark.switch") %>

    diff --git a/app/views/users/edit.html.erb b/app/views/users/edit.html.erb index 55508d1d3b8..e9a5c2d42f8 100644 --- a/app/views/users/edit.html.erb +++ b/app/views/users/edit.html.erb @@ -1,48 +1,40 @@ -

    <%= ts("Edit My Profile") %>

    +

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

    <%= error_messages_for :user %> <%= error_messages_for @user.profile %> -<%= render 'edit_header_navigation' %> +<%= render "edit_header_navigation" %>

    - <%= ts("Any personal information you post on your public AO3 profile, - including your religious or political views, health, racial background, - country of origin, sexual identity and/or personal relationships, - will be accessible by the general public. Read more about our updated - Age and Privacy policies, and see how the AO3 collects data when - you're on the site and why we don't sell it to third parties, - in the %{tos}.", - tos: link_to(ts("Privacy section of the AO3 Terms of Service"), tos_path(anchor: "privacy")) - ).html_safe %> + <%= t(".public_information_notice_html", privacy_policy_link: link_to(t(".privacy_policy"), privacy_path)) %>

    -

    <%= ts("Change Profile") %>

    -<%= form_for(@user) do |f| %> +

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

    +<%= form_for(@user) do |f| %>
    <%= fields_for :profile_attributes, @user.profile do |p| %> -
    <%= p.label :title, "Title" %>
    +
    <%= p.label :title, t(".title") %>
    <%= p.text_field :title, class: "observe_textlength" %> <%= live_validation_for_field("profile_attributes_title", presence: false, maximum_length: Profile::PROFILE_TITLE_MAX) %> <%= generate_countdown_html("profile_attributes_title", Profile::PROFILE_TITLE_MAX) %>
    -
    <%= p.label :location, "Location" %>
    +
    <%= p.label :location, t(".location") %>
    <%= p.text_field :location, class: "observe_textlength" %> <%= live_validation_for_field("profile_attributes_location", presence: false, maximum_length: Profile::LOCATION_MAX) %> <%= generate_countdown_html("profile_attributes_location", Profile::LOCATION_MAX) %>
    -
    <%= p.label :date_of_birth, "Date of Birth" %>
    +
    <%= p.label :date_of_birth, t(".date_of_birth") %>
    <%= p.date_select :date_of_birth, start_year: 12.years.ago.year, end_year: 90.years.ago.year, include_blank: true %>
    -
    <%= p.label :about_me, "About Me" %>
    +
    <%= p.label :about_me, t(".about_me") %>

    <%= allowed_html_instructions %>

    <%= p.text_area :about_me, class: "observe_textlength" %> @@ -54,13 +46,11 @@
    <%= p.label :ticket_number, class: "required" %>
    <%= p.text_field :ticket_number, class: "required" %> - <%= live_validation_for_field("profile_attributes_ticket_number", numericality: true) %> -

    <%= ts("Numbers only.") %>

    <% end %> -
    <%= p.label :update, "Update" %>
    -
    <%= f.submit "Update" %>
    +
    <%= p.label :update, t(".update") %>
    +
    <%= f.submit t(".update") %>
    <% end %>
    <% end %> diff --git a/app/views/users/registrations/_legal.html.erb b/app/views/users/registrations/_legal.html.erb index bf113887bf1..65db354f42c 100644 --- a/app/views/users/registrations/_legal.html.erb +++ b/app/views/users/registrations/_legal.html.erb @@ -1,23 +1,33 @@ -

    - <%= ts('You need to be at least 13 years old to become a registered member of the Archive.') %> - <%= ts("(Sorry to anyone younger! You'll be more than welcome when the time comes.)") %>

    +

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

    <%= f.check_box :age_over_13 %> - <%= f.label :age_over_13, ts("Yes, I am at least 13.") %> + <%= f.label :age_over_13, t(".over_thirteen_confirm") %>

    - <%= ts("And - really important - we need you to agree to our") %> - <%= link_to "Terms of Service (opens new window)", tos_path, :target => '_blank' %>: + <%= t(".agreement_required_html", + terms_of_service_link: link_to(t(".terms_of_service"), tos_path, target: "_blank", rel: "noopener"), + content_policy_link: link_to(t(".content_policy"), content_path, target: "_blank", rel: "noopener"), + privacy_policy_link: link_to(t(".privacy_policy"), privacy_path, target: "_blank", rel: "noopener")) %>

    -
    - <%= render 'home/tos' %> +
    +
    +

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

    + <%= render "home/tos", suppress_footer: true %> +

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

    + <%= render "home/content", suppress_toc: true, suppress_footer: true %> +

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

    + <%= render "home/privacy", suppress_toc: true %> +
    -

    <%= f.check_box :terms_of_service %> - <%= f.label :terms_of_service, ts("Yes, I have read the Terms of Service and agree to them."), :class => 'important' %> + <%= f.label :terms_of_service, t(".agreement_confirm"), class: "important" %> +

    +

    + <%= f.check_box :data_processing %> + <%= f.label :data_processing, t(".data_processing_confirm"), class: "important" %>

    diff --git a/app/views/users/show.html.erb b/app/views/users/show.html.erb index 0ef0d555a65..5f21f81cf38 100644 --- a/app/views/users/show.html.erb +++ b/app/views/users/show.html.erb @@ -2,15 +2,17 @@

    - <%= t(".login_banner.welcome_text", help_link: link_to_modal(t(".login_banner.help_link"), for: first_login_help_path, title: t(".login_banner.help_title"))).html_safe %> + <%= t(".login_banner.welcome_html", + new_user_tips_link: link_to(t(".login_banner.new_user_tips"), first_login_help_path), + our_faqs_link: link_to(t(".login_banner.our_faqs"), archive_faqs_path)) %>

    - <%= t(".login_banner.help_text", - link_abuse: link_to(t(".login_banner.link_abuse"), new_abuse_report_path), - link_faq: link_to(t(".login_banner.link_faq"), archive_faqs_path), - link_support: link_to(t(".login_banner.link_support"), new_feedback_report_path), - link_tos: link_to(t(".login_banner.link_tos"), tos_path) - ).html_safe %> + <%= t(".login_banner.help_html", + contact_support_link: link_to(t(".login_banner.contact_support"), new_feedback_report_path), + tos_link: link_to(t(".login_banner.tos"), tos_path), + content_policy_link: link_to(t(".login_banner.content_policy"), content_path), + privacy_policy_link: link_to(t(".login_banner.privacy_policy"), privacy_path), + contact_abuse_link: link_to(t(".login_banner.contact_abuse"), new_abuse_report_path)) %>

    <%= form_tag end_first_login_user_path(current_user), method: :post, remote: true do %>

    @@ -24,7 +26,7 @@

    <%= render "users/header" %> <%= render "users/contents" %> -
    +
    <%= content_for :footer_js do %> <%= javascript_tag do %> diff --git a/app/views/works/_standard_form.html.erb b/app/views/works/_standard_form.html.erb index f6d6f753ab9..32ac453764d 100644 --- a/app/views/works/_standard_form.html.erb +++ b/app/views/works/_standard_form.html.erb @@ -7,7 +7,7 @@
    <%= t("works.preface.title") %> -

    <%= ts("work.preface.title") %>

    +

    <%= t("works.preface.title") %>

    <%= f.label :title, ts("Work Title") + "*" %> @@ -341,6 +341,11 @@
    <%= ts("Post") %> +

    + <%= t(".post_notice_html", + content_policy_link: link_to(t(".content_policy"), content_path), + tos_faq_link: link_to(t(".tos_faq"), tos_faq_path(anchor: "content_faq"))) %> +

      <% unless @work.new_record? || @work.posted? %>
    • diff --git a/app/views/works/edit_tags.html.erb b/app/views/works/edit_tags.html.erb index 264312eaeec..5fd622da4ab 100644 --- a/app/views/works/edit_tags.html.erb +++ b/app/views/works/edit_tags.html.erb @@ -28,6 +28,11 @@
      <%= ts('Post Work') %> +

      + <%= t(".post_notice_html", + content_policy_link: link_to(t(".content_policy"), content_path), + tos_faq_link: link_to(t(".tos_faq"), tos_faq_path(anchor: "content_faq"))) %> +

        <% unless @work.posted? %>
      • <%= submit_tag ts('Save As Draft'), name: 'save_button' %>
      • diff --git a/app/views/works/new_import.html.erb b/app/views/works/new_import.html.erb index e81a8263b27..c4b239d7406 100644 --- a/app/views/works/new_import.html.erb +++ b/app/views/works/new_import.html.erb @@ -182,6 +182,11 @@
        <%= ts("Submit") %> +

        + <%= t(".post_notice_html", + content_policy_link: link_to(t(".content_policy"), content_path), + tos_faq_link: link_to(t(".tos_faq"), tos_faq_path(anchor: "content_faq"))) %> +

        <%= submit_tag ts("Import") %>

        diff --git a/app/views/works/preview.html.erb b/app/views/works/preview.html.erb index 1d4c0f8ad8b..9534b08995c 100755 --- a/app/views/works/preview.html.erb +++ b/app/views/works/preview.html.erb @@ -45,6 +45,11 @@
        <%= ts("Post Work") %> +

        + <%= t(".post_notice_html", + content_policy_link: link_to(t(".content_policy"), content_path), + tos_faq_link: link_to(t(".tos_faq"), tos_faq_path(anchor: "content_faq"))) %> +

          <% if @work.posted? %>
        • <%= submit_tag ts("Update"), name: "update_button" %>
        • diff --git a/app/views/wrangling_guidelines/index.html.erb b/app/views/wrangling_guidelines/index.html.erb index 20951c859a7..0b29d8943d7 100644 --- a/app/views/wrangling_guidelines/index.html.erb +++ b/app/views/wrangling_guidelines/index.html.erb @@ -1,5 +1,5 @@ -<% if logged_in_as_admin? %> +<% if policy(:wrangling).new? %> <%= render 'admin_index' %> <% else %>

          <%= ts('Wrangling Guidelines') %>

          diff --git a/app/views/wrangling_guidelines/show.html.erb b/app/views/wrangling_guidelines/show.html.erb index 0efc7c7031b..8288367700e 100644 --- a/app/views/wrangling_guidelines/show.html.erb +++ b/app/views/wrangling_guidelines/show.html.erb @@ -8,7 +8,7 @@
          - <% if logged_in_as_admin? %> + <% if policy(:wrangling).edit? %>

          Updated: <%= h @wrangling_guideline.updated_at %> | <%= link_to t(".edit"), edit_wrangling_guideline_path(@wrangling_guideline) %> diff --git a/config/application.rb b/config/application.rb index 318f40bc5fc..0abe3864e14 100644 --- a/config/application.rb +++ b/config/application.rb @@ -131,5 +131,15 @@ class Application < Rails::Application end config.active_support.disable_to_s_conversion = true + + # Disable ActiveStorage things that we don't need and can hit the DB hard + config.active_storage.analyzers = [] + config.active_storage.previewers = [] + + # Set ActiveStorage queue name + config.active_storage.queues.mirror = :active_storage + config.active_storage.queues.preview_image = :active_storage + config.active_storage.queues.purge = :active_storage + config.active_storage.queues.transform = :active_storage end end diff --git a/config/config.yml b/config/config.yml index 1cf613f4d9e..f93e615ec07 100644 --- a/config/config.yml +++ b/config/config.yml @@ -112,6 +112,7 @@ INFO_MAX: 100000 FAQ_MAX: 200000 ICON_ALT_MAX: 250 ICON_COMMENT_MAX: 50 +ICON_SIZE_KB_MAX: 500 LOGIN_LENGTH_MIN: 3 LOGIN_LENGTH_MAX: 40 PASSWORD_LENGTH_MIN: 6 @@ -226,7 +227,7 @@ WARNING_NONE_TAG_DISPLAY_NAME: 'No Archive Warnings Apply' WARNING_VIOLENCE_TAG_NAME: 'Graphic Depictions Of Violence' WARNING_DEATH_TAG_NAME: 'Major Character Death' WARNING_NONCON_TAG_NAME: 'Rape/Non-Con' -WARNING_CHAN_TAG_NAME: 'Underage' +WARNING_CHAN_TAG_NAME: 'Underage Sex' RATING_CATEGORY_NAME: 'Rating' RATING_DEFAULT_TAG_NAME: 'Not Rated' @@ -675,7 +676,8 @@ HIT_COUNT_ROLLOVER_HOUR: 3 # The batch size for calculating a work's filters from its tags: FILTER_UPDATE_BATCH_SIZE: 100 -# URLs for which we should not display the proxy notice. Alphabetical by +# URLs for which we should not display the proxy notice. URLs from these hosts +# are allowed in Abuse reports and disallowed in Work imports. Alphabetical by # environment. PERMITTED_HOSTS: [ # Production diff --git a/config/docker/Dockerfile b/config/docker/Dockerfile index 8e904ec6399..acc860b7f08 100644 --- a/config/docker/Dockerfile +++ b/config/docker/Dockerfile @@ -5,6 +5,7 @@ RUN apt-get update && \ apt-get install -y \ calibre \ default-mysql-client \ + libvips \ shared-mime-info \ zip diff --git a/config/environments/production.rb b/config/environments/production.rb index 7ea3ed1808e..5a0a8ea1d0c 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -33,8 +33,9 @@ # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache # config.action_dispatch.x_sendfile_header = "X-Accel-Redirect" # for NGINX - # Store uploaded files on the local file system (see config/storage.yml for options). - config.active_storage.service = :local + # Store uploaded files in S3, proxied (see config/storage.yml for options). + config.active_storage.service = :s3 + config.active_storage.resolve_model_to_route = :rails_storage_proxy # Use a different cache store in production config.cache_store = :mem_cache_store, ArchiveConfig.MEMCACHED_SERVERS, @@ -94,11 +95,4 @@ # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false - - # paperclip config - Paperclip::Attachment.default_options[:storage] = :s3 - Paperclip::Attachment.default_options[:s3_credentials] = { s3_region: ENV["S3_REGION"], - bucket: ENV["S3_BUCKET"], - access_key_id: ENV["S3_ACCESS_KEY_ID"], - secret_access_key: ENV["S3_SECRET_ACCESS_KEY"] } end diff --git a/config/environments/staging.rb b/config/environments/staging.rb index a5f126d1062..3a76de17b2d 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -69,11 +69,9 @@ Bullet.counter_cache_enable = false end - Paperclip::Attachment.default_options[:storage] = :s3 - Paperclip::Attachment.default_options[:s3_credentials] = { s3_region: ENV["S3_REGION"], - bucket: ENV["S3_BUCKET"], - access_key_id: ENV["S3_ACCESS_KEY_ID"], - secret_access_key: ENV["S3_SECRET_ACCESS_KEY"] } + # Store uploaded files in AWS S3, proxied so we can cache them. + config.active_storage.service = :s3 + config.active_storage.resolve_model_to_route = :rails_storage_proxy config.middleware.use Rack::Attack diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml index bea108f0aac..0646f44702a 100644 --- a/config/i18n-tasks.yml +++ b/config/i18n-tasks.yml @@ -149,8 +149,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 - # 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 - admin.admin_invitations.find.find_email - admin.admin_invitations.find.find_token @@ -173,37 +171,6 @@ ignore_missing: # File: app/views/gifts/_gift_search.html.erb - gifts.gift_search.forms.gift_search - gifts.gift_search.gifts.recipient_field - # File: app/views/home/site_map.html.erb - - home.site_map.ao3_news - - home.site_map.archive_faq - - home.site_map.bookmarks - - home.site_map.collections - - home.site_map.donate - - home.site_map.edit_user_profile - - home.site_map.fandoms - - home.site_map.freeform_tag_cloud - - home.site_map.homepage - - home.site_map.known_issues - - home.site_map.languages - - home.site_map.manage_pseuds - - home.site_map.my_bookmarks - - home.site_map.my_collections - - home.site_map.my_drafts - - home.site_map.my_history - - home.site_map.my_home - - home.site_map.my_inbox - - home.site_map.my_profile - - home.site_map.my_series - - home.site_map.my_subscriptions - - home.site_map.my_works - - home.site_map.people - - home.site_map.post_new - - home.site_map.recent_works - - home.site_map.report_abuse - - home.site_map.set_preferences - - home.site_map.site_map - - home.site_map.support_and_feedback - - home.site_map.terms_of_service # File: app/views/invitations/index.html.erb - invitations.index.choose_invite - invitations.index.email address # should be invitations.index.email_address diff --git a/config/initializers/cookie_rotator.rb b/config/initializers/cookie_rotator.rb new file mode 100644 index 00000000000..83bba105d8c --- /dev/null +++ b/config/initializers/cookie_rotator.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# To support a rolling deploy of SHA256 cookies: +# 1. Done: Read SHA256 cookies, but write back SHA1 cookies (writing is based on current setting of config.active_support.key_generator_hash_digest_class). +# 2. Current: Switch this rotator to read SHA1 and change key_generator_hash_digest_class to write SHA256. +# Explanation: +# During rolling deploy, rotator from step 1 will still be present on some servers. It will read the new SHA256 cookies and write cookies as SHA1. +# While new rotator from step 2 on updated servers converts old SHA1 cookies to new SHA256 cookies. +# After rolling deploy is finished, only new rotator will be present on all servers and will convert all SHA1 cookies to SHA256. +# 3. Next: After the rotator from step 2 has been deployed for a while and all cookies should be converted to SHA256, remove the rotator. +# Ref: https://guides.rubyonrails.org/upgrading_ruby_on_rails.html#key-generator-digest-class-change-requires-a-cookie-rotator +Rails.application.config.after_initialize do + Rails.application.config.action_dispatch.cookies_rotations.tap do |cookies| + authenticated_encrypted_cookie_salt = Rails.application.config.action_dispatch.authenticated_encrypted_cookie_salt + signed_cookie_salt = Rails.application.config.action_dispatch.signed_cookie_salt + + secret_key_base = Rails.application.secret_key_base + + key_generator = ActiveSupport::KeyGenerator.new( + secret_key_base, iterations: 1000, hash_digest_class: OpenSSL::Digest::SHA1 + ) + key_len = ActiveSupport::MessageEncryptor.key_len + + old_encrypted_secret = key_generator.generate_key(authenticated_encrypted_cookie_salt, key_len) + old_signed_secret = key_generator.generate_key(signed_cookie_salt) + + cookies.rotate :encrypted, old_encrypted_secret + cookies.rotate :signed, old_signed_secret + end +end diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb new file mode 100644 index 00000000000..f51a497e118 --- /dev/null +++ b/config/initializers/cookies_serializer.rb @@ -0,0 +1,5 @@ +# Be sure to restart your server when you modify this file. + +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. +Rails.application.config.action_dispatch.cookies_serializer = :hybrid diff --git a/config/initializers/monkeypatches/deliver_after_commit.rb b/config/initializers/monkeypatches/deliver_after_commit.rb index d289ce0ba8a..80e8c591683 100644 --- a/config/initializers/monkeypatches/deliver_after_commit.rb +++ b/config/initializers/monkeypatches/deliver_after_commit.rb @@ -7,3 +7,29 @@ def deliver_after_commit end end end + +module AfterCommitEverywhereWithLocale + def initialize(connection: ActiveRecord::Base.connection, **handlers) + @connection = connection + @handlers = handlers + @locale = I18n.locale + end + + def before_committed!(*) + I18n.with_locale(@locale) { @handlers[:before_commit]&.call } + end + + def committed!(*) + I18n.with_locale(@locale) { @handlers[:after_commit]&.call } + end + + def rolledback!(*) + I18n.with_locale(@locale) { @handlers[:after_rollback]&.call } + end +end + +if AfterCommitEverywhere::VERSION == "1.4.0" + AfterCommitEverywhere::Wrap.prepend(AfterCommitEverywhereWithLocale) +else + puts "WARNING: The monkeypatch #{__FILE__} was written for version 1.4.0 of the after_commit_everywhere gem, but you are running #{AfterCommitEverywhere::VERSION}. Please update or remove the monkeypatch." +end diff --git a/config/locales/controllers/en.yml b/config/locales/controllers/en.yml index bc8d7c3a86c..f1e1c7d4327 100644 --- a/config/locales/controllers/en.yml +++ b/config/locales/controllers/en.yml @@ -1,6 +1,9 @@ --- en: admin: + access: + action_access_denied: Sorry, only an authorized admin can do that. + page_access_denied: Sorry, only an authorized admin can access the page you were trying to reach. admin_invitations: find: user_not_found: No results were found. Try another search. @@ -11,6 +14,14 @@ en: admin_users: destroy_user_creations: success: All creations by user %{login} have been deleted. + archive_faqs: + create: + success: Archive FAQ was successfully created. + default_locale_only: Sorry, this action is only available for English FAQs. + update: + success: Archive FAQ was successfully updated. + update_positions: + success: Archive FAQs order was successfully updated. blocked: users: create: @@ -81,21 +92,44 @@ en: external_works: update: successfully_updated: External work was successfully updated. + home: + content: + page_title: Content Policy + privacy: + page_title: Privacy Policy + tos: + page_title: Terms of Service + tos_faq: + page_title: Terms of Service FAQ + invite_requests: + index: + page_title: Invitation Requests kudos: create: success: Thank you for leaving kudos! + languages: + successfully_added: Language was successfully added. + successfully_updated: Language was successfully updated. muted: users: create: muted: You have muted the user %{name}. destroy: unmuted: You have unmuted the user %{name}. + questions: + not_found: Sorry, we couldn't find the FAQ you were looking for. + update_positions: + success: Question order has been successfully updated. + tag_wranglings: + index: + page_subtitle: fandoms users: + contact_abuse: contact our Policy & Abuse team passwords: 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}. + contact_abuse: contact our Policy & Abuse team + reset_blocked_html: Password resets are disabled for that user. For more information, please %{contact_abuse_link}. + reset_cooldown_html: 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. @@ -105,6 +139,13 @@ en: 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. status: - ban_notice_html: Your account has been banned. You are not permitted to add or edit archive content. Please %{contact_abuse_link} for more information. - contact_abuse: contact Abuse - suspension_notice_html: Your account has been suspended until %{suspended_until}. You may not add or edit content until your suspension has been resolved. Please %{contact_abuse_link} for more information. + ban_notice_html: Your account has been banned. You are not permitted to post or edit content on AO3. Please check your email or %{contact_abuse_link} for more information. + suspension_notice_html: Your account has been suspended until %{suspended_until}. You cannot post, edit, or delete content until your suspension has ended. Please check your email or %{contact_abuse_link} for more information. + works: + drafts: + page_title: "%{username} - Drafts" + wrangling_guidelines: + create: Wrangling Guideline was successfully created. + delete: Wrangling Guideline was successfully deleted. + reorder: Wrangling Guidelines order was successfully updated. + update: Wrangling Guideline was successfully updated. diff --git a/config/locales/mailers/en.yml b/config/locales/mailers/en.yml index 073826e1bc6..cc03607e132 100644 --- a/config/locales/mailers/en.yml +++ b/config/locales/mailers/en.yml @@ -181,23 +181,23 @@ en: any: Any assignment: html: You have been assigned the following request in the %{link} challenge at the Archive of Our Own! + text: You have been assigned the following request in the "%{collection_title}" challenge (%{collection_url}) at the Archive of Our Own! description: 'Description:' due: 'This assignment is due at:' - html: - footer: You're receiving this email because you signed up for the %{title} challenge. For more information about this challenge and contact information for the moderators, please visit %{footer_link}. - footer_link: the challenge profile page - look_up: You can look up this assignment from %{link}. - look_up_link: your Assignments page + footer: + challenge_profile: the challenge profile page + html: You're receiving this email because you signed up for the %{title} challenge. For more information about this challenge and contact information for the moderators, please visit %{challenge_profile_link}. + text: You're receiving this email because you signed up for the %{title} challenge (%{url}). For more information about this challenge and contact information for the moderators, please visit %{challenge_profile_url}. + look_up: + html: You can look up this assignment from %{your_assignments_link}. + text: You can look up this assignment from your Assignments page at %{your_assignments_url}. + your_assignments: your Assignments page optional_tags: 'Optional Tags:' prompt_url: 'Prompt URL:' prompts: 'Prompts:' recipient: 'Recipient:' recipient_missing: 'None: contact a moderator for help!' subject: "[%{app_name}][%{collection_title}] Your assignment!" - text: - assignment: You have been assigned the following request in the "%{collection_title}" challenge (%{collection_url}) at the Archive of Our Own! - footer: You're receiving this email because you signed up for the %{title} challenge (%{url}). For more information about this challenge and contact information for the moderators, please visit %{profile_url}. - look_up: You can look up this assignment from your Assignments page at %{link}. change_email: changed: html: "%{login}, the email associated with your account has been changed to %{email}" @@ -251,6 +251,9 @@ en: assignments_sent: complete: All assignments have now been sent out. subject: Assignments sent + challenge_default: + complete: 'Signed-up participant %{offer_byline} has defaulted on their assignment for %{request_byline}. You may want to assign a pinch hitter on the collection assignments page: %{assignments_page_url}' + subject: Challenge default by %{offer_byline} html: received_message: 'You have received a message about your collection %{collection_link}:' text: @@ -377,16 +380,15 @@ en: text: If you would like Open Doors to update the redirect to point to your pre-existing work, please delete the imported copy, and contact Open Doors at %{open_doors_link} with your AO3 account name, your account name on the imported archive, and the title and URL of the fanwork you would like the redirect to point to. (If you have multiple works you would like to change the redirects for, you can list these in one email.) uploaded_list: 'The works uploaded include:' invite_increase_notification: - html: - body: + body: + html: one: We just wanted to let you know that you have %{count} new invitation, which can be used to create a new account at the Archive. You can invite a friend at %{invitation_page_link}. other: We just wanted to let you know that you have %{count} new invitations, which can be used to create new accounts at the Archive. You can invite a friend at %{invitation_page_link}. - invitation_page_link_text: your invitations page - subject: "[%{app_name}] New invitations" - text: - body: + text: one: We just wanted to let you know that you have %{count} new invitation, which can be used to create a new account at the Archive. You can invite a friend at your invitations page (%{invitation_page_url}). other: We just wanted to let you know that you have %{count} new invitations, which can be used to create new accounts at the Archive. You can invite a friend at your invitations page (%{invitation_page_url}). + invitation_page_link_text: your invitations page + subject: "[%{app_name}] New invitations" invite_request_declined: main: one: We regret to inform you that your request for a new invitation cannot be fulfilled at this time. diff --git a/config/locales/models/en.yml b/config/locales/models/en.yml index 83d9a8a6b4d..41f18db878b 100644 --- a/config/locales/models/en.yml +++ b/config/locales/models/en.yml @@ -16,35 +16,15 @@ en: support: Support tag_wrangling: Tag Wrangling translation: Translation - archive_warning: - name_with_colon: - one: 'Warning:' - other: 'Warnings:' - category: - name_with_colon: - one: 'Category:' - other: 'Categories:' chapters/creatorships: base: 'Invalid creator:' pseud_id: Pseud - character: - name_with_colon: - one: 'Character:' - other: 'Characters:' creatorships: base: 'Invalid creator:' pseud_id: Pseud external_work: author: Creator user_defined_tags_count: Fandom, relationship, and character tags - fandom: - name_with_colon: - one: 'Fandom:' - other: 'Fandoms:' - freeform: - name_with_colon: - one: 'Additional Tag:' - other: 'Additional Tags:' gift_exchange: offers_num_allowed: Number of offers allowed per sign-up offers_num_required: Number of offers required per sign-up @@ -55,12 +35,6 @@ en: meta_tag_id: Metatag sub_tag: Subtag sub_tag_id: Subtag - rating: - name_with_colon: 'Rating:' - relationship: - name_with_colon: - one: 'Relationship:' - other: 'Relationships:' role: archivist: Archivist no_resets: No Resets @@ -94,6 +68,7 @@ en: errors: messages: forbidden: "%{value} is not allowed" + numeric_with_optional_hash: 'may begin with an # and otherwise contain only numbers.' models: abuse_report: attributes: @@ -173,6 +148,12 @@ en: format: "%{message}" user: attributes: + age_over_13: + accepted: Sorry, you have to be over 13! + format: "%{message}" + data_processing: + accepted: Sorry, you need to consent to the processing of your personal data in order to sign up. + format: "%{message}" login: changed_too_recently: one: can only be changed once per day. You last changed your user name on %{renamed_at}. @@ -180,6 +161,9 @@ en: 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. + terms_of_service: + accepted: Sorry, you need to accept the Terms of Service in order to sign up. + format: "%{message}" work: attributes: user_defined_tags_count: @@ -235,11 +219,23 @@ en: other: Works attributes: ticket_number: Ticket ID + challenge_assignment: + offer_byline: + none: "- none -" + pinch_hitter: "%{pinch_hitter_byline}* (pinch hitter)" + request_byline: + none: "- None -" + pinch_recipient: "%{pinch_request_byline}* (pinch recipient)" errors: attributes: + icon: + invalid_format: content type is invalid + too_large: file size must be less than %{maximum_size} ticket_number: closed_ticket: must not be closed. invalid_department: must be in your department. required: must exist and not be spam. + story_parser: + on_archive: URL is for a work on the Archive. Please bookmark it directly instead. subscriptions: deleted: Deleted item diff --git a/config/locales/phrase-exports/ar.yml b/config/locales/phrase-exports/ar.yml index 639c41effb3..9c6628e481e 100644 --- a/config/locales/phrase-exports/ar.yml +++ b/config/locales/phrase-exports/ar.yml @@ -462,8 +462,32 @@ ar: لها، يمكنك إدراجها كلها في بريد إلكتروني واحد.) uploaded_list: 'الأعمال المحمّلة تشمل:' invite_increase_notification: + html: + body: + few: نود إعلامك بأنه لديك %{count} دعوات جديدة يمكن استخدامها لإنشاء حسابات + جديدة في AO3. بإمكانك دعوة صديق من خلال %{invitation_page_link}. + many: نود إعلامك بأنه لديك %{count} دعوة جديدة يمكن استخدامها لإنشاء حسابات + جديدة في AO3. بإمكانك دعوة صديق من خلال %{invitation_page_link}. + one: نود إعلامك بأنه لديك دعوة جديدة يمكن استخدامها لإنشاء حساب جديد في + AO3. بإمكانك دعوة صديق من خلال %{invitation_page_link}. + other: نود إعلامك بأنه لديك %{count} دعوة جديدة يمكن استخدامها لإنشاء حسابات + جديدة في AO3. بإمكانك دعوة صديق من خلال %{invitation_page_link}. + two: نود إعلامك بأنه لديك دعوتان جديدتان يمكن استخدامها لإنشاء حسابان جديدان + في AO3. بإمكانك دعوة صديق من خلال %{invitation_page_link}. invitation_page_link_text: صفحة Invitations (دعواتك) subject: "[%{app_name}] دعوات جديدة" + text: + body: + few: نود إعلامك بأنه لديك %{count} دعوات جديدة يمكن استخدامها لإنشاء حسابات + جديدة في AO3. بإمكانك دعوة صديق من خلال صفحة Invitations (دعواتك) (%{invitation_page_url}). + many: نود إعلامك بأنه لديك %{count} دعوة جديدة يمكن استخدامها لإنشاء حسابات + جديدة في AO3. بإمكانك دعوة صديق من خلال صفحة Invitations (دعواتك) (%{invitation_page_url}). + one: نود إعلامك بأنه لديك دعوة جديدة يمكن استخدامها لإنشاء حساب جديد في + AO3. بإمكانك دعوة صديق من خلال صفحة Invitations (دعواتك) (%{invitation_page_url}). + other: نود إعلامك بأنه لديك %{count} دعوة جديدة يمكن استخدامها لإنشاء حسابات + جديدة في AO3. بإمكانك دعوة صديق من خلال صفحة Invitations (دعواتك) (%{invitation_page_url}). + two: نود إعلامك بأنه لديك دعوتان جديدتان يمكن استخدامها لإنشاء حسابان جديدان + في AO3. بإمكانك دعوة صديق من خلال صفحة Invitations (دعواتك) (%{invitation_page_url}). invite_request_declined: main: few: نأسف لإبلاغك أنه لا يمكن تلبية طلبك للحصول على %{count} دعوات جديدة في diff --git a/config/locales/phrase-exports/bg.yml b/config/locales/phrase-exports/bg.yml index 52b8456f10f..539791d0aa3 100644 --- a/config/locales/phrase-exports/bg.yml +++ b/config/locales/phrase-exports/bg.yml @@ -234,11 +234,11 @@ bg: archivist_added_to_collection_notification: approved_collection_items_page: страница Approved Collection Items (Одобрено съдържание на колекция) - archivist_notice: Тъй като модераторите на колекцията действат в качеството - си на архивисти в Open Doors (Отворени врати), те могат да добавят твоето - произведение към тази колекция, дори да си изключил/а получаването на покани - за колекции. Архивистите ще добавят произведението ти към колекция, ако то - е било част от импортиран архив. + archivist_notice: Тъй като модераторите на колекцията действат в официалното + си качество на архивисти в Open Doors (Отворени врати), те могат да добавят + твоето произведение към тази колекция дори когато опцията за получаване на + покани за колекции е изключена в твоите настройки. Архивистите добавят произведения + към колекции само ако те са били част от импортиран архив. removal_instructions: html: Ако желаеш да премахнеш творбата си от тази колекция, моля, посети твоята %{approved_items_link}. @@ -248,10 +248,11 @@ bg: subject: "[%{app_name}][%{collection_title}] Архивист на Open Doors (Отворени врати) е добавил твоето произведение към колекция" work_added: - html: Модераторите на %{collection_link} са добавили твоето произведение %{work_link} - към тяхната колекция! - text: Модераторите на "%{collection_title}" (%{collection_url}) са добавили - твоето произведение "%{work_title}" (%{work_url}) към тяхната колекция! + html: Доброволците, които поддържат %{collection_link}, са добавили твоето + произведение %{work_link} към тяхната колекция! + text: Доброволците, поддържащи "%{collection_title}" (%{collection_url}), + са добавили твоето произведение "%{work_title}" (%{work_url}) към тяхната + колекция! challenge_assignment_notification: any: Всички assignment: @@ -342,12 +343,12 @@ bg: contact_open_doors: свържи се с Отворени врати html: Ако в импортирания архив има и други твои произведения, но свързани с друг имейл, до който вече нямаш достъп, моля, %{contact_open_doors_link}, - като предоставиш всякаква информация, която може да помогне да потвърди - самоличността ти. + като предоставиш всякаква информация, която може да потвърди самоличността + ти. text: Ако в импортирания архив има и други твои произведения, но свързани с друг имейл, до който вече нямаш достъп, моля, свържи се с Отворени врати, - като предоставиш всякаква информация, която може да помогне да потвърди - самоличността ти. + като предоставиш всякаква информация, която може да потвърди самоличността + ти. questions: contact_support: свържи се с АО3 Поддръжка html: При други въпроси, моля, %{contact_support_link}. @@ -393,12 +394,11 @@ bg: creatorship_notification: explanation: Когато си съавтор на произведение, други потребители могат да те добавят като съавтор и на новите глави, независимо от настройките ти за съавторство. - Ще бъдеш отбелязан и като съавтор на всяка серия, към която е добавено това - произведение. + Това те прави и съавтор на всяка поредица, към която е добавено това произведение. html: creation: "%{creation_link} от %{pseud_links}" edit_chapter: да редактираш главата - edit_series: да редактираш серията + edit_series: да редактираш поредицата remove_chapter: Ако са те добавили по погрешка или не желаеш да фигурираш като създател, можеш %{edit_chapter_link}, за да премахнеш името си като създател. @@ -407,7 +407,7 @@ bg: intro_chapter: 'Потребителят %{adding_user} е посочил псевдонима ти %{pseud} като съавтор на следната глава:' intro_series: 'Потребителят %{adding_user} е посочил псевдонима ти %{pseud} - като съавтор на следната серия:' + като съавтор на следната поредица:' subject: "[%{app_name}] Уведомление за съавторство" text: creation: "%{title} (%{url}) от %{pseuds}" @@ -415,12 +415,12 @@ bg: като създател, можеш да редактираш главата, за да премахнеш името си като създател: %{url}' remove_series: 'Ако са те добавили по погрешка или не желаеш да фигурираш - като създател, можеш да редактираш серията, за да премахнеш името си като - създател: %{url}' + като създател, можеш да редактираш поредицата, за да премахнеш името си + като създател: %{url}' creatorship_notification_archivist: - explanation: В качеството си на архивисти на Open Doors (Отворени врати), те - имат правото да те добавят като съавтор и без заявка, дори и да си изключил/а - функцията за съавторство. + explanation: В качеството си на архивист на Open Doors (Отворени врати), този + потребител има правото да те добавя като съавтор и без заявка, дори ако функцията + за съавторство е изключена в твоите настройки. html: creation: "%{creation_link} от %{pseud_links}" edit_chapter: да редактираш главата diff --git a/config/locales/phrase-exports/ko.yml b/config/locales/phrase-exports/ko.yml index 80b5390c809..d5d55aba72a 100644 --- a/config/locales/phrase-exports/ko.yml +++ b/config/locales/phrase-exports/ko.yml @@ -150,22 +150,22 @@ ko: anonymous_or_unrevealed_notification: anonymous_info: 익명 작품들은 태그 목록에 포함되지만 회원님의 작품 페이지에는 포함되지 않습니다. 작품 페이지에서 회원님의 이름이 "Anonymous" (익명)으로 대체됩니다. - anonymous_unrevealed_info: 추후 관리자가 회원님의 작품을 익명 작품으로서 공개할 수도 있습니다. 회원님을 구독하고 + anonymous_unrevealed_info: 추후 관리자가 회원님의 작품을 익명 작품으로써 공개할 수도 있습니다. 회원님을 구독하고 있는 사용자들에게는 해당 변경사항이 알려지지 않습니다. 공개 시에는 회원님의 작품이 태그 목록에 포함되나 회원님의 작품 페이지에는 나타나지 - 않습니다. 작품 상에서는 회원님의 유저 이름이 "Anonymous" (익명)로 표기됩니다. + 않습니다. 작품상에서는 회원님의 유저 이름이 "Anonymous" (익명)로 표기됩니다. changed_status: anonymous: html: "%{collection_link} 컬렉션 관리자가 회원님의 작품 %{work_link}을(를) 익명 작품으로 전환했습니다." - text: '"%{collection_title}" (%{collection_url}) 콜렉션 관리자들이 회원님의 작품 "%{work_title}" - (%{work_url}) 를/을 익명 저자로 처리했습니다.' + text: '"%{collection_title}" (%{collection_url}) 콜렉션 관리자가 회원님의 작품 "%{work_title}" + (%{work_url}) 를/을 익명으로 처리했습니다.' anonymous_unrevealed: html: "%{collection_link} 컬렉션 관리자가 회원님의 작품을 %{work_link} 을(를) 익명 및 비공개로 전환했습니다." - text: '"%{collection_title}" (%{collection_url}) 콜렉션 관리자들이 회원님의 작품 "%{work_title}" - (%{work_url}) 를/을 익명 저자이자 비공개 등록으로 처리했습니다.' + text: '"%{collection_title}" (%{collection_url}) 콜렉션 관리자가 회원님의 작품 "%{work_title}" + (%{work_url}) 를/을 익명이자 비공개 등록으로 처리했습니다.' unrevealed: html: "%{collection_link} 컬렉션 관리자가 회원님의 작품 %{work_link}을(를) 비공개로 전환했습니다." - text: '"%{collection_title}" (%{collection_url}) 콜렉션 관리자들이 회원님의 작품 "%{work_title}" + text: '"%{collection_title}" (%{collection_url}) 콜렉션 관리자가 회원님의 작품 "%{work_title}" (%{work_url}) 를/을 비공개 등록으로 처리했습니다.' collection_items_link_text: Approved Collection Items (등록된 컬렉션 작품 목록) 페이지 do_not_want: @@ -191,7 +191,7 @@ ko: subject: anonymous: "[%{app_name}] 회원님의 작품이 익명 처리되었습니다." anonymous_unrevealed: "[%{app_name}] 회원님의 작품이 익명 및 비공개 처리되었습니다." - unrevealed: "[%{app_name}] 회원님의 작품이 비공개 처리 되었습니다" + unrevealed: "[%{app_name}] 회원님의 작품이 비공개 처리되었습니다" unrevealed_info: 비공개 작품은 태그 목록 및 회원님의 작품 페이지에 나타나지 않습니다. 해당 작품 링크로 접속하는 모든 이용자에게는 작품이 비공개임을 알리는 공지가 뜨며 작품 내용을 조회할 수 없습니다. archivist_added_to_collection_notification: @@ -210,30 +210,29 @@ ko: text: '"%{collection_title}" (%{collection_url}) 컬렉션 관리자가 회원님의 "%{work_title}" (%{work_url}) 작품을 컬렉션에 추가했습니다!' challenge_assignment_notification: - any: 기타 자유 + any: "'모든' 태그 포함" assignment: - html: 회원님께서는 Archive of Our Own – AO3(우리만의 아카이브) %{link} 챌린지에서 다음과 같은 리퀘를 - 맡게 되셨습니다! + html: 회원님께서는 AO3 %{link} 챌린지에서 다음과 같은 리퀘를 맡게 되셨습니다! description: '설명:' due: '이 챌린지 과제의 마감일:' html: - footer: "%{title} 챌린지에 등록하셨기에 이 이메일을 받으셨습니다. 이 도전에 대한 자세한 내용과 진행자의 연락처는 %{footer_link}를 - 통해 찾으실 수 있습니다." - footer_link: 챌린지 프로파일 페이지 - look_up: 이 챌린지 과제는 %{link}에서 찾으실 수 있습니다. + footer: "%{title} 챌린지에 등록했기 때문에 이 이메일을 받으셨습니다. 이 챌린지에 대한 자세한 내용과 진행자의 연락처는 + %{footer_link}에서 확인할 수 있습니다." + footer_link: 챌린지 프로필 페이지 + look_up: 이 챌린지 과제는 %{link}에서 확인할 수 있습니다. look_up_link: 회원님의 Assignments (챌린지 과제) 페이지 - optional_tags: '선택 태그:' + optional_tags: '선택적 태그:' prompts: '소재:' prompt_url: '소재 URL:' recipient: '받는 사람:' recipient_missing: '없음: 관리자에게 도움을 요청하세요!' subject: "[%{app_name}][%{collection_title}] 회원님의 챌린지 과제입니다!" text: - assignment: 다음은 회원님이 맡게 되신 Archive of Our Own – AO3 (우리만의 아카이브) "%{collection_title}" - 챌린지 (%{collection_url}) 리퀘입니다! - footer: "%{title} 챌린지(%{url})에 등록하셨기에 이 이메일을 받으셨습니다. 이 챌린지에 대한 자세한 내용과 진행자의 - 연락처는 %{profile_url}를 통해 찾으실 수 있습니다." - look_up: 이 챌린지 과제는 회원님의 Assignments (챌린지 과제) 페이지 링크%{link} 에서 찾아보실 수 있습니다. + assignment: 다음은 회원님이 맡게 되신 AO3 "%{collection_title}" 챌린지 (%{collection_url}) + 리퀘입니다! + footer: "%{title} 챌린지(%{url})에 등록했기 때문에 이 이메일을 받으셨습니다. 이 챌린지에 대한 자세한 내용과 진행자의 + 연락처는 %{profile_url}에서 확인할 수 있습니다." + look_up: 이 챌린지 과제는 회원님의 Assignments (챌린지 과제) 페이지 링크 %{link}에서 확인할 수 있습니다. change_email: changed: html: "%{login}님, 회원님의 계정과 연계된 이메일 주소가 %{email}으로 변경되었습니다." @@ -243,8 +242,8 @@ ko: access: contact_support: AO3 지원 위원회에 문의 html: 아카이브에 따라서는 통합 이전 시 회원님의 작품이 (구글 검색을 방지하기 위해) 등록된 회원에게만 보이도록 설정되는 경우가 - 있습니다. 이 때는 전체 공개로 설정을 변경하지 않으면 작품은 로그인을 한 회원들만 조회할 수 있습니다. 작품을 전체 공개하거나, - 버리기, 또는 삭제하는 데 도움이 필요하시면 %{contact_support_link}해 주시기 바랍니다. + 있습니다. 이때는 전체 공개로 설정을 변경하지 않으면 작품은 로그인한 회원들만 조회할 수 있습니다. 작품을 전체 공개하거나, 버리기, + 또는 삭제하는 데 도움이 필요하시면 %{contact_support_link}해 주시기 바랍니다. text: '아카이브에 따라서는 통합 이전 시 회원님의 작품이 (구글 검색을 방지하기 위해) 등록된 회원에게만 보이도록 설정되는 경우가 있습니다. 이 때는 전체 공개로 설정을 변경하지 않으면 작품은 로그인을 한 회원들만 조회할 수 있습니다. 작품을 전체 공개하거나, 버리기, 또는 삭제하는 데 도움이 필요하시면 다음 링크를 통해 AO3 지원 위원해에 문의해 주시기 바랍니다: %{support_url}' @@ -262,7 +261,7 @@ ko: 자동으로 회원님의 AO3 계정에 추가되었습니다.' mistake: contact_open_doors: 오픈 도어 프로젝트에 문의 - html: 이 작품들이 회원님의 작품이 아니라면 해당 작품을 삭제하지 말아주세요! %{contact_open_doors_link}해서 + html: 이 작품이 회원님의 작품이 아니라면 해당 작품을 삭제하지 말아 주세요! %{contact_open_doors_link}해서 알려 주시면 저희가 해결해 드리겠습니다. text: 만약 이 작품들이 귀하의 작품이 아니라면, 해당 작품을 삭제하지 말아주세요! 오픈 도어 위원회(%{open_doors_url})에 문의해서 알려 주시면 저희가 해결해 드리겠습니다. @@ -270,20 +269,20 @@ ko: ao3_news: AO3 소식 contact_support: AO3 지원 위원회에 문의 faq_page: FAQ 페이지 - html: 최신 아카이브 이전에 대한 소식은 %{ao3_news_link}에서, 오픈 도어 프로젝트에 대한 추가 정보는 %{faq_page_link} - 또는 %{tutorial_page_link}에서 확인하실 수 있습니다. FAQ, 튜토리얼, 이메일 등으로 해결되지 못한 못한 질문은 - %{contact_support_link}해 주시기 바랍니다. - text: '최신 아카이브 이전에 대한 정보는 AO3 뉴스 (%{news_url})에서, 오픈 도어 프로젝트에 대한 추가 정보는 오픈 + html: 최근 아카이브 이전에 대한 소식은 %{ao3_news_link}에서, 오픈 도어 프로젝트에 대한 추가 정보는 %{faq_page_link} + 또는 %{tutorial_page_link}에서 확인하실 수 있습니다. FAQ, 튜토리얼, 이메일 등으로 해결되지 못한 질문은 %{contact_support_link}해 + 주시기 바랍니다. + text: '최근 아카이브 이전에 대한 정보는 AO3 뉴스 (%{news_url})에서, 오픈 도어 프로젝트에 대한 추가 정보는 오픈 도어 FAQ 페이지 (%{open_doors_faq_url}) 또는 튜토리얼 페이지(%{open_doors_tutorial_url})에서 확인하실 수 있습니다. FAQ, 튜토리얼, 이메일 등으로 해결하지 못한 질문은 다음 링크를 통해 지원 위원회에 문의해 주시기 바랍니다: %{support_url}' tutorial_page: 튜토리얼 페이지 other_works: contact_open_doors: 오픈도어 프로젝트에 문의 - html: 이전해온 아카이브의 다른 작품들이 회원님이 현 시점 사용할 수 없는 이메일 주소 아래로 들어 있는 경우 무엇이든 회원님의 - 신원을 확인할 수 있는 정보를 넣어 %{contact_open_doors_link}해 주시기 바랍니다. - text: 이전해온 아카이브의 다른 작품들이 회원님이 현 시점 사용할 수 없는 이메일 주소 아래로 들어 있는 경우 무엇이든 회원님의 - 신원을 확인할 수 있는 정보를 넣어 오픈 도어 위원회에 문의해 주시기 바랍니다. + html: 이전해 온 아카이브의 다른 작품들이 회원님이 현시점 사용할 수 없는 이메일 주소로 등록되어 있는 경우 무엇이든 회원님의 신원을 + 확인할 수 있는 정보를 넣어 %{contact_open_doors_link}해 주시기 바랍니다. + text: 이전해온 아카이브의 다른 작품들이 회원님이 현 시점 사용할 수 없는 이메일 주소로 등록되어 있는 경우 무엇이든 회원님의 신원을 + 확인할 수 있는 정보를 넣어 오픈 도어 위원회에 문의해 주시기 바랍니다. questions: contact_support: AO3 지원 위원회에 문의 html: 그 의 질문은 %{contact_support_link}해 주세요. @@ -297,12 +296,12 @@ ko: contact_open_doors: 오픈 도어 프로젝트에 문의 html: 리디렉팅 주소가 회원님의 기존 작품으로 이어지도록 오픈 도어 위원회에서 업데이트를 진행하기를 원하신다면 이전해 온 사본을 삭제한 뒤 AO3 계정 이름, 이전한 아카이브 내 계정 이름, 리디렉팅으로 이동하게 될 팬작품의 제목과 URL을 담아 %{contact_open_doors_link}해 - 주시기 바랍니다. (여러 작품의 라디렉팅 주소를 변경하기 원하시면 한 통의 이메일 내 모두 담으셔도 됩니다.) + 주시기 바랍니다. (여러 작품의 라디렉팅 주소를 변경하기를 원하시면 한 통의 이메일 안에 모두 담으셔도 됩니다.) text: 리디렉팅 주소가 회원님의 기존 작품으로 이어지도록 오픈 도어 위원회에서 업데이트를 진행하기를 원하신다면 이전해 온 사본을 삭제한 뒤 AO3 계정 이름, 이전한 아카이브 내 계정 이름, 리디렉팅으로 이동하게 될 팬작품의 제목과 URL을 담아 %{open_doors_url}를 - 통해 오픈 도어 위원회에 문의해 주시기 바랍니다. (여러 작품의 라디렉팅 주소를 변경하기 원하시면 한 통의 이메일 내 모두 담으셔도 + 통해 오픈 도어 위원회에 문의해 주시기 바랍니다. (여러 작품의 라디렉팅 주소를 변경하기 원하시면 한 통의 이메일 안에 모두 담으셔도 됩니다.) - works_by: '해당 작품은 다음 이메일 아래에 작성되었습니다: %{email}' + works_by: '해당 작품은 다음 이메일 주소로 작성되었습니다: %{email}' work_info: html: "%{work_link} (%{fandom})" text: diff --git a/config/locales/phrase-exports/lv.yml b/config/locales/phrase-exports/lv.yml index 3f02ecf306f..c16692e3344 100644 --- a/config/locales/phrase-exports/lv.yml +++ b/config/locales/phrase-exports/lv.yml @@ -16,6 +16,34 @@ lv: unrequested: Ja Tu nepieprasīji šo paroles nomaiņu, Tu vari ignorēt šo epastu un Tava iepriekšējā parole turpinās darboties. user_mailer: + admin_hidden_work_notification: + access: Kamēr Tavs darbs ir slēpts, Tu joprojām spēsi tam piekļūt, izmantojot + augstāk norādīto saiti, bet tas nebūs redzams Tavu darbu lapā, un tas nebūs + pieejams citiem AO3 lietotājiem. + check_email: Lūdzu, pārbaudi savu epasta adresi, ieskaitot spama sadaļu, jo + Pārkāpumu risināšanas komanda, iespējams, jau ir ar Tevi sazinājusies, lai + paskaidrotu, kāpēc Tavs darbs ir ticis paslēpts. + contact_abuse: sazinies ar Pārkāpumu risināšanas komandu + html: + help: Ja Tu neesi pārliecināts, kāpēc Tavs darbs ir ticis paslēpts un Tu neesi + saņēmis papildus informāciju, lūdzu, %{contact_abuse_link}. + hidden: Tavu darbu %{title} ir paslēpusi Pārkāpumu risināšanas komanda, un + tas vairs nav publiski pieejams. + tos_violation: Ja Tavs darbs tika paslēpts, jo tas pārkāpa AO3 %{tos_link}, + Tev būs jānovērš pārkāpums. Ja tas netiks izdarīts, un darbs neatbildīs + Pakalpojumu sniegšanas noteikumiem, tas tiks dzēsts. + subject: "[%{app_name}] Tavu darbu ir paslēpusi Pārkāpumu risināšanas komanda" + text: + help: 'Ja Tu neesi pārliecināts par darba paslē''pšanas iemeslu un neesi saņēmis + papildus informāciju, lūdzu, sazinies ar Pārkāpumu risināšanas komandu: + %{contact_abuse_url}.' + hidden: Tavu darbu "%{title}" (%{work_url}) ir paslēpusi Pārkāpumu risināšanas + komanda, un tas vairs nav publiski pieejams. + tos_violation: Ja Tavs darbs tika paslēpts, jo tas neatbilst AO3 Pakalpojumu + sniegšanas noteikumiem (%{tos_url}), Tev nāksies veikt darba labojumus. + Ja darbu nepārveidosi, lai tas atbilstu Pakalpojumu sniegšanas noteikumiem. + Tavs darbs tiks dzēsts no AO3. + tos: Pakalpojumu sniegšanas noteikumi anonymous_or_unrevealed_notification: anonymous_info: Anonīmi darbi ir iekļauti birku sarakstos, bet ne Tavā publisko darbu sarakstā. Darbā Tavs lietotājvārds tiks aizstāts ar "Anonīms" (Tulkošana). @@ -211,6 +239,36 @@ lv: text: no_fandom: "- %{work_title} %{work_url}" with_fandom: "- %{work_title} %{work_url} (%{fandom})" + creatorship_notification_archivist: + explanation: Viņi izmanto savas oficiālās iespējas kā Open Doors (Translation) + (Atvērtās Durvis (Tulkošana)), viņiem ir atļauts Tevi pievienot bez uzaicinājuma + arī tad, ja esi bloķējis ko-autora funkciju. + html: + creation: "%{creation_link} %{pseud_links}" + edit_chapter: Mainīt vai labot nodaļu + edit_series: Mainīt vai labot stāstu sērijas + edit_work: mainīt vai labot darbu. + remove_chapter: Ja Tu es pievienots klūdas dēļ vai nevēlies būt atzīmēts kā + autors, Tu vari noņemt sevi kā ko-autoru.%{edit_chapter_link} + remove_series: Ja Tu es pievienots klūdas dēļ, vai nevēlies būt atzīmēts kā + autors, Tu vari %{edit_series_link} noņemt sevi kā ko-autoru. + remove_work: Ja Tu es pievienots klūdas dēļ, vai nevēlies būt atzīmēts kā + autors, Tu vari %{edit_work_link} noņemt sevi kā ko-autoru. + intro_chapter: 'Lietotājs %{archivist} ir Tevi %{pseud} pievienojis kā ko- autoru + sekojošajā nodaļā:' + intro_series: 'Lietotājs %{archivist} ir Tevi pievienojis %{pseud} kā ko- autoru + sekojošajās stāstu sērijās:' + intro_work: 'Lietotājs %{archivist} ir pievienojis Tavu pseud %{pseud} kā ko-autoru + sekojošajā darbā:' + subject: "[%{app_name}] Arhīvista ko -autora (co-creator) ziņa" + text: + creation: "%{title} (%{url})%{pseuds}" + remove_chapter: Ja Tu es pievienots klūdas dēļ, vai nevēlies būt atzīmēts + kā autors, Tu vari labot nodaļu noņemt sevi kā ko-autoru.%{url} + remove_series: Ja Tu es pievienots klūdas dēļ, vai nevēlies būt atzīmēts kā + autors, Tu vari labot stāstu sērijas un noņemt sevi kā ko-autoru.%{url} + remove_work: 'Ja Tu es pievienots klūdas dēļ vai nevēlies būt atzīmēts kā + autors, Tu vari labot darbu un noņemt sevi kā ko-autoru: %{url}' creatorship_request: html: creation: "%{creation_link} autors/i %{pseud_links}" diff --git a/config/locales/phrase-exports/ms.yml b/config/locales/phrase-exports/ms.yml index 1e6dfece1f9..5af7ec8ca29 100644 --- a/config/locales/phrase-exports/ms.yml +++ b/config/locales/phrase-exports/ms.yml @@ -232,6 +232,27 @@ ms: atau pada laman karya anda. Sesiapa yang mengikuti pautan ke karya tersebut akan menerima notis bahawa karya tersebut tidak dipamerkan, dan mereka tidak akan dapat mengakses kandungan tersebut. + archivist_added_to_collection_notification: + approved_collection_items_page: Halaman Approved Collection Items (Item Koleksi + yang Diluluskan) + archivist_notice: Oleh kerana penyelenggara koleksi bertindak dalam kapasiti + rasmi mereka sebagai ahli arkib Open Doors (Projek Pemeliharaan Bahan-Bahan + Digital dan Bukan Digital), mereka dibenarkan untuk menambah karya anda ke + koleksi ini, walaupun anda telah menutup jemputan koleksi. Ahli arkib hanya + akan menambah sesebuah karya ke koleksi jika ia dihoskan di arkib yang diimport. + removal_instructions: + html: Jika anda ingin mengalih keluar karya anda daripada koleksi ini, sila + lawati %{approved_items_link} anda. + text: 'Jika anda ingin mengalih keluar karya anda daripada koleksi ini, sila + lawati halaman Approved Collection Items (Item Koleksi yang Diluluskan) + anda: %{approved_items_url}.' + subject: "[%{app_name}][%{collection_title}] Ahli arkib Open Doors (Projek Pemeliharaan + Bahan-Bahan Digital dan Bukan Digital) telah menambah karya anda ke koleksi" + work_added: + html: Penyelenggara koleksi %{collection_link} telah menambah karya anda %{work_link} + ke koleksi mereka! + text: Penyelenggara koleksi bagi "%{collection_title}" (%{collection_url}) + telah menambah karya anda "%{work_title}" (%{work_url}) ke koleksi mereka! challenge_assignment_notification: any: Mana-mana assignment: diff --git a/config/locales/phrase-exports/pt-BR.yml b/config/locales/phrase-exports/pt-BR.yml index 0105e5cf2bc..1fc1612cad5 100644 --- a/config/locales/phrase-exports/pt-BR.yml +++ b/config/locales/phrase-exports/pt-BR.yml @@ -670,19 +670,7 @@ pt-BR: show: login_banner: dismiss: Desabilitar banner de forma permanente - help_link: Aprenda algumas dicas sobre como usar o AO3 - help_text: Para mais informações sobre como usar o site, %{link_faq}! Se precisar - de ajuda técnica, %{link_support}. Se passar por algum incidente de assédio - ou se tiver perguntas específicas sobre nossos %{link_tos}, %{link_abuse}. - help_title: Banner de ajuda após o primeiro login hide: Desabilitar banner de ajuda para quem efetua login pela primeira vez - link_abuse: entre em contato com a nossa equipe de Diretrizes e Abuso - link_faq: dê uma olhada na nossa página de FAQ - link_support: entre em contato conosco pelo nosso formulário de Suporte e - Feedback - link_tos: Termos de Serviço - welcome_text: Oi! Parece que você efetuou login no AO3 pela primeira vez. - %{help_link} ou desabilite permanentemente essa mensagem de boas-vindas. user_mailer: admin_deleted_work_notification: bye: Uma cópia da obra removida encontra-se em anexo. diff --git a/config/locales/phrase-exports/ro.yml b/config/locales/phrase-exports/ro.yml index 13e4c345ea8..64b026c3a6f 100644 --- a/config/locales/phrase-exports/ro.yml +++ b/config/locales/phrase-exports/ro.yml @@ -320,6 +320,102 @@ ro: text: "%{login}, adresa de e-mail asociată cu contul tău a fost schimbată la %{email}" subject: "[%{app_name}] Adresa de e-mail schimbată" + claim_notification: + access: + contact_support: contactează echipa de Suport Tehnic AO3 + html: În funcție de arhivă, este posibil ca lucrările tale să fi fost importate + cu acces limitat doar la utilizatorii/oarele înregistrați/e (pentru a le + ascunde de căutările Google). Dacă ăsta este cazul, lucrările vor fi accesibile + numai utilizatorilor/elor conectați/e, cu excepția cazului în care alegi + să le faci pe deplin vizibile. Pentru ajutor cu deblocarea, abandonarea + sau ștergerea lucrărilor tale, te rugăm %{contact_support_link}. + text: În funcție de arhivă, este posibil ca lucrările tale să fi fost importate + cu acces limitat doar la utilizatorii/oarele înregistrați/e (pentru a le + ascunde de căutările Google). Dacă ăsta este cazul, lucrările vor fi accesibile + numai utilizatorilor/elor conectați/e, cu excepția cazului în care alegi + să le faci pe deplin vizibile. Pentru ajutor cu deblocarea, abandonarea + sau ștergere a lucrărilor tale, te rugăm contactează echipa de Suport Tehnic + AO3 pe %{support_url}. + email_tips: Dacă ne contactezi, te rugăm să adaugi adrese de email de la @transformativeworks.org + la lista ta de contacte sigure și să verifici folderul de spam pentru răspunsul + nostru. + introduction: + ao3_name: Archive of Our Own – AO3 (Arhiva Noastră) + html: Primești acest email deoarece aveai lucrări într-o arhivă de lucrări + fanice care a fost importată de %{open_doors_name_link} pe %{app_link}. + Deoarece această adresă de email este conectată la o adresă înscrisă pe + arhiva importată, lucrările fanice asociate (enumerate mai jos) au fost + adăugate automat în contul tău AO3. + open_doors_name: Open Doors (Uși Deschise) + text: 'Primești acest email deoarece aveai lucrări într-o arhivă de lucrări + fanice care a fost importată de Open Doors (Uși Deschise) (%{open_doors_url}) + pe Archive of Our Own – AO3 (Arhiva Noastră): %{app_url}. Deoarece această + adresă de email este conectată la o adresă înscrisă pe arhiva importată, + lucrările fanice asociate (enumerate mai jos) au fost adăugate automat în + contul tău AO3.' + mistake: + contact_open_doors: contactează echipa Uși Deschise + html: Dacă e o greșeală și acestea nu sunt lucrările tale, te rugăm să nu + le ștergi! Te rugăm doar %{contact_open_doors_link} și vom rezolva problema. + text: Dacă e o greșeală și acestea nu sunt lucrările tale, te rugăm să nu + le ștergi! Te rugăm doar contactează echipa Uși Deschise (%{open_doors_url}) + și vom rezolva problema. + more_info: + ao3_news: Știri AO3 + contact_support: contactează echipa de Suport Tehnic AO3 + faq_page: pagina de Întrebări Frecvente + html: Poți citi anunțuri despre mutarea recentă a arhivei pe %{ao3_news_link} + și poți găsi informații suplimentare pe %{faq_page_link} sau %{tutorial_page_link} + ale Open Doors. Pentru orice întrebări la care nu ai găsit răspuns în Întrebările + Frecvente, tutoriale sau în acest email, te rugăm %{contact_support_link}. + text: Poți citi anunțuri despre mutarea recentă a arhivei pe AO3 News (%{news_url}) + și poți găsi informații suplimentare pe pagina de Întrebări Frecvente (%{open_doors_faq_url}) + sau pagina de tutoriale ale Open Doors. Pentru orice întrebări la care nu + ai găsit răspuns în Întrebările Frecvente, tutoriale sau in acest email, + te rugăm contactează echipa de Suport Tehnic la %{support_url}. + tutorial_page: pagina de tutorial + other_works: + contact_open_doors: contactează echipa Uși Deschise + html: Dacă ai avut alte lucrări în arhiva importată sub o adresă de e-mail + pe care nu o mai poți accesa, te rugăm %{contact_open_doors_link} cu orice + informații care te pot ajuta să îți dovedești identitatea. + text: Dacă ai avut alte lucrări în arhiva importată sub o adresă de e-mail + pe care nu o mai poți accesa, te rugăm contactează echipa Uși Deschise cu + orice informații care te pot ajuta să îți dovedești identitatea. + questions: + contact_support: contactează echipa de Suport Tehnic AO3 + html: Pentru alte întrebări, te rugăm %{contact_support_link}. + text: Pentru alte întrebări, te rugăm contactează echipa de Suport Tehnic + AO3 la %{support_url}. + redirects: + html: Pentru a păstra listele de recomandări și semnele de carte, adresele + web ale arhivei importate pot redirecționa către copia importată a acestor + lucrări pentru un timp limitat (verifică postarea de anunț pentru arhivata + pentru a fi sigur/ă). Dacă ai încărcat deja o copie a acestor lucrări și + %{negation} ai folosit funcția de import din URL, vor exista două copii + ale aceleiași lucrări pe AO3. + subject: "[%{app_name}] Lucrări încărcate" + update_redirect: + contact_open_doors: contactează echipa Uși Deschise + html: Dacă dorești ca Open Doors să actualizeze redirecționarea pentru a ajunge + la lucrarea ta existentă, te rugăm să ștergi copia importată și %{contact_open_doors_link} + } cu numele contului tău AO3, numele contului tău din arhiva importată și + titlul și adresa URL a lucrării fanice la care ai dori să conducă redirecționarea. + (Dacă ai mai multe lucrări pentru care dorești să modifici redirecționările, + le poți enumera într-un singur email.) + text: Dacă dorești ca Open Doors să actualizeze redirecționarea pentru a ajunge + la lucrarea ta existentă, te rugăm să ștergi copia importată și contactează + echipa Uși Deschise la %{open_doors_url} cu numele contului tău AO3, numele + contului tău din arhiva importată și titlul și adresa URL a lucrării fanice + la care ai dori să conducă redirecționarea. (Dacă ai mai multe lucrări pentru + care dorești să modifici redirecționările, le poți enumera într-un singur + e-mail.) + works_by: 'Aceste lucrări au fost scrise sub e-mailul: %{email}' + work_info: + html: "%{work_link} (%{fandom})" + text: + no_fandom: "- %{work_title} %{work_url}" + with_fandom: "- %{work_title} %{work_url} (%{fandom})" collection_notification: assignments_sent: complete: Toate sarcinile au fost acum trimise. diff --git a/config/locales/phrase-exports/scr.yml b/config/locales/phrase-exports/scr.yml index e3a7b56eeb5..d6dac5ff55f 100644 --- a/config/locales/phrase-exports/scr.yml +++ b/config/locales/phrase-exports/scr.yml @@ -96,6 +96,9 @@ scr: subject: "[%{app_name}] Добили сте похвале!" mailer: general: + closing: + formal: Srdačan pozdrav, + informal: Sve najbolje, creation: link_with_word_count: "%{creation_link} (%{word_count})" title_with_chapter_number: "%{title}: Поглавље %{position}" @@ -104,7 +107,34 @@ scr: few: "%{count} reči" one: "%{count} reč" other: "%{count} reči" + footer: + general: + about: + html: AO3 je arhiva koju vode i podržavaju fanovi, a oslanja se na %{donate_link}. + text: 'AO3 je arhiva koju vode i podržavaju fanovi, a oslanja se na Vaše + donacije: %{donate_url}.' + html: + donate_link_text: Vaše donacije + support_link_text: kontaktirajte Korisničku podršku + unwanted_email: + html: Ako ste greškom primili ovu poruku, molimo Vas %{support_link}. + text: Ako ste greškom primili ovu poruku, molimo Vas kontaktirajte Korisničku + podršku na %{support_url}. + sent_at: Poslato u %{sent_at}. + greeting: + formal_html: Poštovani/a %{name}, + informal: + addressed_html: Zdravo, %{name}! + unaddressed: Zdravo! + introductory: Pozdrav od Archive of Our Own – AO3-a (Naše sopstvene arhive)! metadata_label_indicator: ":" + signature: + abuse_team: Tim Politike i zloupotrebe AO3-a + app_short_name: AO3 + open_doors: Tim Open Doors (Otvorenih vrata) + parent_org: Organization for Transformative Works – OTW (Organizacija za transformativne + radove) + support: Tim Korisničke podrške AO3-a users: mailer: reset_password_instructions: diff --git a/config/locales/phrase-exports/uk.yml b/config/locales/phrase-exports/uk.yml index f1b3ba7181c..1fa4667139b 100644 --- a/config/locales/phrase-exports/uk.yml +++ b/config/locales/phrase-exports/uk.yml @@ -262,6 +262,26 @@ uk: на Вашій сторінці робіт. Кожен, хто перейде за посиланням на Вашу роботу, отримає повідомлення, що вона є закритою, і вони не зможуть переглянути її вміст. + archivist_added_to_collection_notification: + approved_collection_items_page: сторінки Approved Collection Items (Деталі затвердженої + колекції) + archivist_notice: Оскільки розпорядники колекції виступають у ролі архіваріусів + Open Doors (Відкритих Дверей), їм дозволено додати Вашу роботу до цієї колекції, + навіть якщо у Вас вимкнено опцію запрошення до колекції. Архіваріуси додадуть + роботу до колекції, лише якщо вона розміщена в імпортованому архіві. + removal_instructions: + html: Якщо Ви хочете прибрати свою роботу з цієї колекції, будь ласка, перейдіть + до Вашої %{approved_items_link}. + text: 'Якщо Ви хочете прибрати свою роботу з цієї колекції, будь ласка, перейдіть + до Вашої сторінки Approved Collection Items (Деталі затвердженої колекції): + %{approved_items_url}.' + subject: "[%{app_name}][%{collection_title}] Архіваріус Open Doors (Відкритих + Дверей) додав Вашу роботу до колекції" + work_added: + html: Розпорядники колекції %{collection_link} додали Вашу роботу %{work_link} + до їхньої колекції! + text: Розпорядники колекції "%{collection_title}" (%{collection_url}) додали + Вашу роботу "%{work_title}" (%{work_url}) до їхньої колекції! challenge_assignment_notification: any: Будь-які assignment: diff --git a/config/locales/views/en.yml b/config/locales/views/en.yml index f15e14b1a46..4cbc8c0438a 100644 --- a/config/locales/views/en.yml +++ b/config/locales/views/en.yml @@ -91,6 +91,17 @@ en: queue: Manage Queue requests: Manage Requests page_heading: Invite New Users + admin_nav: + ao3_news: AO3 News + archive_faq: Archive FAQ + faq: + reorder_questions: Reorder Questions + known_issues: Known Issues + landmark: Admin Navigation + news: + delete_post: Delete Post + post_ao3_news: Post AO3 News + wrangling_guidelines: Wrangling Guidelines admin_options: delete: bookmark: Delete Bookmark @@ -376,6 +387,24 @@ en: roles: heading: 'Your admin roles:' none: You currently have no admin roles assigned to you. + archive_faqs: + admin_index: + confirm_delete: Are you sure you want to delete this FAQ category? + created_updated_date: Created at %{date_created} and updated at %{date_updated} + delete: Delete + edit: Edit + manage_faqs: Manage Archive FAQs + new_faq_category: New FAQ Category + page_heading: Archive FAQ + reorder_faqs: Reorder FAQs + show: Show + show: + edit: Edit + elasticsearch_news: news post announcing the search and filter updates + elasticsearch_update_notice_html: Our search engine has recently been updated, and this FAQ is based on our old version. We're working on bringing you more up-to-date information, but in the meantime, you can find out more in our %{elasticsearch_news_link}! + no_category_entries: We're sorry, there are currently no entries in this category. + page_heading: Archive FAQ + screencast: 'Screencast:' blocked: block: Block unblock: Unblock @@ -441,6 +470,15 @@ en: preferences_link_text: update your preferences prompt_meme: Signing up for this challenge will allow any user who claims your prompt to gift you a work in response to your prompt regardless of your preference settings. If you wish to receive additional gifts from users who have not claimed your prompts, please %{preferences_link} to allow gifts from anyone. You can always %{refuse_link}. refuse_link_text: refuse a gift + chapters: + chapter_form: + content_policy: Content Policy + post_notice_html: All works you post on AO3 must comply with our %{content_policy_link}. For more information, please refer to our %{tos_faq_link}. + tos_faq: Terms of Service FAQ + preview: + content_policy: Content Policy + post_notice_html: All works you post on AO3 must comply with our %{content_policy_link}. For more information, please refer to our %{tos_faq_link}. + tos_faq: Terms of Service FAQ collection_items: collection_item_form: add: Add @@ -465,6 +503,9 @@ en: unreviewed_by_collection: Works and bookmarks listed here have been added to a collection but need approval from a collection moderator before they are listed in the collection. page_heading: Items by %{username} in Collections collections: + form: + icon: + delete: Delete collection icon and revert to our default. This will also remove the icon alt text and comment text. sidebar: bookmarks: Bookmarked Items (%{count}) challenge_settings: Challenge Settings @@ -583,7 +624,6 @@ en: label: Your question or problem (required) email: label: Your email (required) - ip: Our spam filter does collect IP addresses, but we never see them. language: label: Select language (required) legend: @@ -654,6 +694,168 @@ en: html: You can find out more about the OTW and its projects at its website, %{transformative_works_link}, and learn about how your financial support is vital to the continuation and expansion of the OTW's work on its %{faq_page_link}. If you have a media or research question, please contact the %{communications_team_link}. transformative_works: transformativeworks.org page_title: About the OTW + content: + cc_attribution_4_0_international: Creative Commons Attribution 4.0 International License + commercial_promotion: + heading: II.C. Commercial Promotion + not_allowed: Promotion, solicitation, and advertisement of commercial products or activities are not allowed. + tos_faq: FAQ for Section II.C + tos_faq_in_parens_html: "(%{tos_faq_link})" + tos_faq_link_label: Commercial Promotion FAQ + content_policy: Content Policy + content_policy_heading: II. Content Policy + copyright_infringement: + epigraphs_small_quotations_allowed: Epigraphs and short quotations are allowed, as is Content that is set within or based on an existing work. + heading: II.D. Copyright Infringement + not_allowed: Reproductions of large excerpts of copyrighted works are not allowed without the consent of the copyright owner. This includes stories, artwork, songs, poems, transcripts, and other copyrighted material. Crediting the original creator does not give you the right to upload, podfic, or translate someone else's work without permission, regardless of whether the source is a fanwork or a professionally published work. + tos_faq: FAQ for Section II.D + tos_faq_in_parens_html: "(%{tos_faq_link})" + tos_faq_link_label: Copyright Infringement and Plagiarism FAQ + transformative_fanworks: transformative fanworks + transformative_works_legal_html: We believe that %{transformative_fanworks_link} are legal. Complaints about the mere existence of fanworks that mention trademarks or are based on copyrighted material will not be pursued. + effective: 'Effective: November 19, 2024' + fanworks: + bookmarks: Bookmarks + bookmarks_only_fanworks_html: "%{bookmarks_link} must only be created for fanworks. You may create %{external_bookmarks_link} for fanworks hosted on third-party sites." + external_bookmarks: external bookmarks + heading: II.B. Fanworks + must_be_fanworks_html: Works must be fanworks. Posting a Work that primarily consists of %{non_fanwork_content_link} is not allowed. + non_fanwork_content: non-fanwork Content + tos_faq: FAQ for Section II.B + tos_faq_in_parens_html: "(%{tos_faq_link})" + tos_faq_link_label: Fanworks and Non-Fanwork Content FAQ + harassment: + advocating_harm: + heading: Advocating Harm + text: Content that advocates specific, real, harmful actions towards real people is not allowed. This includes, but is not limited to, directing death threats or slurs at people outside of fiction, as well as encouraging others to harass or harm specific people or groups. + applies_to_all: it applies to all aspects of the Service + blocking: blocking + definition: Harassment is any behavior that produces a generally hostile environment for its target. Examples include bullying, threats, and personal attacks by or towards individuals or groups of people. + filtering: filtering + heading: II.H. Harassment + muting: muting + not_allowed_and_context: Harassment is not allowed. When judging whether a specific incident or item of Content constitutes harassment and/or when determining the appropriate severity of a penalty, the Policy & Abuse committee will consider relevant context. This includes whether the behavior was repeated, targeted, difficult to avoid encountering, or related to a general pattern of harassment by an individual or a group, among other factors. Additionally, submitting repeated and/or baseless complaints, particularly those targeting a specific user or group, may be considered harassment. + otw: + abbreviated: OTW + full: Organization for Transformative Works + policy_applicability_html: The Harassment Policy is primarily focused on user conduct and non-fictional Content. However, %{applies_to_all_link}, including interactions with the Policy & Abuse committee, the Support committee, and other AO3 or %{otw_abbreviation} volunteers. + rpf: + heading: Real-Person Fiction (RPF) + text: Creating RPF never constitutes harassment in and of itself. Posting works where someone dies, is subjected to slurs, or is otherwise harmed as part of the plot is usually not a violation of the Harassment Policy. However, deliberately posting such Content in a manner designed to be seen by the subject of the work, such as by gifting them the work, may result in a judgment of harassment. + threatening_versus_annoying_html: In general, threatening Content will be considered harassment, while Content that is merely rude or annoying will be allowed. Not everyone agrees about what is offensive and unacceptable. Users are encouraged to use tools such as %{blocking_link}, %{muting_link}, and %{filtering_link} to control their own environment on AO3. + tos_faq: FAQ for Section II.H + tos_faq_in_parens_html: "(%{tos_faq_link})" + tos_faq_link_label: Harassment FAQ + illegal_inappropriate_content: + automated_spam_check_html: We may use automated means to filter out spam. If you submit Content that is erroneously caught in a spam filter, please %{contact_ao3_administrators_link}. + conduct_threatening_technical_integrity_html: Conduct that threatens the %{technical_integrity_link} of AO3, for example attempting to hack AO3 or spread viruses through it, is prohibited. Uploading technically misnamed files or Content (such as non-image files with an image file extension used to disguise their actual format) constitutes a threat to the technical integrity of the site. + contact_ao3_administrators: contact AO3 administrators + heading: II.K. Illegal and Inappropriate Content + images_of_real_children: photographic or photorealistic images of real children + no_illegal_content_html: You may not upload Content that appears or purports to contain, link to, or provide instructions for obtaining sexually explicit or suggestive %{images_of_real_children_link}; malware, warez, cracks, hacks, or other executable files and their associated utilities; or trade secrets, restricted technologies, or other classified information. + report_it_to_us: report it to us + spamming_behavior: Spamming behavior is prohibited. Repeated identical or nearly identical posts in multiple places may be considered spam regardless of commercial content. + technical_integrity: technical integrity + tos_faq: FAQ for Section II.K + tos_faq_in_parens_html: "(%{tos_faq_link})" + tos_faq_link_label: Offensive Content vs Illegal Content FAQ + violates_us_law_html: If you encounter Content that you believe violates a specific law of the United States, you can %{report_it_to_us_link}. + impersonation: + function: function + heading: II.G. Impersonation + html: You may not impersonate or misrepresent your affiliation with any person, entity, or %{function_link}. Fiction clearly marked as such is not considered impersonation. + tos_faq: FAQ for Section II.G + tos_faq_in_parens_html: "(%{tos_faq_link})" + tos_faq_link_label: Fannish Identities and Impersonation FAQ + intro: + all_content_must_comply_html: All %{content_link} on AO3 must comply with our Content Policy. + archive_description: The Archive of Our Own (AO3) exists to host transformative, non-commercial works created by fans from all over the world. + content: content + maximum_inclusiveness: maximum inclusiveness of fanwork content + our_goal_html: Our goal is %{maximum_inclusiveness_link}. We want to provide a safe and permanent home for fanworks, including works that might be at risk on other sites due to being deemed immoral, explicit, or otherwise objectionable. Users should always observe and heed the ratings and warnings provided before accessing works on AO3. + report_it_to_us: report it to us + review_before_posting_html: "%{all_content_must_comply_bold} Please review this page carefully before you begin posting. Answers to common questions about the Content Policy can be found in the %{tos_faq_link}." + tos_faq: TOS FAQ + we_do_not_prescreen: We do not prescreen content on AO3. + you_can_report_html: If you encounter content that you believe violates our Content Policy, you can %{report_it_to_us_link}. %{we_do_not_prescreen_bold} + license_html: The AO3 %{terms_of_service_link}, including the %{content_policy_link} and %{privacy_policy_link}, are licensed under the %{cc_attribution_4_0_international_link}. + mandatory_tags: + any_archive_warning: any Archive Warning + ao3_may_designate_html: AO3 software may designate some tag fields (e.g. Rating, Archive Warnings, Fandom, or Language) as mandatory in order to post, import, or edit a Work using the appropriate forms. Tags entered into mandatory fields must meet the %{minimum_criteria_link} described in the TOS FAQ. + applying_nonspecific_tag_html: Applying a non-specific tag (such as "Not Rated", "Creator Chose Not To Use Archive Warnings", or "Unspecified Fandom") is always considered sufficient tagging for that field. A Work that is labeled with a non-specific Rating may contain Content of the highest rating. A Work that is labeled with a non-specific Archive Warning may contain Content pertaining to %{any_archive_warning_link}. + archive_warning: Archive Warning + choose_no_warnings_html: A creator may choose not to apply specific %{rating_link} and/or %{archive_warning_link} tags to their Work, but they must signal such choices by applying AO3's %{non_specific_tags_link} indicating that they have opted out of choosing a specific Rating and/or Archive Warning. + heading: II.J. Mandatory Tags + minimum_criteria: minimum criteria + non_specific_tags: non-specific tag(s) + not_available: not available + rating: Rating + tags_applied_automatically_html: Some tags may be automatically applied to Works. In addition, AO3 administrators may determine that tags in a mandatory tag field on a Work are inaccurate or insufficient. In such cases, AO3 administrators may remove inaccurate tags; add non-specific tags to the Work; add specific tags to the Work in fields where non-specific tags are %{not_available_link}; require the creator to appropriately adjust the Work's tags; hide the Work; or take other appropriate action to address the matter. Refer to the %{tos_faq_link} for details about when AO3 administrators may enforce the presence or removal of tags in mandatory fields. + tos_faq: TOS FAQ + tos_faq_endnote: FAQ for Section II.J + tos_faq_in_parens_html: "(%{tos_faq_link})" + tos_faq_link_label: Ratings and Archive Warnings FAQ + offensive_content: + heading: II.A. Offensive Content + removal_not_just_offensiveness: Unless it violates some other policy, we will not remove Content for offensiveness. + tos_faq: FAQ for Section II.A + tos_faq_in_parens_html: "(%{tos_faq_link})" + tos_faq_link_label: Offensive Content vs Illegal Content FAQ + page_content_landmark: Main Text + page_heading: Content Policy + personal_information: + disclosing_personal_data_html: disclosing personal data, including %{special_categories_of_personal_data_link}. + heading: II.F. Personal Information and Fannish Identities + linking_fannish_identity: linking someone's fannish identity (such as their AO3 username) to their legal or professional identity (such as their "real life" name); + not_allowed: 'You may not upload Content that contains seemingly accurate, non-public information about another individual without authorization. This includes:' + orphaned: Orphaned + revealing_orphaned_creator_html: revealing the identity of the creator of an %{orphaned_link} fanwork; + right_to_hide_delete: We reserve the right to delete, hide, or otherwise make such Content unavailable. + rpf_exception: As Real-Person Fiction (RPF) is fictional, Content in RPF that would normally be deemed personal data (e.g. full names, usernames on social media services, city of residence, or birth date) will not ordinarily be considered as such. + sharing_sufficient_information: sharing information sufficient to identify someone else's legal or professional identity or their physical location (e.g. phone numbers, email addresses, residential addresses, or hotel room numbers); and + special_categories_of_personal_data: Special Categories of Personal Data + tos_faq: FAQ for Section II.F + tos_faq_in_parens_html: "(%{tos_faq_link})" + tos_faq_link_label: Fannish Identities and Impersonation FAQ + plagiarism: + heading: II.E. Plagiarism + html: Plagiarism is the use of someone else's words, or %{their_expressions_of_their_ideas_link}, without attribution. Minor alterations (such as replacing names, substituting synonyms, or rearranging a few words) are insufficient to make a work your own. Plagiarism is not allowed. Deliberately creating a work using the same general idea as another work is not plagiarism, but citation may be appropriate. + their_expressions_of_their_ideas: their expressions of their ideas + tos_faq: FAQ for Section II.E + tos_faq_in_parens_html: "(%{tos_faq_link})" + tos_faq_link_label: Copyright Infringement and Plagiarism FAQ + privacy_policy: Privacy Policy + terms_of_service: Terms of Service + toc: + header: Table of Contents for the Terms of Service (TOS) + intro: The Content Policy is the second part of the AO3 Terms of Service. + user_icons: + heading: II.I. User Icons + text: User icons must be appropriate for general audiences. They must not depict genital nudity, explicit sexual activity, hate symbols, or imagery that promotes, advocates, or causes harm. + tos_faq: FAQ for Section II.I + tos_faq_in_parens_html: "(%{tos_faq_link})" + tos_faq_link_label: Usernames, Icons, and Profiles FAQ + diversity_statement: + archive_description_html: This archive is a permanent, panfandom place for fanworks, built by fans for fans. Whichever way you use the Archive, you're part of this, powering it, shaping it through your use and %{your_feedback_link}. + archive_for_you: 'No matter your appearance, circumstances, configuration or take on the world: if you enjoy consuming, creating or commenting on fanworks, the Archive is for you.' + archive_team: the Archive team + diversity_statement: Diversity Statement + dreamwidth: Dreamwidth's + dreamwidth_remix_html: This is a remix of %{dreamwidth_link} %{diversity_statement_link}. + few_restrictions: few restrictions + license: + creative_commons_by_sa: Creative Commons Attribution-Share Alike 3.0 Unported License + html: This work is licensed under a %{creative_commons_by_sa_link}. + image_alt: Creative Commons License + some_essential_parts: some essential parts + still_missing_html: 'We know that there are %{some_essential_parts_link} that are still missing to make the Archive truly panfandom: the ability to host fanworks other than text, an interface in languages other than English, and more ways for you to connect with each other, to name just a few. But with your support, we''ll get there.' + terms_of_service: Terms of Service + we_build_for: We are building this archive for you. Come be a part of it. + welcome_header: You are welcome at the Archive of Our Own. + what_we_do_html: We, %{archive_team_link}, know that we won't get everything right on the first try, and we won't be able to make everyone equally happy. But we strive to find a good balance, and we promise to respectfully consider your feedback and to take it seriously. + why_we_build: We are building this archive because we believe it's possible for people of all opinions and persuasions to come together and share with each other. + you_can_html: You are free to express your creativity within the %{few_restrictions_link} needed to keep the service viable for other users. The Archive strives to protect your rights to free expression and privacy; you can read about the details in our %{terms_of_service_link}. + your_feedback: your feedback donate: general: text: There are two main ways to support the AO3 - donating your time or money. @@ -675,6 +877,93 @@ en: volunteer_listings: volunteer position listings fandoms: all_fandoms: All Fandoms + first_login_help: + additional_info: + header: Additional Browsing Information + history_mark_later: + header: History and Mark for Later + history: History + history_faq: History and Mark for Later FAQ + html: You can access and manage your history by going to %{your_dashboard_link} and selecting the "%{history_link}" link in the side menu in the default browser skin or on top of the page in mobile devices. You can clear your history, or delete individual entries if you don't want these entries to show up in your history. You can also add works to the "Marked for Later" list in your history. For more information on History, please visit the %{history_faq_link}. + your_dashboard: your Dashboard + subscriptions: + header: Subscriptions + html: You can subscribe to a work, collection, series of works, or user by selecting the "Subscribe" link at the top or bottom of a work, or at the top or bottom of a collection or series page, or the user's profile page. You can check your subscriptions from %{your_dashboard_link} by selecting the "Subscriptions" link. For more information on Subscriptions, please visit the %{subscriptions_feed_faq_link}. + subscriptions_feed_faq: Subscriptions and Feeds FAQ + your_dashboard: your Dashboard + bookmarking_works: + bookmarks_faq: Bookmarks FAQ + header: Bookmarking Works + html: You can bookmark works on the Archive as well as works hosted on other sites. You can choose to make your bookmarks public or private. You can also mark a public bookmark as a rec (recommendation). Additionally, you can add notes and tags to the bookmark, and/or add it to a collection. For more instructions on managing your Bookmarks, please visit the %{bookmarks_faq_link}. + browsing: + all_fandoms: All Fandoms + fandoms: Fandoms + header: Browsing + html: You can start browsing works by going to the "%{fandoms_link}" tab at the top of any Archive page in the default skin and selecting either "%{all_fandoms_link}" or one of the subsets such as "%{movies_link}". Alternatively, you can use the %{search_feature_link} to look for specific fandoms, works, or users. You can use the "Sort and Filter" form to narrow down the results in any fandom page. For more instructions, please visit %{search_browse_faq_link} and the %{search_browse_tutorial_link}. + movies: Movies + search_browse_faq: Search and Browse FAQ + search_browse_tutorial: Searching and browsing tutorial + search_feature: search feature + editing_profile: + edit_my_profile: Edit My Profile + header: Editing Your Profile, Password, and Preferences + html: To add your info, go to %{your_dashboard_link}, select the "%{profile_link}" tab, and select "%{edit_my_profile_link}" from the range of options at the end of the page. Here, you can enter some basic personal information. It's also where to go to change your password. Refer to the %{profile_faq_link} for more information. + profile: Profile + profile_faq: Profile FAQ + your_dashboard: your Dashboard + logging_in_out: + forgot_password: forgot your password + header: Logging In and Logging Out + html: To log in, locate the login link and fill in your Username and Password. The link will be located at the top right of the screen and as indicated by your screenreader if you're using one. If you forget your password, then select "%{forgot_password_link}". To log out, select "Log Out" on the top right corner in the default browser skin. + posting_works: + header: Posting Works + html: To open the Post New Work page, just select the %{post_new_link} link from the menu of the Post tab located at the top right of the page in the default browser skin. Visit the %{posting_editing_faq_link} or check out our %{tutorial_link} for more information. + post_new: Post New + posting_editing_faq: Posting and Editing FAQ + tutorial: 'Tutorial: Posting a Work on AO3' + preferences: + edit_my_profile: Edit My Profile + header: Preferences + html: '"%{set_my_preferences_link}" is located next to the "%{edit_my_profile_link}" button, and allows you to adjust certain settings to personalize your experience on the site. The Preferences area controls both the Archive''s behavior (such as whether your history is saved) and the Archive''s appearance. Go to the %{preferences_faq_link} for more details.' + preferences_faq: Preferences FAQ + set_my_preferences: Set My Preferences + skins_detail_html: For more information on how to customize the skins and interface of the Archive, refer to the %{skins_faq_link} and %{tutorials_list_link}. + skins_faq: Skins and Archive Interface FAQ + tutorials_list: List of Tutorials + pseuds: + header: Pseuds + html: Pseuds are like pen names linked to your account. You can use different pseuds to post your works under the appropriate name while still managing them through the same account. You can %{manage_your_pseuds_link} through your profile page. For more information, please visit the %{pseuds_faq_link}. + manage_your_pseuds: manage your pseuds + pseuds_faq: Pseuds FAQ + support_and_feedback: + archive_faq: Archive FAQ + contact_support: contact Support + header: Support and Feedback + html: Some frequently asked questions about the Archive are answered in the broader %{archive_faq_link}. You may also like to check out our %{known_issues_link}. If you need more help, please %{contact_support_link}. If you want to know more about some user-created tools that work with the Archive, please visit the %{unofficial_tools_faq_link}. + known_issues: Known Issues + unofficial_tools_faq: Unofficial Browser Tools FAQ + table_of_contents: Table of Contents + tags: + header: Tags + html: All tags on the Archive—including those for fandoms, relationships, and characters—start out user-created. You can always use the existing tags by selecting from the autocomplete list. If you cannot find the tags you want to use, feel free to create new tags for your works. Behind the scenes, our tag wrangling team will match the tags up with their synonyms, so that people can find your work whether you tag it "AMTDI", "Aliens Made Them Do It", or "Sex Pollen". Refer to the %{tags_faq_link} to learn more. + tags_faq: Tags FAQ + tips_to_start: Here are some tips to help you get started. + tos: + additional_questions_html: If you have additional questions that are not covered here, please %{contact_abuse_link}. + contact_abuse: contact our Policy & Abuse team + content_policy: Content Policy + header: Terms of Service + info_html: You can learn about our policies and procedures by reviewing the %{tos_link}, including the %{content_policy_link} and %{privacy_policy_link}. Answers to common questions are available in the %{tos_faq_link}. + privacy_policy: Privacy Policy + tos: Terms of Service + tos_faq: Terms of Service FAQ + warnings: + archive_specific_warnings: Archive-specific warnings + description_html: 'The Archive defines four "primary" %{archive_specific_warnings_link}: "Graphic Depictions Of Violence", "Major Character Death", "Rape/Non-Con", and "Underage Sex". When posting a work, you have the option to explicitly select warnings for this content, deny the presence of such content ("No Archive Warnings Apply"), or choose not to apply warnings regardless of whether or not these warnings are applicable ("Creator Chose Not To Use Archive Warnings"). Remember, "No Archive Warnings Apply" only refers to the listed primary warnings.' + header: Warnings + symbols_html: When browsing the Archive, warnings will be displayed in the blurb of each work. Official warning tags are displayed in bold. A four square grid in the top left corner of each work's blurb indicates the work's rating, completion status, pairing category, and any Archive warnings that apply to it. Refer to the %{symbols_key_chart_link} for more information. + symbols_key_chart: Symbols Key Chart + welcome_header: Welcome to the %{app_name}! index: browse_or_favorite: one: Browse fandoms by media or favorite up to %{count} tag to have it listed here! @@ -692,13 +981,425 @@ en: other_outlets: Organization for Transformative Works' news outlets tumblr: ao3org on Tumblr twitter: "@AO3_Status on Twitter" - tos_faq: - fnok: - support: Support team + privacy: + account_termination: + backup_copies_html: '"Active records" do not include the backup copies of Content created for legal and/or disaster recovery purposes (refer to the %{general_principles_link}).' + deletion_after_termination_html: If for any reason you %{terminate_your_account_link}, as soon as reasonably possible we will destroy active records containing your Personal Information that are visible to the public or to AO3 users, with the exception of Personal Information entered as Content that you have not deleted. "Reasonably" here means no more than thirty business days from the termination of the account. + general_principles: General Principles + heading: III.G. Termination of account + legal_enforcement_retention: We will retain some Personal Information accessible to AO3 administrators for legal and TOS enforcement purposes. For example, if we terminate your service or suspend your account, we may retain enough Personal Information to prevent you from creating an account or using AO3 in the future. + orphan: Orphan + orphans_excluded_html: If you choose to %{orphan_link} your works during the account deletion process, and choose to keep your %{pseud_link} associated with the works when Orphaning them, then your pseud will remain visible on the works. + pseud: pseud + terminate_your_account: terminate your account with us + aggregate_anonymous_info: + anonymous_non_personal: Nothing in this Privacy Policy restricts our use of anonymous information or other information or data that does not qualify as "Personal Information". + heading: III.D. Aggregate and anonymous information + understand_ao3_usage: We may use Personal Information in the aggregate to understand how our users use AO3. This is necessary for us to carry out our legitimate interests as a host of Content and a non-profit entity, including managing, maintaining, and ensuring the security of AO3 and our servers. Note that de-identified or aggregate information that is not linked with your other Personal Information, and does not otherwise identify you, will not be treated as Personal Information. + applicability: + consent_to_us_processing: By agreeing to this Privacy Policy, you consent to the processing of your Personal Information in the United States and in other jurisdictions in connection with our provision of AO3 and its related services to you. + global_subprocessors_html: While the OTW is a United States 501(c)(3) corporation, our users are global. Your use of AO3 may result in the transfer of Personal Information across international boundaries. For example, Personal Information collected within the European Economic Area (EEA) and Switzerland may be transferred to and processed in a country outside of the EEA and Switzerland, or by third-party %{subprocessors_link} located outside of the EEA and Switzerland, where you may have different legal rights in relation to your Personal Information. + heading: III.A. Applicability + policy_covers: This Privacy Policy covers the treatment of Personal Information submitted to AO3 or which we collect when you use our services. Except as otherwise stated in these Terms of Service, "Personal Information" (as used in this Privacy Policy) means information related to you that qualifies as "personal data" or "personal information" under the data privacy laws applicable to the Organization for Transformative Works (OTW). Changes to this Privacy Policy will only apply to the Personal Information that is processed by AO3 after the effective date of those changes. If you are concerned about how your Personal Information is used, you should review this Privacy Policy periodically. + subprocessors: Subprocessors + transfers_necessary_html: We make such transfers to the United States and the jurisdictions of our service providers with your consent or because it is necessary for the performance of a contract with you. %{consent_to_us_processing_bold} + cc_attribution_4_0_international: Creative Commons Attribution 4.0 International License + contact_us: + contact_pac: contact the Policy & Abuse committee + heading: III.I. Contact us + html: If you have any questions, concerns, or complaints about this Privacy Policy, or would like to submit a data request, please %{contact_pac_link}. + content_policy: Content Policy + effective: 'Effective: November 19, 2024' + information_scope: + collect_through_use: We may collect your Personal Information (such as your IP address and email address) when you request an AO3 invitation, register for an AO3 account, or otherwise access or use AO3. + content: Content + heading: III.B. Scope of Personal Information we process + information_in_content_html: Any Personal Information you include in your %{content_link} (including %{special_categories_link} and other types of Personal Information) may be accessible by AO3 administrators. It may also be accessible by the general public if the Content is made public, or by AO3 users if the Content is made available to AO3 users. + special_categories: Special Categories of Personal Data + intro: + answers_common_questions_html: Answers to common questions about the Privacy Policy can be found in the %{tos_faq_link}. + ao3_exists_to_host_html: 'AO3 exists to host content by and for fans from all over the world. To do so, we process certain data and information, including %{personal_information_link}. This is so that we can:' + archive_description: The Archive of Our Own (AO3) is a project of the Organization for Transformative Works (OTW), which is committed to fan privacy. + details_how_and_why_html: This Privacy Policy details how and why we collect and process this information. %{common_questions_bold} + enable_post_information: Enable you to post comments, kudos, bookmarks, and other information designed to be seen by other users + host_your_fanworks: Host or share your fanworks + personal_information: Personal Information + show_you_works: Show you works by other people + tos_faq: TOS FAQ + license_html: The AO3 %{terms_of_service_link}, including the %{content_policy_link} and %{privacy_policy_link}, are licensed under the %{cc_attribution_4_0_international_link}. + page_content_landmark: Main Text + page_heading: Privacy Policy + privacy_policy: Privacy Policy + privacy_policy_heading: III. Privacy Policy + retention_of_information: + heading: III.H. Retention of Personal Information + text: We will retain your Personal Information for no longer than reasonably necessary and proportionate to fulfill the purposes for which we originally collected or processed it, as set out in this Privacy Policy or as required by law. Once the purpose(s) for which the Personal Information was collected is/are no longer applicable, we will delete or otherwise dispose of that Personal Information. + terms_of_service: Terms of Service + third_parties: + do_not_sell_information: We will not sell, trade, or rent your Personal Information. We will not use your Personal Information to market third-party products and services to you. This means that we do not collect or use your Personal Information for targeted advertising, nor do we process Personal Information for automated decision-making purposes. If we begin to do so, that change will be reflected in this Privacy Policy, and you will have the right to restrict the use of that Personal Information accordingly. + heading: III.F. Information shared with third parties + sharing_exceptions: + challenge_signup: + challenge: Challenge + heading_html: 'If you sign up for a %{challenge_link}:' + text: We may share your email address with the maintainers of the Challenge. + external_processing: + heading: 'For external processing:' + html: We may use third-party services to store, process, or transmit data, or to perform other technical functions related to operating AO3. For example, we may use third-party email services. A list of third-party services is provided in our %{subprocessor_list_link}. We cannot guarantee other services' technical performance. We or the services we use may store or process your Personal Information in data centers which may be located in the United States or elsewhere. + subprocessor_list: Subprocessor List + handle_complaints: + dmca_notice: Digital Millennium Copyright Act (DMCA) notice + heading: 'To handle complaints submitted by you about TOS violations:' + html: If we receive a %{dmca_notice_link}, we reserve the right to disclose the information provided by the copyright owner or their legal representative to the subject of the complaint. If we receive a complaint from you about other matters, that information will be subject to the %{pac_confidentiality_policy_link}. + pac_confidentiality_policy: Policy & Abuse Confidentiality Policy + intro: 'We will not share your Personal Information with any third parties without your prior consent, except for the following cases:' + legal_reasons: + attempt_to_notify: We will attempt to notify you any time we disclose your Personal Information to a third party for legal reasons, unless legally prohibited from doing so or if, in our sole judgment, notification might hinder an ongoing investigation. In some cases, the Personal Information we have, such as an IP address, may be insufficient for us to notify you. + cooperating_law_enforcement: are cooperating with law enforcement authorities. + good_faith_comply: have a good-faith belief that such action is necessary to comply with a current judicial proceeding, court order, or legal process served on the OTW; or + heading: 'For legal reasons:' + intro: 'We may share Personal Information if we:' + law_enforcement_cooperation_details: We will cooperate with all investigations conducted by law enforcement authorities within the United States when legally required to do so. Cooperation with law enforcement authorities from other countries and cooperation when it is not legally required are at our sole discretion. Our discretion looks favorably on freedom and justice, and unfavorably on oppression and violence. + legally_compelled: are legally compelled to do so; + open_doors_import: + heading_html: 'If your work is imported via %{open_doors_link}:' + open_doors: Open Doors + text: If the email address associated with your AO3 account matches an email address that you used in connection with a non-AO3 archive that is being imported to AO3 via Open Doors, then we may share your email address with the entity that manages that imported archive. + third_party_tools: Third parties have developed applications, software, scripts, browser extensions, or other tools for use in connection with AO3. Such third parties may receive Personal Information from you if you use their tools to access AO3. This Privacy Policy does not cover your use of such tools. + toc: + header: Table of Contents for the Terms of Service (TOS) + intro: The Privacy Policy is the third part of the AO3 Terms of Service. + types_of_information: + cookies: + heading: 'Cookies:' + text: We and our Subprocessors use cookies to collect and store visitors' preferences; customize web pages' content based on visitors' preferences or other Personal Information that the visitor sends; prevent attacks on our servers; and record activity at AO3 in order to provide better service when visitors return to our site. AO3 has no access to cookies set by other sites. If you block or disable cookies, you may be unable to access or use AO3. + emails: + address_usage_html: We will use your email address internally for the purposes of managing AO3 and maintaining site integrity. We may occasionally send emails to you about AO3, your Content and account, or news that we reasonably believe to be of interest to our registered users. We will also send you your invitation to register for AO3 via email. By creating and maintaining an account on AO3, you consent to receiving such emails. We reserve the right to send you notice of complaints raised against you, or alleged violations of the TOS by you, as well as to reply to any email message you send to AO3 and/or its personnel. If you choose to participate in a %{challenge_link}, the Challenge maintainer(s) may have access to your email address for the purposes of communicating with you. + challenge: Challenge + collect_process_retain: We collect, process, and retain the email addresses of and from those who communicate with us via email, and any Content or Personal Information included in emails to us. We need this Personal Information so we can respond to you; so we can handle complaints about AO3 and any users who may have violated the TOS or other policies; and for other legal and accounting/audit reasons, including maintaining the integrity of AO3 and the Content that we host. + heading: 'Emails:' + unsubscribe: Although it is possible to unsubscribe from some emails (such as kudos updates and responses to comments), you cannot unsubscribe from receiving emails that we, in our sole discretion, deem necessary for legal purposes or to maintain the safety and integrity of your account or our services, including other OTW sites. + fnok: + heading: 'Fannish next-of-kin:' + text: We collect the contact information you and/or your fannish next-of-kin provide, and use it only to implement the fannish next-of-kin function. + heading: III.C. Types of Personal Information we collect and process + ip_addresses: + heading: 'IP addresses:' + text: We and our Subprocessors collect, process, and retain the IP addresses of AO3 visitors, including registered users. IP information may also be collected by our servers for logging purposes and used for limited technical assessments of AO3. We need this Personal Information so we can provide you with the Content that you are requesting; to allow you to submit Content to us; and for other legal, TOS enforcement, and accounting/audit reasons, including maintaining the integrity of AO3 and the Content that we host. + logs: + heading: 'Logs:' + text: We and our Subprocessors collect, process, and retain logs of server interactions. We need this Personal Information for legal, TOS enforcement, and accounting/audit reasons, including maintaining the integrity of AO3 and the Content that we host. + other_information: + heading: 'Other user-specific information:' + to_maintain_integrity: We and our Subprocessors collect, process, and retain Personal Information about what pages you access or visit, including your interactions with integral AO3 features such as comments, kudos, and bookmarks; referral information (i.e. data about what site you are coming to AO3 from); and whether there are errors in displaying Content to you. We need this Personal Information to maintain the integrity of AO3 and the Content that we host; to provide you with the Content that you are seeking; to prevent spam and abuse; and for other legal and accounting/audit reasons. + to_make_content_available_html: We and our Subprocessors collect, process, and retain Personal Information about Content that you submit to us. You consent to the collection of your Personal Information (such as your IP address) when you access AO3, including when you are not logged in. We use this Personal Information to make the Content you submit available to people who use AO3. By submitting Content to us, you are requesting that we make it available to those people and consenting to the use of your Content. For explanations of site features that use your Content and/or Personal Information and how they are used, please refer to the %{tos_faq_link}. + tos_faq: TOS FAQ + your_rights: + applicable_jurisdiction: applicable jurisdiction + heading: III.E. Your rights under applicable data privacy laws + other_rights: other rights regarding your Personal Information + potential_other_rights_html: You may have %{other_rights_link} under the laws of the jurisdiction in which you are located. We will not discriminate against you for making a request under the data privacy laws of applicable jurisdictions. + request_data_html: You can request that the OTW assemble the data that AO3 has about you, and provide you with a copy in electronic format. We agree to provide such data to you within a reasonable time if you are a resident or citizen of the European Union or another %{applicable_jurisdiction_link}. + require_user_specific_proof: We reserve the right to require user-specific proof of identity before providing Personal Information to a requester. This is necessary to protect the safety and privacy of our users and personnel. + site_map: + about: + ao3_news: AO3 News + archive_faq: Archive FAQ + content_policy: Content Policy + header: About the Archive of Our Own + known_issues: Known Issues + privacy_policy: Privacy Policy + project_of_otw_html: The Archive of Our Own is a project of the %{otw_link} + terms_of_service: Terms of Service + tos_faq: Terms of Service FAQ + access_your_account: + drafts: Drafts + header: Access your account + my_bookmarks: My Bookmarks + my_collections_and_challenges: My Collections and Challenges + my_history: My History + my_home: My Home + my_inbox: My Inbox + my_series: My Series + my_subscriptions: My Subscriptions + my_works: My Works + post_new: Post New + set_my_preferences: Set My Preferences + change_your_account_settings: + delete_account_confirmation: This will permanently delete your account and cannot be undone. Are you sure? + delete_my_account: Delete My Account + edit_my_profile: Edit My Profile + header: Change your account settings + manage_my_pseuds: Manage My Pseuds + my_profile: My Profile + contact_us: + donations: Donations + header: Contact Us + policy_questions_and_abuse_reports: Policy Questions & Abuse Reports + technical_support_and_feedback: Technical Support & Feedback + explore: + additional_tags_cloud: Additional Tags Cloud + bookmarks: Bookmarks + collections_and_challenges: Collections and Challenges + fandoms: Fandoms + header: Explore + homepage: Homepage + languages: Languages + people: People + recent_works: Recent Works + otw: + abbreviated: OTW + full: Organization for Transformative Works + page_heading: Site Map + tos: + abuse_policy: + answers_common_questions_html: Answers to common questions about the Abuse Policy can be found in the %{tos_faq_link}. + appeals: + appeal_decision: appeal a decision to the Policy & Abuse committee + heading: Appeals + html: The complainant or the subject of a complaint may %{appeal_decision_link}, in which case it will be reviewed by an additional PAC administrator. The original decision will remain in effect while an appeal is being reviewed. We will attempt to resolve appeals as speedily as possible, but we cannot guarantee any particular timeframe for a response. PAC's decisions are final. + content_policy: Content Policy + heading: I.I. Abuse Policy + no_prescreen_html: We do not prescreen Content for violations of the Terms of Service, including violations of the %{content_policy_link}. Complaints are investigated only when they are submitted through the appropriate channels and with the appropriate information. + penalties: + age_barred_individual: Age-Barred Individual + content_policy_ii_k_1: Subsection II.K.1 of the Content Policy + edit_post_while_suspended: A suspended user may not post or edit Content or create an account while they are suspended. A user who was suspended solely due to age may post Content or create an account once they are no longer an Age-Barred Individual. + heading: Penalties + illegal_inappropriate_content_policy: Illegal and Inappropriate Content Policy + non_violating_content_html: A suspended user's non-violating Content will not be automatically removed unless the user is an Age-Barred Individual or has violated the %{illegal_inappropriate_content_policy_link}. Suspended users retain the right to delete or Orphan their fanworks by contacting AO3 administrators. + open_doors_removal_html: The removal of Content imported via Open Doors will not usually result in a penalty for the archivist or creator, except for violations of %{content_policy_ii_k_1_link}. + remove_resolve_lawsuit_html: We may determine that we need to remove Content to resolve a threatened or pending lawsuit or to mitigate other liability. If so, we will remove the Content. Unless said Content was submitted by an %{age_barred_individual_link} or otherwise violates the TOS, removal for such reasons will not lead to a suspension. + tos_faq: TOS FAQ + violations_warnings_suspensions_html: Violations of the TOS may result in warnings or suspensions ("penalties") for all accounts owned by the offending user. The determination of appropriate penalties, including the length of any suspensions, is subject to the discretion of the Policy & Abuse committee. PAC's discretion will be informed by the nature of the violation and the behavior of the user. For more information, please refer to the %{tos_faq_link}. + resolution_of_complaints: + add_or_edit_tags_html: AO3 administrators may also determine that tags need to be added to or edited on a Work, and may add or edit those tags. For more information, please refer to the %{mandatory_tags_policy_link}. + administrators_determine_content_removal_html: When AO3 administrators %{determine_removal_link}, it may be hidden from public view or removed from AO3. If the Content is a Work (defined as Content created via the New Work or Import Work forms, or imported via Open Doors) and the creator provided valid contact information, we will inform the creator as soon as possible. + determine_removal: determine that Content needs to be removed + heading: Resolution of complaints + illegal_and_inappropriate_content_policy: Illegal and Inappropriate Content Policy + immediate_removal: Non-Work Content, or any Content without valid contact information, that violates the TOS may be immediately removed. + mandatory_tags_policy: Mandatory Tags Policy + potentially_legitimate_fanwork_html: If the Content that needs to be removed is a Work that includes potentially legitimate fanwork, the default will be to hide the Work. The Policy & Abuse committee has sole discretion to determine necessary exceptions to this default rule, which may include repeated violations of the TOS, any violations of the %{illegal_and_inappropriate_content_policy_link}, or any circumstances where the OTW reasonably believes that the Content is unlawful, including when it contains a true threat. + voluntary_removal: When Content (whether a Work or otherwise) is hidden from public view, AO3 administrators may identify the nature of the problem with the Content and set a deadline for voluntary removal of the violating material. If the creator does not remove that material within the given deadline, AO3 administrators may permanently remove the Content. At the discretion of the Policy & Abuse committee, the creator may have the option to resubmit any non-violating material that was also removed. + submitting_a_complaint: + dmca_policy: DMCA Policy + heading: Submitting a complaint + html: Complaints may be submitted to the Policy & Abuse committee (PAC) via the %{policy_and_abuse_form_link}, except for Digital Millennium Copyright Act (DMCA) notices filed by the copyright owner or their legal representative. DMCA notices must be submitted to the Legal committee and are not governed by the Abuse Policy. Refer to the %{dmca_policy_link} for more information. + policy_and_abuse_form: Policy & Abuse reporting form + tos_faq: TOS FAQ + treatment_of_complaints: + heading: Treatment of complaints + html: Complaints are covered by the %{privacy_policy_link} and processed by PAC in accordance with the %{pac_confidentiality_policy_link}. Responses to complaints will be provided at the discretion of the OTW, in accordance with the Privacy Policy and other applicable terms of these TOS. + pac_confidentiality_policy: Policy & Abuse Confidentiality Policy + privacy_policy: Privacy Policy + age_policy: + addressing_violations: If AO3 administrators determine that an Age-Barred Individual has created an account or uploaded Content, they may suspend or delete the account, hide or delete the Content, or take other appropriate action to address the matter. + age_barred_not_permitted_html: Age-Barred Individuals are %{not_permitted_account_upload_link} to AO3. By submitting Content to AO3, you confirm that you are not an Age-Barred Individual. + ask_parent_to_upload: If you are a child under the age of thirteen (13) and not a resident or citizen of the European Union, you may ask your parent or legal guardian to upload your Content through their account. + country_disallowing_childrens_data: any country (including a European Economic Area country) which does not allow the processing of personal data from children of that age without written permission from a parent or legal guardian. + eu_country_html: a European Union country where the processing of %{special_categories_of_personal_data_link} from children of that age requires the consent of a parent or legal guardian, or + heading: I.H. Age Policy + individuals_under_13: individuals under the age of thirteen (13), and + individuals_under_16: 'individuals under the age of sixteen (16) who are not old enough, in their country of residence or citizenship, to consent to the processing of personal data. This includes individuals under the age of sixteen (16) who are residents or citizens of:' + intro: 'AO3 does not knowingly solicit or collect information from Age-Barred Individuals. Age-Barred Individuals are:' + not_permitted_account_upload: not permitted to have an account or upload Content + special_categories_of_personal_data: Special Categories of Personal Data + archive_description: The Archive of Our Own (AO3) is a home for fanworks, including fanfiction based on books, movies, TV, comics, other media, and real-person fiction (RPF). + cc_attribution_4_0_international: Creative Commons Attribution 4.0 International License + content_policy: Content Policy + content_you_access: + content_policy: Content Policy + external_links_html: The OTW or users of its services may provide links to or content via sites that are owned or controlled by third parties, and may use such sites (including those identified %{here_link}) to communicate information about the OTW and its sites. If you follow links away from AO3, you should review those sites' terms and privacy policies, which may differ from AO3's. The OTW has no control over any third-party sites or their terms of use or privacy policies, and you agree that the OTW is not responsible for and does not endorse their content, terms, or availability. + heading: I.D. Content you access when using AO3 + here: here + hosted_by_third_party: embedded from and/or hosted by third-party sites + no_otw_endorsement_html: You recognize that the OTW does not endorse Content on AO3 in any way, except when such material appears as an %{official_statement_link} of the OTW. No members, volunteers, administrators, officers, or directors of the OTW are acting in an official capacity when they post fanworks, commentary, or other Content of the type generally provided by site users. + no_prescreen: You understand that the OTW does not prescreen Content or review it for purposes of compliance with the TOS. This includes (but is not limited to) a work's text, graphics, tags, comments, or any other material. Content, including User-Embedded Content, is the sole responsibility of the submitter. You understand that using AO3 may expose you to material that is offensive, atrocious, immoral, obscene, triggering, blasphemous, bigoted, erroneous, or objectionable in other ways. + official_statement: official statement + otw_not_liable: The OTW is not liable to you for any Content to which you are exposed on or because of AO3. + privacy_policy: Privacy Policy + third_party_content_html: Some of the Content displayed on or accessible via AO3 is not hosted on AO3 or by the OTW. Such content can include text, image, audio, or video files %{hosted_by_third_party_link} ("User-Embedded Content"). If you access a page that includes User-Embedded Content, the embedded file may share data with the hosted site as if you were on or at the hosted site. Although User-Embedded Content must comply with our %{content_policy_link}, it is not otherwise governed by these Terms of Service (including the AO3 %{privacy_policy_link}), and is instead governed by the terms of use or privacy policy of the service that hosts it. We reserve the right to provide an indicator to users that User-Embedded Content is present on or visible via your Content. + effective: 'Effective: November 19, 2024' + general_principles_heading: I. General Principles + general_terms: + agreement: + agreement: 'Agreement:' + any_other_form: any other form of content + content_policy: Content Policy + html: '%{agreement} AO3 hosts and shares content created by fans, for fans. Our %{content_policy_link} and %{privacy_policy_link} are part of these Terms of Service (TOS). By submitting a work; bookmark; comment; tag; link; text, image, audio, or video file; username; fannish next-of-kin; %{personal_information_link} (such as an email address); or %{any_other_form_link} ("Content") to the Archive of Our Own service (hereinafter "Service", "AO3", or "Archive"), or by creating an account and/or accessing Content on AO3, you affirm, confirm, and state that you comply with and assent to the TOS.' + personal_information: Personal Information + privacy_policy: Privacy Policy + entirety_of_agreement: + entirety_of_agreement: 'Entirety of agreement:' + html: "%{entirety_of_agreement} These Terms of Service constitute the entire agreement between you and the Organization for Transformative Works (OTW) and govern your use of AO3. They replace all prior agreements between you and the OTW concerning your use of AO3. They do not govern your use of any other OTW sites and/or projects, all of which are covered under separate agreements." + heading: I.A. General terms + jurisdiction: + html: "%{jurisdiction} The AO3 TOS, the relationship between you and the OTW, and all disputes arising out of or related to them shall be governed by the laws of the United States and specifically %{the_state_of_new_york_link}, without regard to any conflict-of-law provisions. You and the OTW agree to submit to the personal and exclusive jurisdiction of the courts located within New York County (Manhattan), New York, and to waive any objection to the laying of venue there." + jurisdiction: 'Jurisdiction:' + the_state_of_new_york: the State of New York + limitation_on_claims: + html: "%{limitation_on_claims} You agree that, regardless of any statute or law to the contrary, any claim or cause of action arising out of or related to any use of AO3 or its TOS must be filed within one (1) year after such claim or cause of action arose or be forever barred." + limitation_on_claims: 'Limitation on claims:' + no_assignment: + html: "%{no_assignment} These TOS are personal to you. You may not assign or transfer your rights or obligations under these TOS to any other person or entity, and any attempted assignment or transfer is void." + no_assignment: 'No assignment:' + non_severability: + html: "%{non_severability} The OTW's failure to enforce any part of the TOS will not waive the OTW's ability to enforce it. Any waiver with regard to a specific instance shall not constitute a waiver of any other breaches of the TOS, even with regard to the same user. If any provision of the TOS is found by a court of competent jurisdiction to be invalid, you agree that the court should give effect to the party's intentions as reflected in the provision, and that all other provisions of the TOS remain in full force and effect." + non_severability: 'Non-severability:' + license_html: The AO3 %{terms_of_service_link}, including the %{content_policy_link} and %{privacy_policy_link}, are licensed under the %{cc_attribution_4_0_international_link}. + page_content_landmark: Main Text + page_heading: Terms of Service + potential_problems: + account_termination_liability: You agree that the OTW shall not be liable to you or any third party for any limitations on, or termination of, your access to AO3. The OTW may change, put on hiatus, restrict or prohibit access to, or end AO3 or parts of its services at any time. + breach_notification: If we learn of a breach which compromises the security or confidentiality of the Personal Information of AO3 users, we will notify affected users and relevant supervisory authorities as required by law as soon as practicable. + content_access_liability: You agree that the OTW shall not be liable to you for any claim arising out of Content you make available, your connection to AO3, your use of AO3 or its TOS, or your violation of anyone else's rights. In other words, the OTW is not liable to you for allowing you to post, access, or download Content, or use or interact with AO3. The OTW does not assume whatever legal risks you face by posting, accessing, or doing other things with Content. + damage_liability: You expressly agree that, to the maximum extent permitted by law, the OTW shall not be liable to you for any damages of any kind (even if the OTW has been advised of the possibility of such damages) resulting from AO3, including but not limited to your use of or inability to use AO3; unauthorized access to or changes in Content or information you submit; and the actions and statements of third parties who use AO3. + disclaim_warranties_html: The OTW expressly disclaims all warranties of any kind, whether express or implied, including (but not limited to) the implied warranties of %{merchantability_link}, %{fitness_for_purpose_link}, and non-infringement. The TOS govern your use of AO3, and therefore no communication from anyone associated with the OTW will create any warranty that isn't expressly stated in the TOS. + fitness_for_purpose: fitness for a particular purpose + heading: I.C. Potential problems with AO3 + merchantability: merchantability + not_personal_storage_html: AO3 is not intended to be used as a personal storage or file recovery service. %{sole_backup_responsibility} + own_risk: Any material you access through AO3 in any way is at your own risk. You will be solely responsible for any damage or loss of data that results from accessing any such material. + service_as_is: The OTW provides services, including AO3, on an "as is" and "as available" basis. The OTW does not warrant (that is, does not make a legally binding promise) that our services will meet your requirements; that our services will be uninterrupted, timely, secure, or error-free; or that the results you get from using our services will be accurate, reliable, or satisfactory to you. + sole_backup_responsibility: You are solely responsible for backing up any Content that you submit to AO3. The OTW will not be liable for any lost or unrecoverable Content. + privacy_policy: Privacy Policy + registration_and_email_addresses: + agree_current_address_html: As part of your registration, you agree to provide an accurate and current email address and to update the address as necessary. If your email address is inaccurate or not current, we may %{suspend_your_account_link}. + email_is_yours_html: By registering or otherwise using an email address in connection with AO3, you assert that the email address is yours and that we may %{lawfully_communicate_with_you_link} as otherwise provided in the TOS. + heading: I.G. Registration and email addresses + lawfully_communicate_with_you: lawfully communicate with you + suspend_your_account: suspend your account + terms_of_service: Terms of Service + toc: + header: Table of Contents for the Terms of Service (TOS) + intro: There are three parts to the AO3 Terms of Service. The General Principles are the first part of the TOS. + updates_to_the_tos: + content_policy: Content Policy + heading: I.B. Updates to the Terms of Service + html: 'You can learn about changes to the TOS by visiting AO3. The TOS, including the %{content_policy_link} and %{privacy_policy_link}, may be modified at any time through the following process: Proposed changes will first be prominently disclosed on AO3 for a public comment period lasting at least two weeks. After the end of the comment period, proposed changes will be voted on by the OTW Board. If the OTW Board votes in favor, the changes will become effective when posted to the relevant Terms page(s). This is the only means by which the TOS may be altered. The TOS cannot be changed by emails or other communications with you.' + privacy_policy: Privacy Policy + what_we_believe: + ao3_run_by_html: AO3 is run by the Organization for Transformative Works (OTW). We are committed to %{defending_fanworks_link}. We have legal resources and alliances on which we can draw. However, that is not a guarantee that the OTW can or will fight each battle. The OTW Board will take into account a variety of factors, both legal and otherwise, when responding to a legal challenge. More information is available %{on_the_otw_site_link}. + defending_fanworks: defending fanworks against legal challenges + faq: + abbreviated: FAQ + full: Frequently Asked Questions + header: What We Believe + maximum_inclusiveness: maximum inclusiveness of fanwork content + on_the_otw_site: on the OTW site + our_goal_html: Our goal is %{maximum_inclusiveness_link}. + readability_html: We strive to make our Terms of Service (TOS) readable. We have provided explanations for the more unusual legal terms, and answers to common questions are available in the %{tos_faq_link}. + tos_faq_html: TOS %{faq_abbreviation} + we_do_not_sell_html: We do not sell any data that you post on, submit to, or share on OTW sites (%{ao3_link}, %{fanlore_link}, and %{transformativeworks_link}), and we do not include or accept paid advertisements from third parties. + what_we_do_with_content: + agree_otw_can_copy_html: You agree that we can make those copies and show your Content to other people. Specifically, by submitting Content, you grant the OTW a %{worldwide_royalty_free_nonexclusive_license_link} to make your Content available. "Making available" includes distributing, reproducing, performing, displaying, compiling, and modifying or adapting. %{modifying_or_adapting_link} here refers strictly to how your work is displayed, not how it is written, drawn, or otherwise created. User-provided tags may be modified or organized, which is a process we call %{tag_wrangling_link}. + challenge: Challenge + content_not_completely_controlled_html: You may provide Content to a part of AO3 that you do not completely control. For example, you may decide to %{orphan_link} a work; participate in a %{challenge_link}; comment on someone else's work; or comment on an official AO3 or OTW post, which may be %{subject_to_moderation_link}. Where this is the case, by submitting Content to those parts of AO3, you agree to the %{rules_for_removing_such_content_link} on those parts of AO3. + heading: I.E. What we do with Content + license_duration: Subject to the next paragraph of this policy, this license exists only for as long as you choose to continue to include such Content on AO3. It will terminate within a reasonable time after you remove (or the OTW removes) such Content from AO3. We will always strive to make your Content unavailable to users as soon as possible should you choose to delete it. However, if the Content is not attached to an account you control, you may be unable to delete that Content. Although removed Content will not be publicly available through AO3, we may retain backup copies for longer periods for legal and disaster recovery purposes. These copies may be accessible to AO3 and OTW administrators. If you delete Content from AO3 or edit Content in a manner that overwrites Content that you had previously submitted, we cannot restore it at your request. + modifying_or_adapting: Modifying or adapting + no_copyright_ownership_html: The OTW does not claim any copyright in or ownership of your Content. %{we_repeat} Nothing in this agreement changes that in any way. However, running AO3 requires us to make copies, and backup copies, on servers that may be located anywhere around the world. + open_doors: Open Doors + orphan: Orphan + preserve_for_legal_reasons_html: You acknowledge and agree that the OTW may preserve Content and may disclose Content if required to do so by law or in the good-faith belief that such preservation or disclosure is reasonably necessary to comply with legal processes; enforce the TOS; respond to claims that any Content violates the rights of third parties; or protect the rights, property, or personal safety of the OTW, users of the OTW's services, or the public. Refer to the %{privacy_policy_link} for details about when we may preserve and/or disclose your Personal Information. + privacy_policy: Privacy Policy + rules_for_removing_such_content: rules for removing and retaining such Content + some_content_open_doors_html: Some of the Content hosted on AO3 is imported via %{open_doors_link}. Open Doors' agreement with each collection's archivist is separate from AO3's TOS. If a work has been imported via Open Doors and its creator requests that the work be removed from AO3, we will remove it. If an archivist chooses to remove their imported collection, any imported works that have been claimed or Orphaned by their creators will remain on AO3. + subject_to_moderation: subject to moderation + tag_wrangling: tag wrangling + we_repeat: 'We repeat: we do not own your Content.' + worldwide_royalty_free_nonexclusive_license: worldwide, royalty-free, nonexclusive license + what_you_cant_do: + account_if_age_barred_html: to create an account if you are an %{age_barred_individual_link}; + age_barred_individual: Age-Barred Individual + break_applicable_law: to break any law that applies to you, including any rules or regulations having the force of law. As a general rule, AO3 follows U.S. law. Each user is responsible for knowing the laws of their own country. + commercial_activity_html: to conduct any commercial activity, whether for direct or indirect commercial advantage, including (without limitation) %{making_available_any_advertising_link}, spam, or other solicitation, or scraping Content in order to commercialize it; + comprehensive_trade_embargo: comprehensive trade embargo + content_policy: Content Policy + content_violating_policy_html: to make available any Content that violates the %{content_policy_link}; + copyright_infringement_html: to make available any Content that a court has ruled constitutes %{infringement_of_a_copyright_link}, patent, trademark, or trade secret; or that violates any other rights of a third party (as consistent with the OTW's %{position_on_fanwork_legality_link}); + forge_identifiers: to forge or otherwise manipulate identifiers (such as email headers or other information intended to route or authenticate Content) in order to disguise the origin of any Content transmitted to or through any OTW sites, servers, networks, or services; + function: function + heading: I.F. What you can't do + impersonate_any_person_or_entity: impersonate any person or entity + impersonate_person_or_entity_html: to %{impersonate_any_person_or_entity_link} (including, but not limited to, an AO3 or OTW representative or volunteer, or an AO3 %{function_link}), or to falsely state or otherwise misrepresent your affiliation with a person or entity; + infringement_of_a_copyright: infringement of a copyright + interfere_disrupt_ao3: interfere with or disrupt AO3 + interfere_disrupt_ao3_html: to %{interfere_disrupt_ao3_link}, any OTW-hosted Content, or any services, sites, servers, or networks connected to OTW sites; + making_available_any_advertising: making available any advertising + position_on_fanwork_legality: position on fanwork legality + resident_embargo_country_html: to create an account if you are a resident or national of any country with which the U.S. has prohibited transactions by mandating a %{comprehensive_trade_embargo_link}, as detailed by the Office of Foreign Assets Control; or + software_viruses: to make available any material that contains software viruses or other computer code, files, or programs designed to interrupt, destroy, or limit the functionality of any computer, hardware, or telecommunications equipment; + you_agree_not_to: 'You agree not to use AO3 (as well as the email addresses and URLs of OTW sites):' + tos_navigation: + content: Content Policy + privacy: Privacy Policy + top: "%{arrow} Top" + tos: Terms of Service + tos_faq: TOS FAQ + tos_toc: + content: + commercial_promotion: Commercial Promotion + copyright_infringement: Copyright Infringement + fanworks: Fanworks + harassment: Harassment + header: Content Policy + header_current: Content Policy (current section) + illegal_and_inappropriate_content: Illegal and Inappropriate Content + impersonation: Impersonation + intro: Content Policy Introduction + mandatory_tags: Mandatory Tags + offensive_content: Offensive Content + personal_information_and_fannish_identities: Personal Information and Fannish Identities + plagiarism: Plagiarism + user_icons: User Icons + privacy: + aggregate_and_anonymous_information: Aggregate and anonymous information + applicability: Applicability + contact_us: Contact us + header: Privacy Policy + header_current: Privacy Policy (current section) + information_shared_with_third_parties: Information shared with third parties + intro: Privacy Policy Introduction + retention_of_personal_information: Retention of Personal Information + scope_of_personal_information_we_process: Scope of Personal Information we process + termination_of_account: Termination of account + types_of_personal_information_we_collect_and_process: Types of Personal Information we collect and process + your_rights_under_applicable_data_privacy_laws: Your rights under applicable data privacy laws + tos: + abuse_policy: Abuse Policy + age_policy: Age Policy + content_you_access: Content you access when using AO3 + general_terms: General terms + header: General Principles + header_current: General Principles (current section) + intro: General Principles Introduction + potential_problems: Potential problems with AO3 + registration_and_email_addresses: Registration and email addresses + updates_to_the_tos: Updates to the Terms of Service + what_we_do_with_content: What we do with Content + what_you_cant_do: What you can't do invitations: invitation: email_address_label: Enter an email address invite_requests: + index_open: + add_to_list: Add me to the list + already_requested_html: If you have already requested an invitation, you can %{check_status_link}. + check_waitlist_position: check your position on the waiting list + content_policy: Content Policy + details_html: To get a free Archive of Our Own account, you need an Invitation. By submitting your email address to our invitation queue, you confirm that you are at least 13 years old, and if you're in a country whose residents/citizens have to be of an age older than 13 to consent, you are old enough to consent to the processing of your personal data without our obtaining written permission from a parent or legal guardian. We will use the email address you submit only to send you an Invitation and to process/manage your account activation. Please don't request an Invitation unless you've read our %{tos_link}, including the %{content_policy_link} and %{privacy_policy_link}, and agree to abide by those Terms. + invitations_per_day: + one: We are sending out %{count} invitation per day. + other: We are sending out %{count} invitations per day. + page_heading: Invitation Requests + privacy_policy: Privacy Policy + request_invitation_header: Request an invitation + tos: Terms of Service + waiting_list_count: + one: There is currently %{count} person on the waiting list. + other: There are currently %{count} people on the waiting list. invite_request: date: 'At our current rate, you should receive an invitation on or around: %{date}.' position_html: You are currently number %{position} on our waiting list! @@ -712,6 +1413,16 @@ en: one: "%{count} more user" other: "%{count} more users" languages: + form: + abuse_support_available: Abuse support available + name: Name + required_notice: Required information + short: Abbreviation + sortable_name: Name for alphabetical sorting + submit: + create: Create Language + update: Update Language + support_available: Support available index: navigation: add: Add a Language @@ -722,6 +1433,33 @@ en: one: "%{formatted_count} work" other: "%{formatted_count} works" layouts: + footer: + about: + content_policy: Content Policy + diversity_statement: Diversity Statement + dmca_policy: DMCA Policy + header: About the Archive + privacy_policy: Privacy Policy + site_map: Site Map + tos: Terms of Service + contact_us: + header: Contact Us + pac: Policy Questions & Abuse Reports + support: Technical Support & Feedback + customize: + default: Default + header: Customize + development: + archive_version: otwarchive %{version_number} + gpl_2_0_or_later: GPL-2.0-or-later + header: Development + known_issues: Known Issues + license_details_html: "%{license_link} by the %{otw_link}" + view_license: View License + landmark: Footer + otw: + abbreviated: OTW + full: Organization for Transformative Works header: collections: new: New Collection @@ -738,6 +1476,17 @@ en: faux_heading: 'Important message:' point1: You are using a proxy site that is not part of the Archive of Our Own. point2: The entity that set up the proxy site can see what you submit, including your IP address. If you log in through the proxy site, it can see your password. + tos_prompt: + agree_and_consent: I agree/consent to these Terms + content: Content + content_policy: Content Policy + data_processing_agreement: By checking this box, you consent to the processing of your personal data in the United States and other jurisdictions in connection with our provision of AO3 and its related services to you. You acknowledge that the data privacy laws of such jurisdictions may differ from those provided in your jurisdiction. For more information about how your personal data will be processed, please refer to our Privacy Policy. + learn_more_html: To learn more, check out our %{tos_link}, including the %{content_policy_link} and %{privacy_policy_link}. + page_heading: Archive of Our Own + privacy_policy: Privacy Policy + read_and_understood: I have read & understood the 2024 Terms of Service, including the Content Policy and Privacy Policy. + summary_html: On the Archive of Our Own (AO3), users can create works, bookmarks, comments, tags, and other %{content_link}. Any information you publish on AO3 may be accessible by the public, AO3 users, and/or AO3 personnel. Be mindful when sharing personal information, including but not limited to your name, email, age, location, personal relationships, gender or sexual identity, racial or ethnic background, religious or political views, and/or account usernames for other sites. + tos: Terms of Service menu: menu_about: about_us: About Us @@ -869,14 +1618,13 @@ en: 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}"? subscriptions: index: button_html: Unsubscribe from %{name} - byline: by %{creators} + byline_html: by %{creators} heading: landmark: list: List of Subscriptions @@ -896,6 +1644,34 @@ en: show: last_wrangled_html: "%{wrangler_login} last wrangled at %{time}." tags_wrangled_csv: Tags Wrangled (CSV) + tag_wranglings: + wrangler_dashboard: + characters_by_fandom: Characters by fandom (%{count}) + fandoms_by_media: Fandoms by media (%{count}) + freeforms_by_fandom: Freeforms by fandom (%{count}) + new_tag: New Tag + relationships_by_fandom: Relationships by fandom (%{count}) + search_tags: Search Tags + tag_type: + character: Characters + fandom: Fandoms + freeform: Freeforms + merger: Mergers + relationship: Relationships + subtag: SubTags + tag_type_and_count: "%{tag_type} (%{count})" + unsorted_tags: Unsorted Tags (%{count}) + use_type: + bookmarks: Bookmarks + drafts: Drafts + external_works: External Works + private_bookmarks: Private Bookmarks + taggings_count: Taggings Count + works: Works + use_type_and_count: "%{use_type} (%{count})" + wranglers: Wranglers + wrangling_home: Wrangling Home + wrangling_tools: Wrangling Tools tags: index: about: @@ -944,8 +1720,13 @@ en: description: Recalculate the filters for this work based on the current set of tags. Use this option if wrangling seems to have gone wrong. (e.g. If you synned one of this work's tags to a canonical but it's not showing up in the tag listings, or if the wrong tags are listed in the sidebar when you drill down to a search that includes only this work.) title: Update Work Filters users: + change_email: + browser_title: Change Email + change_password: + browser_title: Change Password change_username: account_faq: Account FAQ + browser_title: Change Username caution: Please use this feature with caution. change_window: one: You can change your user name once per day. @@ -985,13 +1766,31 @@ en: works_summary: 'You have %{work_count} work(s) under the following pseuds: %{pseuds}.' submit: Save edit: + about_me: About Me browser_title: Edit Profile + change_profile_landmark: Change Profile + date_of_birth: Date of Birth + location: Location + page_heading: Edit My Profile + privacy_policy: Privacy Policy + public_information_notice_html: Any personal information you post on your public AO3 profile, including but not limited to your name, email, age, location, personal relationships, gender or sexual identity, racial or ethnic background, religious or political views, and/or account usernames for other sites, will be accessible by the general public. To learn more about what data AO3 collects when you're on the site and how we use it, check out our %{privacy_policy_link}. + title: Title + update: Update header_navigation: admin_user: User Administration edit_multiple: Edit Works invitations: Invitations new_work: Post New registrations: + legal: + agreement_confirm: Yes, I have read the Terms of Service, including the Content Policy and Privacy Policy, and agree to them. + agreement_required_html: Before you begin using AO3, you must agree to our %{terms_of_service_link}, including the %{content_policy_link} and %{privacy_policy_link}. + content_policy: Content Policy (opens in new window) + data_processing_confirm: By checking this box, you consent to the processing of your personal data in the United States and other jurisdictions in connection with our provision of AO3 and its related services to you. You acknowledge that the data privacy laws of such jurisdictions may differ from those provided in your jurisdiction. For more information about how your personal data will be processed, please refer to our Privacy Policy. + over_thirteen_confirm: Yes, I am at least 13. + over_thirteen_required: You need to be at least 13 years old to become a registered member of the Archive. (Sorry to anyone younger! You'll be more than welcome when the time comes.) + privacy_policy: Privacy Policy (opens in new window) + terms_of_service: Terms of Service (opens in new window) new: cancel: Cancel heading: Create Account @@ -1044,16 +1843,45 @@ en: work_unavailable: This work is only available to registered users of the Archive. show: login_banner: + contact_abuse: contact our Policy & Abuse team + contact_support: contact our Support team + content_policy: Content Policy dismiss: Dismiss permanently - help_link: Learn some tips and tricks - help_text: For more guidance on using the site, %{link_faq}! If you need technical support, you can %{link_support}. If you experience harassment or have specific questions about the %{link_tos}, you can %{link_abuse}. - help_title: First login help + help_html: If you need technical support, %{contact_support_link}. If you experience harassment or have questions about our %{tos_link} (including the %{content_policy_link} and %{privacy_policy_link}), %{contact_abuse_link}. hide: Hide first login help banner - link_abuse: contact our Policy & Abuse team - link_faq: check out our FAQ - link_support: contact us through our Support and Feedback form - 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. + new_user_tips: useful tips for new users + our_faqs: our FAQs + privacy_policy: Privacy Policy + tos: Terms of Service + welcome_html: Hi! It looks like you've just logged in to AO3 for the first time. For help getting started on AO3, check out some %{new_user_tips_link} or browse through %{our_faqs_link}. + sidebar: + catch: + history: History + inbox: Inbox (%{inbox_number}) + statistics: Statistics + subscriptions: Subscriptions + choices: + all_pseuds: All Pseuds (%{pseud_number}) + dashboard: Dashboard + preferences: Preferences + profile: Profile + pseud_switcher: Pseud Switcher + pseuds: Pseuds + skins: Skins + landmark: + catch: Catch + choices: Choices + pitch: Pitch + switch: Switch + pitch: + collections: Collections (%{coll_number}) + drafts: Drafts (%{drafts_number}) + switch: + assignments: Assignments (%{assignment_number}) + claims: Claims (%{claim_number}) + co_creator_requests: Co-Creator Requests (%{count}) + related_works: Related Works (%{related_works_number}) + sign_ups: Sign-ups (%{signup_number}) works: adult: caution: This work could have adult content. If you continue, you have agreed that you are willing to see such content. @@ -1070,10 +1898,18 @@ en: byline: add_co-creators: Add co-creators show_co-creator_options: Add co-creators? + edit_tags: + content_policy: Content Policy + post_notice_html: All works you post on AO3 must comply with our %{content_policy_link}. For more information, please refer to our %{tos_faq_link}. + tos_faq: Terms of Service FAQ meta: original_creators: one: 'Original Creator ID:' other: 'Original Creator IDs:' + new_import: + content_policy: Content Policy + post_notice_html: All works you post on AO3 must comply with our %{content_policy_link}. For more information, please refer to our %{tos_faq_link}. + tos_faq: Terms of Service FAQ permissions: privacy: Privacy visibility: @@ -1085,6 +1921,10 @@ en: unrestricted: Show to all preface: title: Preface + preview: + content_policy: Content Policy + post_notice_html: All works you post on AO3 must comply with our %{content_policy_link}. For more information, please refer to our %{tos_faq_link}. + tos_faq: Terms of Service FAQ search_box: a11y_label: Work label: Work Search @@ -1094,6 +1934,10 @@ en: unposted_deletion_notice_html: This work is a draft and has not been posted. The draft will be scheduled for deletion on %{deletion_date}. skin: select: Select work skin + standard_form: + content_policy: Content Policy + post_notice_html: All works you post on AO3 must comply with our %{content_policy_link}. For more information, please refer to our %{tos_faq_link}. + tos_faq: Terms of Service FAQ work_approved_children: inspired_by: restricted_html: "[Restricted Work] by %{creator_link} (Log in to access.)" diff --git a/config/routes.rb b/config/routes.rb index 35f7e054bdd..a4fedf346dc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -619,8 +619,10 @@ get 'search' => 'works#search' post 'support' => 'feedbacks#create', as: 'feedbacks' get 'support' => 'feedbacks#new', as: 'new_feedback_report' - get 'tos' => 'home#tos' - get 'tos_faq' => 'home#tos_faq' + get "content" => "home#content" + get "privacy" => "home#privacy" + get "tos" => "home#tos" + get "tos_faq" => "home#tos_faq" get 'unicorn_test' => 'home#unicorn_test' get 'dmca' => 'home#dmca' get 'diversity' => 'home#diversity' diff --git a/config/storage.yml b/config/storage.yml index d32f76e8fbf..8ca0618d982 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -6,29 +6,12 @@ local: service: Disk root: <%= Rails.root.join("storage") %> -# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) -# amazon: -# service: S3 -# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> -# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> -# region: us-east-1 -# bucket: your_own_bucket - -# Remember not to checkin your GCS keyfile to a repository -# google: -# service: GCS -# project: your_project -# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> -# bucket: your_own_bucket - -# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) -# microsoft: -# service: AzureStorage -# storage_account_name: your_account_name -# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> -# container: your_container_name - -# mirror: -# service: Mirror -# primary: local -# mirrors: [ amazon, google, microsoft ] +# Example AWS S3 configuration for ActiveStorage. +# This should be overridden by config/storage/.yml +s3: + service: S3 + access_key_id: "" + secret_access_key: "" + region: "" + bucket: "" + public: true diff --git a/db/migrate/20240323013245_create_active_storage_tables.active_storage.rb b/db/migrate/20240323013245_create_active_storage_tables.active_storage.rb new file mode 100644 index 00000000000..9d31ee7472e --- /dev/null +++ b/db/migrate/20240323013245_create_active_storage_tables.active_storage.rb @@ -0,0 +1,58 @@ +# This migration comes from active_storage (originally 20170806125915) +class CreateActiveStorageTables < ActiveRecord::Migration[5.2] + def change + # Use Active Record's configured type for primary and foreign keys + primary_key_type, foreign_key_type = primary_and_foreign_key_types + + create_table :active_storage_blobs, id: primary_key_type do |t| + t.string :key, null: false + t.string :filename, null: false + t.string :content_type + t.text :metadata + t.string :service_name, null: false + t.bigint :byte_size, null: false + t.string :checksum + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index [:key], unique: true + end + + create_table :active_storage_attachments, id: primary_key_type do |t| + t.string :name, null: false + t.references :record, null: false, polymorphic: true, index: false, type: foreign_key_type + t.references :blob, null: false, type: foreign_key_type + + if connection.supports_datetime_with_precision? + t.datetime :created_at, precision: 6, null: false + else + t.datetime :created_at, null: false + end + + t.index [:record_type, :record_id, :name, :blob_id], name: :index_active_storage_attachments_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + + create_table :active_storage_variant_records, id: primary_key_type do |t| + t.belongs_to :blob, null: false, index: false, type: foreign_key_type + t.string :variation_digest, null: false + + t.index [:blob_id, :variation_digest], name: :index_active_storage_variant_records_uniqueness, unique: true + t.foreign_key :active_storage_blobs, column: :blob_id + end + end + + private + + def primary_and_foreign_key_types + config = Rails.configuration.generators + setting = config.options[config.orm][:primary_key_type] + primary_key_type = setting || :primary_key + foreign_key_type = setting || :bigint + [primary_key_type, foreign_key_type] + end +end diff --git a/docker-compose.yml b/docker-compose.yml index d44e9e554dc..3d6ef23c4f9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -46,7 +46,7 @@ services: volumes: - redis-data:/var/lib/redis:rw es: - image: docker.elastic.co/elasticsearch/elasticsearch:7.17.5 + image: docker.elastic.co/elasticsearch/elasticsearch:7.17.26 ports: - "9200:9200" - "9300:9300" @@ -91,7 +91,7 @@ services: chrome: profiles: - test - image: selenium/standalone-chrome + image: selenium/standalone-chromium ports: - "4444:4444" test: diff --git a/factories/tags.rb b/factories/tags.rb index 74244df7220..1dae159abe7 100644 --- a/factories/tags.rb +++ b/factories/tags.rb @@ -108,4 +108,9 @@ factory :banned do |f| f.sequence(:name) { |n| "Banned #{n}" } end + + factory :archive_warning do |f| + f.sequence(:name) { |n| "Archive Warning #{n}" } + canonical { true } + end end diff --git a/factories/users.rb b/factories/users.rb index 181fc4f9f76..8d83adafefb 100644 --- a/factories/users.rb +++ b/factories/users.rb @@ -13,6 +13,7 @@ login { generate(:login) } password { "password" } age_over_13 { "1" } + data_processing { "1" } terms_of_service { "1" } password_confirmation(&:password) email { Faker::Internet.unique.email } diff --git a/features/admins/admin_languages.feature b/features/admins/admin_languages.feature index cda790115f0..83cdd68e72a 100644 --- a/features/admins/admin_languages.feature +++ b/features/admins/admin_languages.feature @@ -22,7 +22,7 @@ Scenario: Adding Abuse support for a language | name | short | | Arabic | ar | | Espanol | es | - When I am logged in as a "translation" admin + When I am logged in as a "policy_and_abuse" admin And I go to the languages page # Languages are sorted by short name, so the first "Edit" is for Arabic And I follow "Edit" @@ -39,7 +39,7 @@ Scenario: Adding a language to the Support form | name | short | | Sindarin | sj | | Klingon | tlh | - When I am logged in as a "translation" admin + When I am logged in as a "support" admin And I go to the languages page # Languages are sorted by short name, so the first "Edit" is for Sindarin And I follow "Edit" diff --git a/features/admins/admin_post_faqs.feature b/features/admins/admin_post_faqs.feature index e30edfdd34d..918c6b19435 100644 --- a/features/admins/admin_post_faqs.feature +++ b/features/admins/admin_post_faqs.feature @@ -3,11 +3,11 @@ Feature: Admin Actions to Post FAQs As an an admin I want to be able to manage the archive FAQ - Scenario: Post and edit a FAQ + Scenario Outline: Authorized admin posts, edits and deletes a FAQ category When I go to the archive_faqs page Then I should see "Some commonly asked questions about the Archive are answered here" And I should not see "Some text" - When I am logged in as an admin + When I am logged in as a "" admin And I follow "Admin Posts" And I follow "Archive FAQ" within "#header" Then I should not see "Some text" @@ -17,7 +17,7 @@ Feature: Admin Actions to Post FAQs And I fill in "Category name*" with "New subsection" And I fill in "Anchor name*" with "whatisao3" And I press "Post" - Then I should see "ArchiveFaq was successfully created" + Then I should see "Archive FAQ was successfully created" When I go to the archive_faqs page And I follow "New subsection" Then I should see "Some text, that is sufficiently long to pass validation" within ".userstuff" @@ -26,10 +26,38 @@ Feature: Admin Actions to Post FAQs And I press "Post" Then I should see "New Content, yay" And I should not see "Some text" + When I go to the archive_faqs page + And I follow "Delete" + Then I should see "Are you sure you want to delete the FAQ Category" + When I press "Yes, Delete FAQ Category" + Then I should not see "New subsection" + + Examples: + | role | + | support | + | superadmin | + | docs | + + @javascript + Scenario Outline: Authorized admin deletes a FAQ question + Given 1 Archive FAQ with 1 question exists + And I am logged in as a "" admin + And I go to the archive_faqs page + And I follow "Edit" + And I follow "Remove Question" + And I press "Post" + Then I should see "Archive FAQ was successfully updated." + And I should see "We're sorry, there are currently no entries in this category." + + Examples: + | role | + | support | + | superadmin | + | docs | Scenario: Post a translated FAQ for a locale, then change the locale's code. Given basic languages - And I am logged in as a "translation" admin + And I am logged in as a "superadmin" admin # Post "en" FAQ When I go to the archive_faqs page @@ -39,23 +67,25 @@ Feature: Admin Actions to Post FAQs And I fill in "Category name*" with "New subsection" And I fill in "Anchor name*" with "whatisao3" And I press "Post" - Then I should see "ArchiveFaq was successfully created" + Then I should see "Archive FAQ was successfully created" # Translate FAQ to "de" - When I follow "Archive FAQ" + When I am logged in as a "translation" admin + And I follow "Admin Posts" + And I follow "Archive FAQ" And I select "Deutsch" from "Language:" And I press "Go" within "div#inner.wrapper" And I follow "Edit" And I fill in "Question*" with "Was ist AO3?" - And I fill in "Answer*" with "Einige Text, das ist lang genug, um die Überprüfung bestanden." + And I fill in "Answer*" with "Einiger Text, der lang genug ist, um die Überprüfung zu bestehen." And I fill in "Category name*" with "Neuer Abschnitt" And I check "Question translated" And I press "Post" - Then I should see "ArchiveFaq was successfully updated." + Then I should see "Archive FAQ was successfully updated." And I should not see "New subsection" And I should see "Neuer Abschnitt" And I should see "Was ist AO3?" - And I should see "Einige Text" + And I should see "Einiger Text" # Change locale "de" to "ger" When I go to the locales page @@ -83,4 +113,89 @@ Feature: Admin Actions to Post FAQs And I press "Go" within "div#inner.wrapper" And I follow "Neuer Abschnitt" Then I should see "Was ist AO3?" - And I should see "Einige Text" + And I should see "Einiger Text" + + Scenario: Links to create, reorder and delete FAQ categories are not shown for non-English language FAQs + Given basic languages + And 1 Archive FAQ exists + And I am logged in as a "superadmin" admin + When I go to the archive_faqs page + Then I should see "New FAQ Category" + And I should see "Reorder FAQs" + And I should see "Delete" + And I should see "Edit" + When I select "Deutsch" from "Language:" + And I press "Go" within "div#inner.wrapper" + Then I should not see "New FAQ Category" + And I should not see "Reorder FAQs" + And I should not see "Delete" + But I should see "Edit" + + @javascript + Scenario: Links to add, reorder and remove FAQ questions are not shown for non-English language FAQs + Given basic languages + And 1 Archive FAQ with 1 question exists + And I am logged in as a "superadmin" admin + When I go to the archive_faqs page + And I follow "Edit" + Then I should see "Reorder Questions" + And I should see "Remove Question" + And I should see "Add Question" + But I should not see "Question translated" + When I select "Deutsch" from "Language:" + And I press "Go" within "div#inner.wrapper" + And I follow "Edit" + Then I should not see "Reorder Questions" + And I should not see "Remove Question" + And I should not see "Add Question" + But I should see "Question translated" + + Scenario: Translation admins do not see links to edit English language FAQs + Given basic languages + And 1 Archive FAQ exists + And I am logged in as a "translation" admin + When I go to the archive_faqs page + Then I should not see "Edit" + And I should not see "New FAQ Category" + And I should not see "Reorder FAQs" + And I should not see "Delete" + When I follow "Show" + Then I should not see "Edit" within ".header" + But I should see "Updated:" within ".header" + When I go to the archive_faqs page + And I select "Deutsch" from "Language:" + And I press "Go" within "div#inner.wrapper" + Then I should see "Edit" + And I should not see "New FAQ Category" + And I should not see "Reorder FAQs" + And I should not see "Delete" + When I follow "Show" + Then I should see "Edit" within ".header" + And I should see "Updated:" within ".header" + + Scenario Outline: Links to create and edit FAQs are not shown to unauthorized admins + Given an archive FAQ category with the title "Very important FAQ" exists + And I am logged in as a "" admin + When I follow "Admin Posts" + Then I should not see "Archive FAQ" within "#header" + When I go to the archive_faqs page + Then I should not see "Edit" + And I should not see "New FAQ Category" + And I should not see "Reorder FAQs" + And I should not see "Delete" + But I should see "Available Categories" + When I follow "Very important FAQ" + Then I should not see "Edit" + And I should not see "Updated:" + + Examples: + | role | + | board | + | board_assistants_team | + | communications | + | development_and_membership | + | elections | + | legal | + | tag_wrangling | + | policy_and_abuse | + | open_doors | diff --git a/features/admins/admin_post_issues.feature b/features/admins/admin_post_issues.feature index fdfdb508fbe..cdedac3a957 100644 --- a/features/admins/admin_post_issues.feature +++ b/features/admins/admin_post_issues.feature @@ -3,9 +3,9 @@ Feature: Admin Actions to Post Known Issues As an an admin I want to be able to report known issues - Scenario: Post known issues - When I am logged in as an admin - And I follow "Admin Posts" + Scenario Outline: Authorized admin posts, edits, and deletes known issues + Given I am logged in as a "" admin + When I follow "Admin Posts" And I follow "Known Issues" within "#header" And I follow "make a new known issues post" And I fill in "known_issue_title" with "First known problem" @@ -18,15 +18,36 @@ Feature: Admin Actions to Post Known Issues And I follow "Known Issues" within "#header" And I follow "Show" Then I should see "First known problem" - - Scenario: Edit known issues - Given I have posted known issues When I edit known issues Then I should see "Known issue was successfully updated" And I should not see "First known problem" And I should see "This is a bit of a problem, and this is too" - - Scenario: Delete known issues - Given I have posted known issues When I delete known issues Then I should not see "First known problem" + + Examples: + | role | + | support | + | superadmin | + + Scenario Outline: Links to edit and create known issues are not shown to unauthorized admins + Given I have posted known issues + And I am logged in as a "" admin + When I follow "Admin Posts" + Then I should not see "Known Issues" within "#header" + When I go to the known issues page + Then I should not see "Edit" within ".actions" + + Examples: + | role | + | board | + | board_assistants_team | + | communications | + | development_and_membership | + | docs | + | elections | + | legal | + | translation | + | tag_wrangling | + | policy_and_abuse | + | open_doors | diff --git a/features/admins/admin_reorder_faq.feature b/features/admins/admin_reorder_faq.feature index d06f3d67a12..0073cb5dd60 100644 --- a/features/admins/admin_reorder_faq.feature +++ b/features/admins/admin_reorder_faq.feature @@ -5,7 +5,7 @@ Feature: Rearrange Archive FAQs I want to be able to reorder the FAQs Scenario: Rearrange FAQs - Given I am logged in as an admin + Given I am logged in as a "superadmin" admin And 3 Archive FAQs exist When I go to the FAQ reorder page And I fill in "archive_faqs_1" with "3" diff --git a/features/admins/admin_reorder_faq_questions.feature b/features/admins/admin_reorder_faq_questions.feature index 4f5c1b40ebe..2142bccc0ec 100644 --- a/features/admins/admin_reorder_faq_questions.feature +++ b/features/admins/admin_reorder_faq_questions.feature @@ -4,7 +4,7 @@ Feature: Admin Actions to re-order questions in a FAQ Category I want to be able to re-order the questions in a FAQ Category Scenario: Re-order the questions in a FAQ Category - Given I am logged in as an admin + Given I am logged in as a "superadmin" admin And I make a multi-question FAQ post When I go to the archive_faqs page And I follow "Standard FAQ Category" diff --git a/features/admins/admin_works.feature b/features/admins/admin_works.feature index 39807cbd650..461773a660a 100644 --- a/features/admins/admin_works.feature +++ b/features/admins/admin_works.feature @@ -330,6 +330,19 @@ Feature: Admin Actions for Works, Comments, Series, Bookmarks Then I should see "rolex" And I should not see "This comment has been marked as spam." + Scenario: Moderated comments cannot be approved by admin + Given the moderated work "Moderation" by "author" + And I am logged in as "commenter" + And I post the comment "Test comment" on the work "Moderation" + When I am logged in as a "superadmin" admin + And I view the work "Moderation" + Then I should see "Unreviewed Comments (1)" + And the comment on "Moderation" should be marked as unreviewed + When I follow "Unreviewed Comments (1)" + Then I should see "Test comment" + And I should not see an "Approve All Unreviewed Comments" button + And I should not see an "Approve" button + Scenario: Admin can edit language on works when posting without previewing Given basic languages And I am logged in as "regular_user" diff --git a/features/bookmarks/bookmark_indexing.feature b/features/bookmarks/bookmark_indexing.feature index 7bd9a41cd36..e32a131c35e 100644 --- a/features/bookmarks/bookmark_indexing.feature +++ b/features/bookmarks/bookmark_indexing.feature @@ -63,7 +63,7 @@ Feature: Bookmark Indexing Given a canonical fandom "Veronica Mars" And a canonical fandom "Veronica Mars (TV)" And bookmarks of external works and series tagged with the fandom tag "Veronica Mars" - And I am logged in as an admin + And I am logged in as a "tag_wrangling" admin When I syn the tag "Veronica Mars" to "Veronica Mars (TV)" And I go to the bookmarks tagged "Veronica Mars (TV)" Then I should see "BookmarkedExternalWork" diff --git a/features/comments_and_kudos/comment_moderation.feature b/features/comments_and_kudos/comment_moderation.feature index 51176bfcbc5..8dbb2863210 100644 --- a/features/comments_and_kudos/comment_moderation.feature +++ b/features/comments_and_kudos/comment_moderation.feature @@ -307,3 +307,25 @@ Feature: Comment Moderation Then I should see "All moderated comments approved." When I view the work "Moderation" Then I should see "Comments (4)" + + Scenario: I can view the parent thread of an unreviewed comment + Given the moderated work "Moderation" by "author" with the approved comment "Test comment" by "commenter" + And I am logged in as "new_commenter" + When I view the work "Moderation" + And I follow "Comments (1)" + And I follow "Reply" within ".odd" + And I fill in "Comment" with "A moderated reply" within ".odd" + And I press "Comment" within ".odd" + When I am logged in as "author" + And I view the work "Moderation" + And I follow "Unreviewed Comments (1)" + And I follow "Parent Thread" + Then I should see "Test comment" + When I view the unreviewed comments page for "Moderation" + And I press "Approve" + When I am logged in as "new_commenter" + And I post the comment "Zero-depth comment" on the work "Moderation" + When I am logged in as "author" + And I view the work "Moderation" + And I follow "Unreviewed Comments (1)" + Then I should not see "Parent Thread" diff --git a/features/gift_exchanges/notification_emails.feature b/features/gift_exchanges/notification_emails.feature index df426b0db16..5aa2a6ab8db 100644 --- a/features/gift_exchanges/notification_emails.feature +++ b/features/gift_exchanges/notification_emails.feature @@ -1,6 +1,77 @@ Feature: Gift Exchange Notification Emails Make sure that gift exchange notification emails are formatted properly + Scenario: Assignment sent notification emails should be sent to two owners in their respective locales when assignments are generated + Given I have created the tagless gift exchange "Holiday Swap" + And I open signups for "Holiday Swap" + + When I am logged in as "participant1" + And I start signing up for "Holiday Swap" + And I press "Submit" + Then I should see "Sign-up was successfully created." + + When I am logged in as "participant2" + And I start signing up for "Holiday Swap" + And I press "Submit" + Then I should see "Sign-up was successfully created." + + When I have added a co-moderator "mod2" to collection "Holiday Swap" + And a locale with translated emails + And the user "mod1" enables translated emails + And I close signups for "Holiday Swap" + And I have generated matches for "Holiday Swap" + And I have sent assignments for "Holiday Swap" + + Then 4 emails should be delivered + And "mod1" should receive 1 email + And the email to "mod1" should be translated + And the email should contain "You have received a message about your collection" + And "mod2" should receive 1 email + And the email to "mod2" should be non-translated + And the email should contain "You have received a message about your collection" + And "participant1" should receive 1 email + And "participant2" should receive 1 email + + Scenario: If collection email is set, use the collection email instead of moderator emails + Given I have created the tagless gift exchange "Holiday Swap" + And I open signups for "Holiday Swap" + And I am logged in as "participant1" + And I start signing up for "Holiday Swap" + And I press "Submit" + And I am logged in as "participant2" + And I start signing up for "Holiday Swap" + And I press "Submit" + And I have added a co-moderator "mod2" to collection "Holiday Swap" + And I go to "Holiday Swap" collection's page + And I follow "Collection Settings" + And I fill in "Collection email" with "test@archiveofourown.org" + And I press "Update" + And I close signups for "Holiday Swap" + And I have generated matches for "Holiday Swap" + And I have sent assignments for "Holiday Swap" + Then 3 emails should be delivered + And 1 email should be delivered to test@archiveofourown.org + And the email should contain "You have received a message about your collection" + + Scenario: Default notification emails should be sent to two owners in their respective locales when a user defaults on an assignment + + Given everyone has their assignments for "Holiday Swap" + And I have added a co-moderator "mod2" to collection "Holiday Swap" + And a locale with translated emails + And the user "mod1" enables translated emails + + When I am logged in as "myname1" + And I go to my assignments page + And I follow "Default" + Then I should see "We have notified the collection maintainers that you had to default on your assignment." + And 7 emails should be delivered + And "mod1" should receive 2 emails + And the last email to "mod1" should be translated + And the last email should contain "defaulted on their assignment" + And "mod2" should receive 1 email + And the email to "mod2" should be non-translated + And the email should contain "defaulted on their assignment" + Scenario: Assignment notifications with linebreaks. Given I have created the tagless gift exchange "Holiday Swap" And I open signups for "Holiday Swap" @@ -77,3 +148,17 @@ Feature: Gift Exchange Notification Emails Then "participant1" should receive 1 email And the notification message to "participant1" should contain the no archive warnings tag + + Scenario: Assignment notifications should be sent to participants in their respective locales + Given the gift exchange "Holiday Swap" is ready for matching + And a locale with translated emails + And the user "myname1" enables translated emails + When I close signups for "Holiday Swap" + And I have generated matches for "Holiday Swap" + And I have sent assignments for "Holiday Swap" + Then "myname1" should receive 1 email + And the email should have "Your assignment!" in the subject + And the email to "myname1" should be translated + And "myname2" should receive 1 email + And the email should have "Your assignment!" in the subject + And the email to "myname2" should be non-translated diff --git a/features/importing/work_import.feature b/features/importing/work_import.feature index 689a006c801..fa98e6e3866 100644 --- a/features/importing/work_import.feature +++ b/features/importing/work_import.feature @@ -44,7 +44,7 @@ Feature: Import Works And I should see "Detected Title" And I should see "Language: Deutsch" And I should see "Explicit" - And I should see "Archive Warning: Underage" + And I should see "Archive Warning: Underage Sex" And I should see "Fandom: Detected Fandom" And I should see "Category: M/M" And I should see "Relationship: Detected 1/Detected 2" @@ -355,3 +355,20 @@ Feature: Import Works And I should not see "This chapter is a draft and hasn't been posted yet!" When I follow "Next Chapter" Then I should not see "This chapter is a draft and hasn't been posted yet!" + + Scenario: Importing as an archivist for an existing Archive author should send translated claim email + Given a locale with translated emails + And the following activated users exist + | login | email | + | sam | sam@example.com | + | notsam | notsam@example.com | + And the user "sam" enables translated emails + And all emails have been delivered + When I import the mock work "http://import-site-without-tags" by "sam" with email "sam@example.com" and by "notsam" with email "notsam@example.com" + Then I should see import confirmation + And 1 email should be delivered to "sam@example.com" + And the email should contain claim information + And the email to "sam" should be translated + And 1 email should be delivered to "notsam@example.com" + And the email should contain claim information + And the email to "notsam" should be non-translated diff --git a/features/importing/work_import_errors.feature b/features/importing/work_import_errors.feature index c8e67120575..cc084d33b56 100644 --- a/features/importing/work_import_errors.feature +++ b/features/importing/work_import_errors.feature @@ -15,3 +15,12 @@ Feature: Import Works Then I should see "We couldn't successfully import that work, sorry: We couldn't download anything from http://no-content. Please make sure that the URL is correct and complete, and try again." When I go to my works page Then I should see "Drafts (0)" + + Scenario: Cannot import works from the current archive + Given I set up importing + And I fill in "urls" with "https://archiveofourown.org/works/54711364" + And I select "English" from "Choose a language" + And I press "Import" + Then I should see "We couldn't successfully import that work, sorry: URL is for a work on the Archive. Please bookmark it directly instead." + When I go to my works page + Then I should see "Drafts (0)" diff --git a/features/other_a/banner_login.feature b/features/other_a/banner_login.feature index 3aef2ce82b5..d2625ce0845 100644 --- a/features/other_a/banner_login.feature +++ b/features/other_a/banner_login.feature @@ -6,15 +6,22 @@ Feature: First login help banner Given I am logged in as "newname" When I am on newname's user page Then I should see the first login banner - And I should see "For more guidance on using the site, check out our FAQ! If you need technical support, you can contact us through our Support and Feedback form. If you experience harassment or have specific questions about the Terms of Service, you can contact our Policy & Abuse team." - When I follow "check out our FAQ" + And I should see "For help getting started on AO3, check out some useful tips for new users or browse through our FAQs." + And I should see "If you need technical support, contact our Support team. If you experience harassment or have questions about our Terms of Service (including the Content Policy and Privacy Policy), contact our Policy & Abuse team." + When I follow "our FAQs" Then I should be on the faq page When I am on newname's user page - And I follow "contact us through our Support and Feedback form" + And I follow "contact our Support team" Then I should be on the support page When I am on newname's user page And I follow "Terms of Service" Then I should be on the tos page + When I am on newname's user page + And I follow "Content Policy" + Then I should be on the content page + When I am on newname's user page + And I follow "Privacy Policy" + Then I should be on the privacy page When I am on newname's user page And I follow "contact our Policy & Abuse team" Then I should see "Policy Questions & Abuse Reports" @@ -23,7 +30,7 @@ Feature: First login help banner Given I am logged in as "newname" When I am on newname's user page - When I follow "Learn some tips and tricks" + When I follow "useful tips for new users" Then I should see the first login popup Scenario: Turn off first login help banner directly diff --git a/features/other_a/homepage.feature b/features/other_a/homepage.feature index e2376466af3..15de4da55be 100644 --- a/features/other_a/homepage.feature +++ b/features/other_a/homepage.feature @@ -15,7 +15,7 @@ Feature: Various things on the homepage Given I am on the homepage When I follow "DMCA Policy" - Then I should see "safe harbor" + Then I should see "Filing a DMCA counternotice" Scenario: Donate diff --git a/features/other_a/invite_request.feature b/features/other_a/invite_request.feature index 8d8a4f5725d..6555b5c5a3b 100644 --- a/features/other_a/invite_request.feature +++ b/features/other_a/invite_request.feature @@ -156,3 +156,33 @@ Feature: Invite requests When I follow "Delete" Then I should see "Invitation successfully destroyed" And "user1" should have "4" invitations + + Scenario: Translated email is sent when invitation request is declined by admin + Given a locale with translated emails + And invitations are required + And the user "user1" exists and is activated + And the user "notuser1" exists and is activated + And the user "user1" enables translated emails + And all emails have been delivered + When as "user1" I request some invites + And as "notuser1" I request some invites + And I view requests as an admin + And I press "Decline All" + Then "user1" should be emailed + And the email should have "Additional invitation request declined" in the subject + And the email to "user1" should be translated + Then "notuser1" should be emailed + And the email should have "Additional invitation request declined" in the subject + And the email to "notuser1" should be non-translated + + Scenario: Translated email is sent when new invitation is given to registered user + Given a locale with translated emails + And invitations are required + And the user "user1" exists and is activated + And the user "user1" enables translated emails + And all emails have been delivered + When as "user1" I request some invites + And an admin grants the request + Then "user1" should be emailed + And the email should have "New invitations" in the subject + And the email to "user1" should be translated \ No newline at end of file diff --git a/features/other_a/page_title.feature b/features/other_a/page_title.feature index bcd7f5b46d7..d192f9ecdb6 100644 --- a/features/other_a/page_title.feature +++ b/features/other_a/page_title.feature @@ -5,29 +5,41 @@ I want page titles to be readable Scenario: user reads a TOS or FAQ page When I go to the TOS page - Then the page title should include "TOS" + Then the page title should include "Terms of Service" When I go to the FAQ page Then the page title should include "FAQ" Scenario: Page title should respect user preference Given I am logged in as "author" - And I go to my preferences page - And I fill in "Browser page title format" with "FANDOM - AUTHOR - TITLE" - And I press "Update" - And I post the work "New Story" with fandom "Stargate" + And I go to my preferences page + And I fill in "Browser page title format" with "FANDOM - AUTHOR - TITLE" + And I press "Update" + And I post the work "New Story" with fandom "Stargate" When I view the work "New Story" Then the page title should include "Stargate - author - New Story" Scenario: Page title should change when tags are edited Given I am logged in as "author" - And I post the work "New Story" with fandom "Stargate" + And I post the work "New Story" with fandom "Stargate" When I view the work "New Story" Then the page title should include "Stargate" When I edit the work "New Story" - And I fill in "Fandoms" with "Harry Potter" - And I press "Post" + And I fill in "Fandoms" with "Harry Potter" + And I press "Post" When I view the work "New Story" Then the page title should include "Harry Potter" - And the page title should not include "Stargate" + And the page title should not include "Stargate" + +Scenario: Page title should be informative on the adult content notice page + + Given I am logged in as "author" + And I post the 2 chapter work "New Story" with fandom "Stargate" with rating "Mature" + When I am logged out + And I view the work "New Story" + Then I should see "This work could have adult content" + And the page title should include "New Story - author - Stargate" + When I follow the recent chapter link for the work "New Story" + Then I should see "This work could have adult content" + And the page title should include "New Story - Chapter 2 - author - Stargate" diff --git a/features/other_a/profile_edit.feature b/features/other_a/profile_edit.feature index 4f6186226ca..deb3c532e24 100644 --- a/features/other_a/profile_edit.feature +++ b/features/other_a/profile_edit.feature @@ -42,7 +42,7 @@ Scenario: Change details as an admin And I fill in "About Me" with "is it merely thy habit, to talk to dolls?" And I fill in "Ticket ID" with "fine" And I press "Update" - Then I should see "Ticket ID is not a number" + Then I should see "may begin with an # and otherwise contain only numbers." And the field labeled "Ticket ID" should contain "fine" When I fill in "Ticket ID" with "480000" And I press "Update" @@ -148,6 +148,17 @@ Scenario: Changing email address -- can't be the same as another user's And I should not see "foo@ao3.org" And I should see "bar@ao3.org" +Scenario: Changing email address -- Translated email is sent when user enables locale settings + Given a locale with translated emails + And the user "editname" enables translated emails + And all emails have been delivered + When I am logged in as "editname" + And I want to edit my profile + And I change my email + Then the email address "bar@ao3.org" should be emailed + And the email should have "Email changed" in the subject + And the email to email address "bar@ao3.org" should be translated + Scenario: Date of birth - under age When I enter a birthdate that shows I am under age diff --git a/features/other_a/pseuds.feature b/features/other_a/pseuds.feature index 077c1407e00..37826b9d433 100644 --- a/features/other_a/pseuds.feature +++ b/features/other_a/pseuds.feature @@ -263,9 +263,18 @@ Scenario: Change details as an admin And I fill in "Description" with "I'd probably be removing text." And I fill in "Ticket ID" with "no 💜" And I press "Update" - Then I should see "Ticket ID is not a number" + Then I should see "may begin with an # and otherwise contain only numbers" And the field labeled "Ticket ID" should contain "no 💜" + When I fill in "Ticket ID" with "#4798454#331" + And I press "Update" + Then I should see "may begin with an # and otherwise contain only numbers" + And the field labeled "Ticket ID" should contain "4798454#331" When I fill in "Ticket ID" with "47" + And I press "Update" + Then I should see "Pseud was successfully updated." + When I go to someone's pseuds page + And I follow "Edit alt" + When I fill in "Ticket ID" with "#47" And I press "Update" Then I should see "Pseud was successfully updated." When I go to someone's pseuds page diff --git a/features/other_b/subscriptions.feature b/features/other_b/subscriptions.feature index 8a5e7432e19..3b1b2fd0746 100644 --- a/features/other_b/subscriptions.feature +++ b/features/other_b/subscriptions.feature @@ -141,8 +141,10 @@ When I am on my subscriptions page Then I should see "My Subscriptions" And I should see "Awesome Series (Series)" + And I should see a link "series_author" And I should see "third_user" And I should see "Awesome Story (Work)" + And I should see a link "wip_author" When I follow "Series Subscriptions" Then I should see "My Series Subscriptions" And I should see "Awesome Series" diff --git a/features/other_b/support.feature b/features/other_b/support.feature index 060c38db00e..521674a5d9b 100644 --- a/features/other_b/support.feature +++ b/features/other_b/support.feature @@ -60,3 +60,28 @@ Feature: Filing a support request # The sanitizer adds the domain in front of relative image URLs as of AO3-6571 And the email should not contain "" But the email should contain "http://www.example.org/foo.jpgHi" + + Scenario: Submit a request with an on-Archive referer + + Given I am logged in as "puzzled" + And basic languages + And Zoho ticket creation is enabled + And "www.example.com" is a permitted Archive host + When I go to the works page + And I follow "Support & Feedback" + And I fill in "Brief summary" with "Just a brief note" + And I fill in "Your question or problem" with "Hi, I came from the Archive" + And I press "Send" + Then a Zoho ticket should be created with referer "http://www.example.com/works" + + Scenario: Submit a request with a referer that is not on-Archive + + Given I am logged in as "puzzled" + And basic languages + And Zoho ticket creation is enabled + When I go to the works page + And I follow "Support & Feedback" + And I fill in "Brief summary" with "Just a brief note" + And I fill in "Your question or problem" with "Hi, I didn't come from the Archive" + And I press "Send" + Then a Zoho ticket should be created with referer "Unknown URL" diff --git a/features/search/works_tags.feature b/features/search/works_tags.feature index 8069b26d401..fb8d3b53fb1 100644 --- a/features/search/works_tags.feature +++ b/features/search/works_tags.feature @@ -117,11 +117,11 @@ Feature: Search works by tag Scenario: Using the header search to exclude works with certain warnings using the warnings' filter_ids Given a set of works with various warnings for searching - When I search for works without the "Rape/Non-Con" and "Underage" filter_ids + When I search for works without the "Rape/Non-Con" and "Underage Sex" filter_ids Then the search summary should include the filter_id for "Rape/Non-Con" - And the search summary should include the filter_id for "Underage" + And the search summary should include the filter_id for "Underage Sex" And I should see "5 Found" - And the results should not contain the warning tag "Underage" + And the results should not contain the warning tag "Underage Sex" And the results should not contain the warning tag "Rape/Non-Con" Scenario: Searching by category returns all works using that category; search diff --git a/features/step_definitions/admin_steps.rb b/features/step_definitions/admin_steps.rb index e20e50c1324..fa8e38d8926 100644 --- a/features/step_definitions/admin_steps.rb +++ b/features/step_definitions/admin_steps.rb @@ -102,8 +102,8 @@ click_button("Update") end -Given /^I have posted known issues$/ do - step %{I am logged in as an admin} +Given "I have posted known issues" do + step %{I am logged in as a super admin} step %{I follow "Admin Posts"} step %{I follow "Known Issues" within "#header"} step %{I follow "make a new known issues post"} @@ -223,6 +223,10 @@ allow(Comment).to receive(:per_page).and_return(amount) end +Given "an archive FAQ category with the title {string} exists" do |title| + FactoryBot.create(:archive_faq, title: title) +end + ### WHEN When /^I visit the last activities item$/ do @@ -272,9 +276,18 @@ click_button("Post") end -When /^(\d+) Archive FAQs? exists?$/ do |n| - (1..n.to_i).each do |i| - FactoryBot.create(:archive_faq, id: i) +When "{int} Archive FAQ(s) exist(s)" do |n| + (1..n).each do |i| + FactoryBot.create(:archive_faq, id: i, title: "The #{i} FAQ") + end +end + +When "{int} Archive FAQ(s) with {int} question(s) exist(s)" do |faqs, questions| + (1..faqs).each do |i| + archive_faq = FactoryBot.create(:archive_faq, id: i) + (1..questions).each do + FactoryBot.create(:question, archive_faq: archive_faq) + end end end @@ -286,8 +299,7 @@ Resque.enqueue(AdminSetting, :check_queue) end -When /^I edit known issues$/ do - step %{I am logged in as an admin} +When "I edit known issues" do step %{I follow "Admin Posts"} step %{I follow "Known Issues" within "#header"} step %{I follow "Edit"} @@ -296,8 +308,7 @@ step %{I press "Post"} end -When /^I delete known issues$/ do - step %{I am logged in as an admin} +When "I delete known issues" do step %{I follow "Admin Posts"} step %{I follow "Known Issues" within "#header"} step %{I follow "Delete"} diff --git a/features/step_definitions/banner_steps.rb b/features/step_definitions/banner_steps.rb index e13c395bc51..e729d327001 100644 --- a/features/step_definitions/banner_steps.rb +++ b/features/step_definitions/banner_steps.rb @@ -127,12 +127,12 @@ page.should_not have_xpath("//div[@class=\"announcement group\"]") end -Then /^I should see the first login banner$/ do - step %{I should see "It looks like you've just logged into the Archive for the first time"} +Then "I should see the first login banner" do + step %{I should see "It looks like you've just logged in to AO3 for the first time."} end -Then /^I should not see the first login banner$/ do - step %{I should not see "It looks like you've just logged into the Archive for the first time"} +Then "I should not see the first login banner" do + step %{I should not see "It looks like you've just logged in to AO3 for the first time."} end Then /^I should see the first login popup$/ do diff --git a/features/step_definitions/email_custom_steps.rb b/features/step_definitions/email_custom_steps.rb index b9c3b2932a0..bb3cf14bb84 100644 --- a/features/step_definitions/email_custom_steps.rb +++ b/features/step_definitions/email_custom_steps.rb @@ -27,6 +27,18 @@ step(%{the email to "#{user}" should not contain "translation missing"}) # missing translations in the target language fall back to English end +Then "the email to email address {string} should be translated" do |email_address| + step(%{the email to email address "#{email_address}" should contain "Translated footer"}) + step(%{the email to email address "#{email_address}" should not contain "fan-run and fan-supported archive"}) # untranslated English text + step(%{the email to email address "#{email_address}" should not contain "translation missing"}) # missing translations in the target language fall back to English +end + +Then "the last email to {string} should be translated" do |user| + step(%{the last email to "#{user}" should contain "Translated footer"}) + step(%{the last email to "#{user}" should not contain "fan-run and fan-supported archive"}) # untranslated English text + step(%{the last email to "#{user}" should not contain "translation missing"}) # missing translations in the target language fall back to English +end + Then "the email to {string} should be non-translated" do |user| step(%{the email to "#{user}" should not contain "Translated footer"}) step(%{the email to "#{user}" should contain "fan-run and fan-supported archive"}) @@ -38,6 +50,10 @@ expect(emails("to: \"#{email_for(@user.email)}\"")).not_to be_empty end +Then "the email address {string} should be emailed" do |email_address| + expect(emails("to: \"#{email_for(email_address)}\"")).not_to be_empty +end + Then "{string} should not be emailed" do |user| @user = User.find_by(login: user) expect(emails("to: \"#{email_for(@user.email)}\"")).to be_empty @@ -54,6 +70,27 @@ end end +Then "the email to email address {string} should contain {string}" do |email_address, text| + email = emails("to: \"#{email_for(email_address)}\"").first + if email.multipart? + expect(email.text_part.body).to match(text) + expect(email.html_part.body).to match(text) + else + expect(email.body).to match(text) + end +end + +Then "the last email to {string} should contain {string}" do |user, text| + @user = User.find_by(login: user) + email = emails("to: \"#{email_for(@user.email)}\"").last + if email.multipart? + expect(email.text_part.body).to match(text) + expect(email.html_part.body).to match(text) + else + expect(email.body).to match(text) + end +end + Then "the email to {string} should not contain {string}" do |user, text| @user = User.find_by(login: user) email = emails("to: \"#{email_for(@user.email)}\"").first @@ -65,6 +102,27 @@ end end +Then "the email to email address {string} should not contain {string}" do |email_address, text| + email = emails("to: \"#{email_for(email_address)}\"").first + if email.multipart? + expect(email.text_part.body).not_to match(text) + expect(email.html_part.body).not_to match(text) + else + expect(email.body).not_to match(text) + end +end + +Then "the last email to {string} should not contain {string}" do |user, text| + @user = User.find_by(login: user) + email = emails("to: \"#{email_for(@user.email)}\"").last + if email.multipart? + expect(email.text_part.body).not_to match(text) + expect(email.html_part.body).not_to match(text) + else + expect(email.body).not_to match(text) + end +end + Then "{string} should receive {int} email(s)" do |user, count| @user = User.find_by(login: user) expect(emails("to: \"#{email_for(@user.email)}\"").size).to eq(count.to_i) diff --git a/features/step_definitions/icon_steps.rb b/features/step_definitions/icon_steps.rb index 0cb200d2d80..203b261fa65 100644 --- a/features/step_definitions/icon_steps.rb +++ b/features/step_definitions/icon_steps.rb @@ -42,12 +42,12 @@ Then /^the "([^"]*)" collection should have an icon$/ do |title| collection = Collection.find_by(title: title) - assert !collection.icon_file_name.blank? + assert collection.icon.attached? end Then /^the "([^"]*)" collection should not have an icon$/ do |title| collection = Collection.find_by(title: title) - assert collection.icon_file_name.blank? + assert !collection.icon.attached? end ### THEN diff --git a/features/step_definitions/invite_steps.rb b/features/step_definitions/invite_steps.rb index 3011381e863..6adc74971ef 100644 --- a/features/step_definitions/invite_steps.rb +++ b/features/step_definitions/invite_steps.rb @@ -128,6 +128,16 @@ def invite(attributes = {}) step %{I press "Send Request"} end +When "as {string} I request some invites" do |user| + step %{I am logged in as "#{user}"} + step %{I go to my user page} + step %{I follow "Invitations"} + step %{I follow "Request invitations"} + step %{I fill in "How many invitations would you like? (max 10)" with "3"} + step %{I fill in "Please specify why you'd like them:" with "I want them for a friend"} + step %{I press "Send Request"} +end + When /^I view requests as an admin$/ do step %{I am logged in as an admin} step %{I follow "Invitations"} diff --git a/features/step_definitions/support_steps.rb b/features/step_definitions/support_steps.rb new file mode 100644 index 00000000000..42e22148ddb --- /dev/null +++ b/features/step_definitions/support_steps.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +Given "Zoho ticket creation is enabled" do + allow_any_instance_of(Feedback).to receive(:zoho_enabled?).and_return(true) + WebMock.stub_request(:get, %r{/contacts/search}) + .to_return(headers: { content_type: "application/json" }, body: '{"data":[{"id":"1"}]}') + WebMock.stub_request(:post, %r{/tickets}) + .to_return(headers: { content_type: "application/json" }, body: '{"id":"3"}') +end + +Given "{string} is a permitted Archive host" do |host| + allow(ArchiveConfig).to receive(:PERMITTED_HOSTS).and_return([host]) +end + +Then "a Zoho ticket should be created with referer {string}" do |referer| + # rubocop:disable Lint/AmbiguousBlockAssociation + expect(WebMock).to have_requested(:post, "https://desk.zoho.com/api/v1/tickets") + .with { |req| JSON.parse(req.body)["cf"]["cf_url"] == referer } + # rubocop:enable Lint/AmbiguousBlockAssociation +end diff --git a/features/step_definitions/tag_steps.rb b/features/step_definitions/tag_steps.rb index 03c2874f5b7..2d88ae3d455 100644 --- a/features/step_definitions/tag_steps.rb +++ b/features/step_definitions/tag_steps.rb @@ -84,10 +84,10 @@ fandom.add_association media end -Given /^I add the fandom "([^\"]*)" to the character "([^\"]*)"$/ do |fandom, character| - char = Character.find_or_create_by(name: character) +Given "I add the fandom {string} to the tag/character {string}" do |fandom, tag| + tag = Tag.find_or_create_by(name: tag) fand = Fandom.find_or_create_by_name(fandom) - char.add_association(fand) + tag.add_association(fand) end Given /^a canonical character "([^\"]*)" in fandom "([^\"]*)"$/ do |character, fandom| @@ -197,7 +197,7 @@ end Given /^I have posted a Wrangling Guideline?(?: titled "([^\"]*)")?$/ do |title| - step %{I am logged in as an admin} + step %{I am logged in as a "tag_wrangling" admin} visit new_wrangling_guideline_path if title fill_in("Guideline text", with: "This is a page about how we wrangle things.") @@ -427,6 +427,11 @@ assert tag.type == tag_type end +Then "the {string} tag should be an unsorted tag" do |tagname| + tag = Tag.find_by(name: tagname) + expect(tag).to be_a(UnsortedTag) +end + Then(/^the "([^"]*)" tag should (be|not be) canonical$/) do |tagname, canonical| tag = Tag.find_by(name: tagname) expected = canonical == "be" diff --git a/features/step_definitions/user_steps.rb b/features/step_definitions/user_steps.rb index e779c02a7a3..a742a90c7ea 100644 --- a/features/step_definitions/user_steps.rb +++ b/features/step_definitions/user_steps.rb @@ -208,12 +208,13 @@ user.update!(failed_attempts: count.to_i) end -When /^I fill in the sign up form with valid data$/ do +When "I fill in the sign up form with valid data" do step(%{I fill in "user_registration_login" with "#{NEW_USER}"}) step(%{I fill in "user_registration_email" with "test@archiveofourown.org"}) step(%{I fill in "user_registration_password" with "password1"}) step(%{I fill in "user_registration_password_confirmation" with "password1"}) step(%{I check "user_registration_age_over_13"}) + step(%{I check "user_registration_data_processing"}) step(%{I check "user_registration_terms_of_service"}) end diff --git a/features/step_definitions/work_import_steps.rb b/features/step_definitions/work_import_steps.rb index 7b080a57e22..16701b894ca 100644 --- a/features/step_definitions/work_import_steps.rb +++ b/features/step_definitions/work_import_steps.rb @@ -2,7 +2,7 @@ def content_fields { - title: "Detected Title", summary: "Detected summary", fandoms: "Detected Fandom", warnings: "Underage", + title: "Detected Title", summary: "Detected summary", fandoms: "Detected Fandom", warnings: "Underage Sex", characters: "Detected 1, Detected 2", rating: "Explicit", relationships: "Detected 1/Detected 2", categories: "F/F", freeform: "Detected tag 1, Detected tag 2", external_author_name: "Detected Author", external_author_email: "detected@foo.com", notes: "This is a content note.", @@ -72,6 +72,17 @@ def mock_external step %{I select "English" from "Choose a language"} end +When "I import the mock work {string} by {string} with email {string} and by {string} with email {string}" do |url, creator_name, creator_email, cocreator_name, cocreator_email| + step(%{I start importing "#{url}" with a mock website as an archivist}) + step(%{I check "Import for others ONLY with permission"}) + step(%{I fill in "external_author_name" with "#{creator_name}"}) + step(%{I fill in "external_author_email" with "#{creator_email}"}) + step(%{I fill in "external_coauthor_name" with "#{cocreator_name}"}) + step(%{I fill in "external_coauthor_email" with "#{cocreator_email}"}) + step(%{I check "Post without previewing"}) + step(%{I press "Import"}) +end + When /^I import "(.*)"( with a mock website)?$/ do |url, mock| step %{I start importing "#{url}"#{mock}} step %{I press "Import"} diff --git a/features/support/paths.rb b/features/support/paths.rb index 237112b7b48..55e6cad5bab 100644 --- a/features/support/paths.rb +++ b/features/support/paths.rb @@ -297,6 +297,8 @@ def path_to(page_name) new_user_password_path when /^the edit user password page$/i edit_user_password_path + when /^the (.*) mass bin$/i + tag_wranglings_path(show: Regexp.last_match(1).pluralize) # Admin Pages when /^the admin-posts page$/i diff --git a/features/tag_sets/tag_set_nominations.feature b/features/tag_sets/tag_set_nominations.feature index d82f636159b..aee7353548b 100644 --- a/features/tag_sets/tag_set_nominations.feature +++ b/features/tag_sets/tag_set_nominations.feature @@ -322,3 +322,19 @@ Feature: Nominating and reviewing nominations for a tag set And I should not see "Someone else has already nominated the tag for this set but in fandom First." And I should not see "Gold" And I should see "None nominated in this fandom." + + Scenario: A nominated canonical tag can be renamed by a tag wrangling admin without affecting the nomination + Given a canonical character "Before" in fandom "Treasure Chest" + And I am logged in as "tagsetter" + And I set up the nominated tag set "Nominated Tags" with 1 fandom nom and 1 character nom + And I nominate fandom "Treasure Chest" and character "Before" in "Nominated Tags" as "tagsetter" + When I am logged in as an "tag_wrangling" admin + And I edit the tag "Before" + And I fill in "Name" with "After" + And I press "Save changes" + Then I should see "Tag was updated." + When I am logged in as "tagsetter" + And I go to the "Nominated Tags" tag set page + And I follow "My Nominations" + Then I should see "Treasure Chest" + And I should see "Before" diff --git a/features/tags_and_wrangling/brand_new_fandoms.feature b/features/tags_and_wrangling/brand_new_fandoms.feature index 57e5c0991f4..501d02204be 100644 --- a/features/tags_and_wrangling/brand_new_fandoms.feature +++ b/features/tags_and_wrangling/brand_new_fandoms.feature @@ -60,19 +60,7 @@ Feature: Brand new fandoms And I post a work "My New Work" with fandom "My Brand New Fandom" And the periodic tag count task is run And all indexing jobs have been run - When I follow "Tag Wrangling" within "#header" - And I follow "Fandoms by media" - Then I should see "My Brand New Fandom" - - Scenario: Fandoms used only on external works should be visible to wranglers. - Given I am logged in as a tag wrangler - And I set up an external work - And I fill in "Fandoms" with "My Brand New Fandom" - And I submit - And the periodic tag count task is run - And all indexing jobs have been run - When I follow "Tag Wrangling" within "#header" - And I follow "Fandoms by media" + When I go to the fandom mass bin Then I should see "My Brand New Fandom" Scenario: When the only work with a brand new fandom is destroyed, the fandom should not be visible to tag wranglers. @@ -86,25 +74,41 @@ Feature: Brand new fandoms Then I should see "Your work My New Work was deleted." When the periodic tag count task is run And all indexing jobs have been run - And I follow "Tag Wrangling" within "#header" - And I follow "Fandoms by media" + And I go to the fandom mass bin Then I should not see "My Brand New Fandom" - Scenario: When the only external work with a brand new fandom is destroyed, the fandom should not be visible to tag wranglers. + Scenario: Fandoms used only on external works should not be visible to wranglers. Given I am logged in as a tag wrangler And I set up an external work - And I fill in "Title" with "External Work To Be Deleted" And I fill in "Fandoms" with "My Brand New Fandom" And I submit And the periodic tag count task is run And all indexing jobs have been run - When I am logged in as a "policy_and_abuse" admin - And I view the external work "External Work To Be Deleted" - And I follow "Delete External Work" - Then I should see "Item was successfully deleted." - When the periodic tag count task is run + When I go to the fandom mass bin + Then I should not see "My Brand New Fandom" + + Scenario: When a brand new fandom used only on external works is tagged on a work, the fandom should be visible to tag wranglers. + Given I am logged in as a tag wrangler + And I set up an external work + And I fill in "Fandoms" with "My Brand New Fandom" + And I submit + When I post the work "Great work" with fandom "My Brand New Fandom" + And the periodic tag count task is run And all indexing jobs have been run And I am logged in as a tag wrangler - And I follow "Tag Wrangling" within "#header" - And I follow "Fandoms by media" + And I go to the fandom mass bin + Then I should see "My Brand New Fandom" + + Scenario: When the only draft using a brand new fandom is published, the fandom should be visible to tag wranglers. + Given I am logged in as a tag wrangler + And I set up the draft "Generic Work" with fandom "My Brand New Fandom" + And I press "Preview" + And the periodic tag count task is run + And all indexing jobs have been run + When I go to the fandom mass bin Then I should not see "My Brand New Fandom" + When I post the work "Generic Work" + And the periodic tag count task is run + And all indexing jobs have been run + And I go to the fandom mass bin + Then I should see "My Brand New Fandom" diff --git a/features/tags_and_wrangling/tag_wrangling.feature b/features/tags_and_wrangling/tag_wrangling.feature index 10c6e7b9544..680d9822844 100644 --- a/features/tags_and_wrangling/tag_wrangling.feature +++ b/features/tags_and_wrangling/tag_wrangling.feature @@ -28,7 +28,7 @@ Feature: Tag wrangling And I am logged in as a tag wrangler When I go to my wrangling page Then I should see "Wrangling Home" - And I should see "Fandoms by media (3)" + And I should see "Fandoms by media (2)" And I should see "Characters by fandom (2)" And I should see "Relationships by fandom (1)" When I follow @@ -40,7 +40,7 @@ Feature: Tag wrangling | "Wrangling Tools" | "Tag Wrangling" | | "Characters by fandom (2)" | "Mass Wrangle New/Unwrangled Tags" | | "Relationships by fandom (1)" | "Mass Wrangle New/Unwrangled Tags" | - | "Fandoms by media (3)" | "Mass Wrangle New/Unwrangled Tags" | + | "Fandoms by media (2)" | "Mass Wrangle New/Unwrangled Tags" | Scenario: Edit tag page Given the tag wrangling setup @@ -81,7 +81,7 @@ Feature: Tag wrangling Then I should see "Stargate SG-1" And I should see "wrangler" within "ul.wranglers" - Scenario: Making a character canonical and assiging it to a fandom + Scenario: Making a character canonical and assigning it to a fandom Given the tag wrangling setup And I have a canonical "TV Shows" fandom tag named "Stargate SG-1" And I am logged in as a tag wrangler @@ -311,7 +311,7 @@ Feature: Tag wrangling Scenario: An admin can see the troubleshoot button on a tag page Given a canonical fandom "Cowboy Bebop" - And I am logged in as an admin + And I am logged in as a "tag_wrangling" admin When I view the tag "Cowboy Bebop" Then I should see "Troubleshoot" diff --git a/features/tags_and_wrangling/tag_wrangling_admin.feature b/features/tags_and_wrangling/tag_wrangling_admin.feature index 5d70da67b07..9768229853b 100644 --- a/features/tags_and_wrangling/tag_wrangling_admin.feature +++ b/features/tags_and_wrangling/tag_wrangling_admin.feature @@ -1,11 +1,18 @@ @users @tag_wrangling @admin Feature: Tag wrangling - Scenario: Admin can rename a tag + Scenario: Admin can rename a tag and it updates works and bookmarks. - Given I am logged in as an admin - And a fandom exists with name: "Amelie", canonical: false - When I edit the tag "Amelie" + Given I am logged in as "audrey" with password "password" + And I post the work "Renoir's Boating Party" + And I bookmark the work "Renoir's Boating Party" with the tags "Amelie" + And I post the work "Luncheon" with fandom "Amelie" + # Visit the relevant pages to make sure the data gets cached. + And I go to my bookmarks page + And I go to my works page + And I go to the work "Luncheon" + When I am logged in as a "tag_wrangling" admin + And I edit the tag "Amelie" And I fill in "Synonym of" with "Amélie" And I press "Save changes" Then I should see "Amélie is considered the same as Amelie by the database" @@ -15,10 +22,19 @@ Feature: Tag wrangling Then I should see "Tag was updated" And I should see "Amélie" And I should not see "Amelie" + When I go to audrey's works page + Then I should not see "Amelie" + And I should see "Amélie" + When I go to the work "Luncheon" + Then I should not see "Amelie" + And I should see "Amélie" + When I go to audrey's bookmarks page + Then I should not see "Amelie" + And I should see "Amélie" Scenario: Admin can rename a tag using Eastern characters - Given I am logged in as an admin + Given I am logged in as a "tag_wrangling" admin And a fandom exists with name: "先生", canonical: false When I edit the tag "先生" And I fill in "Name" with "てりやき" @@ -70,3 +86,87 @@ Feature: Tag wrangling Then I should see "Tags Wrangled (CSV)" When I follow "Tags Wrangled (CSV)" Then I should download a csv file with the header row "Name Last Updated Type Merger Fandoms Unwrangleable" + + Scenario Outline: Authorized admins have the tag wrangling item in the admin navbar + + Given I am logged in as a "" admin + Then I should see "Tag Wrangling" within "ul.admin.primary.navigation" + + Examples: + | role | + | superadmin | + | tag_wrangling | + + Scenario Outline: Unauthorized admins do not have the tag wrangling item in the admin navbar + + Given I am logged in as a "" admin + Then I should not see "Tag Wrangling" within "ul.admin.primary.navigation" + + Examples: + | role | + | board | + | board_assistants_team | + | communications | + | development_and_membership | + | docs | + | elections | + | legal | + | translation | + | support | + | policy_and_abuse | + | open_doors | + + Scenario Outline: Fully-authorized admins get the wrangling dashboard sidebar + + Given I am logged in as a "" admin + And basic tags + When I go to the tags page + Then I should see "Wrangling Tools" within "div#dashboard" + And I should see "Wranglers" within "div#dashboard" + And I should see "Search Tags" within "div#dashboard" + And I should see "New Tag" within "div#dashboard" + But I should not see "Wrangling Home" within "div#dashboard" + + Examples: + | role | + | superadmin | + | tag_wrangling | + + Scenario Outline: Read-authorized admins get a partial wrangling dashboard sidebar + + Given I am logged in as a "" admin + And basic tags + When I go to the tags page + Then I should see "Wrangling Tools" within "div#dashboard" + And I should see "Search Tags" within "div#dashboard" + But I should not see "Wranglers" within "div#dashboard" + And I should not see "New Tag" within "div#dashboard" + And I should not see "Wrangling Home" within "div#dashboard" + + Examples: + | role | + | policy_and_abuse | + + Scenario Outline: Unauthorized admins do not get the wrangling dashboard sidebar + + Given I am logged in as a "" admin + And basic tags + When I go to the tags page + Then I should not see "Wrangling Tools" + And I should not see "Wranglers" + And I should not see "Search Tags" + And I should not see "New Tag" + And I should not see "Wrangling Home" + + Examples: + | role | + | board | + | board_assistants_team | + | communications | + | development_and_membership | + | docs | + | elections | + | legal | + | translation | + | support | + | open_doors | diff --git a/features/tags_and_wrangling/tag_wrangling_characters.feature b/features/tags_and_wrangling/tag_wrangling_characters.feature index 9d65608c959..6c9c2e3561f 100644 --- a/features/tags_and_wrangling/tag_wrangling_characters.feature +++ b/features/tags_and_wrangling/tag_wrangling_characters.feature @@ -53,7 +53,7 @@ Scenario: character wrangling - syns, mergers, characters, autocompletes When I follow "Edit The First Doctor" Then I should not see "Make tag non-canonical and unhook all associations" - Given I am logged in as an admin + Given I am logged in as a "tag_wrangling" admin When I edit the tag "The First Doctor" Then I should see "Make tag non-canonical and unhook all associations" And I should see "The Doctor (1st)" @@ -129,7 +129,7 @@ Scenario: character wrangling - syns, mergers, characters, autocompletes When I follow "First Doctor" Then I should see "John Smith" And I should see "The Doctor" - When I am logged in as an admin + When I am logged in as a "tag_wrangling" admin And I edit the tag "First Doctor" And I fill in "Synonym of" with "First Doctor (DW)" And I press "Save changes" diff --git a/features/tags_and_wrangling/tag_wrangling_fandoms.feature b/features/tags_and_wrangling/tag_wrangling_fandoms.feature index 7ca8a94ceb9..a440ccfd39f 100644 --- a/features/tags_and_wrangling/tag_wrangling_fandoms.feature +++ b/features/tags_and_wrangling/tag_wrangling_fandoms.feature @@ -116,7 +116,7 @@ Scenario: fandoms wrangling - syns, mergers, autocompletes, metatags When I edit the tag "Stargate SG-1" Then I should see "Stargate SG-1: Ark of Truth" within "div#child_SubTag_associations_to_remove_checkboxes" And I should see "Stargate Franchise" within "div#parent_MetaTag_associations_to_remove_checkboxes" - When I am logged in as an admin + When I am logged in as a "tag_wrangling" admin And I edit the tag "Stargate SG-1" And I fill in "Synonym of" with "Stargate SG-1: Greatest Show in the Universe" And I press "Save changes" diff --git a/features/tags_and_wrangling/tag_wrangling_freeforms.feature b/features/tags_and_wrangling/tag_wrangling_freeforms.feature index 5f5e2a18627..f1a8786b92f 100644 --- a/features/tags_and_wrangling/tag_wrangling_freeforms.feature +++ b/features/tags_and_wrangling/tag_wrangling_freeforms.feature @@ -95,7 +95,7 @@ Scenario: freeforms wrangling - syns, mergers, autocompletes, metatags Then I should see "Tag was updated" When I follow "Alternate Universe Pirates" Then I should see "Alternate Universe Space Pirates" - When I am logged in as an admin + When I am logged in as a "tag_wrangling" admin And I edit the tag "Alternate Universe Pirates" And I fill in "Synonym of" with "Alternate Universe Pirrrates" And I press "Save changes" diff --git a/features/tags_and_wrangling/tag_wrangling_more.feature b/features/tags_and_wrangling/tag_wrangling_more.feature index 4ebfef407b2..95dbaca5136 100644 --- a/features/tags_and_wrangling/tag_wrangling_more.feature +++ b/features/tags_and_wrangling/tag_wrangling_more.feature @@ -151,9 +151,9 @@ Feature: Tag wrangling: assigning wranglers, using the filters on the Wranglers And the following typed tags exists | name | type | canonical | | Cowboy Bebop | Fandom | true | + And I post the work "Honky Tonk Women" with fandom "Cowboy Bebop" And all indexing jobs have been run - When I go to the wrangling tools page - And I follow "Fandoms by media (1)" + When I go to the fandom mass bin And I check the wrangling option for "Cowboy Bebop" And I select "Anime & Manga" from "Wrangle to Media" And I press "Wrangle" @@ -166,9 +166,9 @@ Feature: Tag wrangling: assigning wranglers, using the filters on the Wranglers | name | type | canonical | | Toby Daye/Tybalt | Relationship | true | | October Daye Series - Seanan McGuire | Fandom | false | + And I post the work "Honky Tonk Women" with fandom "October Daye Series - Seanan McGuire" with relationship "Toby Daye/Tybalt" And all indexing jobs have been run - When I go to the wrangling tools page - And I follow "Relationships by fandom (1)" + When I go to the relationship mass bin And I check the wrangling option for "Toby Daye/Tybalt" And I fill in "Wrangle to Fandom(s)" with "October Daye Series - Seanan McGuire" And I press "Wrangle" @@ -181,9 +181,9 @@ Feature: Tag wrangling: assigning wranglers, using the filters on the Wranglers | name | type | canonical | | Toby Daye/Tybalt | Relationship | true | | October Daye Series - Seanan McGuire | Fandom | true | + And I post the work "Honky Tonk Women" with fandom "October Daye Series - Seanan McGuire" with relationship "Toby Daye/Tybalt" And all indexing jobs have been run - When I go to the wrangling tools page - And I follow "Relationships by fandom (1)" + When I go to the relationship mass bin And I check the wrangling option for "Toby Daye/Tybalt" And I fill in "Wrangle to Fandom(s)" with "October Daye Series - Seanan McGuire" And I press "Wrangle" @@ -198,8 +198,7 @@ Feature: Tag wrangling: assigning wranglers, using the filters on the Wranglers | Ed | Character | false | And I post the work "Honky Tonk Women" with fandom "Cowboy Bebop" with character "Faye Valentine" with second character "Ed" And all indexing jobs have been run - When I go to the wrangling tools page - And I follow "Characters by fandom (2)" + When I go to the character mass bin And I fill in "Wrangle to Fandom(s)" with "Cowboy Bebop" And I check the canonical option for the tag "Faye Valentine" And I check the canonical option for the tag "Ed" @@ -222,7 +221,7 @@ Feature: Tag wrangling: assigning wranglers, using the filters on the Wranglers When I am logged in as a random user And I view the tag "Cowboy Bebop" Then I should see "Sorry, you don't have permission to access the page you were trying to reach." - When I am logged in as an admin + When I am logged in as a "tag_wrangling" admin And I view the tag "Cowboy Bebop" Then I should not see "Please log in as an admin" And I should see "Cowboy Bebop" @@ -259,3 +258,81 @@ Feature: Tag wrangling: assigning wranglers, using the filters on the Wranglers And all indexing jobs have been run And I view the unwrangled relationship bin for "Canonical Character" Then I should not see "Syn Character/OC" + + Scenario: Tags from draft works don't show in unwrangled bins + Given a canonical fandom "Testing" + And I am logged in as a tag wrangler + And I set up the draft "Generic Work" with fandom "Testing" with character "draft char" with freeform "draft freeform" with relationship "draft rel" + And I press "Preview" + And the periodic tag count task is run + And all indexing jobs have been run + And I flush the wrangling sidebar caches + When I view the unwrangled character bin for "Testing" + Then I should not see "draft char" + When I view the unwrangled freeform bin for "Testing" + Then I should not see "draft freeform" + When I view the unwrangled relationship bin for "Testing" + Then I should not see "draft rel" + When I go to the wrangling tools page + And I follow "Characters by fandom (0)" + Then I should not see "draft char" + When I follow "Freeforms by fandom (0)" + Then I should not see "draft freeform" + When I follow "Relationships by fandom (0)" + Then I should not see "draft rel" + + Scenario: When the only draft using a tag is posted, the tag shows up in unwrangled bins + Given a canonical fandom "Testing" + And I am logged in as a tag wrangler + And I set up the draft "Generic Work" with fandom "Testing" with character "draft char" + And I press "Preview" + And the periodic tag count task is run + And all indexing jobs have been run + When I view the unwrangled character bin for "Testing" + Then I should not see "draft char" + When I post the work "Generic Work" + And the periodic tag count task is run + And all indexing jobs have been run + And I flush the wrangling sidebar caches + And I view the unwrangled character bin for "Testing" + Then I should see "draft char" + When I go to the wrangling tools page + And I follow "Characters by fandom (1)" + Then I should see "draft char" + + Scenario: Tags from bookmarks don't show up in unwrangled bins after being sorted and assigned to a fandom + Given a canonical fandom "Testing" + And I am logged in as a tag wrangler + And I post the work "Generic Work" + And I bookmark the work "Generic Work" with the tags "bookmark rel tag, bookmark char tag" + When I go to the unsorted_tags page + And I select "Relationship" for the unsorted tag "bookmark rel tag" + And I select "Character" for the unsorted tag "bookmark char tag" + And I press "Update" + Then I should see "Tags were successfully sorted" + And the "bookmark rel tag" tag should be a "Relationship" tag + And the "bookmark char tag" tag should be a "Character" tag + When the periodic tag count task is run + And all indexing jobs have been run + And I flush the wrangling sidebar caches + And I go to the wrangling tools page + And I follow "Characters by fandom (0)" + Then I should not see "bookmark char tag" + When I follow "Relationships by fandom (0)" + Then I should not see "bookmark rel tag" + When I add the fandom "Testing" to the tag "bookmark char tag" + And I add the fandom "Testing" to the tag "bookmark rel tag" + Then the "bookmark char tag" tag should be in the "Testing" fandom + And the "bookmark rel tag" tag should be in the "Testing" fandom + And the periodic tag count task is run + And all indexing jobs have been run + And I flush the wrangling sidebar caches + When I view the unwrangled character bin for "Testing" + Then I should not see "bookmark char tag" + When I view the unwrangled relationship bin for "Testing" + Then I should not see "bookmark rel tag" + When I go to the wrangling tools page + And I follow "Characters by fandom (0)" + Then I should not see "bookmark char tag" + When I follow "Relationships by fandom (0)" + Then I should not see "bookmark rel tag" diff --git a/features/tags_and_wrangling/tag_wrangling_relationships.feature b/features/tags_and_wrangling/tag_wrangling_relationships.feature index 5c49e38dabd..f9977998532 100644 --- a/features/tags_and_wrangling/tag_wrangling_relationships.feature +++ b/features/tags_and_wrangling/tag_wrangling_relationships.feature @@ -14,7 +14,7 @@ Scenario: relationship wrangling - syns, mergers, characters, autocompletes And a canonical character "Zoe Washburne" And a canonical character "Jack Harkness" And a canonical character "Ianto Jones" - And I am logged in as an admin + And I am logged in as a "tag_wrangling" admin And I follow "Tag Wrangling" # create a new canonical relationship from tag wrangling interface @@ -126,7 +126,7 @@ Scenario: relationship wrangling - syns, mergers, characters, autocompletes When I follow "Jack Harkness/Ianto Jones" Then I should see "Jack Harkness/Robot Ianto Jones" And I should see "Jack Harkness/Male Character" - When I am logged in as an admin + When I am logged in as a "tag_wrangling" admin And I edit the tag "Jack Harkness/Ianto Jones" And I fill in "Synonym of" with "Captain Jack Harkness/Ianto Jones" And I press "Save changes" @@ -270,7 +270,7 @@ Scenario: AO3-2147 Creating a new merger to a non-can tag while adding character And I should see "Testypants/Testyskirt" And the "Canonical" checkbox should be checked and disabled - When I am logged in as an admin + When I am logged in as a "tag_wrangling" admin And I edit the tag "Testing McTestypants/Testing McTestySkirt" And I fill in "Synonym of" with "Dame Tester/Sir Tester" And I press "Save changes" diff --git a/features/tags_and_wrangling/tag_wrangling_unsorted.feature b/features/tags_and_wrangling/tag_wrangling_unsorted.feature index dfc829f45fd..b95e004b62c 100644 --- a/features/tags_and_wrangling/tag_wrangling_unsorted.feature +++ b/features/tags_and_wrangling/tag_wrangling_unsorted.feature @@ -58,3 +58,49 @@ Feature: Tag Wrangling - Unsorted Tags When I select "UnsortedTag" from "tag_type" And I press "Save changes" Then I should see "Tag was updated." + + Scenario Outline: Editing unsorted tags as a fully authorized admin + Given an unsorted_tag exists with name: "Admin unsorted tag" + And I am logged in as a "" admin + When I go to the unsorted_tags page + And I select "Freeform" for the unsorted tag "Admin unsorted tag" + And I press "Update" + Then I should see "Tags were successfully sorted" + And the "Admin unsorted tag" tag should be a "Freeform" tag + + Examples: + | role | + | superadmin | + | tag_wrangling | + + Scenario Outline: Editing unsorted tags as a view-only admin + Given an unsorted_tag exists with name: "Admin unsorted tag" + And I am logged in as a "" admin + When I go to the unsorted_tags page + And I select "Freeform" for the unsorted tag "Admin unsorted tag" + And I press "Update" + Then I should see "Sorry, only an authorized admin can access the page you were trying to reach." + And the "Admin unsorted tag" tag should be an unsorted tag + + Examples: + | role | + | policy_and_abuse | + + Scenario Outline: Editing unsorted tags as an unauthorized admin + Given an unsorted_tag exists with name: "Admin unsorted tag" + And I am logged in as a "" admin + When I go to the unsorted_tags page + Then I should see "Sorry, only an authorized admin can access the page you were trying to reach." + + Examples: + | role | + | board | + | board_assistants_team | + | communications | + | development_and_membership | + | docs | + | elections | + | legal | + | translation | + | support | + | open_doors | diff --git a/features/tags_and_wrangling/wrangling_guidelines.feature b/features/tags_and_wrangling/wrangling_guidelines.feature index 138b246acac..837b0709ed2 100644 --- a/features/tags_and_wrangling/wrangling_guidelines.feature +++ b/features/tags_and_wrangling/wrangling_guidelines.feature @@ -6,7 +6,7 @@ Feature: Wrangling Guidelines Scenario: Post a Wrangling Guideline - Given I am logged in as an admin + Given I am logged in as a "tag_wrangling" admin And I am on the wrangling guidelines page And I follow "New Wrangling Guideline" And I fill in "Guideline text" with "This series of documents (Wrangling Guidelines) are intended to help tag wranglers remain consistent as they go about the business of wrangling tags by providing a set of formatting guidelines." @@ -29,7 +29,7 @@ Feature: Wrangling Guidelines Scenario: Reorder Wrangling Guidelines - Given I am logged in as an admin + Given I am logged in as a "tag_wrangling" admin And 3 Wrangling Guidelines exist When I go to the Wrangling Guidelines reorder page And I fill in "wrangling_guidelines_1" with "3" @@ -44,7 +44,7 @@ Feature: Wrangling Guidelines Scenario: Delete Wrangling Guideline - Given I am logged in as an admin + Given I am logged in as a "tag_wrangling" admin And I have posted a Wrangling Guideline titled "Relationship Tags" When I go to the Wrangling Guidelines page And I follow "Delete" diff --git a/features/users/suspensions.feature b/features/users/suspensions.feature new file mode 100644 index 00000000000..2ef5b37f936 --- /dev/null +++ b/features/users/suspensions.feature @@ -0,0 +1,17 @@ +Feature: Suspensions + + Scenario: Users suspended on 2024-01-11 before the unban threshold can see they will be unbanned on 2024-02-10 + Given the user "mrparis" exists and is activated + And it is currently 2024-01-11 01:00 AM + And the user "mrparis" is suspended + And I am logged in as "mrparis" + And I go to the new work page + Then I should see "suspended until Sat 10 Feb 2024" + + Scenario: Users suspended on 2024-01-11 after the unban threshold can see they will be unbanned on 2024-02-11 + Given the user "mrparis" exists and is activated + And it is currently 2024-01-11 08:00 PM + And the user "mrparis" is suspended + And I am logged in as "mrparis" + And I go to the new work page + Then I should see "suspended until Sun 11 Feb 2024" diff --git a/features/users/user_create.feature b/features/users/user_create.feature index b769db82b02..a0097dca726 100644 --- a/features/users/user_create.feature +++ b/features/users/user_create.feature @@ -30,21 +30,22 @@ Feature: Sign Up for a new account Then I should see "" And I should not see "Almost Done!" Examples: - | field | error | - | user_registration_age_over_13 | Sorry, you have to be over 13! | - | user_registration_terms_of_service | Sorry, you need to accept the Terms of Service in order to sign up. | + | field | error | + | user_registration_age_over_13 | Sorry, you have to be over 13! | + | user_registration_terms_of_service | Sorry, you need to accept the Terms of Service in order to sign up. | + | user_registration_data_processing | Sorry, you need to consent to the processing of your personal data in order to sign up. | Scenario: The user should be able to sign up after fixing form errors. When I fill in the sign up form with valid data And I fill in "Valid email" with "lyingrobot@example.com" - And I uncheck "Yes, I have read the Terms of Service and agree to them." + And I uncheck "Yes, I have read the Terms of Service, including the Content Policy and Privacy Policy, and agree to them." And I press "Create Account" Then I should see "Sorry, you need to accept the Terms of Service in order to sign up." And I should not see "Sorry, you have to be over 13!" # Email should be what the user filled in, not the invitee email on the invitation And I should see "lyingrobot@example.com" in the "Valid email" input - When I check "Yes, I have read the Terms of Service and agree to them." + When I check "Yes, I have read the Terms of Service, including the Content Policy and Privacy Policy, and agree to them." And I fill in "Password" with "password" And I fill in "Confirm password" with "password" And all emails have been delivered diff --git a/features/works/work_drafts.feature b/features/works/work_drafts.feature index 168daf7753d..d3608c9d658 100644 --- a/features/works/work_drafts.feature +++ b/features/works/work_drafts.feature @@ -69,6 +69,7 @@ Feature: Work Drafts Then I should see "Drafts (1)" When I follow "Drafts (1)" Then I should see "draft to post" + And the page title should include "drafter - Drafts" And I should see "Post Draft" within "#main .own.work.blurb .actions" And I should see "Delete Draft" within "#main .own.work.blurb .actions" When I follow "Post Draft" diff --git a/lib/css_cleaner.rb b/lib/css_cleaner.rb index b5a25b4ee0d..8c3f278beda 100644 --- a/lib/css_cleaner.rb +++ b/lib/css_cleaner.rb @@ -227,7 +227,7 @@ def sanitize_css_content(value) return value if value =~ /^\"([^\"]*)\"$/ # or a valid img url - return value if value.match(URL_FUNCTION_REGEX) + return value if value.match(Regexp.new("^#{URL_FUNCTION_REGEX}$")) # or "none" return value if value == "none" diff --git a/lib/tasks/after_tasks.rake b/lib/tasks/after_tasks.rake index ab75833f9e1..597a0dc59f3 100644 --- a/lib/tasks/after_tasks.rake +++ b/lib/tasks/after_tasks.rake @@ -189,42 +189,6 @@ namespace :After do puts("Added default rating to works: #{updated_works}") && STDOUT.flush end - desc "Fix pseuds with invalid icon data" - task(fix_invalid_pseud_icon_data: :environment) do - # From validates_attachment_content_type in pseuds model. - valid_types = %w[image/gif image/jpeg image/png] - - # If you change either of these, update lookup_invalid_pseuds.rb in - # otwcode/otw-scripts to ensure the proper users are notified. - pseuds_with_invalid_icons = Pseud.where("icon_file_name IS NOT NULL AND icon_content_type NOT IN (?)", valid_types) - pseuds_with_invalid_text = Pseud.where("CHAR_LENGTH(icon_alt_text) > ? OR CHAR_LENGTH(icon_comment_text) > ?", ArchiveConfig.ICON_ALT_MAX, ArchiveConfig.ICON_COMMENT_MAX) - - invalid_pseuds = [pseuds_with_invalid_icons, pseuds_with_invalid_text].flatten.uniq - invalid_pseuds_count = invalid_pseuds.count - - skipped_pseud_ids = [] - - # Update the pseuds. - puts("Updating #{invalid_pseuds_count} pseuds") && STDOUT.flush - - invalid_pseuds.each do |pseud| - # Change icon content type to jpeg if it's jpg. - pseud.icon_content_type = "image/jpeg" if pseud.icon_content_type == "image/jpg" - # Delete the icon if it's not a valid type. - pseud.icon = nil unless (valid_types + ["image/jpg"]).include?(pseud.icon_content_type) - # Delete the icon alt text if it's too long. - pseud.icon_alt_text = "" if pseud.icon_alt_text.length > ArchiveConfig.ICON_ALT_MAX - # Delete the icon comment if it's too long. - pseud.icon_comment_text = "" if pseud.icon_comment_text.length > ArchiveConfig.ICON_COMMENT_MAX - skipped_pseud_ids << pseud.id unless pseud.save - print(".") && STDOUT.flush - end - if skipped_pseud_ids.any? - puts - puts("Couldn't update #{skipped_pseud_ids.size} pseud(s): #{skipped_pseud_ids.join(',')}") && STDOUT.flush - end - end - desc "Backfill renamed_at for existing users" task(add_renamed_at_from_log: :environment) do total_users = User.all.size @@ -302,5 +266,229 @@ namespace :After do puts("Admin not found.") end end + + desc "Add suffix to existing Underage Sex tag in prepartion for Underage warning rename" + task(add_suffix_to_underage_sex_tag: :environment) do + puts("Tags can only be renamed by an admin, who will be listed as the tag's last wrangler. Enter the admin login we should use:") + login = $stdin.gets.chomp.strip + admin = Admin.find_by(login: login) + + if admin.present? + User.current_user = admin + + tag = Tag.find_by_name("Underage Sex") + + if tag.blank? + puts("No Underage Sex tag found.") + elsif tag.is_a?(ArchiveWarning) + puts("Underage Sex is already an Archive Warning.") + else + suffixed_name = "Underage Sex - #{tag.class}" + if tag.update(name: suffixed_name) + puts("Renamed Underage Sex tag to #{tag.reload.name}.") + else + puts("Failed to rename Underage Sex tag to #{suffixed_name}.") + end + $stdout.flush + end + else + puts("Admin not found.") + end + end + + desc "Rename Underage warning to Underage Sex" + task(rename_underage_warning: :environment) do + puts("Tags can only be renamed by an admin, who will be listed as the tag's last wrangler. Enter the admin login we should use:") + login = $stdin.gets.chomp.strip + admin = Admin.find_by(login: login) + + if admin.present? + User.current_user = admin + + tag = ArchiveWarning.find_by_name("Underage") + + if tag.blank? + puts("No Underage warning tag found.") + else + new_name = "Underage Sex" + if tag.update(name: new_name) + puts("Renamed Underage warning tag to #{tag.reload.name}.") + else + puts("Failed to rename Underage warning tag to #{new_name}.") + end + $stdout.flush + end + else + puts("Admin not found.") + end + end + + desc "Migrate collection icons to ActiveStorage paths" + task(migrate_collection_icons: :environment) do + require "aws-sdk-s3" + require "open-uri" + + return unless Rails.env.staging? || Rails.env.production? + + bucket_name = ENV["S3_BUCKET"] + prefix = "collections/icons/" + s3 = Aws::S3::Resource.new( + region: ENV["S3_REGION"], + access_key_id: ENV["S3_ACCESS_KEY_ID"], + secret_access_key: ENV["S3_SECRET_ACCESS_KEY"] + ) + old_bucket = s3.bucket(bucket_name) + new_bucket = s3.bucket(ENV["TARGET_BUCKET"]) + + Collection.no_touching do + old_bucket.objects(prefix: prefix).each do |object| + # Path example: staging/icons/108621/original.png + path_parts = object.key.split("/") + next unless path_parts[-1]&.include?("original") + next if ActiveStorage::Attachment.where(record_type: "Collection", record_id: path_parts[-2]).any? + + collection_id = path_parts[-2] + old_icon = URI.open("https://s3.amazonaws.com/#{bucket_name}/#{object.key}") + checksum = OpenSSL::Digest.new("MD5").tap do |result| + while (chunk = old_icon.read(5.megabytes)) + result << chunk + end + old_icon.rewind + end.base64digest + + key = nil + ActiveRecord::Base.transaction do + blob = ActiveStorage::Blob.create_before_direct_upload!( + filename: path_parts[-1], + byte_size: old_icon.size, + checksum: checksum, + content_type: Marcel::MimeType.for(old_icon) + ) + key = blob.key + blob.attachments.create( + name: "icon", + record_type: "Collection", + record_id: collection_id + ) + end + + new_bucket.put_object(key: key, body: old_icon, acl: "bucket-owner-full-control") + puts "Finished collection #{collection_id}" + $stdout.flush + end + end + end + + desc "Migrate pseud icons to ActiveStorage paths" + task(migrate_pseud_icons: :environment) do + require "aws-sdk-s3" + require "open-uri" + + return unless Rails.env.staging? || Rails.env.production? + + bucket_name = ENV["S3_BUCKET"] + prefix = Rails.env.production? ? "icons/" : "staging/icons/" + s3 = Aws::S3::Resource.new( + region: ENV["S3_REGION"], + access_key_id: ENV["S3_ACCESS_KEY_ID"], + secret_access_key: ENV["S3_SECRET_ACCESS_KEY"] + ) + old_bucket = s3.bucket(bucket_name) + new_bucket = s3.bucket(ENV["TARGET_BUCKET"]) + + Pseud.no_touching do + old_bucket.objects(prefix: prefix).each do |object| + # Path example: staging/icons/108621/original.png + path_parts = object.key.split("/") + next unless path_parts[-1]&.include?("original") + next if ActiveStorage::Attachment.where(record_type: "Pseud", record_id: path_parts[-2]).any? + + pseud_id = path_parts[-2] + old_icon = URI.open("https://s3.amazonaws.com/#{bucket_name}/#{object.key}") + checksum = OpenSSL::Digest.new("MD5").tap do |result| + while (chunk = old_icon.read(5.megabytes)) + result << chunk + end + old_icon.rewind + end.base64digest + + key = nil + ActiveRecord::Base.transaction do + blob = ActiveStorage::Blob.create_before_direct_upload!( + filename: path_parts[-1], + byte_size: old_icon.size, + checksum: checksum, + content_type: Marcel::MimeType.for(old_icon) + ) + key = blob.key + blob.attachments.create( + name: "icon", + record_type: "Pseud", + record_id: pseud_id + ) + end + + new_bucket.put_object(key: key, body: old_icon, acl: "bucket-owner-full-control") + puts "Finished pseud #{pseud_id}" + $stdout.flush + end + end + end + + desc "Migrate skin icons to ActiveStorage paths" + task(migrate_skin_icons: :environment) do + require "aws-sdk-s3" + require "open-uri" + + return unless Rails.env.staging? || Rails.env.production? + + bucket_name = ENV["S3_BUCKET"] + prefix = "skins/icons/" + s3 = Aws::S3::Resource.new( + region: ENV["S3_REGION"], + access_key_id: ENV["S3_ACCESS_KEY_ID"], + secret_access_key: ENV["S3_SECRET_ACCESS_KEY"] + ) + old_bucket = s3.bucket(bucket_name) + new_bucket = s3.bucket(ENV["TARGET_BUCKET"]) + + Skin.no_touching do + old_bucket.objects(prefix: prefix).each do |object| + # Path example: staging/icons/108621/original.png + path_parts = object.key.split("/") + next unless path_parts[-1]&.include?("original") + next if ActiveStorage::Attachment.where(record_type: "Skin", record_id: path_parts[-2]).any? + + skin_id = path_parts[-2] + old_icon = URI.open("https://s3.amazonaws.com/#{bucket_name}/#{object.key}") + checksum = OpenSSL::Digest.new("MD5").tap do |result| + while (chunk = old_icon.read(5.megabytes)) + result << chunk + end + old_icon.rewind + end.base64digest + + key = nil + ActiveRecord::Base.transaction do + blob = ActiveStorage::Blob.create_before_direct_upload!( + filename: path_parts[-1], + byte_size: old_icon.size, + checksum: checksum, + content_type: Marcel::MimeType.for(old_icon) + ) + key = blob.key + blob.attachments.create( + name: "icon", + record_type: "Skin", + record_id: skin_id + ) + end + + new_bucket.put_object(key: key, body: old_icon, acl: "bucket-owner-full-control") + puts "Finished skin #{skin_id}" + $stdout.flush + end + end + end # This is the end that you have to put new tasks above. end diff --git a/lib/tasks/notifications.rake b/lib/tasks/notifications.rake index a4faa83f78f..af422846ef4 100644 --- a/lib/tasks/notifications.rake +++ b/lib/tasks/notifications.rake @@ -1,13 +1,31 @@ namespace :notifications do desc "Send next set of kudos notifications" - task(:deliver_kudos => :environment) do + task(deliver_kudos: :environment) do RedisMailQueue.deliver_kudos end desc "Send next set of subscription notifications" - task(:deliver_subscriptions => :environment) do + task(deliver_subscriptions: :environment) do RedisMailQueue.deliver_subscriptions end - -end \ No newline at end of file + + # Usage with 10473 as admin post id: rails notifications:send_tos_update[10473] + desc "Send TOS Update notification to all users" + task(:send_tos_update, [:admin_post_id] => [:environment]) do |_t, args| + total_users = User.all.size + total_batches = (total_users + 999) / 1000 + puts "Notifying #{total_users} users in #{total_batches} batches" + + User.find_in_batches.with_index do |batch, index| + batch.each do |user| + TosUpdateMailer.tos_update_notification(user, args.admin_post_id).deliver_later(queue: :tos_update) + end + + batch_number = index + 1 + progress_msg = "Batch #{batch_number} of #{total_batches} complete" + puts(progress_msg) && $stdout.flush + end + puts && $stdout.flush + end +end diff --git a/public/help/rating-help.html b/public/help/rating-help.html index 6682ff1d5d1..8744cdc51ab 100644 --- a/public/help/rating-help.html +++ b/public/help/rating-help.html @@ -1,6 +1,6 @@

          Rating Tags

          -

          (For more information, see the Ratings and Warnings section of the Archive Terms of Service.)

          +

          (For more information, see the Ratings and Warnings section of the AO3 Terms of Service.)

          Not Rated (Adult!)
          diff --git a/public/help/skins-creating.html b/public/help/skins-creating.html index f20a539a843..26176462ee3 100644 --- a/public/help/skins-creating.html +++ b/public/help/skins-creating.html @@ -112,7 +112,7 @@
          URLs

          - We allow external image URLs (specified as url('http://somesite.com/my_awesome_image.jpg')) in JPG, GIF, and PNG formats. + We allow external image URLs (specified as url('https://example.com/my_awesome_image.jpg')) in JPG, GIF, and PNG formats. Please note, however, that skins using external images will not be approved for public use.

          diff --git a/public/help/warning-help.html b/public/help/warning-help.html index 1f37291120d..7d4057b1cb3 100644 --- a/public/help/warning-help.html +++ b/public/help/warning-help.html @@ -4,7 +4,7 @@

          Warning Tags

          The Archive of Our Own has chosen, for legal and other reasons, to mandate that users either warn for—or explicitly choose not to warn for—a short list of common warnings: Graphic Depictions of Violence, Major Character - Death, Rape/Non-Con, and Underage. We understand that creators may wish to not + Death, Rape/Non-Con, and Underage Sex. We understand that creators may wish to not warn for some of these things, or to warn for additional content, and have provided options for them to do so within this framework.

          @@ -50,10 +50,10 @@

          Warning Tags

          "Choose Not to Use Archive Warnings" instead.
          - Underage: + Underage Sex:
          - This is for descriptions or depictions of sexual activity by characters + This is for descriptions or depictions of sexual activity involving characters under the age of eighteen. (This doesn't include dating activity like kissing or vague references with no actual description or depiction.) This warning generally applies to humans; if you are creating a pornographic work @@ -67,6 +67,6 @@

          Warning Tags

          You can also use the "Additional Tags" field to give other or more detailed warnings. Our policies regarding warnings can be found in the Terms of Service and Terms of Service FAQ. + href="/content#II.J ">Terms of Service and Terms of Service FAQ.

          diff --git a/public/help/work-search-text-help.html b/public/help/work-search-text-help.html index 181dff88e00..ce0f8348271 100644 --- a/public/help/work-search-text-help.html +++ b/public/help/work-search-text-help.html @@ -31,6 +31,6 @@
          Examples
          will return all works from Fandom X tagged as F/F, and exclude those tagged Explicit
          "Character A" OR "Character B" -"Character Death"
          will return all works including Character A or Character B (or both), and no works tagged with "Character Death" in either the Warnings or the Additional tags
          -
          "Character A/Character B" Underage Mature OR Explicit
          -
          will return all works for this pairing that include an Underage warning and are either rated Mature or Explicit
          +
          "Character A/Character B" "Underage Sex" (Mature OR Explicit)
          +
          will return all works for this pairing that include an Underage Sex warning and are either rated Mature or Explicit
          diff --git a/public/stylesheets/masters/low_vision_default/low_vision_default_site_screen_.css b/public/stylesheets/masters/low_vision_default/low_vision_default_site_screen_.css index a6db7ec05e2..1f027b62292 100644 --- a/public/stylesheets/masters/low_vision_default/low_vision_default_site_screen_.css +++ b/public/stylesheets/masters/low_vision_default/low_vision_default_site_screen_.css @@ -136,10 +136,9 @@ pre { } #header, -#header ul.primary, #header .open a, -#header .open a:focus, -#header .user .open a:focus, +#header .primary .dropdown a:focus, +#header .user .dropdown a:focus, #footer, .actions a:hover, .actions a:focus { @@ -147,7 +146,7 @@ pre { color: #eee; } -#header h1.heading a { +#header h1 a { color: #fff; } diff --git a/public/stylesheets/site/2.0/14-group-preface.css b/public/stylesheets/site/2.0/14-group-preface.css index 9865bd8dd4a..f52bbd7f980 100644 --- a/public/stylesheets/site/2.0/14-group-preface.css +++ b/public/stylesheets/site/2.0/14-group-preface.css @@ -35,11 +35,6 @@ div.preface .module { div.preface .title, div.preface .byline, div.preface .byline a { border: 0; text-align: center; - text-decoration: none; -} - -.preface a:hover { - text-decoration: underline; } .preface blockquote { diff --git a/public/stylesheets/site/2.0/16-zone-system.css b/public/stylesheets/site/2.0/16-zone-system.css index c51fff3543c..a64f38b4f39 100644 --- a/public/stylesheets/site/2.0/16-zone-system.css +++ b/public/stylesheets/site/2.0/16-zone-system.css @@ -214,7 +214,7 @@ SUBSECTIONS: width: 100%; } -/* 2. ZONE: SYSTEM: SESSIONS (login and signup)*/ +/* 2. ZONE: SYSTEM: SESSIONS (login) */ .session #signin { margin-left: 48%; @@ -230,15 +230,18 @@ SUBSECTIONS: min-width: 0; } -#tos-partial .tos { - height: 36em; - overflow: auto; -} +/* 3. ZONE: SYSTEM: DOCS +For practicality, /home .docs have been given the .userstuff class as well, to +save doubling up that sheet or confusing end users. We will just cope with the +inconsistency. Docs encompasses pages in /home as well as FAQs and Wrangling +Guidelines. */ -/*3. ZONE: SYSTEM: DOCS -For practicality, /home .docs have been given the .userstuff class as well, to save doubling up that sheet or confusing end users. We will just cope with the inconsistency.*/ +.docs .userstuff h6 { + border-bottom: none; + margin: 0.714em 0; +} -.docs .userstuff ul, .docs .userstuff ol { +.docs .userstuff ul, .docs .userstuff ol, .docs .userstuff details .toc { margin: 0.643em auto; padding: 0; } @@ -247,12 +250,36 @@ For practicality, /home .docs have been given the .userstuff class as well, to s padding: 0 1.5em; } -.docs .userstuff .toc li { +.docs .userstuff ul ul li { + list-style-type: circle; +} + +.docs .userstuff ol.toc li { list-style-type: upper-roman; } -.docs .userstuff .toc { - margin: 3.215em auto; +.docs .userstuff ol.toc li ol li { + list-style-type: upper-alpha; +} + +.docs .userstuff summary .heading { + display: inline; +} + +/* module: tos +For practicality, the in-page full TOS module on the account creation page +(located in /users/registrations/_legal.html.erb) has been given the .docs and +.system classes to ensure it has the same styling it is given on the individual +/content, /privacy, and /tos pages. */ + +#tos-partial .tos { + height: 36em; + overflow: auto; +} + +#tos-partial .userstuff { + margin: 0.643em; + max-width: 100%; } /*4. ZONE: SYSTEM: ADMIN COMMS @@ -293,7 +320,7 @@ For practicality, /archive_faqs are also .docs and .support and have the .userst padding: 0; } -.faq .categories .questions li, .faq .userstuff .toc li { +.faq .userstuff .toc li { list-style-type: circle; } @@ -314,7 +341,27 @@ For practicality, /archive_faqs are also .docs and .support and have the .userst border: 1px solid #c2d2df; } -/* 7. ZONE: SYSTEM: TOS POPUP */ +.faq .userstuff nav.toc { + margin: 3.215em auto; +} + +/* 7. ZONE: SYSTEM: TOS, TOS FAQ, AND TOS POPUP */ + +.tos .userstuff nav.toc > details { + margin-top: 3em; +} + +.tos .userstuff nav.toc > details > summary { + border-bottom: 0.25em double #333; +} + +.tos .userstuff nav.toc > details > summary h3 { + border-bottom: none; +} + +.tos_faq .userstuff .toc { + margin-bottom: 3.215em; +} #tos_prompt { background: #fff; @@ -337,7 +384,7 @@ For practicality, /archive_faqs are also .docs and .support and have the .userst #tos_prompt .heading span { display: block; - margin: auto; + margin: auto; width: 40rem; max-width: 80%; } @@ -346,20 +393,24 @@ For practicality, /archive_faqs are also .docs and .support and have the .userst margin: auto; overflow: hidden; width: 40rem; - max-width: 80%; + max-width: 80%; } -#tos_prompt .summary { - margin: 2em 0 0.643em 0; +#tos_prompt p { line-height: 1.4; } +#tos_prompt p:first-of-type { + margin: 2em 0 0.643em 0; +} + #tos_prompt .confirmation { margin: 2em 0 1em; } #tos_prompt p.submit { - margin-bottom: 2em; + margin-bottom: 2em; + line-height: 1.125; } #tos_prompt .submit input, #tos_prompt .submit button { @@ -373,6 +424,7 @@ For practicality, /archive_faqs are also .docs and .support and have the .userst } /* 8. ZONE: SYSTEM: PROXY NOTICE */ + #proxy-notice { background: #efd1d1; border-bottom: 1px solid #900; diff --git a/public/stylesheets/site/2.0/21-userstuff.css b/public/stylesheets/site/2.0/21-userstuff.css index f9f0ad8e6cd..058b5a774e7 100644 --- a/public/stylesheets/site/2.0/21-userstuff.css +++ b/public/stylesheets/site/2.0/21-userstuff.css @@ -2,12 +2,12 @@ .userstuff { word-wrap: break-word; + line-height: 1.5; } .userstuff p, .userstuff details { margin: 1.286em auto; padding: 0; - line-height: 1.5; } /*lists */ @@ -29,7 +29,6 @@ .userstuff dd { display: block; - line-height: 1.5; margin-block-start: 1.25em; margin-block-end: 1.25em; margin-inline-start: 3em; @@ -45,6 +44,10 @@ padding: 0 1em; } +.userstuff ul li { + display: list-item; +} + .userstuff li, .userstuff ol ul li { font-weight: normal; display: list-item; @@ -119,8 +122,8 @@ .userstuff caption { font-size: 1em; height: auto; - opacity: 1.0; width: auto; + opacity: 1; } .userstuff table, .userstuff td, .userstuff col, .userstuff tr, .userstuff thead, .userstuff tfoot, .userstuff tbody, .userstuff th, .userstuff thead td, .userstuff th a, .userstuff th a:link { @@ -131,6 +134,7 @@ /* quotes and stresses */ .userstuff blockquote { + line-height: 1.5; margin-inline-start: 1.5em; padding: 0.75em; border-inline-start: 2px solid #999; @@ -179,11 +183,21 @@ /* contexts */ .bookmark .user .userstuff { - line-height: 1.5; + line-height: 1.5; } .bookmark .user .userstuff blockquote { - margin-bottom: 1.286em; + margin-bottom: 1.286em; +} + +.faq .userstuff dd { + margin-block-start: 0.75em; + margin-block-end: 0.75em; + margin-inline-start: 1.75em; +} + +.faq .userstuff dl { + margin: 0.75em 0; } /*END== */ diff --git a/public/stylesheets/site/2.0/28-media-print.css b/public/stylesheets/site/2.0/28-media-print.css index 5fd4a474a55..b1d89c6320e 100644 --- a/public/stylesheets/site/2.0/28-media-print.css +++ b/public/stylesheets/site/2.0/28-media-print.css @@ -46,6 +46,10 @@ a, a:link, a:visited { text-decoration: none; } +.docs .userstuff summary .heading { + display: inline; +} + /*CSS3 perks, print urls after links*/ a:after { @@ -59,4 +63,4 @@ a[href^="/"]:after { .meta a:link:after, .meta a:visited:after, .blurb a:link:after, .blurb a:link:visited, .byline a:link:after, .byline a:visited:after { content: " "; -} \ No newline at end of file +} diff --git a/public/tolk/reset.css b/public/tolk/reset.css deleted file mode 100644 index f210b720e8a..00000000000 --- a/public/tolk/reset.css +++ /dev/null @@ -1,30 +0,0 @@ -/*------------------------------------------------- -RESET --------------------------------------------------*/ - -body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,form,fieldset,input,textarea,p,blockquote,th,td { - margin:0; - padding:0; -} -table { - border-collapse:collapse; - border-spacing:0; -} -fieldset,img {border:0;} -address,caption,cite,code,dfn,th,var { - font-style:normal; - font-weight:normal; -} - -h1,h2,h3,h4,h5,h6 { - font-size:100%; - font-weight:normal; -} - -ol,ul {list-style:none;} -caption,th {text-align:left;} -q:before,q:after {content:'';} -abbr,acronym {border:0;} - -img {border: none;} -em em {font-style: normal;} \ No newline at end of file diff --git a/public/tolk/screen.css b/public/tolk/screen.css deleted file mode 100644 index 6edda910ecb..00000000000 --- a/public/tolk/screen.css +++ /dev/null @@ -1,336 +0,0 @@ -/*------------------------------------------------- -Defaults --------------------------------------------------*/ - -acronym, abbr { - font-variant: small-caps; - text-transform: lowercase; - font-weight: bold; -} - -.center {text-align: center;} -.large {font-size: larger;} -.small {font-size: smaller;} -strong {font-weight: bold;} -em {font-style: italic;} - -.clear {clear: both;} -.clearfix:after { content: "."; display: block; height: 0; clear: both; visibility: hidden; } -.clearfix { display: inline-block; } -.clearfix{ display: block; } - -a {color: #888;} -a:hover {color: #000;} - -/*------------------------------------------------- -Layout --------------------------------------------------*/ - -body { - font-family: "Lucida Sans", "Lucida Grande", "Lucida Sans Unicode", sans-serif; - background: #e5e5e5; - color: #333; - margin: 0; - padding: 0; - font-size: 14px; - line-height: 21px; - text-align: left; -} - -div#container { - margin: 2% 4%; - padding: 25px; - background: #fff; - -webkit-border-radius: 20px; - -moz-border-radius: 20px; - border-radius: 20px; - box-shadow: 0px 0px 15px rgba(00,00,00,0.5); - -moz-box-shadow: 0px 0px 15px rgba(00,00,00,0.5); - -webkit-box-shadow: 0px 0px 15px rgba(00,00,00,0.5); -} - -div#head { - margin: -25px -25px 0; - background: #111; - color: #999; - padding: 25px 25px 20px 15px; - -webkit-border-top-left-radius: 20px; - -webkit-border-top-right-radius: 20px; - -moz-border-radius-topleft: 20px; - -moz-border-radius-topright: 20px; - border-top-left-radius: 20px; - border-top-right-radius: 20px; - font-size: 18px; -} - -div#head a { - color: #2fadcf; - font-weight: bold; - text-decoration: none; -} - -div#head span.home { - -webkit-border-top-left-radius: 10px; - -webkit-border-bottom-left-radius: 10px; - -moz-border-radius-topleft: 10px; - -moz-border-radius-bottomleft: 10px; - border-top-left-radius: 10px; - border-bottom-left-radius: 10px; - background: #333; - padding: 8px 6px 8px 12px; - margin-right: 1px; -} - -div#head span.locale { - color: #fff; - font-weight: bold; - background: #444; - padding: 8px 12px 8px 6px; - -webkit-border-top-right-radius: 10px; - -webkit-border-bottom-right-radius: 10px; - -moz-border-radius-topright: 10px; - -moz-border-radius-bottomright: 10px; - border-top-right-radius: 10px; - border-bottom-right-radius: 10px; -} - -div#head span.locale.empty { - background: #333; - padding-left: 0; - margin-left: -6px; -} - -div#head span.locale em { - font-style: normal; - font-weight: normal; -} - -div#head span.note { - color: #777; - font-size: 13px; - font-weight: normal; - margin-left: 10px; -} - -h2, -h3 { - font-size: 18px; - color: #2fadcf; - margin: 25px 0 10px; -} - -h2 span, -h3 span { - font-size: 13px; - color: #888; -} - -ul.locales { - margin: 25px 0; -} - -ul.locales li { - /*width: 230px;*/ - width: 150px; - display: block; - float: left; - margin: 0 10px 10px 0; - /* background: #fff; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; - border: 1px solid #e5e5e5; - padding: 5px 10px;*/ -} - -ul.locales a { - font-weight: bold; - text-decoration: underline; - color: #2fadcf; -} - -/*ul.locales li:hover { - background: #f5f5f5; - border-color: #ccc; -}*/ - -ul.locales span.missing_translations { - color: #fff; - font-weight: bold; - background: #c00; - font-size: 9px; - padding: 3px; - -webkit-border-radius: 3px; - -moz-border-radius: 3px; - border-radius: 3px; - line-height: 9px; - vertical-align: top; - display: inline-block; -} - -ul.locales span { - font-size: 13px; - color: #666; -} - -div.submit { - background: #f5f5f5; - margin: 25px -25px -25px; - padding: 15px 25px; - border-top: 1px dashed #ccc; - -webkit-border-bottom-right-radius: 20px; - -webkit-border-bottom-left-radius: 20px; - -moz-border-radius-bottomright: 20px; - -moz-border-radius-bottomleft: 20px; - border-bottom-right-radius: 20px; - border-bottom-left-radius: 20px; -} - -span.updated { - font-size: 11px; - padding: 1px; - background: #ffc; - color: #777; - margin-bottom: 10px; - float: right; -} - -div.table_submit { - background: #f5f5f5; - margin: -25px 0 0; - padding: 12px 15px 15px; - text-align: left; -} - -div.translations { - width: 96%; - text-align: center; -} - -span.notice { - background: #ffc; - color: #666; - font-size: 12px; - padding: 2px 5px; - margin: -5px 0 15px; - display: inline-block; -} - -div.original { - color: #999; - margin: 5px 0; - padding: 1px 8px 4px; -} - -div.updated { - background: #ffc; - padding: 1px 8px 4px; -} - -table.translations div.original span.key { - margin: 0 0 -2px; - padding: 0; -} - -table.translations div.updated span.key { - margin: 0 0 -2px; - color: orange !important; - padding: 0; -} - -/*------------------------------------------------- -Translation tables --------------------------------------------------*/ - -table.translations { - margin: 0 0 25px; - width: 100%; - text-align: left; -} - -table.translations td, -table.translations th { - font-size: 14px; - color: #222; - padding: 12px 8px; - border-bottom: 1px solid #e5e5e5; - vertical-align: top; - width: 50%; -} - -table.translations th { - border-bottom-color: #bbb; - font-size: 11px; - font-weight: bold; - text-transform: uppercase; - color: #999; - padding-bottom: 2px; -} - -table.translations textarea.locale { - font-family: "Lucida Sans", "Lucida Grande", "Lucida Sans Unicode", sans-serif; - font-size: 14px; - line-height: 21px; - color: #222; - padding: 1px; - width: 100%; - height: 42px; -} - -table.translations span.key { - color: #aaa; - font-size: 9px; - display: block; -} - -table.translations td.translation { - padding: 10px 8px; -} - -table.translations tr.active td { - background: #edf9fe; -} - -table.translations .highlight { - background-color: yellow; -} - -/*------------------------------------------------- -Pagination --------------------------------------------------*/ - -div.paginate { - margin: 15px auto 20px; - font-size: 12px; - color: #777; -} - -div.paginate a, -div.paginate span { - padding: 2px 6px; - text-decoration: none; -} - -div.paginate a:hover, -div.paginate span.current { - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; - background: #eee; - color: #333; -} - -div.paginate .next_page, -div.paginate .prev_page { - margin: 0 15px; - -webkit-border-radius: 5px; - -moz-border-radius: 5px; - border-radius: 5px; - border: 1px solid #bbb; - padding: 4px 8px; -} - -div.paginate .disabled { - color: #ccc; - border-color: #eee; -} diff --git a/script/gift_exchange/tag_seed.json b/script/gift_exchange/tag_seed.json index e855dfd0c2a..858076cf0ad 100644 --- a/script/gift_exchange/tag_seed.json +++ b/script/gift_exchange/tag_seed.json @@ -76,6 +76,6 @@ "Graphic Depictions Of Violence", "Major Character Death", "Rape/Non-Con", - "Underage" + "Underage Sex" ] } diff --git a/spec/controllers/api/api_helper.rb b/spec/controllers/api/api_helper.rb index e2203cab203..5f87befac01 100644 --- a/spec/controllers/api/api_helper.rb +++ b/spec/controllers/api/api_helper.rb @@ -15,7 +15,7 @@ def valid_headers # Values in API fake content def content_fields { - title: "Detected Title", summary: "Detected summary", fandoms: "Detected Fandom", warnings: "Underage", + title: "Detected Title", summary: "Detected summary", fandoms: "Detected Fandom", warnings: "Underage Sex", characters: "Detected 1, Detected 2", rating: "Explicit", relationships: "Detected 1/Detected 2", categories: "F/F", freeform: "Detected tag 1, Detected tag 2", external_author_name: "Detected Author", external_author_email: "detected@foo.com", notes: "This is a content note.", diff --git a/spec/controllers/archive_faqs_controller_spec.rb b/spec/controllers/archive_faqs_controller_spec.rb index d7109cc5562..678a4c78508 100644 --- a/spec/controllers/archive_faqs_controller_spec.rb +++ b/spec/controllers/archive_faqs_controller_spec.rb @@ -6,6 +6,104 @@ include LoginMacros include RedirectExpectationHelper + fully_authorized_roles = %w[superadmin docs support] + + shared_examples "an action only fully authorized admins can access" do + before { fake_login_admin(admin) } + + context "with no role" do + let(:admin) { create(:admin, roles: []) } + + it "redirects with an error" do + subject + it_redirects_to_with_error(root_url, "Sorry, only an authorized admin can access the page you were trying to reach.") + end + end + + (Admin::VALID_ROLES - fully_authorized_roles).each do |role| + context "with role #{role}" do + let(:admin) { create(:admin, roles: [role]) } + + it "redirects with an error" do + subject + it_redirects_to_with_error(root_url, "Sorry, only an authorized admin can access the page you were trying to reach.") + end + end + end + + fully_authorized_roles.each do |role| + context "with role #{role}" do + let(:admin) { create(:admin, roles: [role]) } + + it "succeeds" do + subject + success + end + end + end + end + + translation_authorized_roles = %w[superadmin docs support translation] + + shared_examples "an action translation authorized admins can access" do + before { fake_login_admin(admin) } + + context "with no role" do + let(:admin) { create(:admin, roles: []) } + + it "redirects with an error" do + subject + it_redirects_to_with_error(root_url, "Sorry, only an authorized admin can access the page you were trying to reach.") + end + end + + (Admin::VALID_ROLES - translation_authorized_roles).each do |role| + context "with role #{role}" do + let(:admin) { create(:admin, roles: [role]) } + + it "redirects with an error" do + subject + it_redirects_to_with_error(root_url, "Sorry, only an authorized admin can access the page you were trying to reach.") + end + end + end + + translation_authorized_roles.each do |role| + context "with role #{role}" do + let(:admin) { create(:admin, roles: [role]) } + + it "succeeds" do + subject + success + end + end + end + end + + shared_examples "a non-English action that nobody can access" do + before { fake_login_admin(admin) } + + context "with no role" do + let(:admin) { create(:admin, roles: []) } + + it "redirects with an error" do + subject + it_redirects_to_with_error(archive_faqs_path, "Sorry, this action is only available for English FAQs.") + end + end + + Admin::VALID_ROLES.each do |role| + context "with role #{role}" do + let(:admin) { create(:admin, roles: [role]) } + + it "redirects with an error" do + subject + it_redirects_to_with_error(archive_faqs_path, "Sorry, this action is only available for English FAQs.") + end + end + end + end + let(:non_standard_locale) { create(:locale) } let(:user_locale) { create(:locale) } let(:user) do @@ -112,7 +210,8 @@ describe "GET #show" do it "raises a 404 for an invalid id" do params = { id: "angst", language_id: "en" } - expect { get :show, params: params }.to raise_error ActiveRecord::RecordNotFound + expect { get :show, params: params } + .to raise_exception(ActiveRecord::RecordNotFound) end end @@ -135,5 +234,155 @@ "The specified locale does not exist.") end end + + subject { patch :update, params: { id: faq, archive_faq: { title: "Changed" }, language_id: locale } } + let(:success) do + I18n.with_locale(locale) do + expect { faq.reload } + .to change { faq.title } + end + it_redirects_to_with_notice(faq, "Archive FAQ was successfully updated.") + end + + context "for the default locale" do + let(:locale) { "en" } + it_behaves_like "an action only fully authorized admins can access" + end + + context "for a non-default locale" do + let(:locale) { non_standard_locale.iso } + it_behaves_like "an action translation authorized admins can access" + end + end + + describe "GET #edit" do + subject { get :edit, params: { id: faq, language_id: locale } } + let(:faq) { create(:archive_faq) } + let(:success) do + expect(response).to render_template(:edit) + end + + context "for the default locale" do + let(:locale) { "en" } + it_behaves_like "an action only fully authorized admins can access" + end + + context "for a non-default locale" do + let(:locale) { non_standard_locale.iso } + it_behaves_like "an action translation authorized admins can access" + end + end + + describe "GET #new" do + subject { get :new, params: { language_id: locale } } + let(:success) do + expect(response).to render_template(:new) + end + + context "for the default locale" do + let(:locale) { "en" } + it_behaves_like "an action only fully authorized admins can access" + end + + context "for a non-default locale" do + let(:locale) { non_standard_locale.iso } + it_behaves_like "a non-English action that nobody can access" + end + end + + describe "POST #create" do + subject { post :create, params: { archive_faq: attributes_for(:archive_faq), language_id: locale } } + let(:success) do + expect(ArchiveFaq.count).to eq(1) + it_redirects_to_with_notice(assigns[:archive_faq], "Archive FAQ was successfully created.") + end + + context "for the default locale" do + let(:locale) { I18n.default_locale } + it_behaves_like "an action only fully authorized admins can access" + end + + context "for a non-default locale" do + let(:locale) { non_standard_locale.iso } + it_behaves_like "a non-English action that nobody can access" + end + end + + describe "GET #manage" do + subject { get :manage, params: { language_id: locale } } + let(:success) do + expect(response).to render_template(:manage) + end + + context "for the default locale" do + let(:locale) { "en" } + it_behaves_like "an action only fully authorized admins can access" + end + + context "for a non-default locale" do + let(:locale) { non_standard_locale.iso } + it_behaves_like "a non-English action that nobody can access" + end + end + + describe "POST #update_positions" do + subject { post :update_positions, params: { archive_faqs: [3, 1, 2], language_id: locale } } + let!(:faq1) { create(:archive_faq, position: 1) } + let!(:faq2) { create(:archive_faq, position: 2) } + let!(:faq3) { create(:archive_faq, position: 3) } + let(:success) do + expect(faq1.reload.position).to eq(3) + expect(faq2.reload.position).to eq(1) + expect(faq3.reload.position).to eq(2) + it_redirects_to_with_notice(archive_faqs_path, "Archive FAQs order was successfully updated.") + end + + context "for the default locale" do + let(:locale) { I18n.default_locale } + it_behaves_like "an action only fully authorized admins can access" + end + + context "for a non-default locale" do + let(:locale) { non_standard_locale.iso } + it_behaves_like "a non-English action that nobody can access" + end + end + + describe "GET #confirm_delete" do + subject { get :confirm_delete, params: { id: faq, language_id: locale } } + let(:faq) { create(:archive_faq) } + let(:success) do + expect(response).to render_template(:confirm_delete) + end + + context "for the default locale" do + let(:locale) { "en" } + it_behaves_like "an action only fully authorized admins can access" + end + + context "for a non-default locale" do + let(:locale) { non_standard_locale.iso } + it_behaves_like "a non-English action that nobody can access" + end + end + + describe "DELETE #destroy" do + subject { delete :destroy, params: { id: faq, language_id: locale } } + let(:faq) { create(:archive_faq) } + let(:success) do + expect { faq.reload } + .to raise_exception(ActiveRecord::RecordNotFound) + it_redirects_to(archive_faqs_path) + end + + context "for the default locale" do + let(:locale) { I18n.default_locale } + it_behaves_like "an action only fully authorized admins can access" + end + + context "for a non-default locale" do + let(:locale) { non_standard_locale.iso } + it_behaves_like "a non-English action that nobody can access" + end end end diff --git a/spec/controllers/comments_controller_spec.rb b/spec/controllers/comments_controller_spec.rb index d52bda54dcf..914efe76f74 100644 --- a/spec/controllers/comments_controller_spec.rb +++ b/spec/controllers/comments_controller_spec.rb @@ -824,7 +824,7 @@ post :create, params: { work_id: work.id, comment: comment_attributes } comment = Comment.last expect(flash[:error]).to be_nil - expect(response).to redirect_to(work_chapter_path(work, comment.commentable, show_comments: true, view_full_work: false, anchor: "comment_#{comment.id}")) + expect(response).to redirect_to(chapter_path(comment.commentable, show_comments: true, view_full_work: false, anchor: "comment_#{comment.id}")) end end end diff --git a/spec/controllers/feedbacks_controller_spec.rb b/spec/controllers/feedbacks_controller_spec.rb new file mode 100644 index 00000000000..f108d497211 --- /dev/null +++ b/spec/controllers/feedbacks_controller_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe FeedbacksController do + include LoginMacros + + describe "POST #create" do + let(:mock_zoho) { instance_double(ZohoResourceClient) } + + let(:default_parameters) do + { + feedback: { + comment: "Hello", + email: "test@example.com", + summary: "Summary", + language: "en" + } + } + end + + before do + allow_any_instance_of(Feedback).to receive(:zoho_enabled?).and_return(true) + allow(mock_zoho).to receive(:retrieve_contact_id) + allow_any_instance_of(FeedbackReporter).to receive(:zoho_resource_client).and_return(mock_zoho) + end + + context "when accessed by a logged-in user" do + let(:user) { create(:user) } + + before do + fake_login_known_user(user) + end + + context "when the user has no skin set" do + before do + admin_setting = AdminSetting.default + admin_setting.default_skin = Skin.default + admin_setting.save(validate: false) + end + + it "sets the skin title in the Zoho ticket" do + expect(mock_zoho).to receive(:create_ticket).with(ticket_attributes: include( + "cf" => include( + "cf_site_skin" => Skin.default.title + ) + )) + post :create, params: default_parameters + end + end + + context "when the user has a public non-default skin set" do + let(:skin) { create(:skin, :public) } + + before do + user.preference.update!(skin: skin) + end + + it "sets the skin title in the Zoho ticket" do + expect(mock_zoho).to receive(:create_ticket).with(ticket_attributes: include( + "cf" => include( + "cf_site_skin" => skin.title + ) + )) + post :create, params: default_parameters + end + end + + context "when the user has a private skin set" do + let(:skin) { create(:skin, author: user) } + + before do + user.preference.update!(skin: skin) + end + + it "sets the expected fields in the support ticket" do + expect(mock_zoho).to receive(:create_ticket).with(ticket_attributes: include( + "cf" => include( + "cf_site_skin" => "Custom skin" + ) + )) + post :create, params: default_parameters + end + end + end + end +end diff --git a/spec/controllers/inbox_controller_spec.rb b/spec/controllers/inbox_controller_spec.rb index 21815b1bbd7..9e29317db9e 100644 --- a/spec/controllers/inbox_controller_spec.rb +++ b/spec/controllers/inbox_controller_spec.rb @@ -19,6 +19,111 @@ "Sorry, you don't have permission to access the page you were trying to reach.") end + context "when logged in as an admin" do + context "when the admin does not have the correct authorization" do + context "when the admin has no role" do + let(:admin) { create(:admin, roles: []) } + + before { fake_login_admin(admin) } + + it "redirects with error" do + get :show, params: { user_id: user.login } + + it_redirects_to_with_error(root_path, "Sorry, only an authorized admin can access the page you were trying to reach.") + end + end + + (Admin::VALID_ROLES - %w[superadmin policy_and_abuse]).each do |role| + context "when the admin has the #{role} role" do + let(:admin) { create(:admin, roles: [role]) } + + before { fake_login_admin(admin) } + + it "redirects with error" do + get :show, params: { user_id: user.login } + + it_redirects_to_with_error(root_path, "Sorry, only an authorized admin can access the page you were trying to reach.") + end + end + end + end + + %w[superadmin policy_and_abuse].each do |role| + context "when the admin is authorized with the #{role} role" do + let(:admin) { create(:admin, roles: [role]) } + + before { fake_login_admin(admin) } + + it "renders the user inbox" do + get :show, params: { user_id: user.login } + expect(response).to render_template("show") + expect(assigns(:inbox_total)).to eq(0) + expect(assigns(:unread)).to eq(0) + end + + context "with unread comments" do + let!(:inbox_comments) do + Array.new(3) do |i| + create(:inbox_comment, user: user, created_at: Time.now.utc + i.days) + end + end + + it "renders non-zero unread count" do + get :show, params: { user_id: user.login } + expect(assigns(:inbox_comments)).to eq(inbox_comments.reverse) + expect(assigns(:inbox_total)).to eq(3) + expect(assigns(:unread)).to eq(3) + end + + it "renders oldest first" do + get :show, params: { user_id: user.login, filters: { date: "asc" } } + expect(assigns(:filters)[:date]).to eq("asc") + expect(assigns(:inbox_comments)).to eq(inbox_comments) + expect(assigns(:inbox_total)).to eq(3) + expect(assigns(:unread)).to eq(3) + end + end + + context "with 1 read and 1 unread" do + let!(:read_comment) { create(:inbox_comment, user: user, read: true) } + let!(:unread_comment) { create(:inbox_comment, user: user) } + + it "renders only unread" do + get :show, params: { user_id: user.login, filters: { read: "false" } } + expect(assigns(:filters)[:read]).to eq("false") + expect(assigns(:inbox_comments)).to eq([unread_comment]) + expect(assigns(:inbox_total)).to eq(2) + expect(assigns(:unread)).to eq(1) + end + end + + context "with 1 replied and 1 unreplied" do + let!(:replied_comment) { create(:inbox_comment, user: user, replied_to: true) } + let!(:unreplied_comment) { create(:inbox_comment, user: user) } + + it "renders only unreplied" do + get :show, params: { user_id: user.login, filters: { replied_to: "false" } } + expect(assigns(:filters)[:replied_to]).to eq("false") + expect(assigns(:inbox_comments)).to eq([unreplied_comment]) + expect(assigns(:inbox_total)).to eq(2) + expect(assigns(:unread)).to eq(2) + end + end + + context "with a deleted comment" do + let(:inbox_comment) { create(:inbox_comment, user: user) } + + it "excludes deleted comments" do + inbox_comment.feedback_comment.destroy! + get :show, params: { user_id: user.login } + expect(assigns(:inbox_total)).to eq(0) + expect(assigns(:unread)).to eq(0) + end + end + end + end + end + context "when logged in as the same user" do before { fake_login_known_user(user) } @@ -82,7 +187,7 @@ let(:inbox_comment) { create(:inbox_comment, user: user) } it "excludes deleted comments" do - inbox_comment.feedback_comment.destroy + inbox_comment.feedback_comment.destroy! get :show, params: { user_id: user.login } expect(assigns(:inbox_total)).to eq(0) expect(assigns(:unread)).to eq(0) @@ -149,92 +254,107 @@ end describe "PUT #update" do - before { fake_login_known_user(user) } - - context "with no comments selected" do - it "redirects to inbox with caution and a notice" do - put :update, params: { user_id: user.login, read: "yeah" } - it_redirects_to_with_caution_and_notice(user_inbox_path(user), - "Please select something first", - "Inbox successfully updated.") - end - - it "redirects to the previously viewed page if HTTP_REFERER is set, with a caution and a notice" do - @request.env['HTTP_REFERER'] = root_path - put :update, params: { user_id: user.login, read: "yeah" } - it_redirects_to_with_caution_and_notice(root_path, - "Please select something first", - "Inbox successfully updated.") + %w[superadmin policy_and_abuse].each do |role| + context "when logged in as an admin with the role #{role}" do + let(:admin) { create(:admin, roles: [role]) } + + before { fake_login_admin(admin) } + + it "redirects to root with error" do + put :update, params: { user_id: user.login } + it_redirects_to_with_error(root_path, "Sorry, only an authorized admin can access the page you were trying to reach.") + end end end - context "with unread comments" do - let!(:inbox_comment_1) { create(:inbox_comment, user: user) } - let!(:inbox_comment_2) { create(:inbox_comment, user: user) } - - it "marks all as read and redirects to inbox with a notice" do - parameters = { - user_id: user.login, - inbox_comments: [inbox_comment_1.id, inbox_comment_2.id], - read: "yeah" - } + context "when logged in as the comment receiver" do + before { fake_login_known_user(user) } - put :update, params: parameters - it_redirects_to_with_notice(user_inbox_path(user), "Inbox successfully updated.") + context "with no comments selected" do + it "redirects to inbox with caution and a notice" do + put :update, params: { user_id: user.login, read: "yeah" } + it_redirects_to_with_caution_and_notice(user_inbox_path(user), + "Please select something first", + "Inbox successfully updated.") + end - inbox_comment_1.reload - expect(inbox_comment_1.read).to be_truthy - inbox_comment_2.reload - expect(inbox_comment_2.read).to be_truthy + it "redirects to the previously viewed page if HTTP_REFERER is set, with a caution and a notice" do + @request.env["HTTP_REFERER"] = root_path + put :update, params: { user_id: user.login, read: "yeah" } + it_redirects_to_with_caution_and_notice(root_path, + "Please select something first", + "Inbox successfully updated.") + end end - it "marks one as read and redirects to inbox with a notice" do - put :update, params: { user_id: user.login, inbox_comments: [inbox_comment_1.id], read: "yeah" } - it_redirects_to_with_notice(user_inbox_path(user), "Inbox successfully updated.") + context "with unread comments" do + let!(:inbox_comment1) { create(:inbox_comment, user: user) } + let!(:inbox_comment2) { create(:inbox_comment, user: user) } + + it "marks all as read and redirects to inbox with a notice" do + parameters = { + user_id: user.login, + inbox_comments: [inbox_comment1.id, inbox_comment2.id], + read: "yeah" + } + + put :update, params: parameters + it_redirects_to_with_notice(user_inbox_path(user), "Inbox successfully updated.") + + inbox_comment1.reload + expect(inbox_comment1.read).to be_truthy + inbox_comment2.reload + expect(inbox_comment2.read).to be_truthy + end - inbox_comment_1.reload - expect(inbox_comment_1.read).to be_truthy - inbox_comment_2.reload - expect(inbox_comment_2.read).to be_falsy - end + it "marks one as read and redirects to inbox with a notice" do + put :update, params: { user_id: user.login, inbox_comments: [inbox_comment1.id], read: "yeah" } + it_redirects_to_with_notice(user_inbox_path(user), "Inbox successfully updated.") - it "deletes one and redirects to inbox with a notice" do - put :update, params: { user_id: user.login, inbox_comments: [inbox_comment_1.id], delete: "yeah" } - it_redirects_to_with_notice(user_inbox_path(user), "Inbox successfully updated.") + inbox_comment1.reload + expect(inbox_comment1.read).to be_truthy + inbox_comment2.reload + expect(inbox_comment2.read).to be_falsy + end - expect(InboxComment.find_by(id: inbox_comment_1.id)).to be_nil - inbox_comment_2.reload - expect(inbox_comment_2.read).to be_falsy + it "deletes one and redirects to inbox with a notice" do + put :update, params: { user_id: user.login, inbox_comments: [inbox_comment1.id], delete: "yeah" } + it_redirects_to_with_notice(user_inbox_path(user), "Inbox successfully updated.") + + expect(InboxComment.find_by(id: inbox_comment1.id)).to be_nil + inbox_comment2.reload + expect(inbox_comment2.read).to be_falsy + end end - end - context "with a read comment and redirects to inbox with a notice" do - let!(:inbox_comment) { create(:inbox_comment, user: user, read: true) } + context "with a read comment and redirects to inbox with a notice" do + let!(:inbox_comment) { create(:inbox_comment, user: user, read: true) } - it "marks as unread and redirects to inbox with a notice" do - put :update, params: { user_id: user.login, inbox_comments: [inbox_comment.id], unread: "yeah" } - it_redirects_to_with_notice(user_inbox_path(user), "Inbox successfully updated.") + it "marks as unread and redirects to inbox with a notice" do + put :update, params: { user_id: user.login, inbox_comments: [inbox_comment.id], unread: "yeah" } + it_redirects_to_with_notice(user_inbox_path(user), "Inbox successfully updated.") - inbox_comment.reload - expect(inbox_comment.read).to be_falsy - end + inbox_comment.reload + expect(inbox_comment.read).to be_falsy + end - it "marks as unread and returns a JSON response" do - parameters = { - user_id: user.login, - inbox_comments: [inbox_comment.id], - unread: "yeah", - format: "json" - } + it "marks as unread and returns a JSON response" do + parameters = { + user_id: user.login, + inbox_comments: [inbox_comment.id], + unread: "yeah", + format: "json" + } - put :update, params: parameters + put :update, params: parameters - inbox_comment.reload - expect(inbox_comment.read).to be_falsy + inbox_comment.reload + expect(inbox_comment.read).to be_falsy - parsed_body = JSON.parse(response.body, symbolize_names: true) - expect(parsed_body[:item_success_message]).to eq("Inbox successfully updated.") - expect(response).to have_http_status(:success) + parsed_body = JSON.parse(response.body, symbolize_names: true) + expect(parsed_body[:item_success_message]).to eq("Inbox successfully updated.") + expect(response).to have_http_status(:success) + end end end end diff --git a/spec/controllers/known_issues_controller_spec.rb b/spec/controllers/known_issues_controller_spec.rb new file mode 100644 index 00000000000..13b797656b9 --- /dev/null +++ b/spec/controllers/known_issues_controller_spec.rb @@ -0,0 +1,187 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe KnownIssuesController do + include LoginMacros + include RedirectExpectationHelper + + allowed_roles = %w[superadmin support] + + shared_examples "denies access to unauthorized admins" do + context "when logged in as an admin with no role" do + let(:admin) { create(:admin) } + + it "redirects with an error" do + it_redirects_to_with_error(root_url, "Sorry, only an authorized admin can access the page you were trying to reach.") + end + end + + (Admin::VALID_ROLES - allowed_roles).each do |admin_role| + context "when logged in as an admin with role #{admin_role}" do + let(:admin) { create(:admin, roles: [admin_role]) } + + it "redirects with an error" do + it_redirects_to_with_error(root_url, "Sorry, only an authorized admin can access the page you were trying to reach.") + end + end + end + end + + describe "GET #show" do + let(:known_issue) { create(:known_issue) } + + it_behaves_like "denies access to unauthorized admins" do + before do + fake_login_admin(admin) + get :show, params: { id: known_issue.id } + end + end + + allowed_roles.each do |admin_role| + context "when logged in as an admin with role #{admin_role}" do + let(:admin) { create(:admin, roles: [admin_role]) } + + before do + fake_login_admin(admin) + end + + it "allows access" do + get :show, params: { id: known_issue.id } + expect(response).to have_http_status(:success) + end + end + end + end + + describe "GET #new" do + it_behaves_like "denies access to unauthorized admins" do + before do + fake_login_admin(admin) + get :new + end + end + + allowed_roles.each do |admin_role| + context "when logged in as an admin with role #{admin_role}" do + let(:admin) { create(:admin, roles: [admin_role]) } + + before do + fake_login_admin(admin) + end + + it "allows access" do + get :new + expect(response).to have_http_status(:success) + end + end + end + end + + describe "GET #edit" do + let(:known_issue) { create(:known_issue) } + + it_behaves_like "denies access to unauthorized admins" do + before do + fake_login_admin(admin) + get :edit, params: { id: known_issue.id } + end + end + + allowed_roles.each do |admin_role| + context "when logged in as an admin with role #{admin_role}" do + let(:admin) { create(:admin, roles: [admin_role]) } + + before do + fake_login_admin(admin) + end + + it "allows access" do + get :edit, params: { id: known_issue.id } + expect(response).to have_http_status(:success) + end + end + end + end + + describe "POST #create" do + let(:params) { { known_issue: attributes_for(:known_issue) } } + + it_behaves_like "denies access to unauthorized admins" do + before do + fake_login_admin(admin) + post :create, params: params + end + end + + allowed_roles.each do |admin_role| + context "when logged in as an admin with role #{admin_role}" do + let(:admin) { create(:admin, roles: [admin_role]) } + + before do + fake_login_admin(admin) + end + + it "creates a known issue" do + expect { post :create, params: params } + .to change { KnownIssue.count } + .by(1) + end + end + end + end + + describe "PUT #update" do + let(:known_issue) { create(:known_issue) } + let(:params) { { id: known_issue.id, known_issue: { title: "Brand new title" } } } + + it_behaves_like "denies access to unauthorized admins" do + before do + fake_login_admin(admin) + put :update, params: params + end + end + + allowed_roles.each do |admin_role| + context "when logged in as an admin with role #{admin_role}" do + let(:admin) { create(:admin, roles: [admin_role]) } + + before do + fake_login_admin(admin) + end + + it "updates the known issue successfully" do + put :update, params: params + expect(known_issue.reload.title).to eq("Brand new title") + end + end + end + end + + describe "DELETE #destroy" do + let(:known_issue) { create(:known_issue) } + + it_behaves_like "denies access to unauthorized admins" do + before do + fake_login_admin(admin) + delete :destroy, params: { id: known_issue.id } + end + end + + allowed_roles.each do |admin_role| + context "when logged in as an admin with role #{admin_role}" do + let(:admin) { create(:admin, roles: [admin_role]) } + + before do + fake_login_admin(admin) + end + + it "deletes the known issue" do + delete :destroy, params: { id: known_issue.id } + expect { known_issue.reload } + .to raise_exception(ActiveRecord::RecordNotFound) + end + end + end + end +end diff --git a/spec/controllers/languages_controller_spec.rb b/spec/controllers/languages_controller_spec.rb index 96065f7f6b3..f445d1e410b 100644 --- a/spec/controllers/languages_controller_spec.rb +++ b/spec/controllers/languages_controller_spec.rb @@ -59,7 +59,7 @@ end describe "POST create" do - let(:language_params) { + let(:language_params) do { language: { name: "Suomi", @@ -69,7 +69,7 @@ sortable_name: "su" } } - } + end context "when not logged in" do it "redirects with error" do @@ -126,7 +126,7 @@ end end - (Admin::VALID_ROLES - %w[superadmin translation]).each do |role| + (Admin::VALID_ROLES - %w[superadmin translation support policy_and_abuse]).each do |role| context "when logged in as an admin with #{role} role" do let(:admin) { create(:admin, roles: [role]) } @@ -139,7 +139,7 @@ end end - %w[translation superadmin].each do |role| + %w[translation superadmin support policy_and_abuse].each do |role| context "when logged in as an admin with #{role} role" do let(:admin) { create(:admin, roles: [role]) } @@ -153,20 +153,41 @@ end describe "PUT update" do - let(:finnish) { Language.create(name: "Suomi", short: "fi") } - let(:language_params) { + let(:finnish) { Language.create(name: "Suomi", short: "fi", support_available: "0", abuse_support_available: "1") } + let(:language_params) do { id: finnish.short, language: { name: "Suomi", short: "fi", - support_available: true, - abuse_support_available: false, + support_available: "1", + abuse_support_available: "0", sortable_name: "su" } } - } + end + let(:language_params_support) do + { + id: finnish.short, + language: { + name: "Suomi", + short: "fi", + support_available: "1", + sortable_name: "" + } + } + end + + let(:language_params_abuse) do + { + id: finnish.short, + language: { + abuse_support_available: "0" + } + } + end + context "when not logged in" do it "redirects with error" do put :update, params: language_params @@ -175,7 +196,7 @@ end end - (Admin::VALID_ROLES - %w[superadmin translation]).each do |role| + (Admin::VALID_ROLES - %w[superadmin translation support policy_and_abuse]).each do |role| context "when logged in as an admin with #{role} role" do let(:admin) { create(:admin, roles: [role]) } @@ -211,5 +232,75 @@ end end end + + + context "when logged in as an admin with policy_and_abuse role and I attempt to edit a non-abuse field" do + let(:admin) { create(:admin, roles: ["policy_and_abuse"]) } + before do + fake_login_admin(admin) + end + it "throws error and doesn't save changes to non-abuse field" do + expect do + put :update, params: language_params + end.to raise_exception(ActionController::UnpermittedParameters) + finnish.reload + expect(finnish.support_available).to eq(false) + end + end + + context "when logged in as an admin with policy_and_abuse role and attempt to edit abuse_support_available field" do + let(:admin) { create(:admin, roles: ["policy_and_abuse"]) } + + before do + fake_login_admin(admin) + put :update, params: language_params_abuse + end + it "updates the language" do + finnish.reload + expect(finnish.name).to eq("Suomi") + expect(finnish.short).to eq("fi") + expect(finnish.support_available).to eq(false) + expect(finnish.abuse_support_available).to eq(false) + expect(finnish.sortable_name).to eq("") + end + + it "redirects and returns success message" do + it_redirects_to_with_notice(languages_path, "Language was successfully updated.") + end + end + + context "when logged in as an admin with support role and attempt to edit abuse_support_available field" do + let(:admin) { create(:admin, roles: ["support"]) } + before do + fake_login_admin(admin) + end + it "throws error and doesn't save changes to abuse_support_available field" do + expect do + put :update, params: language_params + end.to raise_exception(ActionController::UnpermittedParameters) + finnish.reload + expect(finnish.abuse_support_available).to eq(true) + end + end + + context "when logged in as an admin with support role and attempt to edit non-abuse fields" do + let(:admin) { create(:admin, roles: ["support"]) } + before do + fake_login_admin(admin) + put :update, params: language_params_support + end + it "updates the language" do + finnish.reload + expect(finnish.name).to eq("Suomi") + expect(finnish.short).to eq("fi") + expect(finnish.support_available).to eq(true) + expect(finnish.abuse_support_available).to eq(true) + expect(finnish.sortable_name).to eq("") + end + + it "redirects and returns success message" do + it_redirects_to_with_notice(languages_path, "Language was successfully updated.") + end + end end end diff --git a/spec/controllers/pseuds_controller_spec.rb b/spec/controllers/pseuds_controller_spec.rb index 5f39b04b0bd..b42cb9f8bcc 100644 --- a/spec/controllers/pseuds_controller_spec.rb +++ b/spec/controllers/pseuds_controller_spec.rb @@ -140,8 +140,7 @@ 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 + pseud.icon.attach(io: File.open(Rails.root.join("features/fixtures/icon.gif")), filename: "icon.gif", content_type: "image/gif") end it_behaves_like "an attribute that can be updated by an admin" @@ -149,9 +148,9 @@ 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.to change { pseud.reload.icon.attached? } + .from(true) + .to(false) end end diff --git a/spec/controllers/questions_controller_spec.rb b/spec/controllers/questions_controller_spec.rb new file mode 100644 index 00000000000..8cf8e4086da --- /dev/null +++ b/spec/controllers/questions_controller_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe QuestionsController do + include LoginMacros + include RedirectExpectationHelper + + fully_authorized_roles = %w[superadmin docs support] + + shared_examples "an action only fully authorized admins can access" do + before { fake_login_admin(admin) } + + context "with no role" do + let(:admin) { create(:admin, roles: []) } + + it "redirects with an error" do + subject + it_redirects_to_with_error(root_url, "Sorry, only an authorized admin can access the page you were trying to reach.") + end + end + + (Admin::VALID_ROLES - fully_authorized_roles).each do |role| + context "with role #{role}" do + let(:admin) { create(:admin, roles: [role]) } + + it "redirects with an error" do + subject + it_redirects_to_with_error(root_url, "Sorry, only an authorized admin can access the page you were trying to reach.") + end + end + end + + fully_authorized_roles.each do |role| + context "with role #{role}" do + let(:admin) { create(:admin, roles: [role]) } + + it "succeeds" do + subject + success + end + end + end + end + + describe "GET #manage" do + let(:faq) { create(:archive_faq) } + subject { get :manage, params: { archive_faq_id: faq } } + let(:success) do + expect(response).to render_template(:manage) + end + + it_behaves_like "an action only fully authorized admins can access" + end + + describe "POST #update_positions" do + let(:faq) { create(:archive_faq) } + let!(:question1) { create(:question, archive_faq: faq, position: 1) } + let!(:question2) { create(:question, archive_faq: faq, position: 2) } + let!(:question3) { create(:question, archive_faq: faq, position: 3) } + subject { post :update_positions, params: { archive_faq_id: faq, questions: [3, 1, 2] } } + let(:success) do + expect(question1.reload.position).to eq(3) + expect(question2.reload.position).to eq(1) + expect(question3.reload.position).to eq(2) + it_redirects_to_with_notice(faq, "Question order has been successfully updated.") + end + + it_behaves_like "an action only fully authorized admins can access" + end +end diff --git a/spec/controllers/tag_wranglings_controller_spec.rb b/spec/controllers/tag_wranglings_controller_spec.rb index 26d24190301..581a1772925 100644 --- a/spec/controllers/tag_wranglings_controller_spec.rb +++ b/spec/controllers/tag_wranglings_controller_spec.rb @@ -4,60 +4,144 @@ include LoginMacros include RedirectExpectationHelper - before do - fake_login - controller.current_user.roles << Role.new(name: "tag_wrangler") + full_access_roles = %w[superadmin tag_wrangling].freeze + read_access_roles = %w[superadmin policy_and_abuse tag_wrangling].freeze + + shared_examples "an action only authorized admins can access" do |authorized_roles:| + before do + fake_login_admin(admin) + end + + context "when logged in as an admin with no role" do + let(:admin) { create(:admin) } + + it "redirects with an error" do + subject + it_redirects_to_with_error(root_url, "Sorry, only an authorized admin can access the page you were trying to reach.") + end + end + + (Admin::VALID_ROLES - authorized_roles).each do |admin_role| + context "when logged in as an admin with role #{admin_role}" do + let(:admin) { create(:admin, roles: [admin_role]) } + + it "redirects with an error" do + subject + it_redirects_to_with_error(root_url, "Sorry, only an authorized admin can access the page you were trying to reach.") + end + end + end + + authorized_roles.each do |admin_role| + context "when logged in as an admin with role #{admin_role}" do + let(:admin) { create(:admin, roles: [admin_role]) } + + it "succeeds" do + subject + success + end + end + end end - shared_examples "set last wrangling activity" do - it "sets the last wrangling activity time to now", :frozen do - user = controller.current_user - expect(user.last_wrangling_activity.updated_at).to eq(Time.now.utc) + describe "GET #index" do + let(:success) { expect(response).to have_http_status(:success) } + + context "when the show parameter is absent" do + subject { get :index } + + it_behaves_like "an action only authorized admins can access", authorized_roles: read_access_roles + + context "when logged in as a tag wrangler" do + before do + fake_login_known_user(create(:tag_wrangler)) + end + + it "shows the wrangling tools page" do + subject + success + end + end + end + + context "when the show parameter is present" do + subject { get :index, params: { show: "fandoms" } } + + it_behaves_like "an action only authorized admins can access", authorized_roles: read_access_roles + + context "when logged in as a tag wrangler" do + before do + fake_login_known_user(create(:tag_wrangler)) + end + + it "shows the wrangling tools page" do + subject + success + end + end end end - describe "#wrangle" do - let(:page_options) { { page: 1, sort_column: "name", sort_direction: "ASC" } } + describe "POST #wrangle" do + shared_examples "set last wrangling activity" do + before do + fake_login_known_user(create(:tag_wrangler)) + subject + end + + it "sets the last wrangling activity time to now", :frozen do + user = controller.current_user + expect(user.last_wrangling_activity.updated_at).to eq(Time.now.utc) + end + end it "displays error when there are no fandoms to wrangle to" do + fake_login_known_user(create(:tag_wrangler)) character = create(:character) + page_options = { page: 1, sort_column: "name", sort_direction: "ASC" } post :wrangle, params: { fandom_string: "", selected_tags: [character.id] } it_redirects_to_with_error(tag_wranglings_path(page_options), "There were no Fandom tags!") end context "when making tags canonical" do + subject { post :wrangle, params: { canonicals: [tag1.id, tag2.id] } } let(:tag1) { create(:character) } let(:tag2) { create(:character) } - - before do - post :wrangle, params: { canonicals: [tag1.id, tag2.id] } + let(:success) do + expect(tag1.reload.canonical?).to be(true) + expect(tag2.reload.canonical?).to be(true) end - include_examples "set last wrangling activity" + it_behaves_like "set last wrangling activity" + it_behaves_like "an action only authorized admins can access", authorized_roles: full_access_roles end context "when assigning tags to a medium" do + subject { post :wrangle, params: { media: medium.name, selected_tags: [fandom1.id, fandom2.id] } } let(:fandom1) { create(:fandom, canonical: true) } let(:fandom2) { create(:fandom, canonical: true) } let(:medium) { create(:media) } - - before do - post :wrangle, params: { media: medium.name, selected_tags: [fandom1.id, fandom2.id] } + let(:success) do + expect(fandom1.medias).to include(medium) + expect(fandom2.medias).to include(medium) end - include_examples "set last wrangling activity" + it_behaves_like "set last wrangling activity" + it_behaves_like "an action only authorized admins can access", authorized_roles: full_access_roles end context "when adding tags to a fandom" do + subject { post :wrangle, params: { fandom_string: fandom.name, selected_tags: [tag1.id, tag2.id] } } let(:tag1) { create(:character) } let(:tag2) { create(:character) } let(:fandom) { create(:fandom, canonical: true) } - - before do - post :wrangle, params: { fandom_string: fandom.name, selected_tags: [tag1.id, tag2.id] } + let(:success) do + expect(tag1.fandoms).to include(fandom) + expect(tag2.fandoms).to include(fandom) end - include_examples "set last wrangling activity" + it_behaves_like "set last wrangling activity" + it_behaves_like "an action only authorized admins can access", authorized_roles: full_access_roles end end end diff --git a/spec/controllers/tags_controller_spec.rb b/spec/controllers/tags_controller_spec.rb index 9be54190b26..d5a174e9ab2 100644 --- a/spec/controllers/tags_controller_spec.rb +++ b/spec/controllers/tags_controller_spec.rb @@ -1,9 +1,12 @@ -require 'spec_helper' +require "spec_helper" describe TagsController do include LoginMacros include RedirectExpectationHelper + wrangling_full_access_roles = %w[superadmin tag_wrangling].freeze + wrangling_read_access_roles = (wrangling_full_access_roles + %w[policy_and_abuse]).freeze + let(:user) { create(:tag_wrangler) } before { fake_login_known_user(user) } @@ -14,6 +17,41 @@ end end + shared_examples "an action only authorized admins can access" do |authorized_roles:| + before { fake_login_admin(admin) } + + context "with no role" do + let(:admin) { create(:admin, roles: []) } + + it "redirects with an error" do + subject + it_redirects_to_with_error(root_url, "Sorry, only an authorized admin can access the page you were trying to reach.") + end + end + + (Admin::VALID_ROLES - authorized_roles).each do |role| + context "with role #{role}" do + let(:admin) { create(:admin, roles: [role]) } + + it "redirects with an error" do + subject + it_redirects_to_with_error(root_url, "Sorry, only an authorized admin can access the page you were trying to reach.") + end + end + end + + authorized_roles.each do |role| + context "with role #{role}" do + let(:admin) { create(:admin, roles: [role]) } + + it "succeeds" do + subject + success + end + end + end + end + describe "#create" do let(:tag_params) do { name: Faker::FunnyName.name, canonical: "0", type: "Character" } @@ -72,6 +110,13 @@ run_all_indexing_jobs end + subject { get :wrangle, params: { id: fandom.name, show: "freeforms", status: "unwrangled" } } + let(:success) do + expect(assigns(:tags)).to include(freeform1) + end + + it_behaves_like "an action only authorized admins can access", authorized_roles: wrangling_read_access_roles + it "includes unwrangled freeforms" do get :wrangle, params: { id: fandom.name, show: "freeforms", status: "unwrangled" } expect(assigns(:tags)).to include(freeform1) @@ -121,40 +166,35 @@ @character3 = FactoryBot.create(:character, canonical: false) @character2 = FactoryBot.create(:character, canonical: false, merger: @character3) @work = FactoryBot.create(:work, - fandom_string: "#{@fandom1.name}", - character_string: "#{@character1.name},#{@character2.name}", - freeform_string: "#{@freeform1.name}") + fandom_string: @fandom1.name.to_s, + character_string: "#{@character1.name},#{@character2.name}", + freeform_string: @freeform1.name.to_s) end it "should redirect to the wrangle action for that tag" do - expect(put :mass_update, params: { id: @fandom1.name, show: 'freeforms', status: 'unwrangled' }). - to redirect_to wrangle_tag_path(id: @fandom1.name, - show: 'freeforms', - status: 'unwrangled', - page: 1, - sort_column: 'name', - sort_direction: 'ASC') + expect(put(:mass_update, params: { id: @fandom1.name, show: "freeforms", status: "unwrangled" })) + .to redirect_to wrangle_tag_path(id: @fandom1.name, + show: "freeforms", + status: "unwrangled", + page: 1, + sort_column: "name", + sort_direction: "ASC") end - context "with one canonical fandom in the fandom string and a selected freeform" do - before do - put :mass_update, params: { id: @fandom1.name, show: 'freeforms', status: 'unwrangled', fandom_string: @fandom2.name, selected_tags: [@freeform1.id] } - end - - it "updates the tags successfully" do - get :wrangle, params: { id: @fandom1.name, show: 'freeforms', status: 'unwrangled' } - expect(assigns(:tags)).not_to include(@freeform1) - - @freeform1.reload - expect(@freeform1.fandoms).to include(@fandom2) - end + subject { put :mass_update, params: { id: @fandom1.name, show: "freeforms", status: "unwrangled", fandom_string: @fandom2.name, selected_tags: [@freeform1.id] } } + let(:success) do + get :wrangle, params: { id: @fandom1.name, show: "freeforms", status: "unwrangled" } + expect(assigns(:tags)).not_to include(@freeform1) - include_examples "set last wrangling activity" + @freeform1.reload + expect(@freeform1.fandoms).to include(@fandom2) end + it_behaves_like "an action only authorized admins can access", authorized_roles: wrangling_full_access_roles + context "with one canonical and one noncanonical fandoms in the fandom string and a selected freeform" do before do - put :mass_update, params: { id: @fandom1.name, show: 'freeforms', status: 'unwrangled', fandom_string: "#{@fandom2.name},#{@fandom3.name}", selected_tags: [@freeform1.id] } + put :mass_update, params: { id: @fandom1.name, show: "freeforms", status: "unwrangled", fandom_string: "#{@fandom2.name},#{@fandom3.name}", selected_tags: [@freeform1.id] } end it "updates the tags successfully" do @@ -168,7 +208,7 @@ context "with two canonical fandoms in the fandom string and a selected character" do before do - put :mass_update, params: { id: @fandom1.name, show: 'characters', status: 'unwrangled', fandom_string: "#{@fandom1.name},#{@fandom2.name}", selected_tags: [@character1.id] } + put :mass_update, params: { id: @fandom1.name, show: "characters", status: "unwrangled", fandom_string: "#{@fandom1.name},#{@fandom2.name}", selected_tags: [@character1.id] } end it "updates the tags successfully" do @@ -182,7 +222,7 @@ context "with a canonical fandom in the fandom string, a selected unwrangled character, and the same character to be made canonical" do before do - put :mass_update, params: { id: @fandom1.name, show: 'characters', status: 'unwrangled', fandom_string: "#{@fandom1.name}", selected_tags: [@character1.id], canonicals: [@character1.id] } + put :mass_update, params: { id: @fandom1.name, show: "characters", status: "unwrangled", fandom_string: @fandom1.name.to_s, selected_tags: [@character1.id], canonicals: [@character1.id] } end it "updates the tags successfully" do @@ -196,7 +236,7 @@ context "with a canonical fandom in the fandom string, a selected synonym character, and the same character to be made canonical" do before do - put :mass_update, params: { id: @fandom1.name, show: 'characters', status: 'unfilterable', fandom_string: "#{@fandom2.name}", selected_tags: [@character2.id], canonicals: [@character2.id] } + put :mass_update, params: { id: @fandom1.name, show: "characters", status: "unfilterable", fandom_string: @fandom2.name.to_s, selected_tags: [@character2.id], canonicals: [@character2.id] } end it "updates the tags successfully" do @@ -231,6 +271,41 @@ end end + describe "new" do + subject { get :new } + let(:success) do + expect(response).to have_http_status(:success) + end + + it_behaves_like "an action only authorized admins can access", authorized_roles: wrangling_full_access_roles + + context "when logged in as a tag wrangler" do + it "allows access" do + get :new + expect(response).to have_http_status(:success) + end + end + end + + describe "show" do + context "when showing a banned tag" do + let(:tag) { create(:banned) } + + subject { get :edit, params: { id: tag.name } } + let(:success) do + expect(response).to have_http_status(:success) + end + + it_behaves_like "an action only authorized admins can access", authorized_roles: wrangling_read_access_roles + + it "redirects with an error when not an admin" do + get :show, params: { id: tag.name } + it_redirects_to_with_error(tag_wranglings_path, + "Please log in as admin") + end + end + end + describe "show_hidden" do let(:work) { create(:work) } @@ -251,12 +326,17 @@ describe "edit" do context "when editing a banned tag" do - before do - @tag = FactoryBot.create(:banned) + let(:tag) { create(:banned) } + + subject { get :edit, params: { id: tag.name } } + let(:success) do + expect(response).to have_http_status(:success) end + it_behaves_like "an action only authorized admins can access", authorized_roles: wrangling_read_access_roles + it "redirects with an error when not an admin" do - get :edit, params: { id: @tag.name } + get :edit, params: { id: tag.name } it_redirects_to_with_error(tag_wranglings_path, "Please log in as admin") end @@ -299,16 +379,15 @@ end end - context "when logged in as an admin" do - it "succeeds and redirects to the edit page" do - fake_login_admin(create(:admin)) - put :update, params: { id: tag, tag: { syn_string: synonym.name }, commit: "Save changes" } - it_redirects_to_with_notice(edit_tag_path(tag), "Tag was updated.") - - tag.reload - expect(tag.merger_id).to eq(synonym.id) - end + subject { put :update, params: { id: tag, tag: { syn_string: synonym.name }, commit: "Save changes" } } + let(:success) do + it_redirects_to_with_notice(edit_tag_path(tag), "Tag was updated.") + tag.reload + expect(tag.merger_id).to eq(synonym.id) end + + it_behaves_like "an action only authorized admins can access", authorized_roles: wrangling_full_access_roles + end shared_examples "success message" do @@ -369,7 +448,7 @@ end context "when the associated tag has an invalid type" do - # NOTE This will enter the associated tag into the freeform_string + # NOTE: This will enter the associated tag into the freeform_string # field, which is not displayed on the form. This still might come up # in the extremely rare case where a tag wrangler loads the form, a # different tag wrangler goes in and changes the type of the tag being diff --git a/spec/controllers/unsorted_tags_controller_spec.rb b/spec/controllers/unsorted_tags_controller_spec.rb new file mode 100644 index 00000000000..4748903e243 --- /dev/null +++ b/spec/controllers/unsorted_tags_controller_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe UnsortedTagsController do + include LoginMacros + include RedirectExpectationHelper + + describe "POST #mass_update" do + context "when accessing as a guest" do + before do + post :mass_update + end + + it "redirects with an error" do + it_redirects_to_with_error( + new_user_session_path, + "Sorry, you don't have permission to access the page you were trying to reach. Please log in." + ) + end + end + + context "when logged in as a non-tag-wrangler user" do + let(:user) { create(:user) } + + before do + fake_login_known_user(user) + post :mass_update + end + + it "redirects with an error" do + it_redirects_to_with_error( + user_path(user), + "Sorry, you don't have permission to access the page you were trying to reach." + ) + end + end + + context "when logged in as an admin with no roles" do + before do + fake_login_admin(create(:admin)) + post :mass_update + end + + it "redirects with an error" do + it_redirects_to_with_error(root_path, "Sorry, only an authorized admin can access the page you were trying to reach.") + end + end + + (Admin::VALID_ROLES - %w[superadmin tag_wrangling]).each do |admin_role| + context "when logged in as a #{admin_role} admin" do + before do + fake_login_admin(create(:admin, roles: [admin_role])) + post :mass_update + end + + it "redirects with an error" do + it_redirects_to_with_error(root_path, "Sorry, only an authorized admin can access the page you were trying to reach.") + end + end + end + end +end diff --git a/spec/controllers/users/registrations_controller_spec.rb b/spec/controllers/users/registrations_controller_spec.rb index 59ec480930d..19d5181a35d 100644 --- a/spec/controllers/users/registrations_controller_spec.rb +++ b/spec/controllers/users/registrations_controller_spec.rb @@ -1,12 +1,16 @@ -require 'spec_helper' +require "spec_helper" describe Users::RegistrationsController do include RedirectExpectationHelper def valid_user_attributes { - email: "sna.foo@gmail.com", login: "myname", age_over_13: "1", - terms_of_service: "1", password: "password" + email: "sna.foo@gmail.com", + login: "myname", + age_over_13: "1", + data_processing: "1", + terms_of_service: "1", + password: "password" } end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 927aa6cf937..c753700f0d3 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -4,6 +4,36 @@ include RedirectExpectationHelper include LoginMacros + shared_examples "blocks access for banned and suspended users" do + context "when logged in as a banned user" do + let(:user) { create(:user, banned: true) } + + before do + fake_login_known_user(user) + end + + it "redirects with an error" do + subject + it_redirects_to_simple(user_path(user)) + expect(flash[:error]).to match("Your account has been banned") + end + end + + context "when logged in as a suspended user" do + let(:user) { create(:user, suspended: true, suspended_until: 1.week.from_now) } + + before do + fake_login_known_user(user) + end + + it "redirects with an error" do + subject + it_redirects_to_simple(user_path(user)) + expect(flash[:error]).to match("Your account has been suspended") + end + end + end + describe "GET #activate" do let(:user) { create(:user, confirmed_at: nil) } @@ -60,6 +90,46 @@ end end + describe "GET #change_username" do + subject { get :change_username, params: { id: user } } + + context "when logged in as a valid user" do + let(:user) { create(:user) } + + before do + fake_login_known_user(user) + end + + it "shows the change username form" do + subject + expect(response).to render_template(:change_username) + end + end + + it_behaves_like "blocks access for banned and suspended users" + end + + describe "POST #changed_username" do + subject do + post :changed_username, params: { id: user, new_login: "foo1234", password: user.password } + end + + context "when logged in as a valid user" do + let(:user) { create(:user) } + + before do + fake_login_known_user(user) + end + + it "updates the user's username" do + expect { subject } + .to change { user.reload.login } + end + end + + it_behaves_like "blocks access for banned and suspended users" + end + describe "destroy" do let(:user) { create(:user) } diff --git a/spec/controllers/works/default_rails_actions_spec.rb b/spec/controllers/works/default_rails_actions_spec.rb index 8592a0607d8..63d9be03c63 100644 --- a/spec/controllers/works/default_rails_actions_spec.rb +++ b/spec/controllers/works/default_rails_actions_spec.rb @@ -358,6 +358,14 @@ def call_with_params(params) end end + describe "when the fandom id is empty" do + it "returns the work" do + params = { fandom_id: nil } + get :index, params: params + expect(assigns(:works)).to include(@work) + end + end + describe "without caching" do before do AdminSetting.first.update_attribute(:enable_test_caching, false) diff --git a/spec/controllers/wrangling_guidelines_controller_spec.rb b/spec/controllers/wrangling_guidelines_controller_spec.rb index f87eb72de2e..c66430023ac 100644 --- a/spec/controllers/wrangling_guidelines_controller_spec.rb +++ b/spec/controllers/wrangling_guidelines_controller_spec.rb @@ -7,14 +7,14 @@ let(:admin) { create(:admin) } describe "GET #index" do - let!(:guideline_1) { create(:wrangling_guideline, position: 9001) } - let!(:guideline_2) { create(:wrangling_guideline, position: 2) } - let!(:guideline_3) { create(:wrangling_guideline, position: 7) } + let!(:guideline1) { create(:wrangling_guideline, position: 9001) } + let!(:guideline2) { create(:wrangling_guideline, position: 2) } + let!(:guideline3) { create(:wrangling_guideline, position: 7) } it "renders" do get :index expect(response).to render_template("index") - expect(assigns(:wrangling_guidelines)).to eq([guideline_2, guideline_3, guideline_1]) + expect(assigns(:wrangling_guidelines)).to eq([guideline2, guideline3, guideline1]) end end @@ -39,13 +39,28 @@ it_redirects_to_with_notice(root_path, "I'm sorry, only an admin can look at that area") end - context "when logged in as admin" do - before { fake_login_admin(admin) } + %w[board communications translation policy_and_abuse docs support open_doors].each do |role| + context "when logged in as an admin with #{role} role" do + let(:admin) { create(:admin, roles: [role]) } - it "renders" do - get :new - expect(response).to render_template("new") - expect(assigns(:wrangling_guideline)).to be_a_new(WranglingGuideline) + it "redirects with error" do + fake_login_admin(admin) + get :new + it_redirects_to_with_error(root_url, "Sorry, only an authorized admin can access the page you were trying to reach.") + end + end + end + + %w[tag_wrangling superadmin].each do |role| + context "when logged in as an admin with #{role} role" do + let(:admin) { create(:admin, roles: [role]) } + + it "renders" do + fake_login_admin(admin) + get :new + expect(response).to render_template("new") + expect(assigns(:wrangling_guideline)).to be_a_new(WranglingGuideline) + end end end end @@ -61,15 +76,32 @@ it_redirects_to_with_notice(root_path, "I'm sorry, only an admin can look at that area") end - context "when logged in as admin" do - let(:guideline) { create(:wrangling_guideline) } + %w[board communications translation policy_and_abuse docs support open_doors].each do |role| + context "when logged in as an admin with #{role} role" do + let(:guideline) { create(:wrangling_guideline) } + let(:admin) { create(:admin, roles: [role]) } + + before { fake_login_admin(admin) } + + it "redirects with error" do + get :edit, params: { id: guideline.id } + it_redirects_to_with_error(root_url, "Sorry, only an authorized admin can access the page you were trying to reach.") + end + end + end + + %w[tag_wrangling superadmin].each do |role| + context "when logged in as an admin with #{role} role" do + let(:guideline) { create(:wrangling_guideline) } + let(:admin) { create(:admin, roles: [role]) } - before { fake_login_admin(admin) } + before { fake_login_admin(admin) } - it "renders" do - get :edit, params: { id: guideline.id } - expect(response).to render_template("edit") - expect(assigns(:wrangling_guideline)).to eq(guideline) + it "renders" do + get :edit, params: { id: guideline.id } + expect(response).to render_template("edit") + expect(assigns(:wrangling_guideline)).to eq(guideline) + end end end end @@ -85,17 +117,33 @@ it_redirects_to_with_notice(root_path, "I'm sorry, only an admin can look at that area") end - context "when logged in as admin" do - let!(:guideline_1) { create(:wrangling_guideline, position: 9001) } - let!(:guideline_2) { create(:wrangling_guideline, position: 2) } - let!(:guideline_3) { create(:wrangling_guideline, position: 7) } + %w[board communications translation policy_and_abuse docs support open_doors].each do |role| + context "when logged in as an admin with #{role} role" do + let(:admin) { create(:admin, roles: [role]) } + + before { fake_login_admin(admin) } + + it "redirects with error" do + get :manage + it_redirects_to_with_error(root_url, "Sorry, only an authorized admin can access the page you were trying to reach.") + end + end + end + + %w[tag_wrangling superadmin].each do |role| + context "when logged in as an admin with #{role} role" do + let!(:guideline1) { create(:wrangling_guideline, position: 9001) } + let!(:guideline2) { create(:wrangling_guideline, position: 2) } + let!(:guideline3) { create(:wrangling_guideline, position: 7) } + let(:admin) { create(:admin, roles: [role]) } - before { fake_login_admin(admin) } + before { fake_login_admin(admin) } - it "renders" do - get :manage - expect(response).to render_template("manage") - expect(assigns(:wrangling_guidelines)).to eq([guideline_2, guideline_3, guideline_1]) + it "renders" do + get :manage + expect(response).to render_template("manage") + expect(assigns(:wrangling_guidelines)).to eq([guideline2, guideline3, guideline1]) + end end end end @@ -111,24 +159,43 @@ it_redirects_to_with_notice(root_path, "I'm sorry, only an admin can look at that area") end - context "when logged in as admin" do - before { fake_login_admin(admin) } + %w[board communications translation policy_and_abuse docs support open_doors].each do |role| + context "when logged in as an admin with #{role} role" do + let(:admin) { create(:admin, roles: [role]) } - it "creates and redirects to new wrangling guideline" do - title = "Wrangling 101" - content = "JUST DO IT!" - post :create, params: { wrangling_guideline: { title: title, content: content } } + before { fake_login_admin(admin) } - guideline = WranglingGuideline.find_by_title(title) - expect(assigns(:wrangling_guideline)).to eq(guideline) - expect(assigns(:wrangling_guideline).content).to eq(sanitize_value("content", content)) - it_redirects_to_with_notice(wrangling_guideline_path(guideline), "Wrangling Guideline was successfully created.") + it "redirects with error" do + title = "Wrangling 101" + content = "JUST DO IT!" + post :create, params: { wrangling_guideline: { title: title, content: content } } + it_redirects_to_with_error(root_url, "Sorry, only an authorized admin can access the page you were trying to reach.") + end end + end - it "renders new if create fails" do - # Cannot save a content-free guideline - post :create, params: { wrangling_guideline: { title: "Wrangling 101" } } - expect(response).to render_template("new") + %w[tag_wrangling superadmin].each do |role| + context "when logged in as an admin with #{role} role" do + let(:admin) { create(:admin, roles: [role]) } + + before { fake_login_admin(admin) } + + it "creates and redirects to new wrangling guideline" do + title = "Wrangling 101" + content = "JUST DO IT!" + post :create, params: { wrangling_guideline: { title: title, content: content } } + + guideline = WranglingGuideline.find_by(title: title) + expect(assigns(:wrangling_guideline)).to eq(guideline) + expect(assigns(:wrangling_guideline).content).to eq(sanitize_value("content", content)) + it_redirects_to_with_notice(wrangling_guideline_path(guideline), "Wrangling Guideline was successfully created.") + end + + it "renders new if create fails" do + # Cannot save a content-free guideline + post :create, params: { wrangling_guideline: { title: "Wrangling 101" } } + expect(response).to render_template("new") + end end end end @@ -144,25 +211,44 @@ it_redirects_to_with_notice(root_path, "I'm sorry, only an admin can look at that area") end - context "when logged in as admin" do - let(:guideline) { create(:wrangling_guideline) } - - before { fake_login_admin(admin) } - - it "updates and redirects to updated wrangling guideline" do - title = "Wrangling 101" - expect(guideline.title).not_to eq(title) + %w[board communications translation policy_and_abuse docs support open_doors].each do |role| + context "when logged in as an admin with #{role} role" do + let(:guideline) { create(:wrangling_guideline) } + let(:admin) { create(:admin, roles: [role]) } - put :update, params: { id: guideline.id, wrangling_guideline: { title: title } } + before { fake_login_admin(admin) } - expect(assigns(:wrangling_guideline)).to eq(guideline) - expect(assigns(:wrangling_guideline).title).to eq(title) - it_redirects_to_with_notice(wrangling_guideline_path(guideline), "Wrangling Guideline was successfully updated.") + it "redirects with error" do + title = "Wrangling 101" + expect(guideline.title).not_to eq(title) + put :update, params: { id: guideline.id, wrangling_guideline: { title: title } } + it_redirects_to_with_error(root_url, "Sorry, only an authorized admin can access the page you were trying to reach.") + end end + end - it "renders edit if update fails" do - put :update, params: { id: guideline.id, wrangling_guideline: { title: nil } } - expect(response).to render_template("edit") + %w[tag_wrangling superadmin].each do |role| + context "when logged in as an admin with #{role} role" do + let(:guideline) { create(:wrangling_guideline) } + let(:admin) { create(:admin, roles: [role]) } + + before { fake_login_admin(admin) } + + it "updates and redirects to updated wrangling guideline" do + title = "Wrangling 101" + expect(guideline.title).not_to eq(title) + + put :update, params: { id: guideline.id, wrangling_guideline: { title: title } } + + expect(assigns(:wrangling_guideline)).to eq(guideline) + expect(assigns(:wrangling_guideline).title).to eq(title) + it_redirects_to_with_notice(wrangling_guideline_path(guideline), "Wrangling Guideline was successfully updated.") + end + + it "renders edit if update fails" do + put :update, params: { id: guideline.id, wrangling_guideline: { title: nil } } + expect(response).to render_template("edit") + end end end end @@ -178,25 +264,45 @@ it_redirects_to_with_notice(root_path, "I'm sorry, only an admin can look at that area") end - context "when logged in as admin" do - let!(:guideline_1) { create(:wrangling_guideline, position: 1) } - let!(:guideline_2) { create(:wrangling_guideline, position: 2) } - let!(:guideline_3) { create(:wrangling_guideline, position: 3) } - - before { fake_login_admin(admin) } + %w[board communications translation policy_and_abuse docs support open_doors].each do |role| + context "when logged in as an admin with #{role} role" do + let!(:guideline1) { create(:wrangling_guideline, position: 1) } + let!(:guideline2) { create(:wrangling_guideline, position: 2) } + let!(:guideline3) { create(:wrangling_guideline, position: 3) } + let(:admin) { create(:admin, roles: [role]) } - it "updates positions and redirects to index" do - expect(WranglingGuideline.order('position ASC')).to eq([guideline_1, guideline_2, guideline_3]) - post :update_positions, params: { wrangling_guidelines: [3, 2, 1] } + before { fake_login_admin(admin) } - expect(assigns(:wrangling_guidelines)).to eq(WranglingGuideline.order('position ASC')) - expect(assigns(:wrangling_guidelines)).to eq([guideline_3, guideline_2, guideline_1]) - it_redirects_to_with_notice(wrangling_guidelines_path, "Wrangling Guidelines order was successfully updated.") + it "redirects with error" do + expect(WranglingGuideline.order("position ASC")).to eq([guideline1, guideline2, guideline3]) + post :update_positions, params: { wrangling_guidelines: [3, 2, 1] } + it_redirects_to_with_error(root_url, "Sorry, only an authorized admin can access the page you were trying to reach.") + end end + end - it "redirects to index given no params" do - post :update_positions - it_redirects_to(wrangling_guidelines_path) + %w[tag_wrangling superadmin].each do |role| + context "when logged in as an admin with #{role} role" do + let!(:guideline1) { create(:wrangling_guideline, position: 1) } + let!(:guideline2) { create(:wrangling_guideline, position: 2) } + let!(:guideline3) { create(:wrangling_guideline, position: 3) } + let(:admin) { create(:admin, roles: [role]) } + + before { fake_login_admin(admin) } + + it "updates positions and redirects to index" do + expect(WranglingGuideline.order("position ASC")).to eq([guideline1, guideline2, guideline3]) + post :update_positions, params: { wrangling_guidelines: [3, 2, 1] } + + expect(assigns(:wrangling_guidelines)).to eq(WranglingGuideline.order("position ASC")) + expect(assigns(:wrangling_guidelines)).to eq([guideline3, guideline2, guideline1]) + it_redirects_to_with_notice(wrangling_guidelines_path, "Wrangling Guidelines order was successfully updated.") + end + + it "redirects to index given no params" do + post :update_positions + it_redirects_to(wrangling_guidelines_path) + end end end end @@ -212,15 +318,33 @@ it_redirects_to_with_notice(root_path, "I'm sorry, only an admin can look at that area") end - context "when logged in as admin" do - let(:guideline) { create(:wrangling_guideline) } + %w[board communications translation policy_and_abuse docs support open_doors].each do |role| + context "when logged in as an admin with #{role} role" do + let(:guideline) { create(:wrangling_guideline) } + let(:admin) { create(:admin, roles: [role]) } + + before { fake_login_admin(admin) } + + it "redirects with error" do + delete :destroy, params: { id: guideline.id } + + it_redirects_to_with_error(root_url, "Sorry, only an authorized admin can access the page you were trying to reach.") + end + end + end + + %w[tag_wrangling superadmin].each do |role| + context "when logged in as an admin with #{role} role" do + let(:guideline) { create(:wrangling_guideline) } + let(:admin) { create(:admin, roles: [role]) } - before { fake_login_admin(admin) } + before { fake_login_admin(admin) } - it "deletes and redirects to index" do - delete :destroy, params: { id: guideline.id } - expect(WranglingGuideline.find_by_id(guideline.id)).to be_nil - it_redirects_to_with_notice(wrangling_guidelines_path, "Wrangling Guideline was successfully deleted.") + it "deletes and redirects to index" do + delete :destroy, params: { id: guideline.id } + expect(WranglingGuideline.find_by(id: guideline.id)).to be_nil + it_redirects_to_with_notice(wrangling_guidelines_path, "Wrangling Guideline was successfully deleted.") + end end end end diff --git a/spec/helpers/skins_helper_spec.rb b/spec/helpers/skins_helper_spec.rb new file mode 100644 index 00000000000..da6c7a3da5e --- /dev/null +++ b/spec/helpers/skins_helper_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe SkinsHelper do + describe "#current_skin" do + before do + allow(helper).to receive(:current_user) + allow(helper).to receive(:logged_in_as_admin?).and_return(false) + allow(helper).to receive(:logged_in?).and_return(false) + admin_setting = AdminSetting.default + admin_setting.default_skin = Skin.default + admin_setting.save(validate: false) + end + + context "when the parameters include a skin id" do + before do + params[:site_skin] = skin.id + end + + context "when the skin is applied" do + let(:skin) { create(:skin, :public) } + + it "returns the skin matching the parameter" do + expect(helper.current_skin).to eq(skin) + end + end + + context "when the skin is not applied" do + let(:skin) { create(:skin) } + + it "falls back to other options" do + expect(helper.current_skin).to eq(Skin.default) + end + end + end + + context "when the current user has a skin set for the session" do + before do + allow(helper).to receive(:current_user).and_return(create(:user)) + allow(helper).to receive(:logged_in?).and_return(true) + session[:site_skin] = skin.id + end + + context "when the skin is applied" do + let(:skin) { create(:skin, :public) } + + it "returns the skin matching the session attribute" do + expect(helper.current_skin).to eq(skin) + end + end + + context "when the skin is not applied" do + # Non-public skin with a different author + let(:skin) { create(:skin) } + + it "falls back to other options" do + expect(helper.current_skin).to eq(Skin.default) + end + end + end + + context "when the current user has a skin preference set" do + let(:skin) { create(:skin) } + let(:user) { skin.author } + + before do + user.preference.update!(skin: skin) + allow(helper).to receive(:current_user).and_return(user) + end + + it "returns the preferred skin" do + expect(helper.current_skin).to eq(skin) + end + end + end +end diff --git a/spec/lib/tasks/after_tasks.rake_spec.rb b/spec/lib/tasks/after_tasks.rake_spec.rb index 4f997292bcb..b670e771896 100644 --- a/spec/lib/tasks/after_tasks.rake_spec.rb +++ b/spec/lib/tasks/after_tasks.rake_spec.rb @@ -174,85 +174,6 @@ end end -describe "rake After:fix_invalid_pseud_icon_data" do - let(:valid_pseud) { create(:user).default_pseud } - let(:invalid_pseud) { create(:user).default_pseud } - - before do - stub_const("ArchiveConfig", OpenStruct.new(ArchiveConfig)) - ArchiveConfig.ICON_ALT_MAX = 5 - ArchiveConfig.ICON_COMMENT_MAX = 5 - end - - it "removes invalid icon" do - valid_pseud.icon = File.new(Rails.root.join("features/fixtures/icon.gif")) - valid_pseud.save - invalid_pseud.icon = File.new(Rails.root.join("features/fixtures/icon.gif")) - invalid_pseud.save - invalid_pseud.update_column(:icon_content_type, "not/valid") - - subject.invoke - - invalid_pseud.reload - valid_pseud.reload - expect(invalid_pseud.icon.exists?).to be_falsey - expect(invalid_pseud.icon_content_type).to be_nil - expect(valid_pseud.icon.exists?).to be_truthy - expect(valid_pseud.icon_content_type).to eq("image/gif") - end - - it "removes invalid icon_alt_text" do - invalid_pseud.update_column(:icon_alt_text, "not valid") - valid_pseud.update_attribute(:icon_alt_text, "valid") - - subject.invoke - - invalid_pseud.reload - valid_pseud.reload - expect(invalid_pseud.icon_alt_text).to be_empty - expect(valid_pseud.icon_alt_text).to eq("valid") - end - - it "removes invalid icon_comment_text" do - invalid_pseud.update_column(:icon_comment_text, "not valid") - valid_pseud.update_attribute(:icon_comment_text, "valid") - - subject.invoke - - invalid_pseud.reload - valid_pseud.reload - expect(invalid_pseud.icon_comment_text).to be_empty - expect(valid_pseud.icon_comment_text).to eq("valid") - end - - it "updates icon_content_type from jpg to jpeg" do - invalid_pseud.icon = File.new(Rails.root.join("features/fixtures/icon.jpg")) - invalid_pseud.save - invalid_pseud.update_column(:icon_content_type, "image/jpg") - - subject.invoke - - invalid_pseud.reload - expect(invalid_pseud.icon.exists?).to be_truthy - expect(invalid_pseud.icon_content_type).to eq("image/jpeg") - end - - it "updates multiple invalid fields on the same pseud" do - invalid_pseud.icon = File.new(Rails.root.join("features/fixtures/icon.gif")) - invalid_pseud.save - invalid_pseud.update_columns(icon_content_type: "not/valid", - icon_alt_text: "not valid", - icon_comment_text: "not valid") - subject.invoke - - invalid_pseud.reload - expect(invalid_pseud.icon.exists?).to be_falsey - expect(invalid_pseud.icon_content_type).to be_nil - expect(invalid_pseud.icon_alt_text).to be_empty - expect(invalid_pseud.icon_comment_text).to be_empty - end -end - describe "rake After:fix_2009_comment_threads" do before { Comment.delete_all } @@ -392,3 +313,114 @@ end end end + +describe "rake After:add_suffix_to_underage_sex_tag" do + let(:prompt) { "Tags can only be renamed by an admin, who will be listed as the tag's last wrangler. Enter the admin login we should use:\n" } + + context "without a valid admin" do + it "puts an error without a valid admin" do + allow($stdin).to receive(:gets) { "no-admin" } + + expect do + subject.invoke + end.to output("#{prompt}Admin not found.\n").to_stdout + end + end + + context "with a valid admin" do + let!(:admin) { create(:admin, login: "admin") } + + before do + allow($stdin).to receive(:gets) { "admin" } + tag = ArchiveWarning.find_by_name("Underage Sex") + tag.destroy! + end + + it "puts an error if tag does not exist" do + expect do + subject.invoke + end.to output("#{prompt}No Underage Sex tag found.\n").to_stdout + end + + it "puts an error if tag is an ArchiveWarning" do + tag = create(:archive_warning, name: "Underage Sex") + + expect do + subject.invoke + end.to avoid_changing { tag.reload.name } + .and output("#{prompt}Underage Sex is already an Archive Warning.\n").to_stdout + end + + it "puts a success message if tag exists and can be renamed" do + tag = create(:relationship, name: "Underage Sex") + + expect do + subject.invoke + end.to change { tag.reload.name } + .from("Underage Sex") + .to("Underage Sex - Relationship") + .and output("#{prompt}Renamed Underage Sex tag to Underage Sex - Relationship.\n").to_stdout + end + + it "puts an error if tag exists and cannot be renamed" do + tag = create(:freeform, name: "Underage Sex") + allow_any_instance_of(Tag).to receive(:save).and_return(false) + + expect do + subject.invoke + end.to avoid_changing { tag.reload.name } + .and output("#{prompt}Failed to rename Underage Sex tag to Underage Sex - Freeform.\n").to_stdout + end + end +end + +describe "rake After:rename_underage_warning" do + let(:prompt) { "Tags can only be renamed by an admin, who will be listed as the tag's last wrangler. Enter the admin login we should use:\n" } + + context "without a valid admin" do + it "puts an error without a valid admin" do + allow($stdin).to receive(:gets) { "no-admin" } + + expect do + subject.invoke + end.to output("#{prompt}Admin not found.\n").to_stdout + end + end + + context "with a valid admin" do + let!(:admin) { create(:admin, login: "admin") } + + before do + allow($stdin).to receive(:gets) { "admin" } + tag = ArchiveWarning.find_by_name("Underage Sex") + tag.destroy! + end + + it "puts an error if tag does not exist" do + expect do + subject.invoke + end.to output("#{prompt}No Underage warning tag found.\n").to_stdout + end + + it "puts a success message if tag exists and can be renamed" do + tag = create(:archive_warning, name: "Underage") + + expect do + subject.invoke + end.to change { tag.reload.name } + .from("Underage") + .to("Underage Sex") + .and output("#{prompt}Renamed Underage warning tag to Underage Sex.\n").to_stdout + end + + it "puts an error if tag exists and cannot be renamed" do + tag = create(:archive_warning, name: "Underage") + allow_any_instance_of(Tag).to receive(:save).and_return(false) + + expect do + subject.invoke + end.to avoid_changing { tag.reload.name } + .and output("#{prompt}Failed to rename Underage warning tag to Underage Sex.\n").to_stdout + end + end +end diff --git a/spec/lib/tasks/notifications.rake_spec.rb b/spec/lib/tasks/notifications.rake_spec.rb new file mode 100644 index 00000000000..b9e103c7878 --- /dev/null +++ b/spec/lib/tasks/notifications.rake_spec.rb @@ -0,0 +1,27 @@ +require "spec_helper" + +describe "rake notifications:send_tos_update" do + let(:admin_post) { create(:admin_post) } + + context "with one user" do + let!(:user) { create(:user) } + + it "enqueues one tos update notifications" do + ActiveJob::Base.queue_adapter = :test + expect(User.all.size).to eq(1) + expect { subject.invoke(admin_post.id) } + .to have_enqueued_mail(TosUpdateMailer, :tos_update_notification).on_queue(:tos_update).with(user, admin_post.id) + end + end + + context "with multiple users" do + before { create_list(:user, 10) } + + it "enqueues multiple tos update notifications" do + ActiveJob::Base.queue_adapter = :test + expect(User.all.size).to eq(10) + expect { subject.invoke(admin_post.id) } + .to have_enqueued_mail(TosUpdateMailer, :tos_update_notification).on_queue(:tos_update).with(instance_of(User), admin_post.id).exactly(10) + end + end +end diff --git a/spec/mailers/tos_update_mailer_spec.rb b/spec/mailers/tos_update_mailer_spec.rb new file mode 100644 index 00000000000..d3070a0ac4d --- /dev/null +++ b/spec/mailers/tos_update_mailer_spec.rb @@ -0,0 +1,38 @@ +require "spec_helper" + +describe TosUpdateMailer do + describe "#tos_update_notification" do + let(:user) { create(:user) } + let(:admin_post) { create(:admin_post) } + let(:email) { TosUpdateMailer.tos_update_notification(user, admin_post.id) } + + it_behaves_like "an email with a valid sender" + it_behaves_like "a multipart email" + it_behaves_like "a translated email" + + it "has the correct subject line" do + subject = "[#{ArchiveConfig.APP_SHORT_NAME}] Updates to #{ArchiveConfig.APP_SHORT_NAME}'s Terms of Service" + expect(email).to have_subject(subject) + end + + it "delivers to the correct address" do + expect(email).to deliver_to(user.email) + end + + describe "HTML version" do + it "has the correct content" do + expect(email).to have_html_part_content("are or are not allowed. If your fanwork was allowed on AO3 before, then it is still allowed.") + expect(email).to have_html_part_content("href=\"#{admin_post_url(admin_post)}\">news post about the 2024 Terms of Service updates") + end + end + + describe "text version" do + it "has the correct content" do + expect(email).to have_text_part_content("are or are not allowed. If your fanwork was allowed on AO3 before, then it is still allowed.") + expect(email).to have_text_part_content("news post about the 2024 Terms of Service updates: #{admin_post_url(admin_post)}") + end + end + end +end diff --git a/spec/mailers/user_mailer_spec.rb b/spec/mailers/user_mailer_spec.rb index 09aca017cee..0ec3abe00ef 100644 --- a/spec/mailers/user_mailer_spec.rb +++ b/spec/mailers/user_mailer_spec.rb @@ -149,7 +149,7 @@ title = "Façade" title2 = Faker::Book.title - subject(:email) { UserMailer.claim_notification(author.id, [work.id, work2.id], true) } + subject(:email) { UserMailer.claim_notification(author.id, [work.id, work2.id]) } let(:author) { create(:user) } let(:work) { create(:work, title: title, authors: [author.pseuds.first]) } @@ -377,7 +377,7 @@ let!(:collection) { create(:collection, challenge: gift_exchange, challenge_type: "GiftExchange") } let!(:otheruser) { create(:user) } let!(:offer) { create(:challenge_signup, collection: collection, pseud: otheruser.default_pseud) } - let!(:open_assignment) { create(:challenge_assignment, collection: collection, offer_signup: offer) } + let!(:open_assignment) { create(:challenge_assignment, collection: collection, offer_signup: offer, sent_at: Time.current) } # Test the headers it_behaves_like "an email with a valid sender" @@ -678,11 +678,12 @@ end it "formats the date rightfully in French" do - I18n.locale = "fr" - travel_to "2022-03-14 13:27:09 +0000" do - expect(email).to have_html_part_content("Envoyé le 14 mars 2022 13h 27min 09s.") - expect(email).to have_text_part_content("Envoyé le 14 mars 2022 13h 27min 09s.") - end + I18n.with_locale("fr") do + travel_to "2022-03-14 13:27:09 +0000" do + expect(email).to have_html_part_content("Envoyé le 14 mars 2022 13h 27min 09s.") + expect(email).to have_text_part_content("Envoyé le 14 mars 2022 13h 27min 09s.") + end + end end end end @@ -933,7 +934,7 @@ end describe "potential_match_generation_notification" do - subject(:email) { UserMailer.potential_match_generation_notification(collection.id) } + subject(:email) { UserMailer.potential_match_generation_notification(collection.id, "test@example.com") } let(:collection) { create(:collection) } @@ -966,7 +967,7 @@ end describe "invalid_signup_notification" do - subject(:email) { UserMailer.invalid_signup_notification(collection.id, [signup.id]) } + subject(:email) { UserMailer.invalid_signup_notification(collection.id, [signup.id], "test@example.com") } let(:collection) { create(:collection) } let(:signup) { create(:challenge_signup) } @@ -998,7 +999,7 @@ end describe "collection_notification" do - subject(:email) { UserMailer.collection_notification(collection.id, subject_text, message_text) } + subject(:email) { UserMailer.collection_notification(collection.id, subject_text, message_text, "test@example.com") } let(:collection) { create(:collection) } let(:subject_text) { Faker::Hipster.sentence } diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb index 3ef26276bc0..6c68081ff51 100644 --- a/spec/models/abuse_report_spec.rb +++ b/spec/models/abuse_report_spec.rb @@ -359,6 +359,13 @@ .not_to have_enqueued_job end + it "does not attach a download for comment sub-URLs asynchronously" do + allow(subject).to receive(:url).and_return("http://archiveofourown.org/works/#{work.id}/comments/") + + expect { subject.attach_work_download(ticket_id) } + .not_to have_enqueued_job + end + it "attaches a download for work URLs asynchronously" do allow(subject).to receive(:url).and_return("http://archiveofourown.org/works/#{work.id}/") diff --git a/spec/models/collection_spec.rb b/spec/models/collection_spec.rb index 409249070a7..41ec0b5eb08 100644 --- a/spec/models/collection_spec.rb +++ b/spec/models/collection_spec.rb @@ -83,4 +83,34 @@ include /Sorry, a collection can only have 10 tags./ end end + + describe "#clear_icon" do + subject { create(:collection, icon_alt_text: "icon alt", icon_comment_text: "icon comment") } + + before do + subject.icon.attach(io: File.open(Rails.root.join("features/fixtures/icon.gif")), filename: "icon.gif", content_type: "image/gif") + end + + context "when delete_icon is false" do + it "does not clear the icon, icon alt, or icon comment" do + subject.clear_icon + expect(subject.icon.attached?).to be(true) + expect(subject.icon_alt_text).to eq("icon alt") + expect(subject.icon_comment_text).to eq("icon comment") + end + end + + context "when delete_icon is true" do + before do + subject.delete_icon = 1 + end + + it "clears the icon, icon alt, and icon comment" do + subject.clear_icon + expect(subject.icon.attached?).to be(false) + expect(subject.icon_alt_text).to be_nil + expect(subject.icon_comment_text).to be_nil + end + end + end end diff --git a/spec/models/concerns/justifiable_spec.rb b/spec/models/concerns/justifiable_spec.rb index b023e56a6f0..2e68cd2bd48 100644 --- a/spec/models/concerns/justifiable_spec.rb +++ b/spec/models/concerns/justifiable_spec.rb @@ -26,7 +26,7 @@ record.assign_attributes(attributes) expect(record).not_to be_valid - expect(record.errors[:ticket_number]).to contain_exactly("can't be blank", "is not a number") + expect(record.errors[:ticket_number]).to contain_exactly("can't be blank", "may begin with an # and otherwise contain only numbers.") expect(record.ticket_url).to be_nil end diff --git a/spec/models/feedback_reporters/support_reporter_spec.rb b/spec/models/feedback_reporters/support_reporter_spec.rb index bbed68e1494..90180eebbf6 100644 --- a/spec/models/feedback_reporters/support_reporter_spec.rb +++ b/spec/models/feedback_reporters/support_reporter_spec.rb @@ -14,7 +14,10 @@ username: "Walrus", user_agent: "HTTParty", site_revision: "eternal_beta", - rollout: "rollout_value" + rollout: "rollout_value", + ip_address: "127.0.0.1", + referer: "https://example.com/works/1", + site_skin: build(:skin, title: "Reversi", public: true) } end @@ -30,7 +33,10 @@ "cf_name" => "Walrus", "cf_archive_version" => "eternal_beta", "cf_rollout" => "rollout_value", - "cf_user_agent" => "HTTParty" + "cf_user_agent" => "HTTParty", + "cf_ip" => "127.0.0.1", + "cf_url" => "https://example.com/works/1", + "cf_site_skin" => "Reversi" } } end @@ -97,5 +103,55 @@ expect(subject.report_attributes.fetch("description")).to eq("Hi!http://example.com/Camera-icon.svgBye!") end end + + context "if the report has an empty IP address" do + before do + allow(subject).to receive(:ip_address).and_return("") + end + + it "returns a hash containing 'Unknown' for IP address" do + expect(subject.report_attributes.dig("cf", "cf_ip")).to eq("Unknown IP") + end + end + + context "if the report has an empty referer" do + before do + allow(subject).to receive(:referer).and_return("") + end + + it "returns a hash containing a blank string for referer" do + expect(subject.report_attributes.dig("cf", "cf_url")).to eq("Unknown URL") + end + end + + context "if the reporter has a very long referer" do + before do + allow(subject).to receive(:referer).and_return("a" * 256) + end + + it "truncates the referer to 255 characters" do + expect(subject.report_attributes.dig("cf", "cf_url").length).to eq(255) + end + end + + context "if the report has an empty skin" do + before do + allow(subject).to receive(:site_skin).and_return(nil) + end + + it "returns a hash containing the custom skin placeholder" do + expect(subject.report_attributes.dig("cf", "cf_site_skin")).to eq("Custom skin") + end + end + + context "if the report has a private skin" do + before do + allow(subject).to receive(:site_skin).and_return(build(:skin, public: false)) + end + + it "returns a hash containing the custom skin placeholder" do + expect(subject.report_attributes.dig("cf", "cf_site_skin")).to eq("Custom skin") + end + end end end diff --git a/spec/models/pseud_spec.rb b/spec/models/pseud_spec.rb index e18cb964426..0d80c13f100 100644 --- a/spec/models/pseud_spec.rb +++ b/spec/models/pseud_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" describe Pseud do it { is_expected.to have_many(:gifts).conditions(rejected: false).dependent(:destroy) } @@ -17,38 +17,32 @@ end describe "save" do - before do - @user = User.new - @user.login = "mynamepseud" - @user.age_over_13 = "1" - @user.terms_of_service = "1" - @user.email = "foo1@archiveofourown.org" - @user.password = "password" - @user.save + context "when the pseud is valid" do + let(:pseud) { build(:pseud) } + + it "succeeds" do + expect(pseud).to be_valid_verbose + expect(pseud.save).to be_truthy + expect(pseud.errors).to be_empty + end end - before(:each) do - @pseud = Pseud.new - @pseud.user_id = @user.id - @pseud.name = "MyName" - end + context "when the icon alt text is too long" do + let(:pseud) { build(:pseud, icon_alt_text: "a" * 251) } - it "should save a minimalistic pseud" do - @pseud.should be_valid_verbose - expect(@pseud.save).to be_truthy - @pseud.errors.should be_empty + it "fails" do + expect(pseud.save).to be_falsey + expect(pseud.errors[:icon_alt_text]).not_to be_empty + end end - it "should not save pseud with too-long alt text for icon" do - @pseud.icon_alt_text = "Something that is too long blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah blah this needs 250 characters lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum lorem ipsum" - expect(@pseud.save).to be_falsey - @pseud.errors[:icon_alt_text].should_not be_empty - end + context "when the icon comment text is too long" do + let(:pseud) { build(:pseud, icon_comment_text: "a" * 51) } - it "should not save pseud with too-long comment text for icon" do - @pseud.icon_comment_text = "Something that is too long blah blah blah blah blah blah this needs a mere 50 characters" - expect(@pseud.save).to be_falsey - @pseud.errors[:icon_comment_text].should_not be_empty + it "fails" do + expect(pseud.save).to be_falsey + expect(pseud.errors[:icon_comment_text]).not_to be_empty + end end end @@ -99,4 +93,34 @@ expect(subject.length).to eq(ArchiveConfig.ITEMS_PER_PAGE) end end + + describe "#clear_icon" do + subject { create(:pseud, icon_alt_text: "icon alt", icon_comment_text: "icon comment") } + + before do + subject.icon.attach(io: File.open(Rails.root.join("features/fixtures/icon.gif")), filename: "icon.gif", content_type: "image/gif") + end + + context "when delete_icon is false" do + it "does not clear the icon, icon alt, or icon comment" do + subject.clear_icon + expect(subject.icon.attached?).to be(true) + expect(subject.icon_alt_text).to eq("icon alt") + expect(subject.icon_comment_text).to eq("icon comment") + end + end + + context "when delete_icon is true" do + before do + subject.delete_icon = 1 + end + + it "clears the icon, icon alt, and icon comment" do + subject.clear_icon + expect(subject.icon.attached?).to be(false) + expect(subject.icon_alt_text).to be_nil + expect(subject.icon_comment_text).to be_nil + end + end + end end diff --git a/spec/models/skin_spec.rb b/spec/models/skin_spec.rb index 10e32805559..b5b2772b8ba 100644 --- a/spec/models/skin_spec.rb +++ b/spec/models/skin_spec.rb @@ -177,7 +177,8 @@ "errors when saving gradient with xss" => "div {background: -webkit-linear-gradient(url(xss.htc))}", "errors when saving dsf images" => "body {background: url(http://foo.com/bar.dsf)}", "errors when saving urls with invalid domain" => "body {background: url(http://foo.htc/bar.png)}", - "errors when saving xss interrupted with comments" => "div {xss:expr/*XSS*/ession(alert('XSS'))}" + "errors when saving xss interrupted with comments" => "div {xss:expr/*XSS*/ession(alert('XSS'))}", + "errors when saving url followed by something else" => 'a {content: url(/images/fakeimage.png) " (" attr(href) ")"}' }.each_pair do |condition, css| it condition do @skin.css = css diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 5296fc01611..ab0bb71f177 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -103,6 +103,15 @@ end end + context "missing data_processing flag" do + let(:no_data_processing) { build(:user, data_processing: "0") } + + it "does not save" do + expect(no_data_processing.save).to be_falsey + expect(no_data_processing.errors[:data_processing].first).to include("you need to consent to the processing of your personal data") + end + end + context "missing the terms_of_service flag" do let(:no_tos) { build(:user, terms_of_service: "0") } diff --git a/spec/requests/blocked_users_n_plus_one_spec.rb b/spec/requests/blocked_users_n_plus_one_spec.rb new file mode 100644 index 00000000000..800b66c7a65 --- /dev/null +++ b/spec/requests/blocked_users_n_plus_one_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "n+1 queries in the blocked users controller" do + include LoginMacros + + describe "#index", n_plus_one: true do + context "with a logged in user who has blocked someone" do + let!(:blocker) { create(:user) } + + populate do |n| + blocked_users = create_list(:user, n) + blocked_users.each do |blocked| + # Rails doesn't seem to want to include variants, so this won't work right now. + # We can revisit when https://github.com/rails/rails/pull/49231 is released OR we upgrade to Rails 7.1 + # blocked.default_pseud.icon.attach(io: File.open(Rails.root.join("features/fixtures/icon.gif")), filename: "icon.gif", content_type: "image/gif") + Block.create(blocker: blocker, blocked: blocked) + end + end + + before do + fake_login_known_user(blocker) + end + + subject do + proc do + get user_blocked_users_path(user_id: blocker) + end + end + + warmup { subject.call } + + it "produces a constant number of queries" do + expect { subject.call } + .to perform_constant_number_of_queries + end + end + end +end diff --git a/spec/requests/collection_items_n_plus_one_spec.rb b/spec/requests/collection_items_n_plus_one_spec.rb new file mode 100644 index 00000000000..925f0ed80be --- /dev/null +++ b/spec/requests/collection_items_n_plus_one_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "n+1 queries in the collection items controller" do + include LoginMacros + + describe "#index" do + context "when viewing collection items for a specific user", n_plus_one: true do + let!(:user) { create(:user) } + + populate do |n| + create_list(:work, n, authors: [user.default_pseud]).each do |work| + collection_item = create(:collection_item, item: work) + collection_item.collection.icon.attach(io: File.open(Rails.root.join("features/fixtures/icon.gif")), filename: "icon.gif", content_type: "image/gif") + end + end + + subject do + proc do + get user_collection_items_path(user_id: user) + end + end + + before do + fake_login_known_user(user) + end + + warmup { subject.call } + + it "produces a constant number of queries" do + expect { subject.call } + .to perform_constant_number_of_queries + end + end + end +end diff --git a/spec/requests/muted_users_n_plus_one_spec.rb b/spec/requests/muted_users_n_plus_one_spec.rb new file mode 100644 index 00000000000..c5b5d4153f5 --- /dev/null +++ b/spec/requests/muted_users_n_plus_one_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "n+1 queries in the muted users controller" do + include LoginMacros + + describe "#index", n_plus_one: true do + context "with a logged in user who has muted someone" do + let!(:muter) { create(:user) } + + populate do |n| + muted_users = create_list(:user, n) + muted_users.each do |muted| + # Rails doesn't seem to want to include variants, so this won't work right now. + # We can revisit when https://github.com/rails/rails/pull/49231 is released OR we upgrade to Rails 7.1 + # muted.default_pseud.icon.attach(io: File.open(Rails.root.join("features/fixtures/icon.gif")), filename: "icon.gif", content_type: "image/gif") + Mute.create(muter: muter, muted: muted) + end + end + + before do + fake_login_known_user(muter) + end + + subject do + proc do + get user_muted_users_path(user_id: muter) + end + end + + warmup { subject.call } + + it "produces a constant number of queries" do + expect { subject.call } + .to perform_constant_number_of_queries + end + end + end +end diff --git a/spec/requests/people_n_plus_one_spec.rb b/spec/requests/people_n_plus_one_spec.rb new file mode 100644 index 00000000000..675be3eab37 --- /dev/null +++ b/spec/requests/people_n_plus_one_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "n+1 queries in the people controller" do + describe "#index", n_plus_one: true do + context "when viewing people in a collection" do + let!(:collection) { create(:collection) } + + populate do |n| + create_list(:collection_participant, n, collection: collection) + end + + subject do + proc do + get collection_people_path(collection_id: collection) + end + end + + warmup { subject.call } + + it "produces a constant number of queries" do + expect { subject.call } + .to perform_constant_number_of_queries + end + end + end + + describe "#search", n_plus_one: true do + context "when there are search results" do + populate do |n| + PseudIndexer.prepare_for_testing + create_list(:pseud, n, name: "nplusone") + run_all_indexing_jobs + end + + subject do + proc do + get search_people_path, params: { "people_search" => { "name" => "nplusone" } } + end + end + + warmup { subject.call } + + it "produces a constant number of queries" do + expect { subject.call } + .to perform_constant_number_of_queries + end + end + end +end diff --git a/spec/requests/pseuds_n_plus_one_spec.rb b/spec/requests/pseuds_n_plus_one_spec.rb new file mode 100644 index 00000000000..f47370b525d --- /dev/null +++ b/spec/requests/pseuds_n_plus_one_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "n+1 queries in the user pseuds controller" do + include LoginMacros + + describe "#index", n_plus_one: true do + let!(:user) { create(:user) } + + populate do |n| + create_list(:pseud, n, user: user).each do |pseud| + pseud.icon.attach(io: File.open(Rails.root.join("features/fixtures/icon.gif")), filename: "icon.gif", content_type: "image/gif") + end + end + + before do + fake_login_known_user(user) + end + + subject do + proc do + get user_pseuds_path(user_id: user) + end + end + + warmup { subject.call } + + # TODO: https://otwarchive.atlassian.net/browse/AO3-6738 + xit "produces a constant number of queries" do + expect { subject.call } + .to perform_constant_number_of_queries + end + end +end diff --git a/spec/requests/skins_n_plus_one_spec.rb b/spec/requests/skins_n_plus_one_spec.rb new file mode 100644 index 00000000000..9ce29337ece --- /dev/null +++ b/spec/requests/skins_n_plus_one_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe "n+1 queries in the skins controller" do + include LoginMacros + + describe "#index", n_plus_one: true do + context "when displaying a user's work skins" do + let!(:user) { create(:user) } + + populate do |n| + create_list(:work_skin, n, :private, author: user) + end + + subject do + proc do + get user_skins_path(user_id: user), params: { "skin_type" => "WorkSkin" } + end + end + + before do + fake_login_known_user(user) + end + + warmup { subject.call } + + it "produces a constant number of queries" do + expect { subject.call } + .to perform_constant_number_of_queries + end + end + + context "when displaying a user's site skins" do + let!(:user) { create(:user) } + + populate do |n| + create_list(:skin, n, author: user) + end + + subject do + proc do + get user_skins_path(user_id: user), params: { "skin_type" => "Site" } + end + end + + before do + fake_login_known_user(user) + end + + warmup { subject.call } + + it "produces a constant number of queries" do + expect { subject.call } + .to perform_constant_number_of_queries + end + end + + context "when displaying public work skins" do + populate do |n| + create_list(:work_skin, n, :public) + end + + subject do + proc do + get skins_path, params: { "skin_type" => "WorkSkin" } + end + end + + warmup { subject.call } + + it "produces a constant number of queries" do + expect { subject.call } + .to perform_constant_number_of_queries + end + end + + context "when displaying public site skins" do + populate do |n| + create_list(:skin, n, :public) + end + + subject do + proc do + get skins_path, params: { "skin_type" => "Site" } + end + end + + warmup { subject.call } + + it "produces a constant number of queries" do + expect { subject.call } + .to perform_constant_number_of_queries + end + end + end +end diff --git a/test/fixtures/pseuds.yml b/test/fixtures/pseuds.yml index a2da3234dce..ba35a3fc967 100644 --- a/test/fixtures/pseuds.yml +++ b/test/fixtures/pseuds.yml @@ -8,11 +8,7 @@ pseud_00027: is_default: false id: 27 user_id: 21 - icon_file_size: - icon_file_name: - icon_content_type: description: - icon_updated_at: pseud_00016: name: Non-Default Pseud icon_alt_text: "" @@ -22,11 +18,7 @@ pseud_00016: is_default: false id: 16 user_id: 2 - icon_file_size: - icon_file_name: - icon_content_type: description: "" - icon_updated_at: pseud_00005: name: Non-Default Pseud icon_alt_text: "" @@ -36,11 +28,7 @@ pseud_00005: is_default: false id: 5 user_id: 1 - icon_file_size: - icon_file_name: - icon_content_type: description: This is a non default pseud for testuser. - icon_updated_at: pseud_00038: name: tassos icon_alt_text: "" @@ -50,11 +38,7 @@ pseud_00038: is_default: false id: 38 user_id: 31 - icon_file_size: - icon_file_name: - icon_content_type: description: - icon_updated_at: pseud_00039: name: Enigel icon_alt_text: "" @@ -64,11 +48,7 @@ pseud_00039: is_default: false id: 39 user_id: 32 - icon_file_size: - icon_file_name: - icon_content_type: description: - icon_updated_at: pseud_00006: name: orphan_account icon_alt_text: "" @@ -78,11 +58,7 @@ pseud_00006: is_default: true id: 6 user_id: 6 - icon_file_size: - icon_file_name: - icon_content_type: description: Default pseud - icon_updated_at: pseud_00017: name: Some other fucking pseud icon_alt_text: "" @@ -92,11 +68,7 @@ pseud_00017: is_default: false id: 17 user_id: 1 - icon_file_size: - icon_file_name: - icon_content_type: description: some fucking description - icon_updated_at: pseud_00028: name: Jenn_Calaelen icon_alt_text: "" @@ -106,11 +78,7 @@ pseud_00028: is_default: false id: 28 user_id: 22 - icon_file_size: - icon_file_name: - icon_content_type: description: - icon_updated_at: pseud_00040: name: invitest icon_alt_text: "" @@ -120,11 +88,7 @@ pseud_00040: is_default: false id: 40 user_id: 33 - icon_file_size: - icon_file_name: - icon_content_type: description: - icon_updated_at: pseud_00041: name: thelongestpseudinthegoshdarnworldomgwtfo icon_alt_text: "" @@ -134,11 +98,7 @@ pseud_00041: is_default: false id: 41 user_id: 1 - icon_file_size: - icon_file_name: - icon_content_type: description: "" - icon_updated_at: pseud_00029: name: Llwyden icon_alt_text: "" @@ -148,11 +108,7 @@ pseud_00029: is_default: true id: 29 user_id: 23 - icon_file_size: - icon_file_name: - icon_content_type: description: - icon_updated_at: pseud_00018: name: unique snowflake icon_alt_text: "" @@ -162,11 +118,7 @@ pseud_00018: is_default: false id: 18 user_id: 4 - icon_file_size: - icon_file_name: - icon_content_type: description: This pseud is just for reccing / bookmarking. - icon_updated_at: pseud_00007: name: Ash icon_alt_text: "" @@ -176,11 +128,7 @@ pseud_00007: is_default: true id: 7 user_id: 5 - icon_file_size: - icon_file_name: - icon_content_type: description: Default pseud - icon_updated_at: pseud_00030: name: moriann icon_alt_text: "" @@ -190,11 +138,7 @@ pseud_00030: is_default: false id: 30 user_id: 24 - icon_file_size: - icon_file_name: - icon_content_type: description: - icon_updated_at: pseud_00008: name: Boccaccio icon_alt_text: "" @@ -204,11 +148,7 @@ pseud_00008: is_default: true id: 8 user_id: 7 - icon_file_size: - icon_file_name: - icon_content_type: description: Default pseud - icon_updated_at: pseud_00019: name: parenthetical icon_alt_text: "" @@ -218,11 +158,7 @@ pseud_00019: is_default: false id: 19 user_id: 15 - icon_file_size: - icon_file_name: - icon_content_type: description: - icon_updated_at: pseud_00031: name: badjuju icon_alt_text: "" @@ -232,11 +168,7 @@ pseud_00031: is_default: false id: 31 user_id: 25 - icon_file_size: - icon_file_name: - icon_content_type: description: - icon_updated_at: pseud_00020: name: sarkymoocow icon_alt_text: "" @@ -246,11 +178,7 @@ pseud_00020: is_default: false id: 20 user_id: 15 - icon_file_size: - icon_file_name: - icon_content_type: description: parenthetical back in her HP days - icon_updated_at: pseud_00009: name: Chaucer icon_alt_text: "" @@ -260,11 +188,7 @@ pseud_00009: is_default: true id: 9 user_id: 8 - icon_file_size: - icon_file_name: - icon_content_type: description: Default pseud - icon_updated_at: pseud_00010: name: Dean icon_alt_text: "" @@ -274,11 +198,7 @@ pseud_00010: is_default: true id: 10 user_id: 9 - icon_file_size: - icon_file_name: - icon_content_type: description: Default pseud - icon_updated_at: pseud_00021: name: cal icon_alt_text: "" @@ -288,11 +208,7 @@ pseud_00021: is_default: false id: 21 user_id: 16 - icon_file_size: - icon_file_name: - icon_content_type: description: - icon_updated_at: pseud_00032: name: DawningStar icon_alt_text: "" @@ -302,11 +218,7 @@ pseud_00032: is_default: false id: 32 user_id: 26 - icon_file_size: - icon_file_name: - icon_content_type: description: - icon_updated_at: pseud_00033: name: yulechatismadeofawesome icon_alt_text: "" @@ -316,11 +228,7 @@ pseud_00033: is_default: false id: 33 user_id: 27 - icon_file_size: - icon_file_name: - icon_content_type: description: - icon_updated_at: pseud_00022: name: reccer icon_alt_text: "" @@ -330,11 +238,7 @@ pseud_00022: is_default: false id: 22 user_id: 17 - icon_file_size: - icon_file_name: - icon_content_type: description: - icon_updated_at: pseud_00011: name: Sam icon_alt_text: "" @@ -344,11 +248,7 @@ pseud_00011: is_default: true id: 11 user_id: 10 - icon_file_size: - icon_file_name: - icon_content_type: description: Default pseud - icon_updated_at: pseud_00012: name: Ellen icon_alt_text: "" @@ -358,11 +258,7 @@ pseud_00012: is_default: true id: 12 user_id: 11 - icon_file_size: - icon_file_name: - icon_content_type: description: Default pseud - icon_updated_at: pseud_00001: name: testuser icon_alt_text: "" @@ -372,11 +268,7 @@ pseud_00001: is_default: false id: 1 user_id: 1 - icon_file_size: - icon_file_name: - icon_content_type: description: This is a default pseud for testuser. - icon_updated_at: pseud_00034: name: Shusu icon_alt_text: "" @@ -386,11 +278,7 @@ pseud_00034: is_default: false id: 34 user_id: 28 - icon_file_size: - icon_file_name: - icon_content_type: description: - icon_updated_at: pseud_00035: name: Llwyden ferch Gyfrinach icon_alt_text: "" @@ -400,11 +288,7 @@ pseud_00035: is_default: false id: 35 user_id: 23 - icon_file_size: - icon_file_name: - icon_content_type: description: "" - icon_updated_at: pseud_00002: name: testuser2 icon_alt_text: "" @@ -414,11 +298,7 @@ pseud_00002: is_default: true id: 2 user_id: 2 - icon_file_size: - icon_file_name: - icon_content_type: description: This is a default pseud for testuser2. - icon_updated_at: pseud_00013: name: Zooey_Glass icon_alt_text: "" @@ -428,11 +308,7 @@ pseud_00013: is_default: true id: 13 user_id: 12 - icon_file_size: - icon_file_name: - icon_content_type: description: Default pseud - icon_updated_at: pseud_00024: name: Zee icon_alt_text: "" @@ -442,11 +318,7 @@ pseud_00024: is_default: false id: 24 user_id: 19 - icon_file_size: - icon_file_name: - icon_content_type: description: - icon_updated_at: pseud_00025: name: Cal icon_alt_text: "" @@ -456,16 +328,12 @@ pseud_00025: is_default: true id: 25 user_id: 1 - icon_file_size: - icon_file_name: - icon_content_type: description: |- Bold Italics Emphasised Strong This is a link - icon_updated_at: pseud_00014: name: Jessica icon_alt_text: "" @@ -475,11 +343,7 @@ pseud_00014: is_default: true id: 14 user_id: 13 - icon_file_size: - icon_file_name: - icon_content_type: description: Default pseud - icon_updated_at: pseud_00003: name: testuser3 icon_alt_text: "" @@ -489,11 +353,7 @@ pseud_00003: is_default: true id: 3 user_id: 3 - icon_file_size: - icon_file_name: - icon_content_type: description: This is a default pseud. - icon_updated_at: pseud_00036: name: whetherwoman icon_alt_text: "" @@ -503,11 +363,7 @@ pseud_00036: is_default: false id: 36 user_id: 29 - icon_file_size: - icon_file_name: - icon_content_type: description: - icon_updated_at: pseud_00037: name: Lizifer icon_alt_text: "" @@ -517,11 +373,7 @@ pseud_00037: is_default: false id: 37 user_id: 30 - icon_file_size: - icon_file_name: - icon_content_type: description: - icon_updated_at: pseud_00004: name: testuser4 icon_alt_text: "" @@ -531,11 +383,7 @@ pseud_00004: is_default: true id: 4 user_id: 4 - icon_file_size: - icon_file_name: - icon_content_type: description: This is a default pseud. - icon_updated_at: pseud_00015: name: Uriel icon_alt_text: "" @@ -545,11 +393,7 @@ pseud_00015: is_default: true id: 15 user_id: 14 - icon_file_size: - icon_file_name: - icon_content_type: description: Default pseud - icon_updated_at: pseud_00026: name: hh_ook icon_alt_text: "" @@ -559,8 +403,5 @@ pseud_00026: is_default: false id: 26 user_id: 20 - icon_file_size: - icon_file_name: - icon_content_type: description: icon_updated_at: diff --git a/test/fixtures/roles.yml b/test/fixtures/roles.yml index 092f2106a6e..1ac4cf214b2 100644 --- a/test/fixtures/roles.yml +++ b/test/fixtures/roles.yml @@ -1,50 +1,22 @@ ---- -role_00002: +--- +role_00002: name: translator - created_at: 2008-11-09 03:26:02 Z - updated_at: 2008-11-09 03:26:02 Z - authorizable_type: id: 2 - authorizable_id: -role_00004: - name: translator - created_at: 2009-06-27 18:59:55 Z - updated_at: 2009-06-27 18:59:55 Z - authorizable_type: Locale - id: 4 - authorizable_id: 3 -role_00005: - name: translator - created_at: 2009-06-27 18:59:55 Z - updated_at: 2009-06-27 18:59:55 Z - authorizable_type: Locale - id: 5 - authorizable_id: 4 -role_00006: +role_00006: name: archivist - created_at: 2008-11-09 03:26:02 Z - updated_at: 2008-11-09 03:26:02 Z - authorizable_type: id: 6 - authorizable_id: -role_00001: +role_00001: name: tag_wrangler - created_at: 2008-11-09 03:26:02 Z - updated_at: 2008-11-09 03:26:02 Z - authorizable_type: id: 1 - authorizable_id: role_00007: name: opendoors - created_at: 2008-11-09 03:26:02 Z - updated_at: 2008-11-09 03:26:02 Z - authorizable_type: id: 7 - authorizable_id: role_00008: name: official - updated_at: 2021-04-21 14:27:00 Z - created_at: 2021-04-21 14:27:00 Z - authorizable_type: id: 8 - authorizable_id: +role_00009: + name: protected_user + id: 9 +role_00010: + name: no_resets + id: 10 diff --git a/test/fixtures/roles_users.yml b/test/fixtures/roles_users.yml index 63940c74762..9e47bb837e0 100644 --- a/test/fixtures/roles_users.yml +++ b/test/fixtures/roles_users.yml @@ -1,51 +1,36 @@ ---- -join_00004: +--- +join_00004: created_at: 2009-06-27 18:59:55 updated_at: 2009-06-27 18:59:55 - role_id: "5" + role_id: "2" user_id: "2" -join_00005: - created_at: - updated_at: +join_00005: + created_at: + updated_at: role_id: "1" user_id: "1" -join_00006: - created_at: - updated_at: +join_00006: + created_at: + updated_at: role_id: "2" user_id: "1" -join_00007: - created_at: 2009-06-27 19:53:45 - updated_at: 2009-06-27 19:53:45 - role_id: "3" - user_id: "15" -join_00008: +join_00008: created_at: 2008-11-09 04:26:02 updated_at: 2008-11-09 04:26:02 role_id: "1" user_id: "15" -join_00009: +join_00009: created_at: 2008-11-09 03:26:02 updated_at: 2008-11-09 03:26:02 role_id: "2" user_id: "7" -join_00000: +join_00000: created_at: 2008-11-09 03:26:02 updated_at: 2008-11-09 03:26:02 role_id: "1" user_id: "7" -join_00001: +join_00001: created_at: 2009-06-27 18:59:55 updated_at: 2009-06-27 18:59:55 role_id: "6" user_id: "1" -join_00002: - created_at: 2009-06-27 18:53:45 - updated_at: 2009-06-27 18:53:45 - role_id: "3" - user_id: "1" -join_00003: - created_at: 2009-06-27 18:59:55 - updated_at: 2009-06-27 18:59:55 - role_id: "4" - user_id: "1" diff --git a/test/fixtures/tags.yml b/test/fixtures/tags.yml index bf3f8a07f12..f9794cb21da 100644 --- a/test/fixtures/tags.yml +++ b/test/fixtures/tags.yml @@ -5629,7 +5629,7 @@ tag_1063754075: id: 1063754075 type: Freeform tag_1063753743: - name: Underage + name: Underage Sex merger_id: last_wrangler_type: canonical: true diff --git a/test/mailers/previews/tos_update_mailer_preview.rb b/test/mailers/previews/tos_update_mailer_preview.rb new file mode 100644 index 00000000000..459dcb2a085 --- /dev/null +++ b/test/mailers/previews/tos_update_mailer_preview.rb @@ -0,0 +1,8 @@ +class TosUpdateMailerPreview < ApplicationMailerPreview + # Sent by notifications:send_tos_update + def tos_update_notification + user = create(:user, :for_mailer_preview) + admin_post = create(:admin_post) + TosUpdateMailer.tos_update_notification(user, admin_post.id) + end +end diff --git a/test/mailers/previews/user_mailer_preview.rb b/test/mailers/previews/user_mailer_preview.rb index 19ae2739bb0..802b0ce4a0c 100644 --- a/test/mailers/previews/user_mailer_preview.rb +++ b/test/mailers/previews/user_mailer_preview.rb @@ -34,12 +34,91 @@ def feedback UserMailer.feedback(feedback.id) end - def claim_notification_registered + # Sent by gift exchanges to the participants + # Variant with tag fields set to "Any" and no due date + # URL: /rails/mailers/user_mailer/challenge_assignment_notification_any?sent_at=2025-01-23T20:00 + def challenge_assignment_notification_any + assignment = create(:challenge_assignment, sent_at: (params[:sent_at] ? params[:sent_at].to_time : Time.current)) + + signup = assignment.request_signup + signup.update(pseud: create(:user, :for_mailer_preview).default_pseud) + + # Fill all tag fields with "Any" + prompt = signup.requests.first + TagSet::TAG_TYPES.each do |type| + prompt.send(:"any_#{type}=", true) + end + prompt.title = "This is a title" + prompt.save! + + UserMailer.challenge_assignment_notification(assignment.collection.id, assignment.offering_user.id, assignment.id) + end + + # Sent by gift exchanges to the participants + # Variant with flexible due date, 3 tags per type and all fields filled out + # URL: /rails/mailers/user_mailer/challenge_assignment_notification_filled?sent_at=2025-01-23T20:00&due=2021-12-15T13:45 + def challenge_assignment_notification_filled + assignment = create(:challenge_assignment, sent_at: (params[:sent_at] ? params[:sent_at].to_time : Time.current)) + + challenge = assignment.collection.challenge + challenge.update(assignments_due_at: params[:due] ? params[:due].to_time : Time.current) + + signup = assignment.request_signup + signup.update(pseud: create(:user, :for_mailer_preview).default_pseud) + + # Allow up to 3 tags per type + request_restriction = challenge.request_restriction + TagSet::TAG_TYPES.each do |type| + request_restriction.send(:"#{type}_num_allowed=", 3) + end + request_restriction.save! + + # Tag set with 3 tags per type + tag_set = create(:tag_set, tags: []) + tag_set.archive_warning_tagnames = [ArchiveConfig.WARNING_VIOLENCE_TAG_NAME, ArchiveConfig.WARNING_DEATH_TAG_NAME, ArchiveConfig.WARNING_NONCON_TAG_NAME].join(ArchiveConfig.DELIMITER_FOR_OUTPUT) + tag_set.rating_tagnames = [ArchiveConfig.RATING_EXPLICIT_TAG_NAME, ArchiveConfig.RATING_MATURE_TAG_NAME, ArchiveConfig.RATING_TEEN_TAG_NAME].join(ArchiveConfig.DELIMITER_FOR_OUTPUT) + tag_set.category_tagnames = [ArchiveConfig.CATEGORY_GEN_TAG_NAME, ArchiveConfig.CATEGORY_HET_TAG_NAME, ArchiveConfig.CATEGORY_SLASH_TAG_NAME].join(ArchiveConfig.DELIMITER_FOR_OUTPUT) + %w[fandom character relationship freeform].each do |type| + tag_set.tags += [create(:"canonical_#{type}"), create(:"canonical_#{type}"), create(:"canonical_#{type}")] + end + tag_set.save! + + prompt = signup.requests.first + prompt.tag_set = tag_set + prompt.title = "This is a title" + prompt.url = "https://example.com/" + prompt.optional_tag_set = create(:tag_set, tags: [create(:freeform), create(:freeform), create(:freeform)]) + prompt.save! + + UserMailer.challenge_assignment_notification(assignment.collection.id, assignment.offering_user.id, assignment.id) + end + + def claim_notification work = create(:work) creator_id = work.pseuds.first.user.id - UserMailer.claim_notification(creator_id, [work.id], true) + UserMailer.claim_notification(creator_id, [work.id]) + end + + def invite_request_declined + user = create(:user, :for_mailer_preview) + total = params[:total] ? params[:total].to_i : 1 + reason = "test reason" + UserMailer.invite_request_declined(user.id, total, reason) end + def change_email + user = create(:user, :for_mailer_preview) + old_email = user.email + new_email = "new_email" + UserMailer.change_email(user.id, old_email, new_email) + end + + def invite_increase_notification + user = create(:user, :for_mailer_preview) + total = params[:total] || 1 + UserMailer.invite_increase_notification(user.id, total.to_i) + end + private def creatorship_notification_data(creation_type)