diff options
477 files changed, 4930 insertions, 2096 deletions
diff --git a/.github/issue_template.md b/.github/issue_template.md index 2d071d4a71..2ff6a271db 100644 --- a/.github/issue_template.md +++ b/.github/issue_template.md @@ -1,7 +1,7 @@ ### Steps to reproduce (Guidelines for creating a bug report are [available -here](http://guides.rubyonrails.org/contributing_to_ruby_on_rails.html#creating-a-bug-report)) +here](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#creating-a-bug-report)) ### Expected behavior Tell us what should happen diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 214d63740c..a36687ec99 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -16,6 +16,6 @@ CHANGELOG files by reviewers, please add the CHANGELOG entry at the top of the f Finally, if your pull request affects documentation or any non-code changes, guidelines for those changes are [available -here](http://guides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation) +here](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation) Thanks for contributing to Rails! diff --git a/.rubocop.yml b/.rubocop.yml index 3c765d5b1d..08d1c875dc 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -1,3 +1,5 @@ +require: './ci/custom_cops/lib/custom_cops' + AllCops: TargetRubyVersion: 2.4 # RuboCop has a bunch of cops enabled by default. This setting tells RuboCop @@ -7,6 +9,17 @@ AllCops: - '**/templates/**/*' - '**/vendor/**/*' - 'actionpack/lib/action_dispatch/journey/parser.rb' + - 'railties/test/fixtures/tmp/**/*' + +# Prefer assert_not_x over refute_x +CustomCops/RefuteNot: + Include: + - '**/*_test.rb' + +# Prefer assert_not over assert ! +CustomCops/AssertNot: + Include: + - '**/*_test.rb' # Prefer &&/|| over and/or. Style/AndOr: diff --git a/.travis.yml b/.travis.yml index 2513e87114..0fdea1367c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -24,6 +24,7 @@ addons: - ffmpeg - mupdf - mupdf-tools + - poppler-utils bundler_args: --without test --jobs 3 --retry 3 before_install: @@ -32,7 +33,7 @@ before_install: - "travis_retry gem install bundler" - "[ -f /tmp/beanstalkd-1.10/Makefile ] || (curl -L https://github.com/kr/beanstalkd/archive/v1.10.tar.gz | tar xz -C /tmp)" - "pushd /tmp/beanstalkd-1.10 && make && (./beanstalkd &); popd" - - "[[ -z $encrypted_8a915ebdd931_key && -z $encrypted_8a915ebdd931_iv ]] || openssl aes-256-cbc -K $encrypted_8a915ebdd931_key -iv $encrypted_8a915ebdd931_iv -in activestorage/test/service/configurations.yml.enc -out activestorage/test/service/configurations.yml -d" + - "[[ -z $encrypted_0fb9444d0374_key && -z $encrypted_0fb9444d0374_iv ]] || openssl aes-256-cbc -K $encrypted_0fb9444d0374_key -iv $encrypted_0fb9444d0374_iv -in activestorage/test/service/configurations.yml.enc -out activestorage/test/service/configurations.yml -d" - "[[ $GEM != 'av:ujs' ]] || nvm install node" - "[[ $GEM != 'av:ujs' ]] || node --version" - "[[ $GEM != 'av:ujs' ]] || (cd actionview && npm install)" @@ -60,21 +61,21 @@ env: - "GEM=ac:integration" rvm: - - 2.4.3 - - 2.5.0 + - 2.4.4 + - 2.5.1 - ruby-head matrix: include: - - rvm: 2.5.0 + - rvm: 2.5.1 env: "GEM=av:ujs" - - rvm: 2.4.3 + - rvm: 2.4.4 env: "GEM=aj:integration" services: - memcached - redis-server - rabbitmq - - rvm: 2.5.0 + - rvm: 2.5.1 env: "GEM=aj:integration" services: - memcached @@ -86,15 +87,15 @@ matrix: - memcached - redis-server - rabbitmq - - rvm: 2.5.0 + - rvm: 2.5.1 env: - "GEM=ar:mysql2 MYSQL=mariadb" addons: mariadb: 10.2 - - rvm: 2.5.0 + - rvm: 2.5.1 env: - "GEM=ar:sqlite3_mem" - - rvm: 2.5.0 + - rvm: 2.5.1 env: - "GEM=ar:postgresql POSTGRES=9.2" addons: diff --git a/Brewfile b/Brewfile new file mode 100644 index 0000000000..4ac325e80a --- /dev/null +++ b/Brewfile @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +tap "homebrew/core" +tap "homebrew/bundle" +tap "homebrew/services" +tap "caskroom/cask" +brew "ffmpeg" +brew "memcached" +brew "mysql" +brew "postgresql" +brew "redis" +brew "yarn" +cask "xquartz" +brew "mupdf" +brew "poppler" diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 078d5f1219..ecd56b87d6 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -8,5 +8,5 @@ http://rubyonrails.org/conduct/ For a history of updates, see the page history here: -https://github.com/rails/rails.github.com/commits/master/conduct/index.html +https://github.com/rails/homepage/commits/master/conduct.html @@ -9,17 +9,14 @@ gemspec # We need a newish Rake since Active Job sets its test tasks' descriptions. gem "rake", ">= 11.1" -# This needs to be with require false to ensure correct loading order, as it has to -# be loaded after loading the test library. -gem "mocha", require: false +gem "mocha" -gem "capybara", "~> 2.15" +gem "capybara", ">= 2.15" gem "rack-cache", "~> 1.2" gem "coffee-rails" gem "sass-rails" gem "turbolinks", "~> 5" -gem "webmock" # require: false so bcrypt is loaded only when has_secure_password is used. # This is to avoid Active Model (and by extension the entire framework) @@ -46,7 +43,7 @@ group :doc do end # Active Support. -gem "dalli", ">= 2.2.1" +gem "dalli" gem "listen", ">= 3.0.5", "< 3.2", require: false gem "libxml-ruby", platforms: :ruby gem "connection_pool", require: false @@ -65,9 +62,6 @@ group :job do gem "sneakers", require: false gem "que", require: false gem "backburner", require: false - # TODO: add qu after it support Rails 5.1 - # gem 'qu-rails', github: "bkeepers/qu", branch: "master", require: false - # gem "qu-redis", require: false gem "delayed_job_active_record", require: false gem "sequel", require: false end @@ -151,7 +145,7 @@ end platforms :rbx do # The rubysl-yaml gem doesn't ship with Psych by default as it needs # libyaml that isn't always available. - gem "psych", "~> 2.0" + gem "psych", "~> 3.0" end # Gems that are necessary for Active Record tests with Oracle. diff --git a/Gemfile.lock b/Gemfile.lock index baac1cee7e..23b668ac72 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -156,21 +156,21 @@ GEM childprocess faraday selenium-webdriver - bootsnap (1.1.2) + bootsnap (1.2.1) msgpack (~> 1.0) - bootsnap (1.1.2-java) + bootsnap (1.2.1-java) msgpack (~> 1.0) builder (3.2.3) bunny (2.6.6) amq-protocol (>= 2.1.0) byebug (9.0.6) - capybara (2.15.1) + capybara (3.0.1) addressable mini_mime (>= 0.1.3) - nokogiri (>= 1.3.3) - rack (>= 1.0.0) - rack-test (>= 0.5.4) - xpath (~> 2.0) + nokogiri (~> 1.8) + rack (>= 1.6.0) + rack-test (>= 0.6.3) + xpath (~> 3.0) childprocess (0.7.1) ffi (~> 1.0, >= 1.0.11) chromedriver-helper (1.1.0) @@ -187,12 +187,10 @@ GEM concurrent-ruby (1.0.5-java) connection_pool (2.2.1) cookiejar (0.3.3) - crack (0.4.3) - safe_yaml (~> 1.0.0) crass (1.0.3) curses (1.0.2) daemons (1.2.4) - dalli (2.7.6) + dalli (2.7.8) dante (0.2.0) declarative (0.0.10) declarative-option (0.1.0) @@ -261,7 +259,6 @@ GEM multi_json (~> 1.11) os (~> 0.9) signet (~> 0.7) - hashdiff (0.3.7) hiredis (0.6.1) hiredis (0.6.1-java) http_parser.rb (0.6.0) @@ -288,7 +285,7 @@ GEM logging (2.2.2) little-plugger (~> 1.1) multi_json (~> 1.10) - loofah (2.1.1) + loofah (2.2.1) crass (~> 1.0.2) nokogiri (>= 1.5.9) mail (2.7.0) @@ -303,7 +300,7 @@ GEM mime-types-data (3.2016.0521) mimemagic (0.3.2) mini_magick (4.8.0) - mini_mime (0.1.4) + mini_mime (1.0.0) mini_portile2 (2.3.0) minitest (5.11.3) minitest-bisect (1.4.0) @@ -311,28 +308,28 @@ GEM path_expander (~> 1.0) minitest-server (1.0.5) minitest (~> 5.0) - mocha (1.3.0) + mocha (1.5.0) metaclass (~> 0.0.1) mono_logger (1.1.0) - msgpack (1.1.0) - msgpack (1.1.0-java) - msgpack (1.1.0-x64-mingw32) - msgpack (1.1.0-x86-mingw32) + msgpack (1.2.4) + msgpack (1.2.4-java) + msgpack (1.2.4-x64-mingw32) + msgpack (1.2.4-x86-mingw32) multi_json (1.12.2) multipart-post (2.0.0) mustache (1.0.5) mustermann (1.0.2) - mysql2 (0.4.10) - mysql2 (0.4.10-x64-mingw32) - mysql2 (0.4.10-x86-mingw32) + mysql2 (0.5.0) + mysql2 (0.5.0-x64-mingw32) + mysql2 (0.5.0-x86-mingw32) nio4r (2.2.0) nio4r (2.2.0-java) - nokogiri (1.8.1) + nokogiri (1.8.2) mini_portile2 (~> 2.3.0) - nokogiri (1.8.1-java) - nokogiri (1.8.1-x64-mingw32) + nokogiri (1.8.2-java) + nokogiri (1.8.2-x64-mingw32) mini_portile2 (~> 2.3.0) - nokogiri (1.8.1-x86-mingw32) + nokogiri (1.8.2-x86-mingw32) mini_portile2 (~> 2.3.0) os (0.9.6) parallel (1.12.1) @@ -343,8 +340,8 @@ GEM pg (1.0.0-x64-mingw32) pg (1.0.0-x86-mingw32) powerpack (0.1.1) - psych (2.2.4) - public_suffix (3.0.1) + psych (3.0.2) + public_suffix (3.0.2) puma (3.9.1) puma (3.9.1-java) que (0.14.0) @@ -357,7 +354,7 @@ GEM rack (>= 0.4) rack-protection (2.0.1) rack - rack-test (0.8.0) + rack-test (1.0.0) rack (>= 1.0, < 3) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) @@ -400,7 +397,6 @@ GEM rubyzip (1.2.1) rufus-scheduler (3.4.2) et-orbi (~> 1.0) - safe_yaml (1.0.4) sass (3.5.3) sass-listen (~> 4.0.0) sass-listen (4.0.0) @@ -464,9 +460,9 @@ GEM thread_safe (0.3.6) thread_safe (0.3.6-java) tilt (2.0.8) - turbolinks (5.0.1) - turbolinks-source (~> 5) - turbolinks-source (5.0.3) + turbolinks (5.1.1) + turbolinks-source (~> 5.1) + turbolinks-source (5.1.0) tzinfo (1.2.3) thread_safe (~> 0.1) tzinfo-data (1.2017.2) @@ -482,18 +478,14 @@ GEM json (>= 1.8) nokogiri (~> 1.6) wdm (0.1.1) - webmock (3.2.1) - addressable (>= 2.3.6) - crack (>= 0.3.2) - hashdiff websocket (1.2.4) websocket-driver (0.6.5) websocket-extensions (>= 0.1.0) websocket-driver (0.6.5-java) websocket-extensions (>= 0.1.0) websocket-extensions (0.1.2) - xpath (2.1.0) - nokogiri (~> 1.3) + xpath (3.0.0) + nokogiri (~> 1.8) PLATFORMS java @@ -514,11 +506,11 @@ DEPENDENCIES blade-sauce_labs_plugin bootsnap (>= 1.1.0) byebug - capybara (~> 2.15) + capybara (>= 2.15) chromedriver-helper coffee-rails connection_pool - dalli (>= 2.2.1) + dalli delayed_job delayed_job_active_record google-cloud-storage (~> 1.8) @@ -533,7 +525,7 @@ DEPENDENCIES mysql2 (>= 0.4.10) nokogiri (>= 1.8.1) pg (>= 0.18.0) - psych (~> 2.0) + psych (~> 3.0) puma que queue_classic! @@ -563,7 +555,6 @@ DEPENDENCIES uglifier (>= 1.3.0) w3c_validators wdm (>= 0.1.0) - webmock websocket-client-simple! BUNDLED WITH @@ -1,48 +1,55 @@ # Welcome to Rails +## What's Rails + Rails is a web-application framework that includes everything needed to create database-backed web applications according to the [Model-View-Controller (MVC)](http://en.wikipedia.org/wiki/Model-view-controller) pattern. Understanding the MVC pattern is key to understanding Rails. MVC divides your -application into three layers, each with a specific responsibility. +application into three layers: Model, View, and Controller, each with a specific responsibility. + +## Model layer -The _Model layer_ represents your domain model (such as Account, Product, -Person, Post, etc.) and encapsulates the business logic that is specific to +The _**Model layer**_ represents the domain model (such as Account, Product, +Person, Post, etc.) and encapsulates the business logic specific to your application. In Rails, database-backed model classes are derived from -`ActiveRecord::Base`. Active Record allows you to present the data from +`ActiveRecord::Base`. [Active Record](activerecord/README.rdoc) allows you to present the data from database rows as objects and embellish these data objects with business logic -methods. You can read more about Active Record in its [README](activerecord/README.rdoc). +methods. Although most Rails models are backed by a database, models can also be ordinary Ruby classes, or Ruby classes that implement a set of interfaces as provided by -the Active Model module. You can read more about Active Model in its [README](activemodel/README.rdoc). +the [Active Model](activemodel/README.rdoc) module. + +## Controller layer -The _Controller layer_ is responsible for handling incoming HTTP requests and +The _**Controller layer**_ is responsible for handling incoming HTTP requests and providing a suitable response. Usually this means returning HTML, but Rails controllers can also generate XML, JSON, PDFs, mobile-specific views, and more. Controllers load and manipulate models, and render view templates in order to generate the appropriate HTTP response. In Rails, incoming requests are routed by Action Dispatch to an appropriate controller, and controller classes are derived from `ActionController::Base`. Action Dispatch and Action Controller -are bundled together in Action Pack. You can read more about Action Pack in its -[README](actionpack/README.rdoc). +are bundled together in [Action Pack](actionpack/README.rdoc). -The _View layer_ is composed of "templates" that are responsible for providing +## View layer + +The _**View layer**_ is composed of "templates" that are responsible for providing appropriate representations of your application's resources. Templates can come in a variety of formats, but most view templates are HTML with embedded Ruby code (ERB files). Views are typically rendered to generate a controller response, -or to generate the body of an email. In Rails, View generation is handled by Action View. -You can read more about Action View in its [README](actionview/README.rdoc). +or to generate the body of an email. In Rails, View generation is handled by [Action View](actionview/README.rdoc). + +## Frameworks and libraries -Active Record, Active Model, Action Pack, and Action View can each be used independently outside Rails. -In addition to that, Rails also comes with Action Mailer ([README](actionmailer/README.rdoc)), a library -to generate and send emails; Active Job ([README](activejob/README.md)), a +[Active Record](activerecord/README.rdoc), [Active Model](activemodel/README.rdoc), [Action Pack](actionpack/README.rdoc), and [Action View](actionview/README.rdoc) can each be used independently outside Rails. +In addition to that, Rails also comes with [Action Mailer](actionmailer/README.rdoc), a library +to generate and send emails; [Active Job](activejob/README.md), a framework for declaring jobs and making them run on a variety of queueing -backends; Action Cable ([README](actioncable/README.md)), a framework to -integrate WebSockets with a Rails application; -Active Storage ([README](activestorage/README.md)), a library to attach cloud +backends; [Action Cable](actioncable/README.md), a framework to +integrate WebSockets with a Rails application; [Active Storage](activestorage/README.md), a library to attach cloud and local files to Rails applications; -and Active Support ([README](activesupport/README.rdoc)), a collection +and [Active Support](activesupport/README.rdoc), a collection of utility classes and standard library extensions that are useful for Rails, and may also be used independently outside Rails. @@ -65,7 +72,7 @@ and may also be used independently outside Rails. Run with `--help` or `-h` for options. -4. Using a browser, go to `http://localhost:3000` and you'll see: +4. Go to `http://localhost:3000` and you'll see: "Yay! You’re on Rails!" 5. Follow the guidelines to start developing your application. You may find diff --git a/RELEASING_RAILS.md b/RELEASING_RAILS.md index c61fe7eb13..60434451fe 100644 --- a/RELEASING_RAILS.md +++ b/RELEASING_RAILS.md @@ -14,14 +14,14 @@ Today is mostly coordination tasks. Here are the things you must do today: Do not release with a Red CI. You can find the CI status here: ``` -http://travis-ci.org/rails/rails +https://travis-ci.org/rails/rails ``` ### Is Sam Ruby happy? If not, make him happy. Sam Ruby keeps a [test suite](https://github.com/rubys/awdwr) that makes sure the code samples in his book -([Agile Web Development with Rails](https://pragprog.com/titles/rails5/agile-web-development-with-rails-5th-edition)) +([Agile Web Development with Rails](https://pragprog.com/book/rails51/agile-web-development-with-rails-51)) all work. These are valuable system tests for Rails. You can check the status of these tests here: diff --git a/actioncable/CHANGELOG.md b/actioncable/CHANGELOG.md index a28e2fe75d..959943016f 100644 --- a/actioncable/CHANGELOG.md +++ b/actioncable/CHANGELOG.md @@ -1,5 +1,3 @@ -## Rails 6.0.0.alpha (Unreleased) ## - * Rails 6 requires Ruby 2.4.1 or newer. *Jeremy Daer* diff --git a/actioncable/lib/action_cable/channel/broadcasting.rb b/actioncable/lib/action_cable/channel/broadcasting.rb index acc791817b..9a96720f4a 100644 --- a/actioncable/lib/action_cable/channel/broadcasting.rb +++ b/actioncable/lib/action_cable/channel/broadcasting.rb @@ -9,7 +9,7 @@ module ActionCable delegate :broadcasting_for, to: :class - class_methods do + module ClassMethods # Broadcast a hash to a unique broadcasting for this <tt>model</tt> in this channel. def broadcast_to(model, message) ActionCable.server.broadcast(broadcasting_for([ channel_name, model ]), message) diff --git a/actioncable/lib/action_cable/channel/callbacks.rb b/actioncable/lib/action_cable/channel/callbacks.rb index 4223c0d996..e4cb19b26a 100644 --- a/actioncable/lib/action_cable/channel/callbacks.rb +++ b/actioncable/lib/action_cable/channel/callbacks.rb @@ -13,7 +13,7 @@ module ActionCable define_callbacks :unsubscribe end - class_methods do + module ClassMethods def before_subscribe(*methods, &block) set_callback(:subscribe, :before, *methods, &block) end diff --git a/actioncable/lib/action_cable/channel/naming.rb b/actioncable/lib/action_cable/channel/naming.rb index 03a5dcd3a0..9c324a2a53 100644 --- a/actioncable/lib/action_cable/channel/naming.rb +++ b/actioncable/lib/action_cable/channel/naming.rb @@ -5,7 +5,7 @@ module ActionCable module Naming extend ActiveSupport::Concern - class_methods do + module ClassMethods # Returns the name of the channel, underscored, without the <tt>Channel</tt> ending. # If the channel is in a namespace, then the namespaces are represented by single # colon separators in the channel name. diff --git a/actioncable/lib/action_cable/connection/identification.rb b/actioncable/lib/action_cable/connection/identification.rb index 4b5f9ca115..cc544685dd 100644 --- a/actioncable/lib/action_cable/connection/identification.rb +++ b/actioncable/lib/action_cable/connection/identification.rb @@ -11,7 +11,7 @@ module ActionCable class_attribute :identifiers, default: Set.new end - class_methods do + module ClassMethods # Mark a key as being a connection identifier index that can then be used to find the specific connection again later. # Common identifiers are current_user and current_account, but could be anything, really. # diff --git a/actioncable/test/channel/stream_test.rb b/actioncable/test/channel/stream_test.rb index e9e8849637..df9d44d8dd 100644 --- a/actioncable/test/channel/stream_test.rb +++ b/actioncable/test/channel/stream_test.rb @@ -200,7 +200,7 @@ module ActionCable::StreamTests end def receive(connection, command:, identifiers:, channel: "ActionCable::StreamTests::ChatChannel") - identifier = JSON.generate(channel: channel, **identifiers) + identifier = JSON.generate(identifiers.merge(channel: channel)) connection.dispatch_websocket_message JSON.generate(command: command, identifier: identifier) wait_for_async end diff --git a/actioncable/test/connection/base_test.rb b/actioncable/test/connection/base_test.rb index c69d4d0b36..ac10b3d4d2 100644 --- a/actioncable/test/connection/base_test.rb +++ b/actioncable/test/connection/base_test.rb @@ -83,7 +83,7 @@ class ActionCable::Connection::BaseTest < ActionCable::TestCase connection.subscriptions.expects(:unsubscribe_from_all) connection.send :handle_close - assert ! connection.connected + assert_not connection.connected assert_equal [], @server.connections end end diff --git a/actioncable/test/test_helper.rb b/actioncable/test/test_helper.rb index 2a4611fb37..2f186b7193 100644 --- a/actioncable/test/test_helper.rb +++ b/actioncable/test/test_helper.rb @@ -4,7 +4,7 @@ require "action_cable" require "active_support/testing/autorun" require "puma" -require "mocha/setup" +require "mocha/minitest" require "rack/mock" begin diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md index c5dbb0f1d4..9fb2e44210 100644 --- a/actionmailer/CHANGELOG.md +++ b/actionmailer/CHANGELOG.md @@ -1,4 +1,6 @@ -## Rails 6.0.0.alpha (Unreleased) ## +* Perform email jobs in `assert_emails`. + + *Gannon McGibbon* * Rails 6 requires Ruby 2.4.1 or newer. diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb index 8ee4d06915..6906472660 100644 --- a/actionmailer/lib/action_mailer/test_helper.rb +++ b/actionmailer/lib/action_mailer/test_helper.rb @@ -28,13 +28,13 @@ module ActionMailer # # assert_emails 2 do # ContactMailer.welcome.deliver_now - # ContactMailer.welcome.deliver_now + # ContactMailer.welcome.deliver_later # end # end - def assert_emails(number) + def assert_emails(number, &block) if block_given? original_count = ActionMailer::Base.deliveries.size - yield + perform_enqueued_jobs(only: [ActionMailer::DeliveryJob, ActionMailer::Parameterized::DeliveryJob], &block) new_count = ActionMailer::Base.deliveries.size assert_equal number, new_count - original_count, "#{number} emails expected, but #{new_count - original_count} were sent" else @@ -115,11 +115,11 @@ module ActionMailer # end # end # - # If `args` is provided as a Hash, a parameterized email is matched. + # If +args+ is provided as a Hash, a parameterized email is matched. # # def test_parameterized_email # assert_enqueued_email_with ContactMailer, :welcome, - # args: {email: 'user@example.com} do + # args: {email: 'user@example.com'} do # ContactMailer.with(email: 'user@example.com').welcome.deliver_later # end # end diff --git a/actionmailer/test/caching_test.rb b/actionmailer/test/caching_test.rb index 574840c30e..22f310f39f 100644 --- a/actionmailer/test/caching_test.rb +++ b/actionmailer/test/caching_test.rb @@ -40,14 +40,14 @@ class FragmentCachingTest < BaseCachingTest def test_fragment_exist_with_caching_enabled @store.write("views/name", "value") assert @mailer.fragment_exist?("name") - assert !@mailer.fragment_exist?("other_name") + assert_not @mailer.fragment_exist?("other_name") end def test_fragment_exist_with_caching_disabled @mailer.perform_caching = false @store.write("views/name", "value") - assert !@mailer.fragment_exist?("name") - assert !@mailer.fragment_exist?("other_name") + assert_not @mailer.fragment_exist?("name") + assert_not @mailer.fragment_exist?("other_name") end def test_write_fragment_with_caching_enabled @@ -90,7 +90,7 @@ class FragmentCachingTest < BaseCachingTest buffer = "generated till now -> ".html_safe buffer << view_context.send(:fragment_for, "expensive") { fragment_computed = true } - assert !fragment_computed + assert_not fragment_computed assert_equal "generated till now -> fragment content", buffer end diff --git a/actionmailer/test/test_helper_test.rb b/actionmailer/test/test_helper_test.rb index 3866097389..8fdc687a8b 100644 --- a/actionmailer/test/test_helper_test.rb +++ b/actionmailer/test/test_helper_test.rb @@ -69,6 +69,16 @@ class TestHelperMailerTest < ActionMailer::TestCase end end + def test_assert_emails_with_enqueued_emails + assert_nothing_raised do + assert_emails 1 do + silence_stream($stdout) do + TestHelperMailer.test.deliver_later + end + end + end + end + def test_repeated_assert_emails_calls assert_nothing_raised do assert_emails 1 do @@ -105,6 +115,18 @@ class TestHelperMailerTest < ActionMailer::TestCase end end + def test_assert_no_emails_with_enqueued_emails + error = assert_raise ActiveSupport::TestCase::Assertion do + assert_no_emails do + silence_stream($stdout) do + TestHelperMailer.test.deliver_later + end + end + end + + assert_match(/0 .* but 1/, error.message) + end + def test_assert_emails_too_few_sent error = assert_raise ActiveSupport::TestCase::Assertion do assert_emails 2 do diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 44d87878a4..a370c3fc99 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,4 +1,21 @@ -## Rails 6.0.0.alpha (Unreleased) ## +* Output only one Content-Security-Policy nonce header value per request. + + Fixes #32597. + + *Andrey Novikov*, *Andrew White* + +* Move default headers configuration into their own module that can be included in controllers. + + *Kevin Deisz* + +* Add method `dig` to `session`. + + *claudiob*, *Takumi Shotoku* + +* Controller level `force_ssl` has been deprecated in favor of + `config.force_ssl`. + + *Derek Prior* * Rails 6 requires Ruby 2.4.1 or newer. diff --git a/actionpack/Rakefile b/actionpack/Rakefile index 4dd7c59ce8..e99eb1723a 100644 --- a/actionpack/Rakefile +++ b/actionpack/Rakefile @@ -28,7 +28,7 @@ namespace :test do end task :lines do - load File.expand_path("..", __dir__) + "/tools/line_statistics" + load File.expand_path("../tools/line_statistics", __dir__) files = FileList["lib/**/*.rb"] CodeTools::LineStatistics.new(files).print_loc end diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb index 146d17cf40..42bab411d2 100644 --- a/actionpack/lib/abstract_controller/callbacks.rb +++ b/actionpack/lib/abstract_controller/callbacks.rb @@ -103,6 +103,10 @@ module AbstractController # :call-seq: before_action(names, block) # # Append a callback before actions. See _insert_callbacks for parameter details. + # + # If the callback renders or redirects, the action will not run. If there + # are additional callbacks scheduled to run after that callback, they are + # also cancelled. ## # :method: prepend_before_action @@ -110,6 +114,10 @@ module AbstractController # :call-seq: prepend_before_action(names, block) # # Prepend a callback before actions. See _insert_callbacks for parameter details. + # + # If the callback renders or redirects, the action will not run. If there + # are additional callbacks scheduled to run after that callback, they are + # also cancelled. ## # :method: skip_before_action @@ -124,6 +132,10 @@ module AbstractController # :call-seq: append_before_action(names, block) # # Append a callback before actions. See _insert_callbacks for parameter details. + # + # If the callback renders or redirects, the action will not run. If there + # are additional callbacks scheduled to run after that callback, they are + # also cancelled. ## # :method: after_action diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb index f43784f9f2..29d61c3ceb 100644 --- a/actionpack/lib/action_controller.rb +++ b/actionpack/lib/action_controller.rb @@ -25,6 +25,7 @@ module ActionController autoload :ContentSecurityPolicy autoload :Cookies autoload :DataStreaming + autoload :DefaultHeaders autoload :EtagWithTemplateDigest autoload :EtagWithFlash autoload :Flash diff --git a/actionpack/lib/action_controller/api.rb b/actionpack/lib/action_controller/api.rb index b192e496de..93ffff1bd6 100644 --- a/actionpack/lib/action_controller/api.rb +++ b/actionpack/lib/action_controller/api.rb @@ -122,6 +122,7 @@ module ActionController ForceSSL, DataStreaming, + DefaultHeaders, # Before callbacks should also be executed as early as possible, so # also include them at the bottom. diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index 204a3d400c..3378d6db0f 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -232,6 +232,7 @@ module ActionController HttpAuthentication::Basic::ControllerMethods, HttpAuthentication::Digest::ControllerMethods, HttpAuthentication::Token::ControllerMethods, + DefaultHeaders, # Before callbacks should also be executed as early as possible, so # also include them at the bottom. @@ -264,12 +265,6 @@ module ActionController PROTECTED_IVARS end - def self.make_response!(request) - ActionDispatch::Response.create.tap do |res| - res.request = request - end - end - ActiveSupport.run_load_hooks(:action_controller_base, self) ActiveSupport.run_load_hooks(:action_controller, self) end diff --git a/actionpack/lib/action_controller/metal/content_security_policy.rb b/actionpack/lib/action_controller/metal/content_security_policy.rb index 95f2f3242d..b8fab4ebe3 100644 --- a/actionpack/lib/action_controller/metal/content_security_policy.rb +++ b/actionpack/lib/action_controller/metal/content_security_policy.rb @@ -14,13 +14,17 @@ module ActionController #:nodoc: end module ClassMethods - def content_security_policy(**options, &block) + def content_security_policy(enabled = true, **options, &block) before_action(options) do if block_given? - policy = request.content_security_policy.clone + policy = current_content_security_policy yield policy request.content_security_policy = policy end + + unless enabled + request.content_security_policy = nil + end end end @@ -40,5 +44,9 @@ module ActionController #:nodoc: def content_security_policy_nonce request.content_security_policy_nonce end + + def current_content_security_policy + request.content_security_policy.try(:clone) || ActionDispatch::ContentSecurityPolicy.new + end end end diff --git a/actionpack/lib/action_controller/metal/default_headers.rb b/actionpack/lib/action_controller/metal/default_headers.rb new file mode 100644 index 0000000000..eef0602fcd --- /dev/null +++ b/actionpack/lib/action_controller/metal/default_headers.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module ActionController + # Allows configuring default headers that will be automatically merged into + # each response. + module DefaultHeaders + extend ActiveSupport::Concern + + module ClassMethods + def make_response!(request) + ActionDispatch::Response.create.tap do |res| + res.request = request + end + end + end + end +end diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb index a65857d6ef..58db4a1458 100644 --- a/actionpack/lib/action_controller/metal/exceptions.rb +++ b/actionpack/lib/action_controller/metal/exceptions.rb @@ -22,7 +22,7 @@ module ActionController end end - class ActionController::UrlGenerationError < ActionControllerError #:nodoc: + class UrlGenerationError < ActionControllerError #:nodoc: end class MethodNotAllowed < ActionControllerError #:nodoc: diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb index 7de500d119..8d53a30e93 100644 --- a/actionpack/lib/action_controller/metal/force_ssl.rb +++ b/actionpack/lib/action_controller/metal/force_ssl.rb @@ -4,18 +4,10 @@ require "active_support/core_ext/hash/except" require "active_support/core_ext/hash/slice" module ActionController - # This module provides a method which will redirect the browser to use the secured HTTPS - # protocol. This will ensure that users' sensitive information will be - # transferred safely over the internet. You _should_ always force the browser - # to use HTTPS when you're transferring sensitive information such as - # user authentication, account information, or credit card information. - # - # Note that if you are really concerned about your application security, - # you might consider using +config.force_ssl+ in your config file instead. - # That will ensure all the data is transferred via HTTPS, and will - # prevent the user from getting their session hijacked when accessing the - # site over unsecured HTTP protocol. - module ForceSSL + # This module is deprecated in favor of +config.force_ssl+ in your environment + # config file. This will ensure all communication to non-whitelisted endpoints + # served by your application occurs over HTTPS. + module ForceSSL # :nodoc: extend ActiveSupport::Concern include AbstractController::Callbacks @@ -23,45 +15,17 @@ module ActionController URL_OPTIONS = [:protocol, :host, :domain, :subdomain, :port, :path] REDIRECT_OPTIONS = [:status, :flash, :alert, :notice] - module ClassMethods - # Force the request to this particular controller or specified actions to be - # through the HTTPS protocol. - # - # If you need to disable this for any reason (e.g. development) then you can use - # an +:if+ or +:unless+ condition. - # - # class AccountsController < ApplicationController - # force_ssl if: :ssl_configured? - # - # def ssl_configured? - # !Rails.env.development? - # end - # end - # - # ==== URL Options - # You can pass any of the following options to affect the redirect URL - # * <tt>host</tt> - Redirect to a different host name - # * <tt>subdomain</tt> - Redirect to a different subdomain - # * <tt>domain</tt> - Redirect to a different domain - # * <tt>port</tt> - Redirect to a non-standard port - # * <tt>path</tt> - Redirect to a different path - # - # ==== Redirect Options - # You can pass any of the following options to affect the redirect status and response - # * <tt>status</tt> - Redirect with a custom status (default is 301 Moved Permanently) - # * <tt>flash</tt> - Set a flash message when redirecting - # * <tt>alert</tt> - Set an alert message when redirecting - # * <tt>notice</tt> - Set a notice message when redirecting - # - # ==== Action Options - # You can pass any of the following options to affect the before_action callback - # * <tt>only</tt> - The callback should be run only for this action - # * <tt>except</tt> - The callback should be run for all actions except this action - # * <tt>if</tt> - A symbol naming an instance method or a proc; the - # callback will be called only when it returns a true value. - # * <tt>unless</tt> - A symbol naming an instance method or a proc; the - # callback will be called only when it returns a false value. + module ClassMethods # :nodoc: def force_ssl(options = {}) + ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) + Controller-level `force_ssl` is deprecated and will be removed from + Rails 6.1. Please enable `config.force_ssl` in your environment + configuration to enable the ActionDispatch::SSL middleware to more + fully enforce that your application communicate over HTTPS. If needed, + you can use `config.ssl_options` to exempt matching endpoints from + being redirected to HTTPS. + MESSAGE + action_options = options.slice(*ACTION_OPTIONS) redirect_options = options.except(*ACTION_OPTIONS) before_action(action_options) do @@ -70,11 +34,6 @@ module ActionController end end - # Redirect the existing request to use the HTTPS protocol. - # - # ==== Parameters - # * <tt>host_or_options</tt> - Either a host name or any of the URL and - # redirect options available to the <tt>force_ssl</tt> method. def force_ssl_redirect(host_or_options = nil) unless request.ssl? options = { diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb index 94092de96c..fc9cf8aaff 100644 --- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb +++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb @@ -417,7 +417,7 @@ module ActionController #:nodoc: NULL_ORIGIN_MESSAGE = <<~MSG The browser returned a 'null' origin for a request with origin-based forgery protection turned on. This usually - means you have the 'no-referrer' Referrer-Policy header enabled, or that you the request came from a site that + means you have the 'no-referrer' Referrer-Policy header enabled, or that the request came from a site that refused to give its origin. This makes it impossible for Rails to verify the source of the requests. Likely the best solution is to change your referrer policy to something less strict like same-origin or strict-same-origin. If you cannot change the referrer policy, you can disable origin checking with the diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 615c90c496..5a06bf86e3 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -374,7 +374,7 @@ module ActionController # Person.new(params) # => #<Person id: nil, name: "Francesco"> def permit! each_pair do |key, value| - Array.wrap(value).each do |v| + Array.wrap(value).flatten.each do |v| v.permit! if v.respond_to? :permit! end end @@ -580,19 +580,18 @@ module ActionController ) end - if Hash.method_defined?(:dig) - # Extracts the nested parameter from the given +keys+ by calling +dig+ - # at each step. Returns +nil+ if any intermediate step is +nil+. - # - # params = ActionController::Parameters.new(foo: { bar: { baz: 1 } }) - # params.dig(:foo, :bar, :baz) # => 1 - # params.dig(:foo, :zot, :xyz) # => nil - # - # params2 = ActionController::Parameters.new(foo: [10, 11, 12]) - # params2.dig(:foo, 1) # => 11 - def dig(*keys) - convert_value_to_parameters(@parameters.dig(*keys)) - end + # Extracts the nested parameter from the given +keys+ by calling +dig+ + # at each step. Returns +nil+ if any intermediate step is +nil+. + # + # params = ActionController::Parameters.new(foo: { bar: { baz: 1 } }) + # params.dig(:foo, :bar, :baz) # => 1 + # params.dig(:foo, :zot, :xyz) # => nil + # + # params2 = ActionController::Parameters.new(foo: [10, 11, 12]) + # params2.dig(:foo, 1) # => 11 + def dig(*keys) + convert_hashes_to_parameters(keys.first, @parameters[keys.first]) + @parameters.dig(*keys) end # Returns a new <tt>ActionController::Parameters</tt> instance that diff --git a/actionpack/lib/action_dispatch/http/content_security_policy.rb b/actionpack/lib/action_dispatch/http/content_security_policy.rb index a3407c9698..17e72b46ff 100644 --- a/actionpack/lib/action_dispatch/http/content_security_policy.rb +++ b/actionpack/lib/action_dispatch/http/content_security_policy.rb @@ -21,13 +21,8 @@ module ActionDispatch #:nodoc: return response if policy_present?(headers) if policy = request.content_security_policy - if policy.directives["script-src"] - if nonce = request.content_security_policy_nonce - policy.directives["script-src"] << "'nonce-#{nonce}'" - end - end - - headers[header_name(request)] = policy.build(request.controller_instance) + nonce = request.content_security_policy_nonce + headers[header_name(request)] = policy.build(request.controller_instance, nonce) end response @@ -113,7 +108,9 @@ module ActionDispatch #:nodoc: blob: "blob:", filesystem: "filesystem:", report_sample: "'report-sample'", - strict_dynamic: "'strict-dynamic'" + strict_dynamic: "'strict-dynamic'", + ws: "ws:", + wss: "wss:" }.freeze DIRECTIVES = { @@ -134,7 +131,9 @@ module ActionDispatch #:nodoc: worker_src: "worker-src" }.freeze - private_constant :MAPPINGS, :DIRECTIVES + NONCE_DIRECTIVES = %w[script-src].freeze + + private_constant :MAPPINGS, :DIRECTIVES, :NONCE_DIRECTIVES attr_reader :directives @@ -203,8 +202,8 @@ module ActionDispatch #:nodoc: end end - def build(context = nil) - build_directives(context).compact.join("; ") + def build(context = nil, nonce = nil) + build_directives(context, nonce).compact.join("; ") end private @@ -227,10 +226,14 @@ module ActionDispatch #:nodoc: end end - def build_directives(context) + def build_directives(context, nonce) @directives.map do |directive, sources| if sources.is_a?(Array) - "#{directive} #{build_directive(sources, context).join(' ')}" + if nonce && nonce_directive?(directive) + "#{directive} #{build_directive(sources, context).join(' ')} 'nonce-#{nonce}'" + else + "#{directive} #{build_directive(sources, context).join(' ')}" + end elsif sources directive else @@ -259,5 +262,9 @@ module ActionDispatch #:nodoc: raise RuntimeError, "Unexpected content security policy source: #{source.inspect}" end end + + def nonce_directive?(directive) + NONCE_DIRECTIVES.include?(directive) + end end end diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb index 3e11846778..fd05eec172 100644 --- a/actionpack/lib/action_dispatch/middleware/flash.rb +++ b/actionpack/lib/action_dispatch/middleware/flash.rb @@ -73,7 +73,7 @@ module ActionDispatch end end - def reset_session # :nodoc + def reset_session # :nodoc: super self.flash = nil end diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb index 6d9f36ad75..240269d1c7 100644 --- a/actionpack/lib/action_dispatch/middleware/ssl.rb +++ b/actionpack/lib/action_dispatch/middleware/ssl.rb @@ -15,6 +15,8 @@ module ActionDispatch # # config.ssl_options = { redirect: { exclude: -> request { request.path =~ /healthcheck/ } } } # + # Cookies will not be flagged as secure for excluded requests. + # # 2. <b>Secure cookies</b>: Sets the +secure+ flag on cookies to tell browsers they # must not be sent along with +http://+ requests. Enabled by default. Set # +config.ssl_options+ with <tt>secure_cookies: false</tt> to disable this feature. @@ -71,7 +73,7 @@ module ActionDispatch if request.ssl? @app.call(env).tap do |status, headers, body| set_hsts_header! headers - flag_cookies_as_secure! headers if @secure_cookies + flag_cookies_as_secure! headers if @secure_cookies && !@exclude.call(request) end else return redirect_to_https request unless @exclude.call(request) diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb index 23492e14eb..acd999444a 100644 --- a/actionpack/lib/action_dispatch/middleware/static.rb +++ b/actionpack/lib/action_dispatch/middleware/static.rb @@ -16,7 +16,7 @@ module ActionDispatch # does not exist, a 404 "File not Found" response will be returned. class FileHandler def initialize(root, index: "index", headers: {}) - @root = root.chomp("/") + @root = root.chomp("/").b @file_server = ::Rack::File.new(@root, headers) @index = index end @@ -35,7 +35,7 @@ module ActionDispatch paths = [path, "#{path}#{ext}", "#{path}/#{@index}#{ext}"] if match = paths.detect { |p| - path = File.join(@root, p.dup.force_encoding(Encoding::UTF_8)) + path = File.join(@root, p.b) begin File.file?(path) && File.readable?(path) rescue SystemCallError @@ -43,7 +43,7 @@ module ActionDispatch end } - return ::Rack::Utils.escape_path(match) + return ::Rack::Utils.escape_path(match).b end end diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb index 000847e193..bc5e0670e0 100644 --- a/actionpack/lib/action_dispatch/request/session.rb +++ b/actionpack/lib/action_dispatch/request/session.rb @@ -93,6 +93,14 @@ module ActionDispatch @delegate[key.to_s] end + # Returns the nested value specified by the sequence of keys, returning + # +nil+ if any intermediate step is +nil+. + def dig(*keys) + load_for_read! + keys = keys.map.with_index { |key, i| i.zero? ? key.to_s : key } + @delegate.dig(*keys) + end + # Returns true if the session has the given key or false. def has_key?(key) load_for_read! diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb index 776058d98e..5cde677051 100644 --- a/actionpack/lib/action_dispatch/routing.rb +++ b/actionpack/lib/action_dispatch/routing.rb @@ -243,9 +243,9 @@ module ActionDispatch # # rails routes # - # Target specific controllers by prefixing the command with <tt>-c</tt> option. Use - # <tt>--expanded</tt> to turn on the expanded table formatting mode. - # + # Target a specific controller with <tt>-c</tt>, or grep routes + # using <tt>-g</tt>. Useful in conjunction with <tt>--expanded</tt> + # which displays routes vertically. module Routing extend ActiveSupport::Autoload diff --git a/actionpack/lib/action_dispatch/routing/endpoint.rb b/actionpack/lib/action_dispatch/routing/endpoint.rb index 24dced1efd..28bb20d688 100644 --- a/actionpack/lib/action_dispatch/routing/endpoint.rb +++ b/actionpack/lib/action_dispatch/routing/endpoint.rb @@ -3,12 +3,15 @@ module ActionDispatch module Routing class Endpoint # :nodoc: - def dispatcher?; false; end - def redirect?; false; end - def engine?; rack_app.respond_to?(:routes); end - def matches?(req); true; end - def app; self; end - def rack_app; app; end + def dispatcher?; false; end + def redirect?; false; end + def matches?(req); true; end + def app; self; end + def rack_app; app; end + + def engine? + rack_app.is_a?(Class) && rack_app < Rails::Engine + end end end end diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb index de200fada0..bae50f6a43 100644 --- a/actionpack/lib/action_dispatch/routing/inspector.rb +++ b/actionpack/lib/action_dispatch/routing/inspector.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "delegate" +require "io/console/size" module ActionDispatch module Routing @@ -60,11 +61,11 @@ module ActionDispatch @routes = routes end - def format(formatter, filter = nil) + def format(formatter, filter = {}) routes_to_display = filter_routes(normalize_filter(filter)) routes = collect_routes(routes_to_display) if routes.none? - formatter.no_routes(collect_routes(@routes)) + formatter.no_routes(collect_routes(@routes), filter) return formatter.result end @@ -80,12 +81,12 @@ module ActionDispatch end private - def normalize_filter(filter) - if filter.is_a?(Hash) && filter[:controller] + if filter[:controller] { controller: /#{filter[:controller].downcase.sub(/_?controller\z/, '').sub('::', '/')}/ } - elsif filter - { controller: /#{filter}/, action: /#{filter}/, verb: /#{filter}/, name: /#{filter}/, path: /#{filter}/ } + elsif filter[:grep] + { controller: /#{filter[:grep]}/, action: /#{filter[:grep]}/, + verb: /#{filter[:grep]}/, name: /#{filter[:grep]}/, path: /#{filter[:grep]}/ } end end @@ -126,7 +127,7 @@ module ActionDispatch end module ConsoleFormatter - class Sheet + class Base def initialize @buffer = [] end @@ -136,30 +137,44 @@ module ActionDispatch end def section_title(title) - @buffer << "\n#{title}:" end def section(routes) - @buffer << draw_section(routes) end def header(routes) - @buffer << draw_header(routes) end - def no_routes(routes) + def no_routes(routes, filter) @buffer << - if routes.none? - <<~MESSAGE - You don't have any routes defined! + if routes.none? + <<~MESSAGE + You don't have any routes defined! + + Please add some routes in config/routes.rb. + MESSAGE + elsif filter.key?(:controller) + "No routes were found for this controller." + elsif filter.key?(:grep) + "No routes were found for this grep pattern." + end - Please add some routes in config/routes.rb. - MESSAGE - else - "No routes were found for this controller" - end @buffer << "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html." end + end + + class Sheet < Base + def section_title(title) + @buffer << "\n#{title}:" + end + + def section(routes) + @buffer << draw_section(routes) + end + + def header(routes) + @buffer << draw_header(routes) + end private @@ -185,54 +200,36 @@ module ActionDispatch end end - class Expanded - def initialize - @buffer = [] - end - - def result - @buffer.join("") - end - + class Expanded < Base def section_title(title) - @buffer << "\n#{"[ #{title} ]"}\n" + @buffer << "\n#{"[ #{title} ]"}" end def section(routes) @buffer << draw_expanded_section(routes) end - def header(routes) - @buffer - end - - def no_routes(routes) - @buffer << - if routes.none? - <<~MESSAGE - You don't have any routes defined! - - Please add some routes in config/routes.rb.\n - MESSAGE - else - "No routes were found for this controller\n" - end - @buffer << "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html." - end - private def draw_expanded_section(routes) routes.map.each_with_index do |r, i| - <<~MESSAGE - --[ Route #{i + 1} ]#{'-' * 60} - Prefix | #{r[:name]} - Verb | #{r[:verb]} - URI | #{r[:path]} - Controller#Action | #{r[:reqs]} + <<~MESSAGE.chomp + #{route_header(index: i + 1)} + Prefix | #{r[:name]} + Verb | #{r[:verb]} + URI | #{r[:path]} + Controller#Action | #{r[:reqs]} MESSAGE end end + + def route_header(index:) + console_width = IO.console_size.second + header_prefix = "--[ Route #{index} ]" + dash_remainder = [console_width - header_prefix.size, 0].max + + "#{header_prefix}#{'-' * dash_remainder}" + end end end @@ -264,7 +261,7 @@ module ActionDispatch <a href="http://guides.rubyonrails.org/routing.html">Rails Routing from the Outside In</a>. </li> </ul> - MESSAGE + MESSAGE end def result diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index f3970d5445..d9dd24935b 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -664,6 +664,7 @@ module ActionDispatch def define_generate_prefix(app, name) _route = @set.named_routes.get name _routes = @set + _url_helpers = @set.url_helpers script_namer = ->(options) do prefix_options = options.slice(*_route.segment_keys) @@ -675,7 +676,7 @@ module ActionDispatch # We must actually delete prefix segment keys to avoid passing them to next url_for. _route.segment_keys.each { |k| options.delete(k) } - _routes.url_helpers.send("#{name}_path", prefix_options) + _url_helpers.send("#{name}_path", prefix_options) end app.routes.define_mounted_helper(name, script_namer) diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 6da869c0c2..e17ccaf986 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -120,8 +120,7 @@ module ActionDispatch opts end - # Returns the path component of a URL for the given record. It uses - # <tt>polymorphic_url</tt> with <tt>routing_type: :path</tt>. + # Returns the path component of a URL for the given record. def polymorphic_path(record_or_hash_or_array, options = {}) if Hash === record_or_hash_or_array options = record_or_hash_or_array.merge(options) diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb index fa345dccdf..1a31c7dbb8 100644 --- a/actionpack/lib/action_dispatch/routing/url_for.rb +++ b/actionpack/lib/action_dispatch/routing/url_for.rb @@ -191,7 +191,25 @@ module ActionDispatch end end - def route_for(name, *args) # :nodoc: + # Allows calling direct or regular named route. + # + # resources :buckets + # + # direct :recordable do |recording| + # route_for(:bucket, recording.bucket) + # end + # + # direct :threadable do |threadable| + # route_for(:recordable, threadable.parent) + # end + # + # This maintains the context of the original caller on + # whether to return a path or full URL, e.g: + # + # threadable_path(threadable) # => "/buckets/1" + # threadable_url(threadable) # => "http://example.com/buckets/1" + # + def route_for(name, *args) public_send(:"#{name}_url", *args) end diff --git a/actionpack/lib/action_dispatch/system_test_case.rb b/actionpack/lib/action_dispatch/system_test_case.rb index f85f816bb9..c74c0ccced 100644 --- a/actionpack/lib/action_dispatch/system_test_case.rb +++ b/actionpack/lib/action_dispatch/system_test_case.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -gem "capybara", "~> 2.15" +gem "capybara", ">= 2.15" require "capybara/dsl" require "capybara/minitest" diff --git a/actionpack/lib/action_dispatch/system_testing/browser.rb b/actionpack/lib/action_dispatch/system_testing/browser.rb index 10e6888ab3..1b0bce6b9e 100644 --- a/actionpack/lib/action_dispatch/system_testing/browser.rb +++ b/actionpack/lib/action_dispatch/system_testing/browser.rb @@ -33,7 +33,7 @@ module ActionDispatch def headless_chrome_browser_options options = Selenium::WebDriver::Chrome::Options.new options.args << "--headless" - options.args << "--disable-gpu" + options.args << "--disable-gpu" if Gem.win_platform? options end diff --git a/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb index ffa85f4e14..e47d5020f4 100644 --- a/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +++ b/actionpack/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb @@ -19,6 +19,7 @@ module ActionDispatch def after_teardown take_failed_screenshot Capybara.reset_sessions! + ensure super end end diff --git a/actionpack/test/abstract/callbacks_test.rb b/actionpack/test/abstract/callbacks_test.rb index fdc09bd951..4512ea27b3 100644 --- a/actionpack/test/abstract/callbacks_test.rb +++ b/actionpack/test/abstract/callbacks_test.rb @@ -154,7 +154,7 @@ module AbstractController test "when :except is specified, an after action is not triggered on that action" do @controller.process(:index) - assert !@controller.instance_variable_defined?("@authenticated") + assert_not @controller.instance_variable_defined?("@authenticated") end end @@ -198,7 +198,7 @@ module AbstractController test "when :except is specified with an array, an after action is not triggered on that action" do @controller.process(:index) - assert !@controller.instance_variable_defined?("@authenticated") + assert_not @controller.instance_variable_defined?("@authenticated") end end diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb index 504c77b8ef..552b6cbf47 100644 --- a/actionpack/test/controller/action_pack_assertions_test.rb +++ b/actionpack/test/controller/action_pack_assertions_test.rb @@ -290,13 +290,13 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase def test_template_objects_exist process :assign_this - assert !@controller.instance_variable_defined?(:"@hi") + assert_not @controller.instance_variable_defined?(:"@hi") assert @controller.instance_variable_get(:"@howdy") end def test_template_objects_missing process :nothing - assert !@controller.instance_variable_defined?(:@howdy) + assert_not @controller.instance_variable_defined?(:@howdy) end def test_empty_flash @@ -366,7 +366,7 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase process :redirect_external assert_predicate @response, :redirect? assert_match(/rubyonrails/, @response.redirect_url) - assert !/perloffrails/.match(@response.redirect_url) + assert_not /perloffrails/.match(@response.redirect_url) end def test_redirection diff --git a/actionpack/test/controller/api/force_ssl_test.rb b/actionpack/test/controller/api/force_ssl_test.rb index 07459c3753..8191578eb0 100644 --- a/actionpack/test/controller/api/force_ssl_test.rb +++ b/actionpack/test/controller/api/force_ssl_test.rb @@ -3,7 +3,9 @@ require "abstract_unit" class ForceSSLApiController < ActionController::API - force_ssl + ActiveSupport::Deprecation.silence do + force_ssl + end def one; end def two diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb index 8b596083d5..6fe036dd15 100644 --- a/actionpack/test/controller/caching_test.rb +++ b/actionpack/test/controller/caching_test.rb @@ -94,14 +94,14 @@ class FragmentCachingTest < ActionController::TestCase def test_fragment_exist_with_caching_enabled @store.write("views/name", "value") assert @controller.fragment_exist?("name") - assert !@controller.fragment_exist?("other_name") + assert_not @controller.fragment_exist?("other_name") end def test_fragment_exist_with_caching_disabled @controller.perform_caching = false @store.write("views/name", "value") - assert !@controller.fragment_exist?("name") - assert !@controller.fragment_exist?("other_name") + assert_not @controller.fragment_exist?("name") + assert_not @controller.fragment_exist?("other_name") end def test_write_fragment_with_caching_enabled @@ -144,7 +144,7 @@ class FragmentCachingTest < ActionController::TestCase buffer = "generated till now -> ".html_safe buffer << view_context.send(:fragment_for, "expensive") { fragment_computed = true } - assert !fragment_computed + assert_not fragment_computed assert_equal "generated till now -> fragment content", buffer end @@ -173,6 +173,9 @@ class FunctionalCachingController < CachingController end end + def xml_fragment_cached_with_html_partial + end + def formatted_fragment_cached respond_to do |format| format.html @@ -308,6 +311,11 @@ CACHED @store.read("views/functional_caching/formatted_fragment_cached_with_variant:#{template_digest("functional_caching/formatted_fragment_cached_with_variant")}/fragment") end + def test_fragment_caching_with_html_partials_in_xml + get :xml_fragment_cached_with_html_partial, format: "*/*" + assert_response :success + end + private def template_digest(name) ActionView::Digestor.digest(name: name, finder: @controller.lookup_context) diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb index 2b16a555bb..425a6e25cc 100644 --- a/actionpack/test/controller/filters_test.rb +++ b/actionpack/test/controller/filters_test.rb @@ -787,7 +787,7 @@ class FilterTest < ActionController::TestCase assert_equal %w( ensure_login find_user ), @controller.instance_variable_get(:@ran_filter) test_process(ConditionalSkippingController, "login") - assert !@controller.instance_variable_defined?("@ran_after_action") + assert_not @controller.instance_variable_defined?("@ran_after_action") test_process(ConditionalSkippingController, "change_password") assert_equal %w( clean_up ), @controller.instance_variable_get("@ran_after_action") end diff --git a/actionpack/test/controller/flash_hash_test.rb b/actionpack/test/controller/flash_hash_test.rb index 6c3ac26de1..e3ec5bb7fc 100644 --- a/actionpack/test/controller/flash_hash_test.rb +++ b/actionpack/test/controller/flash_hash_test.rb @@ -44,7 +44,7 @@ module ActionDispatch @hash["foo"] = "bar" @hash.delete "foo" - assert !@hash.key?("foo") + assert_not @hash.key?("foo") assert_nil @hash["foo"] end @@ -53,7 +53,7 @@ module ActionDispatch assert_equal({ "foo" => "bar" }, @hash.to_hash) @hash.to_hash["zomg"] = "aaron" - assert !@hash.key?("zomg") + assert_not @hash.key?("zomg") assert_equal({ "foo" => "bar" }, @hash.to_hash) end diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb index 84ac1fda3c..7f59f6acaf 100644 --- a/actionpack/test/controller/force_ssl_test.rb +++ b/actionpack/test/controller/force_ssl_test.rb @@ -13,19 +13,23 @@ class ForceSSLController < ActionController::Base end class ForceSSLControllerLevel < ForceSSLController - force_ssl + ActiveSupport::Deprecation.silence do + force_ssl + end end class ForceSSLCustomOptions < ForceSSLController - force_ssl host: "secure.example.com", only: :redirect_host - force_ssl port: 8443, only: :redirect_port - force_ssl subdomain: "secure", only: :redirect_subdomain - force_ssl domain: "secure.com", only: :redirect_domain - force_ssl path: "/foo", only: :redirect_path - force_ssl status: :found, only: :redirect_status - force_ssl flash: { message: "Foo, Bar!" }, only: :redirect_flash - force_ssl alert: "Foo, Bar!", only: :redirect_alert - force_ssl notice: "Foo, Bar!", only: :redirect_notice + ActiveSupport::Deprecation.silence do + force_ssl host: "secure.example.com", only: :redirect_host + force_ssl port: 8443, only: :redirect_port + force_ssl subdomain: "secure", only: :redirect_subdomain + force_ssl domain: "secure.com", only: :redirect_domain + force_ssl path: "/foo", only: :redirect_path + force_ssl status: :found, only: :redirect_status + force_ssl flash: { message: "Foo, Bar!" }, only: :redirect_flash + force_ssl alert: "Foo, Bar!", only: :redirect_alert + force_ssl notice: "Foo, Bar!", only: :redirect_notice + end def force_ssl_action render plain: action_name @@ -55,15 +59,21 @@ class ForceSSLCustomOptions < ForceSSLController end class ForceSSLOnlyAction < ForceSSLController - force_ssl only: :cheeseburger + ActiveSupport::Deprecation.silence do + force_ssl only: :cheeseburger + end end class ForceSSLExceptAction < ForceSSLController - force_ssl except: :banana + ActiveSupport::Deprecation.silence do + force_ssl except: :banana + end end class ForceSSLIfCondition < ForceSSLController - force_ssl if: :use_force_ssl? + ActiveSupport::Deprecation.silence do + force_ssl if: :use_force_ssl? + end def use_force_ssl? action_name == "cheeseburger" @@ -71,7 +81,9 @@ class ForceSSLIfCondition < ForceSSLController end class ForceSSLFlash < ForceSSLController - force_ssl except: [:banana, :set_flash, :use_flash] + ActiveSupport::Deprecation.silence do + force_ssl except: [:banana, :set_flash, :use_flash] + end def set_flash flash["that"] = "hello" diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb index 560157dc61..3f211cd60d 100644 --- a/actionpack/test/controller/http_digest_authentication_test.rb +++ b/actionpack/test/controller/http_digest_authentication_test.rb @@ -202,7 +202,7 @@ class HttpDigestAuthenticationTest < ActionController::TestCase test "validate_digest_response should fail with nil returning password_procedure" do @request.env["HTTP_AUTHORIZATION"] = encode_credentials(username: nil, password: nil) - assert !ActionController::HttpAuthentication::Digest.validate_digest_response(@request, "SuperSecret") { nil } + assert_not ActionController::HttpAuthentication::Digest.validate_digest_response(@request, "SuperSecret") { nil } end test "authentication request with request-uri ending in '/'" do diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index a685f5868e..9cdf04b886 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -135,7 +135,7 @@ class IntegrationTestTest < ActiveSupport::TestCase session1 = @test.open_session { |sess| } session2 = @test.open_session # implicit session - assert !session1.equal?(session2) + assert_not session1.equal?(session2) end # RSpec mixes Matchers (which has a #method_missing) into @@ -345,7 +345,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest follow_redirect! assert_response :ok - refute_same previous_html_document, html_document + assert_not_same previous_html_document, html_document end end @@ -375,7 +375,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest a = open_session b = open_session - refute_same(a.integration_session, b.integration_session) + assert_not_same(a.integration_session, b.integration_session) end def test_get_with_query_string diff --git a/actionpack/test/controller/parameters/accessors_test.rb b/actionpack/test/controller/parameters/accessors_test.rb index 07a897a103..674b2c6266 100644 --- a/actionpack/test/controller/parameters/accessors_test.rb +++ b/actionpack/test/controller/parameters/accessors_test.rb @@ -284,4 +284,12 @@ class ParametersAccessorsTest < ActiveSupport::TestCase value.is_a?(ActionController::Parameters) end end + + test "mutating #dig return value mutates underlying parameters" do + @params.dig(:person, :name)[:first] = "Bill" + assert_equal "Bill", @params.dig(:person, :name, :first) + + @params.dig(:person, :addresses)[0] = { city: "Boston", state: "Massachusetts" } + assert_equal "Boston", @params.dig(:person, :addresses, 0, :city) + end end diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index 295f3a03ef..34b9ac0ab8 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -136,7 +136,7 @@ class ParametersPermitTest < ActiveSupport::TestCase test "key: it is not assigned if not present in params" do params = ActionController::Parameters.new(name: "Joe") permitted = params.permit(:id) - assert !permitted.has_key?(:id) + assert_not permitted.has_key?(:id) end test "key to empty array: empty arrays pass" do @@ -309,7 +309,7 @@ class ParametersPermitTest < ActiveSupport::TestCase merged_params = @params.reverse_merge(default_params) assert_equal "1234", merged_params[:id] - refute_predicate merged_params[:person], :empty? + assert_not_predicate merged_params[:person], :empty? end test "#with_defaults is an alias of reverse_merge" do @@ -317,11 +317,11 @@ class ParametersPermitTest < ActiveSupport::TestCase merged_params = @params.with_defaults(default_params) assert_equal "1234", merged_params[:id] - refute_predicate merged_params[:person], :empty? + assert_not_predicate merged_params[:person], :empty? end test "not permitted is sticky beyond reverse_merge" do - refute_predicate @params.reverse_merge(a: "b"), :permitted? + assert_not_predicate @params.reverse_merge(a: "b"), :permitted? end test "permitted is sticky beyond reverse_merge" do @@ -334,7 +334,7 @@ class ParametersPermitTest < ActiveSupport::TestCase @params.reverse_merge!(default_params) assert_equal "1234", @params[:id] - refute_predicate @params[:person], :empty? + assert_not_predicate @params[:person], :empty? end test "#with_defaults! is an alias of reverse_merge!" do @@ -342,7 +342,7 @@ class ParametersPermitTest < ActiveSupport::TestCase @params.with_defaults!(default_params) assert_equal "1234", @params[:id] - refute_predicate @params[:person], :empty? + assert_not_predicate @params[:person], :empty? end test "modifying the parameters" do @@ -353,12 +353,15 @@ class ParametersPermitTest < ActiveSupport::TestCase assert_equal "Jonas", @params[:person][:family][:brother] end - test "permit is recursive" do + test "permit! is recursive" do + @params[:nested_array] = [[{ x: 2, y: 3 }, { x: 21, y: 42 }]] @params.permit! assert_predicate @params, :permitted? assert_predicate @params[:person], :permitted? assert_predicate @params[:person][:name], :permitted? assert_predicate @params[:person][:addresses][0], :permitted? + assert_predicate @params[:nested_array][0][0], :permitted? + assert_predicate @params[:nested_array][0][1], :permitted? end test "permitted takes a default value when Parameters.permit_all_parameters is set" do diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb index 7d4850294d..d1122abba6 100644 --- a/actionpack/test/controller/test_case_test.rb +++ b/actionpack/test/controller/test_case_test.rb @@ -740,6 +740,14 @@ XML assert_equal "application/json", @response.body end + def test_request_format_kwarg_doesnt_mutate_params + params = { foo: "bar" }.freeze + + assert_nothing_raised do + get :test_format, format: "json", params: params + end + end + def test_should_have_knowledge_of_client_side_cookie_state_even_if_they_are_not_set cookies["foo"] = "bar" get :no_op diff --git a/actionpack/test/dispatch/content_security_policy_test.rb b/actionpack/test/dispatch/content_security_policy_test.rb index b88f90190a..c4c7f53903 100644 --- a/actionpack/test/dispatch/content_security_policy_test.rb +++ b/actionpack/test/dispatch/content_security_policy_test.rb @@ -51,6 +51,12 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase @policy.script_src :strict_dynamic assert_equal "script-src 'strict-dynamic'", @policy.build + @policy.script_src :ws + assert_equal "script-src ws:", @policy.build + + @policy.script_src :wss + assert_equal "script-src wss:", @policy.build + @policy.script_src :none, :report_sample assert_equal "script-src 'none' 'report-sample'", @policy.build end @@ -194,7 +200,7 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase end def test_dynamic_directives - request = Struct.new(:host).new("www.example.com") + request = ActionDispatch::Request.new("HTTP_HOST" => "www.example.com") controller = Struct.new(:request).new(request) @policy.script_src -> { request.host } @@ -203,7 +209,9 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase def test_mixed_static_and_dynamic_directives @policy.script_src :self, -> { "foo.com" }, "bar.com" - assert_equal "script-src 'self' foo.com bar.com", @policy.build(Object.new) + request = ActionDispatch::Request.new({}) + controller = Struct.new(:request).new(request) + assert_equal "script-src 'self' foo.com bar.com", @policy.build(controller) end def test_invalid_directive_source @@ -235,6 +243,73 @@ class ContentSecurityPolicyTest < ActiveSupport::TestCase end end +class DefaultContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest + class PolicyController < ActionController::Base + def index + head :ok + end + end + + ROUTES = ActionDispatch::Routing::RouteSet.new + ROUTES.draw do + scope module: "default_content_security_policy_integration_test" do + get "/", to: "policy#index" + end + end + + POLICY = ActionDispatch::ContentSecurityPolicy.new do |p| + p.default_src :self + p.script_src :https + end + + class PolicyConfigMiddleware + def initialize(app) + @app = app + end + + def call(env) + env["action_dispatch.content_security_policy"] = POLICY + env["action_dispatch.content_security_policy_nonce_generator"] = proc { "iyhD0Yc0W+c=" } + env["action_dispatch.content_security_policy_report_only"] = false + env["action_dispatch.show_exceptions"] = false + + @app.call(env) + end + end + + APP = build_app(ROUTES) do |middleware| + middleware.use PolicyConfigMiddleware + middleware.use ActionDispatch::ContentSecurityPolicy::Middleware + end + + def app + APP + end + + def test_adds_nonce_to_script_src_content_security_policy_only_once + get "/" + get "/" + assert_policy "default-src 'self'; script-src https: 'nonce-iyhD0Yc0W+c='" + end + + private + + def assert_policy(expected, report_only: false) + assert_response :success + + if report_only + expected_header = "Content-Security-Policy-Report-Only" + unexpected_header = "Content-Security-Policy" + else + expected_header = "Content-Security-Policy" + unexpected_header = "Content-Security-Policy-Report-Only" + end + + assert_nil response.headers[unexpected_header] + assert_equal expected, response.headers[expected_header] + end +end + class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest class PolicyController < ActionController::Base content_security_policy only: :inline do |p| @@ -258,6 +333,8 @@ class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest p.script_src :self end + content_security_policy(false, only: :no_policy) + content_security_policy_report_only only: :report_only def index @@ -280,6 +357,10 @@ class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest head :ok end + def no_policy + head :ok + end + private def condition? params[:condition] == "true" @@ -294,6 +375,7 @@ class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest get "/conditional", to: "policy#conditional" get "/report-only", to: "policy#report_only" get "/script-src", to: "policy#script_src" + get "/no-policy", to: "policy#no_policy" end end @@ -353,19 +435,14 @@ class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest assert_policy "script-src 'self' 'nonce-iyhD0Yc0W+c='" end - private - - def env_config - Rails.application.env_config - end + def test_generates_no_content_security_policy + get "/no-policy" - def content_security_policy - env_config["action_dispatch.content_security_policy"] - end + assert_nil response.headers["Content-Security-Policy"] + assert_nil response.headers["Content-Security-Policy-Report-Only"] + end - def content_security_policy=(policy) - env_config["action_dispatch.content_security_policy"] = policy - end + private def assert_policy(expected, report_only: false) assert_response :success @@ -382,3 +459,61 @@ class ContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest assert_equal expected, response.headers[expected_header] end end + +class DisabledContentSecurityPolicyIntegrationTest < ActionDispatch::IntegrationTest + class PolicyController < ActionController::Base + content_security_policy only: :inline do |p| + p.default_src "https://example.com" + end + + def index + head :ok + end + + def inline + head :ok + end + end + + ROUTES = ActionDispatch::Routing::RouteSet.new + ROUTES.draw do + scope module: "disabled_content_security_policy_integration_test" do + get "/", to: "policy#index" + get "/inline", to: "policy#inline" + end + end + + class PolicyConfigMiddleware + def initialize(app) + @app = app + end + + def call(env) + env["action_dispatch.content_security_policy"] = nil + env["action_dispatch.content_security_policy_nonce_generator"] = nil + env["action_dispatch.content_security_policy_report_only"] = false + env["action_dispatch.show_exceptions"] = false + + @app.call(env) + end + end + + APP = build_app(ROUTES) do |middleware| + middleware.use PolicyConfigMiddleware + middleware.use ActionDispatch::ContentSecurityPolicy::Middleware + end + + def app + APP + end + + def test_generates_no_content_security_policy_by_default + get "/" + assert_nil response.headers["Content-Security-Policy"] + end + + def test_generates_content_security_policy_header_when_globally_disabled + get "/inline" + assert_equal "default-src https://example.com", response.headers["Content-Security-Policy"] + end +end diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 94cff10fe4..aba778fad6 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -65,8 +65,8 @@ class CookieJarTest < ActiveSupport::TestCase end def test_key_methods - assert !request.cookie_jar.key?(:foo) - assert !request.cookie_jar.has_key?("foo") + assert_not request.cookie_jar.key?(:foo) + assert_not request.cookie_jar.has_key?("foo") request.cookie_jar[:foo] = :bar assert request.cookie_jar.key?(:foo) diff --git a/actionpack/test/dispatch/executor_test.rb b/actionpack/test/dispatch/executor_test.rb index 8eb6450385..5b8be39b6d 100644 --- a/actionpack/test/dispatch/executor_test.rb +++ b/actionpack/test/dispatch/executor_test.rb @@ -81,7 +81,7 @@ class ExecutorTest < ActiveSupport::TestCase running = false body.close - assert !running + assert_not running end def test_complete_callbacks_are_called_on_close @@ -89,7 +89,7 @@ class ExecutorTest < ActiveSupport::TestCase executor.to_complete { completed = true } body = call_and_return_body - assert !completed + assert_not completed body.close assert completed @@ -116,7 +116,7 @@ class ExecutorTest < ActiveSupport::TestCase call_and_return_body.close assert result - assert !defined?(@in_shared_context) # it's not in the test itself + assert_not defined?(@in_shared_context) # it's not in the test itself end private diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb index 6167ea46df..fa264417e1 100644 --- a/actionpack/test/dispatch/mime_type_test.rb +++ b/actionpack/test/dispatch/mime_type_test.rb @@ -180,8 +180,8 @@ class MimeTypeTest < ActiveSupport::TestCase assert Mime[:js] =~ "text/javascript" assert Mime[:js] =~ "application/javascript" assert Mime[:js] !~ "text/html" - assert !(Mime[:js] !~ "text/javascript") - assert !(Mime[:js] !~ "application/javascript") + assert_not (Mime[:js] !~ "text/javascript") + assert_not (Mime[:js] !~ "application/javascript") assert Mime[:html] =~ "application/xhtml+xml" end end diff --git a/actionpack/test/dispatch/reloader_test.rb b/actionpack/test/dispatch/reloader_test.rb index e529229fae..edc4cd62a3 100644 --- a/actionpack/test/dispatch/reloader_test.rb +++ b/actionpack/test/dispatch/reloader_test.rb @@ -115,7 +115,7 @@ class ReloaderTest < ActiveSupport::TestCase reloader.to_complete { completed = true } body = call_and_return_body - assert !completed + assert_not completed body.close assert completed @@ -129,7 +129,7 @@ class ReloaderTest < ActiveSupport::TestCase prepared = false body.close - assert !prepared + assert_not prepared end def test_complete_callbacks_are_called_on_exceptions diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb index bf5a74e694..74da2fe7d3 100644 --- a/actionpack/test/dispatch/request/session_test.rb +++ b/actionpack/test/dispatch/request/session_test.rb @@ -118,6 +118,18 @@ module ActionDispatch end end + def test_dig + session = Session.create(store, req, {}) + session["one"] = { "two" => "3" } + + assert_equal "3", session.dig("one", "two") + assert_equal "3", session.dig(:one, "two") + + assert_nil session.dig("three", "two") + assert_nil session.dig("one", "three") + assert_nil session.dig("one", :two) + end + private def store Class.new { diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb index 4c8d528507..6d87314e97 100644 --- a/actionpack/test/dispatch/response_test.rb +++ b/actionpack/test/dispatch/response_test.rb @@ -191,7 +191,7 @@ class ResponseTest < ActiveSupport::TestCase test "does not include Status header" do @response.status = "200 OK" _, headers, _ = @response.to_a - assert !headers.has_key?("Status") + assert_not headers.has_key?("Status") end test "response code" do diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb index 127212b228..9150d5010b 100644 --- a/actionpack/test/dispatch/routing/inspector_test.rb +++ b/actionpack/test/dispatch/routing/inspector_test.rb @@ -3,6 +3,7 @@ require "abstract_unit" require "rails/engine" require "action_dispatch/routing/inspector" +require "io/console/size" class MountedRackApp def self.call(env) @@ -15,16 +16,10 @@ end module ActionDispatch module Routing class RoutesInspectorTest < ActiveSupport::TestCase - def setup + setup do @set = ActionDispatch::Routing::RouteSet.new end - def draw(options = nil, formater = ActionDispatch::Routing::ConsoleFormatter::Sheet.new, &block) - @set.draw(&block) - inspector = ActionDispatch::Routing::RoutesInspector.new(@set.routes) - inspector.format(formater, options).split("\n") - end - def test_displaying_routes_for_engines engine = Class.new(Rails::Engine) do def self.inspect @@ -305,7 +300,7 @@ module ActionDispatch end def test_routes_can_be_filtered - output = draw("posts") do + output = draw(grep: "posts") do resources :articles resources :posts end @@ -322,6 +317,9 @@ module ActionDispatch end def test_routes_when_expanded + previous_console_winsize = IO.console.winsize + IO.console.winsize = [0, 23] + engine = Class.new(Rails::Engine) do def self.inspect "Blog::Engine" @@ -331,50 +329,51 @@ module ActionDispatch get "/cart", to: "cart#show" end - output = draw(nil, ActionDispatch::Routing::ConsoleFormatter::Expanded.new) do + output = draw(formatter: ActionDispatch::Routing::ConsoleFormatter::Expanded.new) do get "/custom/assets", to: "custom_assets#show" get "/custom/furnitures", to: "custom_furnitures#show" mount engine => "/blog", :as => "blog" end - assert_equal ["--[ Route 1 ]------------------------------------------------------------", + assert_equal ["--[ Route 1 ]----------", "Prefix | custom_assets", "Verb | GET", "URI | /custom/assets(.:format)", "Controller#Action | custom_assets#show", - "--[ Route 2 ]------------------------------------------------------------", + "--[ Route 2 ]----------", "Prefix | custom_furnitures", "Verb | GET", "URI | /custom/furnitures(.:format)", "Controller#Action | custom_furnitures#show", - "--[ Route 3 ]------------------------------------------------------------", + "--[ Route 3 ]----------", "Prefix | blog", "Verb | ", "URI | /blog", "Controller#Action | Blog::Engine", "", "[ Routes for Blog::Engine ]", - "--[ Route 1 ]------------------------------------------------------------", + "--[ Route 1 ]----------", "Prefix | cart", "Verb | GET", "URI | /cart(.:format)", "Controller#Action | cart#show"], output + ensure + IO.console.winsize = previous_console_winsize end - def test_no_routes_matched_filter_when_expanded - output = draw("rails/dummy", ActionDispatch::Routing::ConsoleFormatter::Expanded.new) do + output = draw(grep: "rails/dummy", formatter: ActionDispatch::Routing::ConsoleFormatter::Expanded.new) do get "photos/:id" => "photos#show", :id => /[A-Z]\d{5}/ end assert_equal [ - "No routes were found for this controller", + "No routes were found for this grep pattern.", "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html." ], output end def test_not_routes_when_expanded - output = draw("rails/dummy", ActionDispatch::Routing::ConsoleFormatter::Expanded.new) {} + output = draw(grep: "rails/dummy", formatter: ActionDispatch::Routing::ConsoleFormatter::Expanded.new) {} assert_equal [ "You don't have any routes defined!", @@ -386,7 +385,7 @@ module ActionDispatch end def test_routes_can_be_filtered_with_namespaced_controllers - output = draw("admin/posts") do + output = draw(grep: "admin/posts") do resources :articles namespace :admin do resources :posts @@ -434,24 +433,24 @@ module ActionDispatch end assert_equal [ - "No routes were found for this controller", + "No routes were found for this controller.", "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html." ], output end def test_no_routes_matched_filter - output = draw("rails/dummy") do + output = draw(grep: "rails/dummy") do get "photos/:id" => "photos#show", :id => /[A-Z]\d{5}/ end assert_equal [ - "No routes were found for this controller", + "No routes were found for this grep pattern.", "For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html." ], output end def test_no_routes_were_defined - output = draw("Rails::DummyController") {} + output = draw(grep: "Rails::DummyController") {} assert_equal [ "You don't have any routes defined!", @@ -484,6 +483,13 @@ module ActionDispatch "custom_assets GET /custom/assets(.:format) custom_assets#show", ], output end + + private + def draw(formatter: ActionDispatch::Routing::ConsoleFormatter::Sheet.new, **options, &block) + @set.draw(&block) + inspector = ActionDispatch::Routing::RoutesInspector.new(@set.routes) + inspector.format(formatter, options).split("\n") + end end end end diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb index fe314e26b1..dd6adcbfd1 100644 --- a/actionpack/test/dispatch/routing_test.rb +++ b/actionpack/test/dispatch/routing_test.rb @@ -3166,7 +3166,7 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest end end - assert !respond_to?(:routes_no_collision_path) + assert_not respond_to?(:routes_no_collision_path) end def test_controller_name_with_leading_slash_raise_error diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb index 90f2ee46ea..baf46e7c7e 100644 --- a/actionpack/test/dispatch/ssl_test.rb +++ b/actionpack/test/dispatch/ssl_test.rb @@ -208,6 +208,14 @@ class SecureCookiesTest < SSLTest assert_cookies(*DEFAULT.split("\n")) end + def test_cookies_as_not_secure_with_exclude + excluding = { exclude: -> request { request.domain =~ /example/ } } + get headers: { "Set-Cookie" => DEFAULT }, ssl_options: { redirect: excluding } + + assert_cookies(*DEFAULT.split("\n")) + assert_response :ok + end + def test_no_cookies get assert_nil response.headers["Set-Cookie"] diff --git a/actionpack/test/dispatch/static_test.rb b/actionpack/test/dispatch/static_test.rb index 0bdff68692..6b69cd9999 100644 --- a/actionpack/test/dispatch/static_test.rb +++ b/actionpack/test/dispatch/static_test.rb @@ -71,7 +71,16 @@ module StaticTests end def test_served_static_file_with_non_english_filename - assert_html "means hello in Japanese\n", get("/foo/#{Rack::Utils.escape("こんにちは.html")}") + assert_html "means hello in Japanese\n", get("/foo/%E3%81%93%E3%82%93%E3%81%AB%E3%81%A1%E3%81%AF.html") + end + + def test_served_gzipped_static_file_with_non_english_filename + response = get("/foo/%E3%81%95%E3%82%88%E3%81%86%E3%81%AA%E3%82%89.html", "HTTP_ACCEPT_ENCODING" => "gzip") + + assert_gzip "/foo/さようなら.html", response + assert_equal "text/html", response.headers["Content-Type"] + assert_equal "Accept-Encoding", response.headers["Vary"] + assert_equal "gzip", response.headers["Content-Encoding"] end def test_serves_static_file_with_exclamation_mark_in_filename diff --git a/actionpack/test/dispatch/system_testing/server_test.rb b/actionpack/test/dispatch/system_testing/server_test.rb index 95e411faf4..740e90a4da 100644 --- a/actionpack/test/dispatch/system_testing/server_test.rb +++ b/actionpack/test/dispatch/system_testing/server_test.rb @@ -17,7 +17,7 @@ class ServerTest < ActiveSupport::TestCase test "server is changed from `default` to `puma`" do Capybara.server = :default ActionDispatch::SystemTesting::Server.new.run - refute_equal Capybara.server, Capybara.servers[:default] + assert_not_equal Capybara.server, Capybara.servers[:default] end test "server is not changed to `puma` when is different than default" do diff --git a/actionpack/test/fixtures/functional_caching/_formatted_partial.html.erb b/actionpack/test/fixtures/functional_caching/_formatted_partial.html.erb new file mode 100644 index 0000000000..aad73c0d6b --- /dev/null +++ b/actionpack/test/fixtures/functional_caching/_formatted_partial.html.erb @@ -0,0 +1 @@ +<p>Hello!</p> diff --git a/actionpack/test/fixtures/functional_caching/xml_fragment_cached_with_html_partial.xml.builder b/actionpack/test/fixtures/functional_caching/xml_fragment_cached_with_html_partial.xml.builder new file mode 100644 index 0000000000..2bdda3af18 --- /dev/null +++ b/actionpack/test/fixtures/functional_caching/xml_fragment_cached_with_html_partial.xml.builder @@ -0,0 +1,5 @@ +cache do + xml.title "Hello!" +end + +xml.body cdata_section(render("formatted_partial")) diff --git a/actionpack/test/fixtures/public/foo/さようなら.html b/actionpack/test/fixtures/public/foo/さようなら.html new file mode 100644 index 0000000000..627bb2469f --- /dev/null +++ b/actionpack/test/fixtures/public/foo/さようなら.html @@ -0,0 +1 @@ +means goodbye in Japanese diff --git a/actionpack/test/fixtures/public/foo/さようなら.html.gz b/actionpack/test/fixtures/public/foo/さようなら.html.gz Binary files differnew file mode 100644 index 0000000000..4f484cfe86 --- /dev/null +++ b/actionpack/test/fixtures/public/foo/さようなら.html.gz diff --git a/actionpack/test/fixtures/公共/foo/さようなら.html b/actionpack/test/fixtures/公共/foo/さようなら.html new file mode 100644 index 0000000000..627bb2469f --- /dev/null +++ b/actionpack/test/fixtures/公共/foo/さようなら.html @@ -0,0 +1 @@ +means goodbye in Japanese diff --git a/actionpack/test/fixtures/公共/foo/さようなら.html.gz b/actionpack/test/fixtures/公共/foo/さようなら.html.gz Binary files differnew file mode 100644 index 0000000000..4f484cfe86 --- /dev/null +++ b/actionpack/test/fixtures/公共/foo/さようなら.html.gz diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 55f2148640..d833f9cd98 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,12 +1,37 @@ -## Rails 6.0.0.alpha (Unreleased) ## +* Add the `nonce: true` option for `javascript_include_tag` helper to + support automatic nonce generation for Content Security Policy. + Works the same way as `javascript_tag nonce: true` does. + + *Yaroslav Markin* + +* Remove `ActionView::Helpers::RecordTagHelper`. + + *Yoshiyuki Hirano* + +* Disable `ActionView::Template` finalizers in test environment. + + Template finalization can be expensive in large view test suites. + Add a configuration option, + `action_view.finalize_compiled_template_methods`, and turn it off in + the test environment. + + *Simon Coffey* + +* Extract the `confirm` call in its own, overridable method in `rails_ujs`. + Example : + Rails.confirm = function(message, element) { + return (my_bootstrap_modal_confirm(message)); + } + + *Mathieu Mahé* * Enable select tag helper to mark `prompt` option as `selected` and/or `disabled` for `required` field. Example: - select :post, - :category, - ["lifestyle", "programming", "spiritual"], - { selected: "", disabled: "", prompt: "Choose one" }, + select :post, + :category, + ["lifestyle", "programming", "spiritual"], + { selected: "", disabled: "", prompt: "Choose one" }, { required: true } Placeholder option would be selected and disabled. The HTML produced: @@ -19,7 +44,7 @@ *Sergey Prikhodko* -* Don't enforce UTF-8 by default +* Don't enforce UTF-8 by default. With the disabling of TLS 1.0 by most major websites, continuing to run IE8 or lower becomes increasingly difficult so default to not enforcing diff --git a/actionview/Rakefile b/actionview/Rakefile index 8650f541b0..bdfd96c141 100644 --- a/actionview/Rakefile +++ b/actionview/Rakefile @@ -29,6 +29,7 @@ namespace :test do t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end + desc "Run tests for rails-ujs" task :ujs do begin Dir.mkdir("log") @@ -56,7 +57,7 @@ namespace :test do end namespace :integration do - desc "ActiveRecord Integration Tests" + # Active Record Integration Tests Rake::TestTask.new(:active_record) do |t| t.libs << "test" t.test_files = Dir.glob("test/activerecord/*_test.rb") @@ -65,7 +66,7 @@ namespace :test do t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) end - desc "ActionPack Integration Tests" + # Action Pack Integration Tests Rake::TestTask.new(:action_pack) do |t| t.libs << "test" t.test_files = Dir.glob("test/actionpack/**/*_test.rb") @@ -130,7 +131,7 @@ namespace :assets do end task :lines do - load File.join(File.expand_path("..", __dir__), "/tools/line_statistics") + load File.expand_path("../tools/line_statistics", __dir__) files = FileList["lib/**/*.rb"] CodeTools::LineStatistics.new(files).print_loc end diff --git a/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee b/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee index 72b5aaa218..0738ffcdc9 100644 --- a/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee +++ b/actionview/app/assets/javascripts/rails-ujs/features/confirm.coffee @@ -5,6 +5,10 @@ Rails.handleConfirm = (e) -> stopEverything(e) unless allowAction(this) +# Default confirm dialog, may be overridden with custom confirm dialog in Rails.confirm +Rails.confirm = (message, element) -> + confirm(message) + # For 'data-confirm' attribute: # - Fires `confirm` event # - Shows the confirmation dialog @@ -20,7 +24,7 @@ allowAction = (element) -> answer = false if fire(element, 'confirm') - try answer = confirm(message) + try answer = Rails.confirm(message, element) callback = fire(element, 'confirm:complete', [answer]) answer and callback diff --git a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee index 2a8f5659e3..cf31c796df 100644 --- a/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee +++ b/actionview/app/assets/javascripts/rails-ujs/utils/ajax.coffee @@ -69,7 +69,7 @@ processResponse = (response, type) -> script.nonce = cspNonce() script.text = response document.head.appendChild(script).parentNode.removeChild(script) - else if type.match(/\b(xml|html|svg)\b/) + else if type.match(/\bxml\b/) parser = new DOMParser() type = type.replace(/;.+/, '') # remove something like ';charset=utf-8' try response = parser.parseFromString(response, type) diff --git a/actionview/lib/action_view/context.rb b/actionview/lib/action_view/context.rb index 665a9e3171..3c605c3ee3 100644 --- a/actionview/lib/action_view/context.rb +++ b/actionview/lib/action_view/context.rb @@ -10,10 +10,11 @@ module ActionView # Action View contexts are supplied to Action Controller to render a template. # The default Action View context is ActionView::Base. # - # In order to work with ActionController, a Context must just include this module. - # The initialization of the variables used by the context (@output_buffer, @view_flow, - # and @virtual_path) is responsibility of the object that includes this module - # (although you can call _prepare_context defined below). + # In order to work with Action Controller, a Context must just include this + # module. The initialization of the variables used by the context + # (@output_buffer, @view_flow, and @virtual_path) is responsibility of the + # object that includes this module (although you can call _prepare_context + # defined below). module Context include CompiledTemplates attr_accessor :output_buffer, :view_flow diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index 1cf0bd3016..3832293251 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -46,10 +46,7 @@ module ActionView def tree(name, finder, partial = false, seen = {}) logical_name = name.gsub(%r|/_|, "/") - options = {} - options[:formats] = [finder.rendered_format] if finder.rendered_format - - if template = finder.disable_cache { finder.find_all(logical_name, [], partial, [], options).first } + if template = find_template(finder, logical_name, [], partial, []) finder.rendered_format ||= template.formats.first if node = seen[template.identifier] # handle cycles in the tree @@ -71,6 +68,22 @@ module ActionView seen[name] ||= Missing.new(name, logical_name, nil) end end + + private + def find_template(finder, *args) + name = args.first + prefixes = args[1] || [] + partial = args[2] || false + keys = args[3] || [] + options = args[4] || {} + finder.disable_cache do + if format = finder.rendered_format + finder.find_all(name, prefixes, partial, keys, options.merge(formats: [format])).first || finder.find_all(name, prefixes, partial, keys, options).first + else + finder.find_all(name, prefixes, partial, keys, options).first + end + end + end end class Node diff --git a/actionview/lib/action_view/helpers.rb b/actionview/lib/action_view/helpers.rb index 8cc8013718..0d77f74171 100644 --- a/actionview/lib/action_view/helpers.rb +++ b/actionview/lib/action_view/helpers.rb @@ -23,7 +23,6 @@ module ActionView #:nodoc: autoload :JavaScriptHelper, "action_view/helpers/javascript_helper" autoload :NumberHelper autoload :OutputSafetyHelper - autoload :RecordTagHelper autoload :RenderingHelper autoload :SanitizeHelper autoload :TagHelper @@ -57,7 +56,6 @@ module ActionView #:nodoc: include JavaScriptHelper include NumberHelper include OutputSafetyHelper - include RecordTagHelper include RenderingHelper include SanitizeHelper include TagHelper diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 76b1c3fb6e..257080d902 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -55,6 +55,8 @@ module ActionView # that path. # * <tt>:skip_pipeline</tt> - This option is used to bypass the asset pipeline # when it is set to true. + # * <tt>:nonce<tt> - When set to true, adds an automatic nonce value if + # you have Content Security Policy enabled. # # ==== Examples # @@ -79,6 +81,9 @@ module ActionView # # javascript_include_tag "http://www.example.com/xmlhr.js" # # => <script src="http://www.example.com/xmlhr.js"></script> + # + # javascript_include_tag "http://www.example.com/xmlhr.js", nonce: true + # # => <script src="http://www.example.com/xmlhr.js" nonce="..."></script> def javascript_include_tag(*sources) options = sources.extract_options!.stringify_keys path_options = options.extract!("protocol", "extname", "host", "skip_pipeline").symbolize_keys @@ -90,6 +95,9 @@ module ActionView tag_options = { "src" => href }.merge!(options) + if tag_options["nonce"] == true + tag_options["nonce"] = content_security_policy_nonce + end content_tag("script".freeze, "", tag_options) }.join("\n").html_safe @@ -105,7 +113,7 @@ module ActionView # to "screen", so you must explicitly set it to "all" for the stylesheet(s) to # apply to all media types. # - # If the server supports Early Hints header links for these assets will be + # If the server supports Early Hints header links for these assets will be # automatically pushed. # # stylesheet_link_tag "style" diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 4c45f122fe..620e1e9f21 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -302,15 +302,15 @@ module ActionView # time_select("article", "start_time", include_seconds: true) # # # You can set the <tt>:minute_step</tt> to 15 which will give you: 00, 15, 30, and 45. - # time_select 'game', 'game_time', {minute_step: 15} + # time_select 'game', 'game_time', { minute_step: 15 } # # # Creates a time select tag with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. - # time_select("article", "written_on", prompt: {hour: 'Choose hour', minute: 'Choose minute', second: 'Choose seconds'}) - # time_select("article", "written_on", prompt: {hour: true}) # generic prompt for hours + # time_select("article", "written_on", prompt: { hour: 'Choose hour', minute: 'Choose minute', second: 'Choose seconds' }) + # time_select("article", "written_on", prompt: { hour: true }) # generic prompt for hours # time_select("article", "written_on", prompt: true) # generic prompts for all # # # You can set :ampm option to true which will show the hours as: 12 PM, 01 AM .. 11 PM. - # time_select 'game', 'game_time', {ampm: true} + # time_select 'game', 'game_time', { ampm: true } # # The selects are prepared for multi-parameter assignment to an Active Record object. # @@ -346,8 +346,8 @@ module ActionView # datetime_select("article", "written_on", discard_type: true) # # # Generates a datetime select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. - # datetime_select("article", "written_on", prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'}) - # datetime_select("article", "written_on", prompt: {hour: true}) # generic prompt for hours + # datetime_select("article", "written_on", prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' }) + # datetime_select("article", "written_on", prompt: { hour: true }) # generic prompt for hours # datetime_select("article", "written_on", prompt: true) # generic prompts for all # # The selects are prepared for multi-parameter assignment to an Active Record object. @@ -397,8 +397,8 @@ module ActionView # select_datetime(my_date_time, prefix: 'payday') # # # Generates a datetime select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. - # select_datetime(my_date_time, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'}) - # select_datetime(my_date_time, prompt: {hour: true}) # generic prompt for hours + # select_datetime(my_date_time, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' }) + # select_datetime(my_date_time, prompt: { hour: true }) # generic prompt for hours # select_datetime(my_date_time, prompt: true) # generic prompts for all def select_datetime(datetime = Time.current, options = {}, html_options = {}) DateTimeSelector.new(datetime, options, html_options).select_datetime @@ -436,8 +436,8 @@ module ActionView # select_date(my_date, prefix: 'payday') # # # Generates a date select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts. - # select_date(my_date, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'}) - # select_date(my_date, prompt: {hour: true}) # generic prompt for hours + # select_date(my_date, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' }) + # select_date(my_date, prompt: { hour: true }) # generic prompt for hours # select_date(my_date, prompt: true) # generic prompts for all def select_date(date = Date.current, options = {}, html_options = {}) DateTimeSelector.new(date, options, html_options).select_date @@ -476,8 +476,8 @@ module ActionView # select_time(my_time, start_hour: 2, end_hour: 14) # # # Generates a time select with a custom prompt. Use <tt>:prompt</tt> to true for generic prompts. - # select_time(my_time, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'}) - # select_time(my_time, prompt: {hour: true}) # generic prompt for hours + # select_time(my_time, prompt: { day: 'Choose day', month: 'Choose month', year: 'Choose year' }) + # select_time(my_time, prompt: { hour: true }) # generic prompt for hours # select_time(my_time, prompt: true) # generic prompts for all def select_time(datetime = Time.current, options = {}, html_options = {}) DateTimeSelector.new(datetime, options, html_options).select_time @@ -681,9 +681,8 @@ module ActionView options = args.extract_options! format = options.delete(:format) || :long content = args.first || I18n.l(date_or_time, format: format) - datetime = date_or_time.acts_like?(:time) ? date_or_time.xmlschema : date_or_time.iso8601 - content_tag("time".freeze, content, options.reverse_merge(datetime: datetime), &block) + content_tag("time".freeze, content, options.reverse_merge(datetime: date_or_time.iso8601), &block) end private diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index afd49286e6..2d5c5684c1 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -1970,7 +1970,7 @@ module ActionView convert_to_legacy_options(options) - fields_for(scope || model, model, **options, &block) + fields_for(scope || model, model, options, &block) end # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index fe5e0b693e..d02f641867 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -16,7 +16,7 @@ module ActionView # # * <tt>:include_blank</tt> - set to true or a prompt string if the first option element of the select element is a blank. Useful if there is not a default value required for the select element. # - # select("post", "category", Post::CATEGORIES, {include_blank: true}) + # select("post", "category", Post::CATEGORIES, { include_blank: true }) # # could become: # @@ -30,7 +30,7 @@ module ActionView # # Example with <tt>@post.person_id => 2</tt>: # - # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: 'None'}) + # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: 'None' }) # # could become: # @@ -43,7 +43,7 @@ module ActionView # # * <tt>:prompt</tt> - set to true or a prompt string. When the select element doesn't have a value yet, this prepends an option with a generic prompt -- "Please select" -- or the given prompt string. # - # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {prompt: 'Select Person'}) + # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { prompt: 'Select Person' }) # # could become: # @@ -69,7 +69,7 @@ module ActionView # # * <tt>:disabled</tt> - can be a single value or an array of values that will be disabled options in the final output. # - # select("post", "category", Post::CATEGORIES, {disabled: 'restricted'}) + # select("post", "category", Post::CATEGORIES, { disabled: 'restricted' }) # # could become: # @@ -82,7 +82,7 @@ module ActionView # # When used with the <tt>collection_select</tt> helper, <tt>:disabled</tt> can also be a Proc that identifies those options that should be disabled. # - # collection_select(:post, :category_id, Category.all, :id, :name, {disabled: -> (category) { category.archived? }}) + # collection_select(:post, :category_id, Category.all, :id, :name, { disabled: -> (category) { category.archived? } }) # # If the categories "2008 stuff" and "Christmas" return true when the method <tt>archived?</tt> is called, this would return: # <select name="post[category_id]" id="post_category_id"> @@ -107,7 +107,7 @@ module ActionView # # For example: # - # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, { include_blank: true }) + # select("post", "person_id", Person.all.collect { |p| [ p.name, p.id ] }, { include_blank: true }) # # would become: # @@ -323,12 +323,12 @@ module ActionView # # You can optionally provide HTML attributes as the last element of the array. # - # options_for_select([ "Denmark", ["USA", {class: 'bold'}], "Sweden" ], ["USA", "Sweden"]) + # options_for_select([ "Denmark", ["USA", { class: 'bold' }], "Sweden" ], ["USA", "Sweden"]) # # => <option value="Denmark">Denmark</option> # # => <option value="USA" class="bold" selected="selected">USA</option> # # => <option value="Sweden" selected="selected">Sweden</option> # - # options_for_select([["Dollar", "$", {class: "bold"}], ["Kroner", "DKK", {onclick: "alert('HI');"}]]) + # options_for_select([["Dollar", "$", { class: "bold" }], ["Kroner", "DKK", { onclick: "alert('HI');" }]]) # # => <option value="$" class="bold">Dollar</option> # # => <option value="DKK" onclick="alert('HI');">Kroner</option> # diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 5a8b8555a0..ba09738beb 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -389,8 +389,8 @@ module ActionView # * Any other key creates standard HTML options for the tag. # # ==== Examples - # radio_button_tag 'gender', 'male' - # # => <input id="gender_male" name="gender" type="radio" value="male" /> + # radio_button_tag 'favorite_color', 'maroon' + # # => <input id="favorite_color_maroon" name="favorite_color" type="radio" value="maroon" /> # # radio_button_tag 'receive_updates', 'no', true # # => <input checked="checked" id="receive_updates_no" name="receive_updates" type="radio" value="no" /> @@ -551,7 +551,8 @@ module ActionView # # => <input src="/assets/save.png" data-confirm="Are you sure?" type="image" /> def image_submit_tag(source, options = {}) options = options.stringify_keys - tag :input, { "type" => "image", "src" => path_to_image(source) }.update(options) + src = path_to_image(source, skip_pipeline: options.delete("skip_pipeline")) + tag :input, { "type" => "image", "src" => src }.update(options) end # Creates a field set for grouping HTML form elements. diff --git a/actionview/lib/action_view/helpers/record_tag_helper.rb b/actionview/lib/action_view/helpers/record_tag_helper.rb deleted file mode 100644 index a6953ee905..0000000000 --- a/actionview/lib/action_view/helpers/record_tag_helper.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module ActionView - module Helpers #:nodoc: - module RecordTagHelper - def div_for(*) # :nodoc: - raise NoMethodError, "The `div_for` method has been removed from " \ - "Rails. To continue using it, add the `record_tag_helper` gem to " \ - "your Gemfile:\n" \ - " gem 'record_tag_helper', '~> 1.0'\n" \ - "Consult the Rails upgrade guide for details." - end - - def content_tag_for(*) # :nodoc: - raise NoMethodError, "The `content_tag_for` method has been removed from " \ - "Rails. To continue using it, add the `record_tag_helper` gem to " \ - "your Gemfile:\n" \ - " gem 'record_tag_helper', '~> 1.0'\n" \ - "Consult the Rails upgrade guide for details." - end - end - end -end diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index a6cec3f69c..b73c4be1ee 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -227,10 +227,10 @@ module ActionView # tag("img", src: "open & shut.png") # # => <img src="open & shut.png" /> # - # tag("img", {src: "open & shut.png"}, false, false) + # tag("img", { src: "open & shut.png" }, false, false) # # => <img src="open & shut.png" /> # - # tag("div", data: {name: 'Stephen', city_state: %w(Chicago IL)}) + # tag("div", data: { name: 'Stephen', city_state: %w(Chicago IL) }) # # => <div data-name="Stephen" data-city-state="["Chicago","IL"]" /> def tag(name = nil, options = nil, open = false, escape = true) if name.nil? diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb index f1eca2268a..eef527d36f 100644 --- a/actionview/lib/action_view/helpers/tags/base.rb +++ b/actionview/lib/action_view/helpers/tags/base.rb @@ -109,11 +109,11 @@ module ActionView # a little duplication to construct less strings case when @object_name.empty? - "#{sanitized_method_name}#{"[]" if multiple}" + "#{sanitized_method_name}#{multiple ? "[]" : ""}" when index - "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}" + "#{@object_name}[#{index}][#{sanitized_method_name}]#{multiple ? "[]" : ""}" else - "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}" + "#{@object_name}[#{sanitized_method_name}]#{multiple ? "[]" : ""}" end end diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb index 1860bc4732..db44fdbfee 100644 --- a/actionview/lib/action_view/helpers/translation_helper.rb +++ b/actionview/lib/action_view/helpers/translation_helper.rb @@ -60,7 +60,11 @@ module ActionView def translate(key, options = {}) options = options.dup has_default = options.has_key?(:default) - remaining_defaults = Array(options.delete(:default)).compact + if has_default + remaining_defaults = Array(options.delete(:default)).compact + else + remaining_defaults = [] + end if has_default && !remaining_defaults.first.kind_of?(Symbol) options[:default] = remaining_defaults @@ -122,9 +126,12 @@ module ActionView private def scope_key_by_partial(key) - if key.to_s.first == "." + stringified_key = key.to_s + if stringified_key.first == "." if @virtual_path - @virtual_path.gsub(%r{/_?}, ".") + key.to_s + @_scope_key_by_partial_cache ||= {} + @_scope_key_by_partial_cache[@virtual_path] ||= @virtual_path.gsub(%r{/_?}, ".") + "#{@_scope_key_by_partial_cache[@virtual_path]}#{stringified_key}" else raise "Cannot use t(#{key.inspect}) shortcut because path is not available" end diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb index 889562c478..cae62f2312 100644 --- a/actionview/lib/action_view/helpers/url_helper.rb +++ b/actionview/lib/action_view/helpers/url_helper.rb @@ -636,7 +636,7 @@ module ActionView # to_form_params(name: 'David', nationality: 'Danish') # # => [{name: :name, value: 'David'}, {name: 'nationality', value: 'Danish'}] # - # to_form_params(country: {name: 'Denmark'}) + # to_form_params(country: { name: 'Denmark' }) # # => [{name: 'country[name]', value: 'Denmark'}] # # to_form_params(countries: ['Denmark', 'Sweden']}) diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb index 12bdc642d4..12d06bf376 100644 --- a/actionview/lib/action_view/railtie.rb +++ b/actionview/lib/action_view/railtie.rb @@ -10,6 +10,7 @@ module ActionView config.action_view.embed_authenticity_token_in_remote_forms = nil config.action_view.debug_missing_translation = true config.action_view.default_enforce_utf8 = nil + config.action_view.finalize_compiled_template_methods = true config.eager_load_namespaces << ActionView @@ -45,6 +46,13 @@ module ActionView end end + initializer "action_view.finalize_compiled_template_methods" do |app| + ActiveSupport.on_load(:action_view) do + ActionView::Template.finalize_compiled_template_methods = + app.config.action_view.delete(:finalize_compiled_template_methods) + end + end + initializer "action_view.logger" do ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger } end diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb index 0c4bb73acb..ee1cd61f12 100644 --- a/actionview/lib/action_view/template.rb +++ b/actionview/lib/action_view/template.rb @@ -9,6 +9,8 @@ module ActionView class Template extend ActiveSupport::Autoload + mattr_accessor :finalize_compiled_template_methods, default: true + # === Encodings in ActionView::Template # # ActionView::Template is one of a few sources of potential @@ -307,7 +309,9 @@ module ActionView end mod.module_eval(source, identifier, 0) - ObjectSpace.define_finalizer(self, Finalizer[method_name, mod]) + if finalize_compiled_template_methods + ObjectSpace.define_finalizer(self, Finalizer[method_name, mod]) + end end def handle_render_error(view, e) diff --git a/actionview/test/activerecord/multifetch_cache_test.rb b/actionview/test/activerecord/multifetch_cache_test.rb new file mode 100644 index 0000000000..12be069e69 --- /dev/null +++ b/actionview/test/activerecord/multifetch_cache_test.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require "active_record_unit" +require "active_record/railties/collection_cache_association_loading" + +ActionView::PartialRenderer.prepend(ActiveRecord::Railties::CollectionCacheAssociationLoading) + +class MultifetchCacheTest < ActiveRecordTestCase + fixtures :topics, :replies + + def setup + view_paths = ActionController::Base.view_paths + + @view = Class.new(ActionView::Base) do + def view_cache_dependencies + [] + end + + def combined_fragment_cache_key(key) + [ :views, key ] + end + end.new(view_paths, {}) + end + + def test_only_preloading_for_records_that_miss_the_cache + @view.render partial: "test/partial", collection: [topics(:rails)], cached: true + + @topics = Topic.preload(:replies) + + @view.render partial: "test/partial", collection: @topics, cached: true + + assert_not @topics.detect { |topic| topic.id == topics(:rails).id }.replies.loaded? + assert @topics.detect { |topic| topic.id != topics(:rails).id }.replies.loaded? + end +end diff --git a/actionview/test/fixtures/digestor/comments/show.js.erb b/actionview/test/fixtures/digestor/comments/show.js.erb new file mode 100644 index 0000000000..38b37dfa2b --- /dev/null +++ b/actionview/test/fixtures/digestor/comments/show.js.erb @@ -0,0 +1 @@ +alert("<%=j render("comments/comment") %>") diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index 6d98eacfb8..e68f03d1f4 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -29,6 +29,10 @@ class AssetTagHelperTest < ActionView::TestCase "http://www.example.com" end + def content_security_policy_nonce + "iyhD0Yc0W+c=" + end + AssetPathToTag = { %(asset_path("")) => %(), %(asset_path(" ")) => %(), @@ -421,6 +425,10 @@ class AssetTagHelperTest < ActionView::TestCase assert_dom_equal %(<script src="//assets.example.com/javascripts/prototype.js"></script>), javascript_include_tag("prototype") end + def test_javascript_include_tag_nonce + assert_dom_equal %(<script src="/javascripts/bank.js" nonce="iyhD0Yc0W+c="></script>), javascript_include_tag("bank", nonce: true) + end + def test_stylesheet_path StylePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) } end diff --git a/actionview/test/template/capture_helper_test.rb b/actionview/test/template/capture_helper_test.rb index 31c280a91c..131e49327e 100644 --- a/actionview/test/template/capture_helper_test.rb +++ b/actionview/test/template/capture_helper_test.rb @@ -49,21 +49,21 @@ class CaptureHelperTest < ActionView::TestCase end def test_content_for_with_multiple_calls - assert ! content_for?(:title) + assert_not content_for?(:title) content_for :title, "foo" content_for :title, "bar" assert_equal "foobar", content_for(:title) end def test_content_for_with_multiple_calls_and_flush - assert ! content_for?(:title) + assert_not content_for?(:title) content_for :title, "foo" content_for :title, "bar", flush: true assert_equal "bar", content_for(:title) end def test_content_for_with_block - assert ! content_for?(:title) + assert_not content_for?(:title) content_for :title do output_buffer << "foo" output_buffer << "bar" @@ -73,7 +73,7 @@ class CaptureHelperTest < ActionView::TestCase end def test_content_for_with_block_and_multiple_calls_with_flush - assert ! content_for?(:title) + assert_not content_for?(:title) content_for :title do "foo" end @@ -84,7 +84,7 @@ class CaptureHelperTest < ActionView::TestCase end def test_content_for_with_block_and_multiple_calls_with_flush_nil_content - assert ! content_for?(:title) + assert_not content_for?(:title) content_for :title do "foo" end @@ -95,7 +95,7 @@ class CaptureHelperTest < ActionView::TestCase end def test_content_for_with_block_and_multiple_calls_without_flush - assert ! content_for?(:title) + assert_not content_for?(:title) content_for :title do "foo" end @@ -106,7 +106,7 @@ class CaptureHelperTest < ActionView::TestCase end def test_content_for_with_whitespace_block - assert ! content_for?(:title) + assert_not content_for?(:title) content_for :title, "foo" content_for :title do output_buffer << " \n " @@ -117,7 +117,7 @@ class CaptureHelperTest < ActionView::TestCase end def test_content_for_with_whitespace_block_and_flush - assert ! content_for?(:title) + assert_not content_for?(:title) content_for :title, "foo" content_for :title, flush: true do output_buffer << " \n " @@ -128,7 +128,7 @@ class CaptureHelperTest < ActionView::TestCase end def test_content_for_returns_nil_when_writing - assert ! content_for?(:title) + assert_not content_for?(:title) assert_nil content_for(:title, "foo") assert_nil content_for(:title) { output_buffer << "bar"; nil } assert_nil content_for(:title) { output_buffer << " \n "; nil } @@ -144,14 +144,14 @@ class CaptureHelperTest < ActionView::TestCase end def test_content_for_question_mark - assert ! content_for?(:title) + assert_not content_for?(:title) content_for :title, "title" assert content_for?(:title) - assert ! content_for?(:something_else) + assert_not content_for?(:something_else) end def test_content_for_should_be_html_safe_after_flush_empty - assert ! content_for?(:title) + assert_not content_for?(:title) content_for :title do content_tag(:p, "title") end @@ -164,7 +164,7 @@ class CaptureHelperTest < ActionView::TestCase end def test_provide - assert !content_for?(:title) + assert_not content_for?(:title) provide :title, "hi" assert content_for?(:title) assert_equal "hi", content_for(:title) diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb index 1bfa39a319..ddaa7febb3 100644 --- a/actionview/test/template/digestor_test.rb +++ b/actionview/test/template/digestor_test.rb @@ -160,6 +160,18 @@ class TemplateDigestorTest < ActionView::TestCase assert_equal [:html], tree_template_formats("messages/show").uniq end + def test_template_dependencies_with_fallback_from_js_to_html_format + finder.rendered_format = :js + assert_equal ["comments/comment"], dependencies("comments/show") + end + + def test_template_digest_with_fallback_from_js_to_html_format + finder.rendered_format = :js + assert_digest_difference("comments/show") do + change_template("comments/_comment") + end + end + def test_recursion_in_renders assert digest("level/recursion") # assert recursion is possible assert_not_nil digest("level/recursion") # assert digest is stored diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb index beee76f711..38469cbe3d 100644 --- a/actionview/test/template/lookup_context_test.rb +++ b/actionview/test/template/lookup_context_test.rb @@ -195,7 +195,7 @@ class LookupContextTest < ActiveSupport::TestCase assert @lookup_context.cache template = @lookup_context.disable_cache do - assert !@lookup_context.cache + assert_not @lookup_context.cache @lookup_context.find("foo", %w(test), true) end assert @lookup_context.cache diff --git a/actionview/test/template/record_tag_helper_test.rb b/actionview/test/template/record_tag_helper_test.rb deleted file mode 100644 index 7bbbfccdd0..0000000000 --- a/actionview/test/template/record_tag_helper_test.rb +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -require "abstract_unit" - -class RecordTagPost - extend ActiveModel::Naming - - attr_accessor :id, :body - - def initialize - @id = 45 - @body = "What a wonderful world!" - - yield self if block_given? - end -end - -class RecordTagHelperTest < ActionView::TestCase - tests ActionView::Helpers::RecordTagHelper - - def setup - super - @post = RecordTagPost.new - end - - def test_content_tag_for - assert_raises(NoMethodError) { content_tag_for(:li, @post) } - end - - def test_div_for - assert_raises(NoMethodError) { div_for(@post, class: "special") } - end -end diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb index 05e5f21ce4..d98fd4f9a2 100644 --- a/actionview/test/template/test_case_test.rb +++ b/actionview/test/template/test_case_test.rb @@ -192,7 +192,7 @@ module ActionView helper HelperThatInvokesProtectAgainstForgery test "protect_from_forgery? in any helpers returns false" do - assert !view.help_me + assert_not view.help_me end end diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb index 8bccda481b..08cb5dfea7 100644 --- a/actionview/test/template/url_helper_test.rb +++ b/actionview/test/template/url_helper_test.rb @@ -508,16 +508,16 @@ class UrlHelperTest < ActiveSupport::TestCase def test_current_page_considering_params @request = request_for_url("/?order=desc&page=1") - assert !current_page?(url_hash, check_parameters: true) - assert !current_page?(url_hash.merge(check_parameters: true)) - assert !current_page?(ActionController::Parameters.new(url_hash.merge(check_parameters: true)).permit!) - assert !current_page?("http://www.example.com/", check_parameters: true) + assert_not current_page?(url_hash, check_parameters: true) + assert_not current_page?(url_hash.merge(check_parameters: true)) + assert_not current_page?(ActionController::Parameters.new(url_hash.merge(check_parameters: true)).permit!) + assert_not current_page?("http://www.example.com/", check_parameters: true) end def test_current_page_considering_params_when_options_does_not_respond_to_to_hash @request = request_for_url("/?order=desc&page=1") - assert !current_page?(:back, check_parameters: false) + assert_not current_page?(:back, check_parameters: false) end def test_current_page_with_params_that_match @@ -562,7 +562,7 @@ class UrlHelperTest < ActiveSupport::TestCase def test_current_page_with_not_get_verb @request = request_for_url("/events", method: :post) - assert !current_page?("/events") + assert_not current_page?("/events") end def test_link_unless_current diff --git a/actionview/test/ujs/public/test/call-remote.js b/actionview/test/ujs/public/test/call-remote.js index 5932195363..e5277a2a03 100644 --- a/actionview/test/ujs/public/test/call-remote.js +++ b/actionview/test/ujs/public/test/call-remote.js @@ -128,6 +128,17 @@ asyncTest('execution of JS code does not modify current DOM', 1, function() { }) }) +asyncTest('HTML content should be plain-text', 1, function() { + buildForm({ method: 'post', 'data-type': 'html' }) + + $('form').append('<input type="text" name="content_type" value="text/html">') + $('form').append('<input type="text" name="content" value="<p>hello</p>">') + + submit(function(e, data, status, xhr) { + ok(data === '<p>hello</p>', 'returned data should be a plain-text string') + }) +}) + asyncTest('XML document should be parsed', 1, function() { buildForm({ method: 'post', 'data-type': 'html' }) diff --git a/actionview/test/ujs/public/test/data-confirm.js b/actionview/test/ujs/public/test/data-confirm.js index d1ea82ea7e..74f373148f 100644 --- a/actionview/test/ujs/public/test/data-confirm.js +++ b/actionview/test/ujs/public/test/data-confirm.js @@ -314,3 +314,29 @@ asyncTest('clicking on the children of a disabled button should not trigger a co start() }, 50) }) + +asyncTest('clicking on a link with data-confirm attribute with custom confirm handler. Confirm yes.', 7, function() { + var message, element + // redefine confirm function so we can make sure it's not called + window.confirm = function(msg) { + ok(false, 'confirm dialog should not be called') + } + // custom auto-confirm: + Rails.confirm = function(msg, elem) { message = msg; element = elem; return true } + + $('a[data-confirm]') + .bindNative('confirm:complete', function(e, data) { + App.assertCallbackInvoked('confirm:complete') + ok(data == true, 'confirm:complete passes in confirm answer (true)') + }) + .bindNative('ajax:success', function(e, data, status, xhr) { + App.assertCallbackInvoked('ajax:success') + App.assertRequestPath(data, '/echo') + App.assertGetRequest(data) + + equal(message, 'Are you absolutely sure?') + equal(element, $('a[data-confirm]').get(0)) + start() + }) + .triggerNative('click') +}) diff --git a/activejob/CHANGELOG.md b/activejob/CHANGELOG.md index c6a3ad8ade..a3d13ad162 100644 --- a/activejob/CHANGELOG.md +++ b/activejob/CHANGELOG.md @@ -1,6 +1,12 @@ -## Rails 6.0.0.alpha (Unreleased) ## +* Remove support for Qu gem. -* Add support for timezones to Active Job + Reasons are that the Qu gem wasn't compatible since Rails 5.1, + gem development was stopped in 2014 and maintainers have + confirmed its demise. See issue #32273 + + *Alberto Almagro* + +* Add support for timezones to Active Job. Record what was the current timezone in effect when the job was enqueued and then restore when the job is executed in same way diff --git a/activejob/README.md b/activejob/README.md index f1ebb76e08..d49fcfe3c2 100644 --- a/activejob/README.md +++ b/activejob/README.md @@ -88,6 +88,12 @@ Active Job has built-in adapters for multiple queueing backends (Sidekiq, Resque, Delayed Job and others). To get an up-to-date list of the adapters see the API Documentation for [ActiveJob::QueueAdapters](http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html). +**Please note:** We are not accepting pull requests for new adapters. We +encourage library authors to provide an ActiveJob adapter as part of +their gem, or as a stand-alone gem. For discussion about this see the +following PRs: [23311](https://github.com/rails/rails/issues/23311#issuecomment-176275718), +[21406](https://github.com/rails/rails/pull/21406#issuecomment-138813484), and [#32285](https://github.com/rails/rails/pull/32285). + ## Auxiliary gems * [activejob-stats](https://github.com/seuros/activejob-stats) diff --git a/activejob/Rakefile b/activejob/Rakefile index 77f3e7f8ff..b4da75adab 100644 --- a/activejob/Rakefile +++ b/activejob/Rakefile @@ -2,7 +2,6 @@ require "rake/testtask" -# TODO: add qu back to the list after it support Rails 5.1 ACTIVEJOB_ADAPTERS = %w(async inline delayed_job que queue_classic resque sidekiq sneakers sucker_punch backburner test) ACTIVEJOB_ADAPTERS.delete("queue_classic") if defined?(JRUBY_VERSION) diff --git a/activejob/lib/active_job/queue_adapters.rb b/activejob/lib/active_job/queue_adapters.rb index c1a1d3c510..7854467cc1 100644 --- a/activejob/lib/active_job/queue_adapters.rb +++ b/activejob/lib/active_job/queue_adapters.rb @@ -7,7 +7,6 @@ module ActiveJob # # * {Backburner}[https://github.com/nesquena/backburner] # * {Delayed Job}[https://github.com/collectiveidea/delayed_job] - # * {Qu}[https://github.com/bkeepers/qu] # * {Que}[https://github.com/chanks/que] # * {queue_classic}[https://github.com/QueueClassic/queue_classic] # * {Resque}[https://github.com/resque/resque] @@ -16,6 +15,7 @@ module ActiveJob # * {Sucker Punch}[https://github.com/brandonhilkert/sucker_punch] # * {Active Job Async Job}[http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/AsyncAdapter.html] # * {Active Job Inline}[http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters/InlineAdapter.html] + # * Please Note: We are not accepting pull requests for new adapters. See the README for more details. # # === Backends Features # @@ -23,7 +23,6 @@ module ActiveJob # |-------------------|-------|--------|------------|------------|---------|---------| # | Backburner | Yes | Yes | Yes | Yes | Job | Global | # | Delayed Job | Yes | Yes | Yes | Job | Global | Global | - # | Qu | Yes | Yes | No | No | No | Global | # | Que | Yes | Yes | Yes | Job | No | Job | # | queue_classic | Yes | Yes | Yes* | No | No | No | # | Resque | Yes | Yes | Yes (Gem) | Queue | Global | Yes | @@ -114,7 +113,6 @@ module ActiveJob autoload :InlineAdapter autoload :BackburnerAdapter autoload :DelayedJobAdapter - autoload :QuAdapter autoload :QueAdapter autoload :QueueClassicAdapter autoload :ResqueAdapter diff --git a/activejob/lib/active_job/queue_adapters/qu_adapter.rb b/activejob/lib/active_job/queue_adapters/qu_adapter.rb deleted file mode 100644 index bd7003e177..0000000000 --- a/activejob/lib/active_job/queue_adapters/qu_adapter.rb +++ /dev/null @@ -1,46 +0,0 @@ -# frozen_string_literal: true - -require "qu" - -module ActiveJob - module QueueAdapters - # == Qu adapter for Active Job - # - # Qu is a Ruby library for queuing and processing background jobs. It is - # heavily inspired by delayed_job and Resque. Qu was created to overcome - # some shortcomings in the existing queuing libraries. - # The advantages of Qu are: Multiple backends (redis, mongo), jobs are - # requeued when worker is killed, resque-like API. - # - # Read more about Qu {here}[https://github.com/bkeepers/qu]. - # - # To use Qu set the queue_adapter config to +:qu+. - # - # Rails.application.config.active_job.queue_adapter = :qu - class QuAdapter - def enqueue(job, *args) #:nodoc: - qu_job = Qu::Payload.new(klass: JobWrapper, args: [job.serialize]).tap do |payload| - payload.instance_variable_set(:@queue, job.queue_name) - end.push - - # qu_job can be nil depending on the configured backend - job.provider_job_id = qu_job.id unless qu_job.nil? - qu_job - end - - def enqueue_at(job, timestamp, *args) #:nodoc: - raise NotImplementedError, "This queueing backend does not support scheduling jobs. To see what features are supported go to http://api.rubyonrails.org/classes/ActiveJob/QueueAdapters.html" - end - - class JobWrapper < Qu::Job #:nodoc: - def initialize(job_data) - @job_data = job_data - end - - def perform - Base.execute @job_data - end - end - end - end -end diff --git a/activejob/lib/active_job/serializers/object_serializer.rb b/activejob/lib/active_job/serializers/object_serializer.rb index 1dfd1e44be..6d280969be 100644 --- a/activejob/lib/active_job/serializers/object_serializer.rb +++ b/activejob/lib/active_job/serializers/object_serializer.rb @@ -38,7 +38,7 @@ module ActiveJob { Arguments::OBJECT_SERIALIZER_KEY => self.class.name }.merge!(hash) end - # Deserilizes an argument form a JSON primiteve type. + # Deserializes an argument from a JSON primitive type. def deserialize(_argument) raise NotImplementedError end diff --git a/activejob/test/adapters/qu.rb b/activejob/test/adapters/qu.rb deleted file mode 100644 index 5b471fa347..0000000000 --- a/activejob/test/adapters/qu.rb +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -require "qu-immediate" - -ActiveJob::Base.queue_adapter = :qu diff --git a/activejob/test/integration/queuing_test.rb b/activejob/test/integration/queuing_test.rb index 7a95d3d039..32afb5ca62 100644 --- a/activejob/test/integration/queuing_test.rb +++ b/activejob/test/integration/queuing_test.rb @@ -82,7 +82,7 @@ class QueuingTest < ActiveSupport::TestCase end test "should supply a provider_job_id when available for immediate jobs" do - skip unless adapter_is?(:async, :delayed_job, :sidekiq, :qu, :que, :queue_classic) + skip unless adapter_is?(:async, :delayed_job, :sidekiq, :que, :queue_classic) test_job = TestJob.perform_later @id assert test_job.provider_job_id, "Provider job id should be set by provider" end diff --git a/activejob/test/support/integration/adapters/qu.rb b/activejob/test/support/integration/adapters/qu.rb deleted file mode 100644 index 67db03e279..0000000000 --- a/activejob/test/support/integration/adapters/qu.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -module QuJobsManager - def setup - require "qu-rails" - require "qu-redis" - ActiveJob::Base.queue_adapter = :qu - ENV["REDISTOGO_URL"] = "redis://127.0.0.1:6379/12" - backend = Qu::Backend::Redis.new - backend.namespace = "active_jobs_int_test" - Qu.backend = backend - Qu.logger = Rails.logger - Qu.interval = 0.5 - unless can_run? - puts "Cannot run integration tests for qu. To be able to run integration tests for qu you need to install and start redis.\n" - exit - end - end - - def clear_jobs - Qu.clear "integration_tests" - end - - def start_workers - @thread = Thread.new { Qu::Worker.new("integration_tests").start } - end - - def stop_workers - @thread.kill - end - - def can_run? - begin - Qu.backend.connection.client.connect - rescue - return false - end - true - end -end diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index b28c83e4ed..6b557a7cb1 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,5 +1,3 @@ -## Rails 6.0.0.alpha (Unreleased) ## - * Rails 6 requires Ruby 2.4.1 or newer. *Jeremy Daer* diff --git a/activemodel/lib/active_model/attribute_mutation_tracker.rb b/activemodel/lib/active_model/attribute_mutation_tracker.rb index 8e92c8807f..6abf37bd44 100644 --- a/activemodel/lib/active_model/attribute_mutation_tracker.rb +++ b/activemodel/lib/active_model/attribute_mutation_tracker.rb @@ -11,6 +11,10 @@ module ActiveModel @forced_changes = Set.new end + def changed_attribute_names + attr_names.select { |attr_name| changed?(attr_name) } + end + def changed_values attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| if changed?(attr_name) @@ -23,7 +27,7 @@ module ActiveModel attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| change = change_to_attribute(attr_name) if change - result[attr_name] = change + result.merge!(attr_name => change) end end end @@ -35,10 +39,6 @@ module ActiveModel end end - def changed_attribute_names - attr_names.select { |attr| changed?(attr) } - end - def any_changes? attr_names.any? { |attr| changed?(attr) } end @@ -80,6 +80,10 @@ module ActiveModel class NullMutationTracker # :nodoc: include Singleton + def changed_attribute_names(*) + [] + end + def changed_values(*) {} end @@ -108,5 +112,8 @@ module ActiveModel def original_value(*) end + + def force_change(*) + end end end diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 0044fde6c5..eaf8dfb223 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -3,7 +3,6 @@ require "active_support/hash_with_indifferent_access" require "active_support/core_ext/object/duplicable" require "active_model/attribute_mutation_tracker" -require "active_model/attribute_set" module ActiveModel # == Active \Model \Dirty @@ -143,8 +142,11 @@ module ActiveModel end def changes_applied # :nodoc: - _prepare_changes + unless defined?(@attributes) + @previously_changed = changes + end @mutations_before_last_save = mutations_from_database + @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new forget_attribute_assignments @mutations_from_database = nil end @@ -155,7 +157,7 @@ module ActiveModel # person.name = 'bob' # person.changed? # => true def changed? - mutations_from_database.any_changes? + changed_attributes.present? end # Returns an array with the name of the attributes with unsaved changes. @@ -164,24 +166,24 @@ module ActiveModel # person.name = 'bob' # person.changed # => ["name"] def changed - mutations_from_database.changed_attribute_names + changed_attributes.keys end # Handles <tt>*_changed?</tt> for +method_missing+. def attribute_changed?(attr, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) # :nodoc: - !!mutations_from_database.changed?(attr) && + !!changes_include?(attr) && (to == OPTION_NOT_GIVEN || to == _read_attribute(attr)) && - (from == OPTION_NOT_GIVEN || from == attribute_was(attr)) + (from == OPTION_NOT_GIVEN || from == changed_attributes[attr]) end # Handles <tt>*_was</tt> for +method_missing+. def attribute_was(attr) # :nodoc: - mutations_from_database.original_value(attr) + attribute_changed?(attr) ? changed_attributes[attr] : _read_attribute(attr) end # Handles <tt>*_previously_changed?</tt> for +method_missing+. def attribute_previously_changed?(attr) #:nodoc: - mutations_before_last_save.changed?(attr) + previous_changes_include?(attr) end # Restore all previous data of the provided attributes. @@ -191,12 +193,15 @@ module ActiveModel # Clears all dirty data: current changes and previous changes. def clear_changes_information + @previously_changed = ActiveSupport::HashWithIndifferentAccess.new @mutations_before_last_save = nil + @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new forget_attribute_assignments @mutations_from_database = nil end def clear_attribute_changes(attr_names) + attributes_changed_by_setter.except!(*attr_names) attr_names.each do |attr_name| clear_attribute_change(attr_name) end @@ -209,7 +214,13 @@ module ActiveModel # person.name = 'robert' # person.changed_attributes # => {"name" => "bob"} def changed_attributes - mutations_from_database.changed_values.freeze + # This should only be set by methods which will call changed_attributes + # multiple times when it is known that the computed value cannot change. + if defined?(@cached_changed_attributes) + @cached_changed_attributes + else + attributes_changed_by_setter.reverse_merge(mutations_from_database.changed_values).freeze + end end # Returns a hash of changed attributes indicating their original @@ -219,8 +230,9 @@ module ActiveModel # person.name = 'bob' # person.changes # => { "name" => ["bill", "bob"] } def changes - _prepare_changes - mutations_from_database.changes + cache_changed_attributes do + ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }] + end end # Returns a hash of attributes that were changed before the model was saved. @@ -230,7 +242,8 @@ module ActiveModel # person.save # person.previous_changes # => {"name" => ["bob", "robert"]} def previous_changes - mutations_before_last_save.changes + @previously_changed ||= ActiveSupport::HashWithIndifferentAccess.new + @previously_changed.merge(mutations_before_last_save.changes) end def attribute_changed_in_place?(attr_name) # :nodoc: @@ -246,17 +259,11 @@ module ActiveModel unless defined?(@mutations_from_database) @mutations_from_database = nil end - - unless defined?(@attributes) - @_pseudo_attributes = true - @attributes = AttributeSet.new( - Hash.new { |h, attr| - h[attr] = Attribute.with_cast_value(attr, _clone_attribute(attr), Type.default_value) - } - ) + @mutations_from_database ||= if defined?(@attributes) + ActiveModel::AttributeMutationTracker.new(@attributes) + else + NullMutationTracker.instance end - - @mutations_from_database ||= ActiveModel::AttributeMutationTracker.new(@attributes) end def forget_attribute_assignments @@ -267,45 +274,68 @@ module ActiveModel @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance end + def cache_changed_attributes + @cached_changed_attributes = changed_attributes + yield + ensure + clear_changed_attributes_cache + end + + def clear_changed_attributes_cache + remove_instance_variable(:@cached_changed_attributes) if defined?(@cached_changed_attributes) + end + + # Returns +true+ if attr_name is changed, +false+ otherwise. + def changes_include?(attr_name) + attributes_changed_by_setter.include?(attr_name) || mutations_from_database.changed?(attr_name) + end + alias attribute_changed_by_setter? changes_include? + + # Returns +true+ if attr_name were changed before the model was saved, + # +false+ otherwise. + def previous_changes_include?(attr_name) + previous_changes.include?(attr_name) + end + # Handles <tt>*_change</tt> for +method_missing+. def attribute_change(attr) - [attribute_was(attr), _read_attribute(attr)] if attribute_changed?(attr) + [changed_attributes[attr], _read_attribute(attr)] if attribute_changed?(attr) end # Handles <tt>*_previous_change</tt> for +method_missing+. def attribute_previous_change(attr) - mutations_before_last_save.change_to_attribute(attr) + previous_changes[attr] if attribute_previously_changed?(attr) end # Handles <tt>*_will_change!</tt> for +method_missing+. def attribute_will_change!(attr) - attr = attr.to_s - mutations_from_database.force_change(attr).tap do - @attributes[attr] if defined?(@_pseudo_attributes) + unless attribute_changed?(attr) + begin + value = _read_attribute(attr) + value = value.duplicable? ? value.clone : value + rescue TypeError, NoMethodError + end + + set_attribute_was(attr, value) end + mutations_from_database.force_change(attr) end # Handles <tt>restore_*!</tt> for +method_missing+. def restore_attribute!(attr) if attribute_changed?(attr) - __send__("#{attr}=", attribute_was(attr)) + __send__("#{attr}=", changed_attributes[attr]) clear_attribute_changes([attr]) end end - def _prepare_changes - if defined?(@_pseudo_attributes) - changed.each do |attr| - @attributes.write_from_user(attr, _read_attribute(attr)) - end - end + def attributes_changed_by_setter + @attributes_changed_by_setter ||= ActiveSupport::HashWithIndifferentAccess.new end - def _clone_attribute(attr) - value = _read_attribute(attr) - value.duplicable? ? value.clone : value - rescue TypeError, NoMethodError - value + # Force an attribute to have a particular "before" value + def set_attribute_was(attr, old_value) + attributes_changed_by_setter[attr] = old_value end end end diff --git a/activemodel/lib/active_model/type/time.rb b/activemodel/lib/active_model/type/time.rb index ad7ba0351a..c094ee0013 100644 --- a/activemodel/lib/active_model/type/time.rb +++ b/activemodel/lib/active_model/type/time.rb @@ -28,14 +28,10 @@ module ActiveModel private def cast_value(value) - return value unless value.is_a?(::String) + return apply_seconds_precision(value) unless value.is_a?(::String) return if value.empty? - if value.start_with?("2000-01-01") - dummy_time_value = value - else - dummy_time_value = "2000-01-01 #{value}" - end + dummy_time_value = value.sub(/\A(\d\d\d\d-\d\d-\d\d |)/, "2000-01-01 ") fast_string_to_time(dummy_time_value) || begin time_hash = ::Date._parse(dummy_time_value) diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb index 3104e7e329..9c12dc14c5 100644 --- a/activemodel/lib/active_model/validations/inclusion.rb +++ b/activemodel/lib/active_model/validations/inclusion.rb @@ -19,7 +19,7 @@ module ActiveModel # particular enumerable object. # # class Person < ActiveRecord::Base - # validates_inclusion_of :gender, in: %w( m f ) + # validates_inclusion_of :role, in: %w( admin contributor ) # validates_inclusion_of :age, in: 0..99 # validates_inclusion_of :format, in: %w( jpg gif png ), message: "extension %{value} is not included in the list" # validates_inclusion_of :states, in: ->(person) { STATES[person.country] } diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb index e28e7e9219..88cca318ef 100644 --- a/activemodel/lib/active_model/validations/validates.rb +++ b/activemodel/lib/active_model/validations/validates.rb @@ -63,7 +63,7 @@ module ActiveModel # and strings in shortcut form. # # validates :email, format: /@/ - # validates :gender, inclusion: %w(male female) + # validates :role, inclusion: %(admin contributor) # validates :password, length: 6..20 # # When using shortcut form, ranges and arrays are passed to your diff --git a/activemodel/test/cases/attributes_dirty_test.rb b/activemodel/test/cases/attributes_dirty_test.rb index c991176389..f9693a23cd 100644 --- a/activemodel/test/cases/attributes_dirty_test.rb +++ b/activemodel/test/cases/attributes_dirty_test.rb @@ -39,7 +39,7 @@ class AttributesDirtyTest < ActiveModel::TestCase end test "changes to attribute values" do - assert !@model.changes["name"] + assert_not @model.changes["name"] @model.name = "John" assert_equal [nil, "John"], @model.changes["name"] end diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb index f769eb0da1..b120e68027 100644 --- a/activemodel/test/cases/dirty_test.rb +++ b/activemodel/test/cases/dirty_test.rb @@ -78,7 +78,7 @@ class DirtyTest < ActiveModel::TestCase end test "changes to attribute values" do - assert !@model.changes["name"] + assert_not @model.changes["name"] @model.name = "John" assert_equal [nil, "John"], @model.changes["name"] end diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index cb6a8c43d5..6ff3be1308 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -207,26 +207,26 @@ class ErrorsTest < ActiveModel::TestCase test "added? returns false when no errors are present" do person = Person.new - assert !person.errors.added?(:name) + assert_not person.errors.added?(:name) end test "added? returns false when checking a nonexisting error and other errors are present for the given attribute" do person = Person.new person.errors.add(:name, "is invalid") - assert !person.errors.added?(:name, "cannot be blank") + assert_not person.errors.added?(:name, "cannot be blank") end test "added? returns false when checking for an error, but not providing message arguments" do person = Person.new person.errors.add(:name, "cannot be blank") - assert !person.errors.added?(:name) + assert_not person.errors.added?(:name) end test "added? returns false when checking for an error by symbol and a different error with same message is present" do I18n.backend.store_translations("en", errors: { attributes: { name: { wrong: "is wrong", used: "is wrong" } } }) person = Person.new person.errors.add(:name, :wrong) - assert !person.errors.added?(:name, :used) + assert_not person.errors.added?(:name, :used) end test "size calculates the number of error messages" do diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index d19e81a119..912cb6d5ab 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -187,7 +187,7 @@ class SecurePasswordTest < ActiveModel::TestCase test "authenticate" do @user.password = "secret" - assert !@user.authenticate("wrong") + assert_not @user.authenticate("wrong") assert @user.authenticate("secret") end diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 325c4abfc8..d8bf7df63b 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,4 +1,10 @@ -## Rails 6.0.0.alpha (Unreleased) ## +* Add `ActiveRecord::Base.base_class?` predicate. + + *Bogdan Gusiev* + +* Add custom prefix option to ActiveRecord::Store.store_accessor. + + *Tan Huynh* * Rails 6 requires Ruby 2.4.1 or newer. diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 591c451da5..170c95b827 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -41,9 +41,11 @@ namespace :test do end end -desc "Build MySQL and PostgreSQL test databases" namespace :db do + desc "Build MySQL and PostgreSQL test databases" task create: ["db:mysql:build", "db:postgresql:build"] + + desc "Drop MySQL and PostgreSQL test databases" task drop: ["db:mysql:drop", "db:postgresql:drop"] end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index d43378c64f..d198466dbf 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -40,6 +40,7 @@ module ActiveRecord autoload :Core autoload :ConnectionHandling autoload :CounterCache + autoload :DatabaseConfigurations autoload :DynamicMatchers autoload :Enum autoload :InternalMetadata diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index b1cad0d0a4..0e68e49182 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -241,7 +241,7 @@ module ActiveRecord association end - def association_cached?(name) # :nodoc + def association_cached?(name) # :nodoc: @association_cache.key?(name) end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index bd2012df84..1109fee462 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -5,7 +5,15 @@ module ActiveRecord # = Active Record Belongs To Association class BelongsToAssociation < SingularAssociation #:nodoc: def handle_dependency - target.send(options[:dependent]) if load_target + return unless load_target + + case options[:dependent] + when :destroy + target.destroy + raise ActiveRecord::Rollback unless target.destroyed? + else + target.send(options[:dependent]) + end end def replace(record) diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 7953b89f61..090b082cb0 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -60,6 +60,7 @@ module ActiveRecord when :destroy target.destroyed_by_association = reflection target.destroy + throw(:abort) unless target.destroyed? when :nullify target.update_columns(reflection.foreign_key => nil) if target.persisted? end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 0f38d6bbda..d6f7359055 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -42,11 +42,11 @@ module ActiveRecord def associate_records_to_owner(owner, records) association = owner.association(reflection.name) + association.loaded! if reflection.collection? - association.loaded! association.target.concat(records) else - association.target = records.first + association.target = records.first unless records.empty? end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 0ca0e5dcdb..83b5a5e698 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -59,7 +59,7 @@ module ActiveRecord # attribute methods. generated_attribute_methods.synchronize do return false if @attribute_methods_generated - superclass.define_attribute_methods unless self == base_class + superclass.define_attribute_methods unless base_class? super(attribute_names) @attribute_methods_generated = true end @@ -175,9 +175,20 @@ module ActiveRecord # Regexp whitelist. Matches the following: # "#{table_name}.#{column_name}" # "#{table_name}.#{column_name} #{direction}" + # "#{table_name}.#{column_name} #{direction} NULLS FIRST" + # "#{table_name}.#{column_name} NULLS LAST" # "#{column_name}" # "#{column_name} #{direction}" - COLUMN_NAME_ORDER_WHITELIST = /\A(?:\w+\.)?\w+(?:\s+asc|\s+desc)?\z/i + # "#{column_name} #{direction} NULLS FIRST" + # "#{column_name} NULLS LAST" + COLUMN_NAME_ORDER_WHITELIST = / + \A + (?:\w+\.)? + \w+ + (?:\s+asc|\s+desc)? + (?:\s+nulls\s+(?:first|last))? + \z + /ix def enforce_raw_sql_whitelist(args, whitelist: COLUMN_NAME_WHITELIST) # :nodoc: unexpected = args.reject do |arg| diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index df4c79b0f6..7224f970e0 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -32,7 +32,9 @@ module ActiveRecord # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do + @previously_changed = ActiveSupport::HashWithIndifferentAccess.new @mutations_before_last_save = nil + @attributes_changed_by_setter = ActiveSupport::HashWithIndifferentAccess.new @mutations_from_database = nil end end @@ -117,7 +119,7 @@ module ActiveRecord # Alias for +changed_attributes+ def attributes_in_database - mutations_from_database.changed_values + changes_to_save.transform_values(&:first) end private diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 2907547634..9b267bb7c0 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -83,7 +83,7 @@ module ActiveRecord end def reset_primary_key #:nodoc: - if self == base_class + if base_class? self.primary_key = get_primary_key(base_class.name) else self.primary_key = base_class.primary_key diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 14f700b6a9..0f7bcba564 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -69,7 +69,7 @@ module ActiveRecord if defined?(JRUBY_VERSION) # This form is significantly faster on JRuby, and this is one of our biggest hotspots. # https://github.com/jruby/jruby/pull/2562 - def _read_attribute(attr_name, &block) # :nodoc + def _read_attribute(attr_name, &block) # :nodoc: @attributes.fetch_value(attr_name.to_s, &block) end else diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index cc99401390..7ab9160265 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -290,6 +290,7 @@ module ActiveRecord #:nodoc: extend CollectionCacheKey include Core + include DatabaseConfigurations include Persistence include ReadonlyAttributes include ModelSchema diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 9dbdf845bd..fd6819d08f 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -75,21 +75,7 @@ module ActiveRecord # end # # Now, when <tt>Topic#destroy</tt> is run only +destroy_author+ is called. When <tt>Reply#destroy</tt> is - # run, both +destroy_author+ and +destroy_readers+ are called. Contrast this to the following situation - # where the +before_destroy+ method is overridden: - # - # class Topic < ActiveRecord::Base - # def before_destroy() destroy_author end - # end - # - # class Reply < Topic - # def before_destroy() destroy_readers end - # end - # - # In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+. - # So, use the callback macros when you want to ensure that a certain callback is called for the entire - # hierarchy, and use the regular overwritable methods when you want to leave it up to each descendant - # to decide whether they want to call +super+ and trigger the inherited callbacks. + # run, both +destroy_author+ and +destroy_readers+ are called. # # *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the # callbacks before specifying the associations. Otherwise, you might trigger the loading of a diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 92e46ccf9f..aec5fa6ba1 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -130,7 +130,7 @@ module ActiveRecord end def quoted_time(value) # :nodoc: - quoted_date(value).sub(/\A2000-01-01 /, "") + quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "") end def quoted_binary(value) # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 584a86da21..6a498b353c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -101,6 +101,10 @@ module ActiveRecord end alias validated? validate? + def export_name_on_schema_dump? + name !~ ActiveRecord::SchemaDumper.fk_ignore_pattern + end + def defined_for?(to_table_ord = nil, to_table: nil, **options) if to_table_ord self.to_table == to_table_ord.to_s diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index e2147b7fcf..ac73337aef 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -305,7 +305,8 @@ module ActiveRecord yield td if block_given? if options[:force] - drop_table(table_name, **options, if_exists: true) + drop_opts = { if_exists: true }.merge(**options) + drop_table(table_name, drop_opts) end result = execute schema_creation.accept td @@ -908,7 +909,7 @@ module ActiveRecord foreign_key_options = { to_table: reference_name } end foreign_key_options[:column] ||= "#{ref_name}_id" - remove_foreign_key(table_name, **foreign_key_options) + remove_foreign_key(table_name, foreign_key_options) end remove_column(table_name, "#{ref_name}_id") diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index d9ac8db6a8..0ce3796829 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -75,7 +75,6 @@ module ActiveRecord class Transaction #:nodoc: attr_reader :connection, :state, :records, :savepoint_name - attr_writer :joinable def initialize(connection, options, run_commit_callbacks: false) @connection = connection diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 508132accb..901717ae3d 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -156,7 +156,6 @@ module ActiveRecord env_config = config[env] if config[env].is_a?(Hash) && !(config[env].key?("adapter") || config[env].key?("url")) end - config.reject! { |k, v| v.is_a?(Hash) && !(v.key?("adapter") || v.key?("url")) } config.merge! env_config if env_config config.each do |key, value| diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index bfdc7995f0..4c57bd48ab 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -3,7 +3,7 @@ require "active_record/connection_adapters/abstract_mysql_adapter" require "active_record/connection_adapters/mysql/database_statements" -gem "mysql2", "~> 0.4.4" +gem "mysql2", ">= 0.4.4", "< 0.6.0" require "mysql2" module ActiveRecord diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index 45b230f0f9..e20e5f2914 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -107,7 +107,7 @@ module ActiveRecord oid = row[4] comment = row[5] - using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/).flatten + using, expressions, where = inddef.scan(/ USING (\w+?) \((.+?)\)(?: WHERE (.+))?\z/m).flatten orders = {} opclasses = {} diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb index 8042dbfea2..70de96326c 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -17,7 +17,7 @@ module ActiveRecord end def quoted_time(value) - quoted_date(value) + quoted_date(value).sub(/\A\d\d\d\d-\d\d-\d\d /, "2000-01-01 ") end def quoted_binary(value) diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 88d28dc52a..ee0e651912 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -57,6 +57,10 @@ module ActiveRecord spec = resolver.resolve(config).symbolize_keys spec[:name] = spec_name + # use the primary config if a config is not passed in and + # it's a three tier config + spec = spec[spec_name.to_sym] if spec[spec_name.to_sym] + connection_handler.establish_connection(spec) end diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb new file mode 100644 index 0000000000..ffeed45030 --- /dev/null +++ b/activerecord/lib/active_record/database_configurations.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module ActiveRecord + module DatabaseConfigurations # :nodoc: + class DatabaseConfig + attr_reader :env_name, :spec_name, :config + + def initialize(env_name, spec_name, config) + @env_name = env_name + @spec_name = spec_name + @config = config + end + end + + # Selects the config for the specified environment and specification name + # + # For example if passed :development, and :animals it will select the database + # under the :development and :animals configuration level + def self.config_for_env_and_spec(environment, specification_name, configs = ActiveRecord::Base.configurations) # :nodoc: + configs_for(environment, configs).find do |db_config| + db_config.spec_name == specification_name + end + end + + # Collects the configs for the environment passed in. + # + # If a block is given returns the specification name and configuration + # otherwise returns an array of DatabaseConfig structs for the environment. + def self.configs_for(env, configs = ActiveRecord::Base.configurations, &blk) # :nodoc: + env_with_configs = db_configs(configs).select do |db_config| + db_config.env_name == env + end + + if block_given? + env_with_configs.each do |env_with_config| + yield env_with_config.spec_name, env_with_config.config + end + else + env_with_configs + end + end + + # Given an env, spec and config creates DatabaseConfig structs with + # each attribute set. + def self.walk_configs(env_name, spec_name, config) # :nodoc: + if config["database"] || config["url"] || config["adapter"] + DatabaseConfig.new(env_name, spec_name, config) + else + config.each_pair.map do |sub_spec_name, sub_config| + walk_configs(env_name, sub_spec_name, sub_config) + end + end + end + + # Walks all the configs passed in and returns an array + # of DatabaseConfig structs for each configuration. + def self.db_configs(configs = ActiveRecord::Base.configurations) # :nodoc: + configs.each_pair.flat_map do |env_name, config| + walk_configs(env_name, "primary", config) + end + end + end +end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 208ba95c52..6891c575c7 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -55,7 +55,7 @@ module ActiveRecord if has_attribute?(inheritance_column) subclass = subclass_from_attributes(attributes) - if subclass.nil? && base_class == self + if subclass.nil? && base_class? subclass = subclass_from_attributes(column_defaults) end end @@ -104,6 +104,12 @@ module ActiveRecord end end + # Returns whether the class is a base class. + # See #base_class for more information. + def base_class? + base_class == self + end + # Set this to +true+ if this is an abstract class (see # <tt>abstract_class?</tt>). # If you are using inheritance with Active Record and don't want a class diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index af361a8d2d..7f096bb532 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -61,13 +61,6 @@ module ActiveRecord end private - - def increment_lock - lock_col = self.class.locking_column - previous_lock_value = send(lock_col) - send("#{lock_col}=", previous_lock_value + 1) - end - def _create_record(attribute_names = self.attribute_names, *) if locking_enabled? # We always want to persist the locking version, even if we don't detect @@ -77,41 +70,13 @@ module ActiveRecord super end - def _update_record(attribute_names = self.attribute_names) - attribute_names &= self.class.column_names - return super unless locking_enabled? - return 0 if attribute_names.empty? - - begin - lock_col = self.class.locking_column - - previous_lock_value = read_attribute_before_type_cast(lock_col) - - increment_lock - - attribute_names.push(lock_col) - - affected_rows = self.class._update_record( - attributes_with_values_for_update(attribute_names), - self.class.primary_key => id_in_database, - lock_col => previous_lock_value - ) - - unless affected_rows == 1 - raise ActiveRecord::StaleObjectError.new(self, "update") - end - - affected_rows - - # If something went wrong, revert the locking_column value. - rescue Exception - send("#{lock_col}=", previous_lock_value.to_i) - - raise - end + def _touch_row(attribute_names, time) + super + ensure + clear_attribute_change(self.class.locking_column) if locking_enabled? end - def _update_row(attribute_names, attempted_action) + def _update_row(attribute_names, attempted_action = "update") return super unless locking_enabled? begin @@ -135,10 +100,8 @@ module ActiveRecord # If something went wrong, revert the locking_column value. rescue Exception - self[locking_column] = previous_lock_value + self[locking_column] = previous_lock_value.to_i raise - ensure - clear_attribute_change(locking_column) end end diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index bb85c47e06..5d1d15c94d 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -62,7 +62,7 @@ module ActiveRecord # the locked record. def lock!(lock = true) if persisted? - if changed? + if has_changes_to_save? raise(<<-MSG.squish) Locking a record with unpersisted changes is not supported. Use `save` to persist the changes, or `reload` to discard them diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 663b3c590a..025201c20b 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require "benchmark" require "set" require "zlib" require "active_support/core_ext/module/attribute_accessors" diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index b04dc04899..694ff85fa1 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -276,7 +276,7 @@ module ActiveRecord end def sequence_name - if base_class == self + if base_class? @sequence_name ||= reset_sequence_name else (@sequence_name ||= nil) || base_class.sequence_name @@ -501,8 +501,7 @@ module ActiveRecord # Computes and returns a table name according to default conventions. def compute_table_name - base = base_class - if self == base + if base_class? # Nested classes are prefixed with singular parent table name. if parent < Base && !parent.abstract_class? contained = parent.table_name @@ -513,7 +512,7 @@ module ActiveRecord "#{full_table_name_prefix}#{contained}#{undecorated_table_name(name)}#{full_table_name_suffix}" else # STI subclasses always use their superclass' table. - base.table_name + base_class.table_name end end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index ce16587326..7721e6b691 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -374,6 +374,7 @@ module ActiveRecord became.send(:initialize) became.instance_variable_set("@attributes", @attributes) became.instance_variable_set("@mutations_from_database", @mutations_from_database) if defined?(@mutations_from_database) + became.instance_variable_set("@changed_attributes", attributes_changed_by_setter) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) became.errors.copy!(errors) @@ -656,18 +657,11 @@ module ActiveRecord MSG end - time ||= current_time_from_proper_timezone attribute_names = timestamp_attributes_for_update_in_model - attribute_names.concat(names) + attribute_names |= names.map(&:to_s) unless attribute_names.empty? - attribute_names.each do |attr_name| - write_attribute(attr_name, time) - clear_attribute_change(attr_name) - end - - affected_rows = _update_row(attribute_names, "touch") - + affected_rows = _touch_row(attribute_names, time) @_trigger_update_callback = affected_rows == 1 else true @@ -688,7 +682,18 @@ module ActiveRecord self.class._delete_record(self.class.primary_key => id_in_database) end - def _update_row(attribute_names, attempted_action) + def _touch_row(attribute_names, time) + time ||= current_time_from_proper_timezone + + attribute_names.each do |attr_name| + write_attribute(attr_name, time) + clear_attribute_change(attr_name) + end + + _update_row(attribute_names, "touch") + end + + def _update_row(attribute_names, attempted_action = "update") self.class._update_record( attributes_with_values(attribute_names), self.class.primary_key => id_in_database @@ -712,7 +717,7 @@ module ActiveRecord affected_rows = 0 @_trigger_update_callback = true else - affected_rows = _update_row(attribute_names, "update") + affected_rows = _update_row(attribute_names) @_trigger_update_callback = affected_rows == 1 end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index c8e340712d..28194c7c46 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -26,19 +26,12 @@ module ActiveRecord end def self.run - ActiveRecord::Base.connection_handler.connection_pool_list.map do |pool| - caching_was_enabled = pool.query_cache_enabled - - pool.enable_query_cache! - - [pool, caching_was_enabled] - end + ActiveRecord::Base.connection_handler.connection_pool_list. + reject { |p| p.query_cache_enabled }.each { |p| p.enable_query_cache! } end - def self.complete(caching_pools) - caching_pools.each do |pool, caching_was_enabled| - pool.disable_query_cache! unless caching_was_enabled - end + def self.complete(pools) + pools.each { |pool| pool.disable_query_cache! } ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool| pool.release_connection if pool.active_connection? && !pool.connection.transaction_open? diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 7c8f794910..d33d36ac02 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -13,7 +13,7 @@ module ActiveRecord :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, :having, :create_with, :distinct, :references, :none, :unscope, :merge, to: :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all - delegate :pluck, :ids, to: :all + delegate :pluck, :pick, :ids, to: :all # Executes a custom SQL query against your database and returns all the results. The results will # be returned as an array with columns requested encapsulated as attributes of the model you call diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index ade5946dd5..6ab80a654d 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -157,6 +157,13 @@ end_warning end end + initializer "active_record.collection_cache_association_loading" do + require "active_record/railties/collection_cache_association_loading" + ActiveSupport.on_load(:action_view) do + ActionView::PartialRenderer.prepend(ActiveRecord::Railties::CollectionCacheAssociationLoading) + end + end + initializer "active_record.set_reloader_hooks" do ActiveSupport.on_load(:active_record) do ActiveSupport::Reloader.before_class_unload do diff --git a/activerecord/lib/active_record/railties/collection_cache_association_loading.rb b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb new file mode 100644 index 0000000000..b5129e4239 --- /dev/null +++ b/activerecord/lib/active_record/railties/collection_cache_association_loading.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module ActiveRecord + module Railties # :nodoc: + module CollectionCacheAssociationLoading #:nodoc: + def setup(context, options, block) + @relation = relation_from_options(options) + + super + end + + def relation_from_options(cached: nil, partial: nil, collection: nil, **_) + return unless cached + + relation = partial if partial.is_a?(ActiveRecord::Relation) + relation ||= collection if collection.is_a?(ActiveRecord::Relation) + + if relation && !relation.loaded? + relation.skip_preloading! + end + end + + def collection_without_template + @relation.preload_associations(@collection) if @relation + super + end + + def collection_with_template + @relation.preload_associations(@collection) if @relation + super + end + end + end +end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 662a8bc720..24449e8df3 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -22,6 +22,14 @@ db_namespace = namespace :db do task all: :load_config do ActiveRecord::Tasks::DatabaseTasks.create_all end + + ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + desc "Create #{spec_name} database for current environment" + task spec_name => :load_config do + db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + ActiveRecord::Tasks::DatabaseTasks.create(db_config.config) + end + end end desc "Creates the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:create:all to create all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to creating the development and test databases." @@ -33,6 +41,14 @@ db_namespace = namespace :db do task all: [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.drop_all end + + ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + desc "Drop #{spec_name} database for current environment" + task spec_name => [:load_config, :check_protected_environments] do + db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config) + end + end end desc "Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV or when RAILS_ENV is development, it defaults to dropping the development and test databases." @@ -57,7 +73,10 @@ db_namespace = namespace :db do desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task migrate: :load_config do - ActiveRecord::Tasks::DatabaseTasks.migrate + ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| + ActiveRecord::Base.establish_connection(config) + ActiveRecord::Tasks::DatabaseTasks.migrate + end db_namespace["_dump"].invoke end @@ -77,6 +96,15 @@ db_namespace = namespace :db do end namespace :migrate do + ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| + desc "Migrate #{spec_name} database for current environment" + task spec_name => :load_config do + db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + ActiveRecord::Base.establish_connection(db_config.config) + ActiveRecord::Tasks::DatabaseTasks.migrate + end + end + # desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).' task redo: :load_config do raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty? @@ -246,10 +274,15 @@ db_namespace = namespace :db do desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record" task dump: :load_config do require "active_record/schema_dumper" - filename = ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema.rb") - File.open(filename, "w:utf-8") do |file| - ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) + + ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :ruby) + File.open(filename, "w:utf-8") do |file| + ActiveRecord::Base.establish_connection(config) + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) + end end + db_namespace["schema:dump"].reenable end @@ -276,22 +309,24 @@ db_namespace = namespace :db do rm_f filename, verbose: false end end - end namespace :structure do desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql" task dump: :load_config do - filename = ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql") - current_config = ActiveRecord::Tasks::DatabaseTasks.current_config - ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) - - if ActiveRecord::SchemaMigration.table_exists? - File.open(filename, "a") do |f| - f.puts ActiveRecord::Base.connection.dump_schema_information - f.print "\n" + ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| + ActiveRecord::Base.establish_connection(config) + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :sql) + ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, filename) + + if ActiveRecord::SchemaMigration.table_exists? + File.open(filename, "a") do |f| + f.puts ActiveRecord::Base.connection.dump_schema_information + f.print "\n" + end end end + db_namespace["structure:dump"].reenable end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 736173ae1b..3c66d2f8be 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -9,6 +9,7 @@ module ActiveRecord SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, :reverse_order, :distinct, :create_with, :skip_query_cache] + CLAUSE_METHODS = [:where, :having, :from] INVALID_METHODS_FOR_DELETE_ALL = [:distinct, :group, :having] @@ -18,6 +19,7 @@ module ActiveRecord include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation attr_reader :table, :klass, :loaded, :predicate_builder + attr_accessor :skip_preloading_value alias :model :klass alias :loaded? :loaded alias :locked? :lock_value @@ -29,6 +31,7 @@ module ActiveRecord @offsets = {} @loaded = false @predicate_builder = predicate_builder + @delegate_to_klass = false end def initialize_copy(other) @@ -312,6 +315,13 @@ module ActiveRecord klass.current_scope = previous end + def _exec_scope(*args, &block) # :nodoc: + @delegate_to_klass = true + instance_exec(*args, &block) || self + ensure + @delegate_to_klass = false + end + # Updates all records in the current relation with details given. This method constructs a single SQL UPDATE # statement and sends it straight to the database. It does not instantiate the involved models and it does not # trigger Active Record callbacks or validations. However, values passed to #update_all will still go through @@ -445,6 +455,7 @@ module ActiveRecord end def reset + @delegate_to_klass = false @to_sql = @arel = @loaded = @should_eager_load = nil @records = [].freeze @offsets = {} @@ -546,6 +557,16 @@ module ActiveRecord ActiveRecord::Associations::AliasTracker.create(connection, table.name, joins) end + def preload_associations(records) + preload = preload_values + preload += includes_values unless eager_loading? + preloader = nil + preload.each do |associations| + preloader ||= build_preloader + preloader.preload records, associations + end + end + protected def load_records(records) @@ -575,13 +596,7 @@ module ActiveRecord klass.find_by_sql(arel, &block).freeze end - preload = preload_values - preload += includes_values unless eager_loading? - preloader = nil - preload.each do |associations| - preloader ||= build_preloader - preloader.preload @records, associations - end + preload_associations(@records) unless skip_preloading_value @records.each(&:readonly!) if readonly_value diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 561869017a..ec4bb06c57 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -251,25 +251,31 @@ module ActiveRecord end end - attr = Relation::QueryAttribute.new(primary_key, primary_key_offset, klass.type_for_attribute(primary_key)) - batch_relation = relation.where(arel_attribute(primary_key).gt(Arel::Nodes::BindParam.new(attr))) + bind = primary_key_bind(primary_key_offset) + batch_relation = relation.where(arel_attribute(primary_key).gt(bind)) end end private def apply_limits(relation, start, finish) - if start - attr = Relation::QueryAttribute.new(primary_key, start, klass.type_for_attribute(primary_key)) - relation = relation.where(arel_attribute(primary_key).gteq(Arel::Nodes::BindParam.new(attr))) - end - if finish - attr = Relation::QueryAttribute.new(primary_key, finish, klass.type_for_attribute(primary_key)) - relation = relation.where(arel_attribute(primary_key).lteq(Arel::Nodes::BindParam.new(attr))) - end + relation = apply_start_limit(relation, start) if start + relation = apply_finish_limit(relation, finish) if finish relation end + def apply_start_limit(relation, start) + relation.where(arel_attribute(primary_key).gteq(primary_key_bind(start))) + end + + def apply_finish_limit(relation, finish) + relation.where(arel_attribute(primary_key).lteq(primary_key_bind(finish))) + end + + def primary_key_bind(value) + predicate_builder.build_bind_attribute(primary_key, value) + end + def batch_order arel_attribute(primary_key).asc end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 4b16b49cdf..488f71cdde 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -82,6 +82,11 @@ module ActiveRecord if @klass.respond_to?(method) self.class.delegate_to_scoped_klass(method) scoping { @klass.public_send(method, *args, &block) } + elsif @delegate_to_klass && @klass.respond_to?(method, true) + ActiveSupport::Deprecation.warn \ + "Delegating missing #{method} method to #{@klass}. " \ + "Accessibility of private/protected class methods in :scope is deprecated and will be removed in Rails 6.0." + @klass.send(method, *args, &block) elsif arel.respond_to?(method) ActiveSupport::Deprecation.warn \ "Delegating #{method} to arel is deprecated and will be removed in Rails 6.0." diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 3afa368575..4e60863e52 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -899,6 +899,11 @@ module ActiveRecord self end + def skip_preloading! # :nodoc: + self.skip_preloading_value = true + self + end + # Returns the Arel object associated with the relation. def arel(aliases = nil) # :nodoc: @arel ||= build_arel(aliases) diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 562e04194c..b092399657 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -10,6 +10,7 @@ module ActiveRecord def spawn #:nodoc: clone end + alias :all :spawn # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an ActiveRecord::Relation. # Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array. diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index b8d848b999..9974c28445 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -17,6 +17,12 @@ module ActiveRecord # Only strings are accepted if ActiveRecord::Base.schema_format == :sql. cattr_accessor :ignore_tables, default: [] + ## + # :singleton-method: + # Specify a custom regular expression matching foreign keys which name + # should not be dumped to db/schema.rb. + cattr_accessor :fk_ignore_pattern, default: /^fk_rails_[0-9a-f]{10}$/ + class << self def dump(connection = ActiveRecord::Base.connection, stream = STDOUT, config = ActiveRecord::Base) connection.create_schema_dumper(generate_options(config)).dump(stream) @@ -210,7 +216,7 @@ HEADER parts << "primary_key: #{foreign_key.primary_key.inspect}" end - if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/ + if foreign_key.export_name_on_schema_dump? parts << "name: #{foreign_key.name.inspect}" end diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 752655aa05..a784001587 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -183,7 +183,7 @@ module ActiveRecord if body.respond_to?(:to_proc) singleton_class.send(:define_method, name) do |*args| scope = all - scope = scope.instance_exec(*args, &body) || scope + scope = scope._exec_scope(*args, &body) scope = scope.extending(extension) if extension scope end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 6dbc977f9a..8d628359c3 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -17,8 +17,8 @@ module ActiveRecord # You can set custom coder to encode/decode your serialized attributes to/from different formats. # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+. # - # NOTE: If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for - # the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store]. + # NOTE: If you are using structured database data types (eg. PostgreSQL +hstore+/+json+, or MySQL 5.7+ + # +json+) there is no need for the serialization provided by {.store}[rdoc-ref:rdoc-ref:ClassMethods#store]. # Simply use {.store_accessor}[rdoc-ref:ClassMethods#store_accessor] instead to generate # the accessor methods. Be aware that these columns use a string keyed hash and do not allow access # using a symbol. @@ -31,10 +31,14 @@ module ActiveRecord # # class User < ActiveRecord::Base # store :settings, accessors: [ :color, :homepage ], coder: JSON + # store :parent, accessors: [ :name ], coder: JSON, prefix: true + # store :spouse, accessors: [ :name ], coder: JSON, prefix: :partner # end # - # u = User.new(color: 'black', homepage: '37signals.com') + # u = User.new(color: 'black', homepage: '37signals.com', parent_name: 'Mary', partner_name: 'Lily') # u.color # Accessor stored attribute + # u.parent_name # Accessor stored attribute with prefix + # u.partner_name # Accessor stored attribute with custom prefix # u.settings[:country] = 'Denmark' # Any attribute, even if not specified with an accessor # # # There is no difference between strings and symbols for accessing custom attributes @@ -44,6 +48,7 @@ module ActiveRecord # # Add additional accessors to an existing store through store_accessor # class SuperUser < User # store_accessor :settings, :privileges, :servants + # store_accessor :parent, :birthday, prefix: true # end # # The stored attribute names can be retrieved using {.stored_attributes}[rdoc-ref:rdoc-ref:ClassMethods#stored_attributes]. @@ -81,19 +86,29 @@ module ActiveRecord module ClassMethods def store(store_attribute, options = {}) serialize store_attribute, IndifferentCoder.new(store_attribute, options[:coder]) - store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors + store_accessor(store_attribute, options[:accessors], prefix: options[:prefix]) if options.has_key? :accessors end - def store_accessor(store_attribute, *keys) + def store_accessor(store_attribute, *keys, prefix: nil) keys = keys.flatten + accessor_prefix = + case prefix + when String, Symbol + "#{prefix}_" + when TrueClass + "#{store_attribute}_" + else + "" + end + _store_accessors_module.module_eval do keys.each do |key| - define_method("#{key}=") do |value| + define_method("#{accessor_prefix}#{key}=") do |value| write_store_attribute(store_attribute, key, value) end - define_method(key) do + define_method("#{accessor_prefix}#{key}") do read_store_attribute(store_attribute, key) end end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index af1bbc7e93..521375954b 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -134,6 +134,18 @@ module ActiveRecord end end + def for_each + databases = Rails.application.config.load_database_yaml + database_configs = ActiveRecord::DatabaseConfigurations.configs_for(Rails.env, databases) + + # if this is a single database application we don't want tasks for each primary database + return if database_configs.count == 1 + + database_configs.each do |db_config| + yield db_config.spec_name + end + end + def create_current(environment = env) each_current_configuration(environment) { |configuration| create configuration @@ -233,8 +245,8 @@ module ActiveRecord class_for_adapter(configuration["adapter"]).new(*arguments).structure_load(filename, structure_load_flags) end - def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil, environment = env) # :nodoc: - file ||= schema_file(format) + def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil, environment = env, spec_name = "primary") # :nodoc: + file ||= dump_filename(spec_name, format) check_schema_file(file) ActiveRecord::Base.establish_connection(configuration) @@ -252,17 +264,31 @@ module ActiveRecord end def schema_file(format = ActiveRecord::Base.schema_format) + File.join(db_dir, schema_file_type(format)) + end + + def schema_file_type(format = ActiveRecord::Base.schema_format) case format when :ruby - File.join(db_dir, "schema.rb") + "schema.rb" when :sql - File.join(db_dir, "structure.sql") + "structure.sql" end end + def dump_filename(namespace, format = ActiveRecord::Base.schema_format) + filename = if namespace == "primary" + schema_file_type(format) + else + "#{namespace}_#{schema_file_type(format)}" + end + + ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, filename) + end + def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env) - each_current_configuration(environment) { |configuration, configuration_environment| - load_schema configuration, format, file, configuration_environment + each_current_configuration(environment) { |configuration, spec_name, env| + load_schema(configuration, format, file, env, spec_name) } ActiveRecord::Base.establish_connection(environment.to_sym) end @@ -312,10 +338,10 @@ module ActiveRecord environments = [environment] environments << "test" if environment == "development" - ActiveRecord::Base.configurations.slice(*environments).each do |configuration_environment, configuration| - next unless configuration["database"] - - yield configuration, configuration_environment + environments.each do |env| + ActiveRecord::DatabaseConfigurations.configs_for(env) do |spec_name, configuration| + yield configuration, spec_name, env + end end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 5da3759e5a..54aa7aca2c 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -52,7 +52,7 @@ module ActiveRecord clear_timestamp_attributes end - class_methods do + module ClassMethods # :nodoc: private def timestamp_attributes_for_create_in_model timestamp_attributes_for_create.select { |c| column_names.include?(c) } diff --git a/activerecord/lib/active_record/translation.rb b/activerecord/lib/active_record/translation.rb index 3cf70eafb8..82661a328a 100644 --- a/activerecord/lib/active_record/translation.rb +++ b/activerecord/lib/active_record/translation.rb @@ -10,7 +10,7 @@ module ActiveRecord classes = [klass] return classes if klass == ActiveRecord::Base - while klass != klass.base_class + while !klass.base_class? classes << klass = klass.superclass end classes diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 4c41a68407..02c7e47583 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -84,7 +84,7 @@ module ActiveRecord indexes = @connection.indexes("accounts") assert_equal "accounts", indexes.first.table assert_equal idx_name, indexes.first.name - assert !indexes.first.unique + assert_not indexes.first.unique assert_equal ["firm_id"], indexes.first.columns ensure @connection.remove_index(:accounts, name: idx_name) rescue nil diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 9ae2c42368..976c5dde58 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -148,8 +148,8 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase t.timestamps null: true end ActiveRecord::Base.connection.remove_timestamps :delete_me, null: true - assert !column_present?("delete_me", "updated_at", "datetime") - assert !column_present?("delete_me", "created_at", "datetime") + assert_not column_present?("delete_me", "updated_at", "datetime") + assert_not column_present?("delete_me", "created_at", "datetime") ensure ActiveRecord::Base.connection.drop_table :delete_me rescue nil end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index 6fdb353368..1c85ff5674 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -55,4 +55,11 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase assert_equal "'2000-01-01 12:30:00.999999'", @conn.quote(type.serialize(value)) end + + def test_quoted_time_normalizes_date_qualified_time + value = ::Time.utc(2018, 3, 11, 12, 30, 0, 999999) + type = ActiveRecord::Type::Time.new + + assert_equal "'2000-01-01 12:30:00.999999'", @conn.quote(type.serialize(value)) + end end diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 140d7cbcae..f05dcac7dd 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -116,8 +116,8 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase end end - assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null + assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null end def test_timestamps_without_null_set_null_to_false_on_change_table @@ -129,8 +129,8 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase end end - assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null + assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null end def test_timestamps_without_null_set_null_to_false_on_add_timestamps @@ -139,7 +139,7 @@ class ActiveRecordSchemaTest < ActiveRecord::TestCase add_timestamps :has_timestamps, default: Time.now end - assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + assert_not @connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null + assert_not @connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null end end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 9e6d94191b..6b4f826766 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -931,6 +931,30 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal error.message, "The :dependent option must be one of [:destroy, :delete], but is :nullify" end + class DestroyableBook < ActiveRecord::Base + self.table_name = "books" + belongs_to :author, class_name: "UndestroyableAuthor", dependent: :destroy + end + + class UndestroyableAuthor < ActiveRecord::Base + self.table_name = "authors" + has_one :book, class_name: "DestroyableBook", foreign_key: "author_id" + before_destroy :dont + + def dont + throw(:abort) + end + end + + def test_dependency_should_halt_parent_destruction + author = UndestroyableAuthor.create!(name: "Test") + book = DestroyableBook.create!(author: author) + + assert_no_difference ["UndestroyableAuthor.count", "DestroyableBook.count"] do + assert_not book.destroy + end + end + def test_attributes_are_being_set_when_initialized_from_belongs_to_association_with_where_clause new_firm = accounts(:signals37).build_firm(name: "Apple") assert_equal new_firm.name, "Apple" diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index f18c6177ac..6e0cf30092 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1218,6 +1218,7 @@ class EagerAssociationTest < ActiveRecord::TestCase client = assert_queries(2) { Client.preload(:firm).find(c.id) } assert_no_queries { assert_nil client.firm } + assert_equal c.client_of, client.client_of end def test_preloading_empty_belongs_to_polymorphic @@ -1225,6 +1226,7 @@ class EagerAssociationTest < ActiveRecord::TestCase tagging = assert_queries(2) { Tagging.preload(:taggable).find(t.id) } assert_no_queries { assert_nil tagging.taggable } + assert_equal t.taggable_id, tagging.taggable_id end def test_preloading_through_empty_belongs_to diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 5f771fe85f..5d9735d98a 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -569,7 +569,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase developer = Developer.create name: "Bryan", salary: 50_000 assert_not_predicate project.developers, :loaded? - assert ! project.developers.include?(developer) + assert_not project.developers.include?(developer) end def test_find_with_merged_options diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 00821f2319..b00a3880f6 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -497,8 +497,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase person = Person.new person.first_name = "Naruto" person.references << Reference.new - person.id = 10 - person.references person.save! assert_equal 1, person.references.update_all(favourite: true) end @@ -507,8 +505,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase person = Person.new person.first_name = "Sasuke" person.references << Reference.new - person.id = 10 - person.references person.save! assert_predicate person.references, :exists? end @@ -2013,7 +2009,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_none_on_loaded_association_should_not_use_query firm = companies(:first_firm) firm.clients.load # force load - assert_no_queries { assert ! firm.clients.none? } + assert_no_queries { assert_not firm.clients.none? } end def test_calling_none_should_defer_to_collection_if_using_a_block @@ -2048,7 +2044,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_one_on_loaded_association_should_not_use_query firm = companies(:first_firm) firm.clients.load # force load - assert_no_queries { assert ! firm.clients.one? } + assert_no_queries { assert_not firm.clients.one? } end def test_calling_one_should_defer_to_collection_if_using_a_block diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 3b3d4037b9..0facc286da 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -866,7 +866,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase author = authors(:mary) category = author.named_categories.create(name: "Primary") author.named_categories.delete(category) - assert !Categorization.exists?(author_id: author.id, named_category_name: category.name) + assert_not Categorization.exists?(author_id: author.id, named_category_name: category.name) assert_empty author.named_categories.reload end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 1a213ef7e4..d7e898a1c0 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -452,7 +452,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal new_ship, pirate.ship assert_predicate new_ship, :new_record? assert_nil orig_ship.pirate_id - assert !orig_ship.changed? # check it was saved + assert_not orig_ship.changed? # check it was saved end def test_creation_failure_with_dependent_option @@ -678,7 +678,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase book = SpecialBook.create!(status: "published") author.book = book - refute_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count + assert_not_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count end def test_association_enum_works_properly_with_nested_join @@ -725,4 +725,28 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_not DestroyByParentBook.exists?(book.id) end + + class UndestroyableBook < ActiveRecord::Base + self.table_name = "books" + belongs_to :author, class_name: "DestroyableAuthor" + before_destroy :dont + + def dont + throw(:abort) + end + end + + class DestroyableAuthor < ActiveRecord::Base + self.table_name = "authors" + has_one :book, class_name: "UndestroyableBook", foreign_key: "author_id", dependent: :destroy + end + + def test_dependency_should_halt_parent_destruction + author = DestroyableAuthor.create!(name: "Test") + UndestroyableBook.create!(author: author) + + assert_no_difference ["DestroyableAuthor.count", "UndestroyableBook.count"] do + assert_not author.destroy + end + end end diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index f19a9f5f7a..9d1c73c33b 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -369,7 +369,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase Tag.has_many :null_taggings, -> { none }, class_name: :Tagging Tag.has_many :null_tagged_posts, through: :null_taggings, source: "taggable", source_type: "Post" assert_equal [], tags(:general).null_tagged_posts - refute_equal [], tags(:general).tagged_posts + assert_not_equal [], tags(:general).tagged_posts end def test_eager_has_many_polymorphic_with_source_type @@ -732,7 +732,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase category = Category.create!(name: "Not Associated") assert_not_predicate david.categories, :loaded? - assert ! david.categories.include?(category) + assert_not david.categories.include?(category) end def test_has_many_through_goes_through_all_sti_classes diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index 0170a6e98d..54512068ee 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -12,7 +12,7 @@ module ActiveRecord def setup @klass = Class.new(Class.new { def self.initialize_generated_modules; end }) do def self.superclass; Base; end - def self.base_class; self; end + def self.base_class?; true; end def self.decorate_matching_attribute_types(*); end include ActiveRecord::DefineCallbacks diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index dc6638d45d..1a37ad963f 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -63,8 +63,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase t.author_name = "" assert t.attribute_present?("title") assert t.attribute_present?("written_on") - assert !t.attribute_present?("content") - assert !t.attribute_present?("author_name") + assert_not t.attribute_present?("content") + assert_not t.attribute_present?("author_name") end test "attribute_present with booleans" do @@ -77,7 +77,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert b2.attribute_present?(:value) b3 = Boolean.new - assert !b3.attribute_present?(:value) + assert_not b3.attribute_present?(:value) b4 = Boolean.new b4.value = false @@ -827,7 +827,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase self.table_name = "computers" end - assert !klass.instance_method_already_implemented?(:system) + assert_not klass.instance_method_already_implemented?(:system) computer = klass.new assert_nil computer.system end @@ -841,8 +841,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase self.table_name = "computers" end - assert !klass.instance_method_already_implemented?(:system) - assert !subklass.instance_method_already_implemented?(:system) + assert_not klass.instance_method_already_implemented?(:system) + assert_not subklass.instance_method_already_implemented?(:system) computer = subklass.new assert_nil computer.system end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index b8243d148a..7915599f72 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -116,7 +116,7 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas assert_not_predicate firm.account, :valid? assert_not_predicate firm, :valid? - assert !firm.save + assert_not firm.save assert_equal ["is invalid"], firm.errors["account"] end @@ -237,7 +237,7 @@ class TestDefaultAutosaveAssociationOnABelongsToAssociation < ActiveRecord::Test log.developer = Developer.new assert_not_predicate log.developer, :valid? assert_not_predicate log, :valid? - assert !log.save + assert_not log.save assert_equal ["is invalid"], log.errors["developer"] end @@ -499,10 +499,10 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_invalid_adding firm = Firm.find(1) - assert !(firm.clients_of_firm << c = Client.new) + assert_not (firm.clients_of_firm << c = Client.new) assert_not_predicate c, :persisted? assert_not_predicate firm, :valid? - assert !firm.save + assert_not firm.save assert_not_predicate c, :persisted? end @@ -512,7 +512,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert_not_predicate c, :persisted? assert_not_predicate c, :valid? assert_not_predicate new_firm, :valid? - assert !new_firm.save + assert_not new_firm.save assert_not_predicate c, :persisted? assert_not_predicate new_firm, :persisted? end @@ -550,7 +550,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert_not_predicate new_client, :persisted? assert_not_predicate new_client, :valid? assert_equal new_client, companies(:first_firm).clients_of_firm.last - assert !companies(:first_firm).save + assert_not companies(:first_firm).save assert_not_predicate new_client, :persisted? assert_equal 2, companies(:first_firm).clients_of_firm.reload.size end @@ -795,7 +795,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @ship.pirate.catchphrase = "Changed Catchphrase" @ship.name_will_change! - assert_raise(RuntimeError) { assert !@pirate.save } + assert_raise(RuntimeError) { assert_not @pirate.save } assert_not_nil @pirate.reload.ship end @@ -855,7 +855,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @ship.pirate.catchphrase = "Changed Catchphrase" - assert_raise(RuntimeError) { assert !@ship.save } + assert_raise(RuntimeError) { assert_not @ship.save } assert_not_nil @ship.reload.pirate end @@ -871,7 +871,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase def test_should_destroy_has_many_as_part_of_the_save_transaction_if_they_were_marked_for_destruction 2.times { |i| @pirate.birds.create!(name: "birds_#{i}") } - assert !@pirate.birds.any?(&:marked_for_destruction?) + assert_not @pirate.birds.any?(&:marked_for_destruction?) @pirate.birds.each(&:mark_for_destruction) klass = @pirate.birds.first.class @@ -937,7 +937,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end end - assert_raise(RuntimeError) { assert !@pirate.save } + assert_raise(RuntimeError) { assert_not @pirate.save } assert_equal before, @pirate.reload.birds end @@ -1003,7 +1003,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase def test_should_destroy_habtm_as_part_of_the_save_transaction_if_they_were_marked_for_destruction 2.times { |i| @pirate.parrots.create!(name: "parrots_#{i}") } - assert !@pirate.parrots.any?(&:marked_for_destruction?) + assert_not @pirate.parrots.any?(&:marked_for_destruction?) @pirate.parrots.each(&:mark_for_destruction) assert_no_difference "Parrot.count" do @@ -1065,7 +1065,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end end - assert_raise(RuntimeError) { assert !@pirate.save } + assert_raise(RuntimeError) { assert_not @pirate.save } assert_equal before, @pirate.reload.parrots end @@ -1213,7 +1213,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase assert_no_difference "Pirate.count" do assert_no_difference "Ship.count" do - assert !pirate.save + assert_not pirate.save end end end @@ -1232,7 +1232,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase end end - assert_raise(RuntimeError) { assert !@pirate.save } + assert_raise(RuntimeError) { assert_not @pirate.save } assert_equal before, [@pirate.reload.catchphrase, @pirate.ship.name] end @@ -1337,7 +1337,7 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase assert_no_difference "Ship.count" do assert_no_difference "Pirate.count" do - assert !ship.save + assert_not ship.save end end end @@ -1356,7 +1356,7 @@ class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase end end - assert_raise(RuntimeError) { assert !@ship.save } + assert_raise(RuntimeError) { assert_not @ship.save } assert_equal before, [@ship.pirate.reload.catchphrase, @ship.reload.name] end @@ -1480,7 +1480,7 @@ module AutosaveAssociationOnACollectionAssociationTests @child_1.name = "Changed" @child_1.cancel_save_from_callback = true - assert !@pirate.save + assert_not @pirate.save assert_equal "Don' botharrr talkin' like one, savvy?", @pirate.reload.catchphrase assert_equal "Posideons Killer", @child_1.reload.name @@ -1490,7 +1490,7 @@ module AutosaveAssociationOnACollectionAssociationTests assert_no_difference "Pirate.count" do assert_no_difference "#{new_child.class.name}.count" do - assert !new_pirate.save + assert_not new_pirate.save end end end @@ -1510,7 +1510,7 @@ module AutosaveAssociationOnACollectionAssociationTests end end - assert_raise(RuntimeError) { assert !@pirate.save } + assert_raise(RuntimeError) { assert_not @pirate.save } assert_equal before, [@pirate.reload.catchphrase, *@pirate.send(@association_name).map(&:name)] end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 7dfb05a6a5..fd008ca8e3 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -307,7 +307,7 @@ class BasicsTest < ActiveRecord::TestCase assert_equal "Dude", cbs[0].name assert_equal "Bob", cbs[1].name assert cbs[0].frickinawesome - assert !cbs[1].frickinawesome + assert_not cbs[1].frickinawesome end def test_load @@ -856,11 +856,11 @@ class BasicsTest < ActiveRecord::TestCase def test_clone_of_new_object_marks_as_dirty_only_changed_attributes developer = Developer.new name: "Bjorn" assert developer.name_changed? # obviously - assert !developer.salary_changed? # attribute has non-nil default value, so treated as not changed + assert_not developer.salary_changed? # attribute has non-nil default value, so treated as not changed cloned_developer = developer.clone assert_predicate cloned_developer, :name_changed? - assert !cloned_developer.salary_changed? # ... and cloned instance should behave same + assert_not cloned_developer.salary_changed? # ... and cloned instance should behave same end def test_dup_of_saved_object_marks_attributes_as_dirty @@ -875,12 +875,12 @@ class BasicsTest < ActiveRecord::TestCase def test_dup_of_saved_object_marks_as_dirty_only_changed_attributes developer = Developer.create! name: "Bjorn" - assert !developer.name_changed? # both attributes of saved object should be treated as not changed + assert_not developer.name_changed? # both attributes of saved object should be treated as not changed assert_not_predicate developer, :salary_changed? cloned_developer = developer.dup assert cloned_developer.name_changed? # ... but on cloned object should be - assert !cloned_developer.salary_changed? # ... BUT salary has non-nil default which should be treated as not changed on cloned instance + assert_not cloned_developer.salary_changed? # ... BUT salary has non-nil default which should be treated as not changed on cloned instance end def test_bignum @@ -1497,7 +1497,7 @@ class BasicsTest < ActiveRecord::TestCase end test "column names are quoted when using #from clause and model has ignored columns" do - refute_empty Developer.ignored_columns + assert_not_empty Developer.ignored_columns query = Developer.from("developers").to_sql quoted_id = "#{Developer.quoted_table_name}.#{Developer.quoted_primary_key}" diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 74228b2796..080d2a54bc 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -811,6 +811,11 @@ class CalculationsTest < ActiveRecord::TestCase assert_nil Topic.where("1=0").pick(:author_name, :author_email_address) end + def test_pick_delegate_to_all + cool_first = minivans(:cool_first) + assert_equal cool_first.color, Minivan.pick(:color) + end + def test_grouped_calculation_with_polymorphic_relation part = ShipPart.create!(name: "has trinket") part.trinkets.create! diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 3b283a3aa6..b9ba51c730 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -385,9 +385,9 @@ class CallbacksTest < ActiveRecord::TestCase end def assert_save_callbacks_not_called(someone) - assert !someone.after_save_called - assert !someone.after_create_called - assert !someone.after_update_called + assert_not someone.after_save_called + assert_not someone.after_create_called + assert_not someone.after_update_called end private :assert_save_callbacks_not_called @@ -395,27 +395,27 @@ class CallbacksTest < ActiveRecord::TestCase someone = CallbackHaltedDeveloper.new someone.cancel_before_create = true assert_predicate someone, :valid? - assert !someone.save + assert_not someone.save assert_save_callbacks_not_called(someone) end def test_before_save_throwing_abort david = DeveloperWithCanceledCallbacks.find(1) assert_predicate david, :valid? - assert !david.save + assert_not david.save exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! } assert_equal david, exc.record david = DeveloperWithCanceledCallbacks.find(1) david.salary = 10_000_000 assert_not_predicate david, :valid? - assert !david.save + assert_not david.save assert_raise(ActiveRecord::RecordInvalid) { david.save! } someone = CallbackHaltedDeveloper.find(1) someone.cancel_before_save = true assert_predicate someone, :valid? - assert !someone.save + assert_not someone.save assert_save_callbacks_not_called(someone) end @@ -423,22 +423,22 @@ class CallbacksTest < ActiveRecord::TestCase someone = CallbackHaltedDeveloper.find(1) someone.cancel_before_update = true assert_predicate someone, :valid? - assert !someone.save + assert_not someone.save assert_save_callbacks_not_called(someone) end def test_before_destroy_throwing_abort david = DeveloperWithCanceledCallbacks.find(1) - assert !david.destroy + assert_not david.destroy exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } assert_equal david, exc.record assert_not_nil ImmutableDeveloper.find_by_id(1) someone = CallbackHaltedDeveloper.find(1) someone.cancel_before_destroy = true - assert !someone.destroy + assert_not someone.destroy assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! } - assert !someone.after_destroy_called + assert_not someone.after_destroy_called end def test_callback_throwing_abort @@ -467,12 +467,12 @@ class CallbacksTest < ActiveRecord::TestCase def test_inheritance_of_callbacks parent = ParentDeveloper.new - assert !parent.after_save_called + assert_not parent.after_save_called parent.save assert parent.after_save_called child = ChildDeveloper.new - assert !child.after_save_called + assert_not child.after_save_called child.save assert child.after_save_called end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index f4cc251fb9..57b3a5844e 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -71,6 +71,56 @@ module ActiveRecord ENV["RAILS_ENV"] = previous_env end + unless in_memory_db? + def test_establish_connection_using_3_level_config_defaults_to_default_env_primary_db + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + config = { + "default_env" => { + "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }, + "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" } + }, + "another_env" => { + "primary" => { "adapter" => "sqlite3", "database" => "db/another-primary.sqlite3" }, + "readonly" => { "adapter" => "sqlite3", "database" => "db/another-readonly.sqlite3" } + } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.establish_connection + + assert_equal "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + ENV["RAILS_ENV"] = previous_env + ActiveRecord::Base.establish_connection(:arunit) + FileUtils.rm_rf "db" + end + + def test_establish_connection_using_2_level_config_defaults_to_default_env_primary_db + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + config = { + "default_env" => { + "adapter" => "sqlite3", "database" => "db/primary.sqlite3" + }, + "another_env" => { + "adapter" => "sqlite3", "database" => "db/bad-primary.sqlite3" + } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + ActiveRecord::Base.establish_connection + + assert_equal "db/primary.sqlite3", ActiveRecord::Base.connection.pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + ENV["RAILS_ENV"] = previous_env + ActiveRecord::Base.establish_connection(:arunit) + FileUtils.rm_rf "db" + end + end + def test_establish_connection_using_two_level_configurations config = { "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } } @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config @@ -278,7 +328,7 @@ module ActiveRecord pool = klass2.establish_connection(ActiveRecord::Base.connection_pool.spec.config) assert_same klass2.connection, pool.connection - refute_same klass2.connection, ActiveRecord::Base.connection + assert_not_same klass2.connection, ActiveRecord::Base.connection klass2.remove_connection @@ -297,7 +347,7 @@ module ActiveRecord def test_remove_connection_should_not_remove_parent klass2 = Class.new(Base) { def self.name; "klass2"; end } klass2.remove_connection - refute_nil ActiveRecord::Base.connection + assert_not_nil ActiveRecord::Base.connection assert_same klass2.connection, ActiveRecord::Base.connection end end diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb index 51f6164138..e64a8372d0 100644 --- a/activerecord/test/cases/date_time_precision_test.rb +++ b/activerecord/test/cases/date_time_precision_test.rb @@ -27,6 +27,24 @@ if subsecond_precision_supported? assert_equal 5, Foo.columns_hash["updated_at"].precision end + def test_datetime_precision_is_truncated_on_assignment + @connection.create_table(:foos, force: true) + @connection.add_column :foos, :created_at, :datetime, precision: 0 + @connection.add_column :foos, :updated_at, :datetime, precision: 6 + + time = ::Time.now.change(nsec: 123456789) + foo = Foo.new(created_at: time, updated_at: time) + + assert_equal 0, foo.created_at.nsec + assert_equal 123456000, foo.updated_at.nsec + + foo.save! + foo.reload + + assert_equal 0, foo.created_at.nsec + assert_equal 123456000, foo.updated_at.nsec + end + def test_timestamps_helper_with_custom_precision @connection.create_table(:foos, force: true) do |t| t.timestamps precision: 4 diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 2f1434b2bc..83cc2aa319 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -366,7 +366,7 @@ class DirtyTest < ActiveRecord::TestCase def test_changed_attributes_should_be_preserved_if_save_failure pirate = Pirate.new pirate.parrot_id = 1 - assert !pirate.save + assert_not pirate.save check_pirate_after_save_failure(pirate) pirate = Pirate.new @@ -473,6 +473,14 @@ class DirtyTest < ActiveRecord::TestCase end end + def test_changes_to_save_should_not_mutate_array_of_hashes + topic = Topic.new(author_name: "Bill", content: [{ a: "a" }]) + + topic.changes_to_save + + assert_equal [{ a: "a" }], topic.content + end + def test_previous_changes # original values should be in previous_changes pirate = Pirate.new @@ -488,7 +496,7 @@ class DirtyTest < ActiveRecord::TestCase assert_not_nil pirate.previous_changes["updated_on"][1] assert_nil pirate.previous_changes["created_on"][0] assert_not_nil pirate.previous_changes["created_on"][1] - assert !pirate.previous_changes.key?("parrot_id") + assert_not pirate.previous_changes.key?("parrot_id") # original values should be in previous_changes pirate = Pirate.new @@ -502,7 +510,7 @@ class DirtyTest < ActiveRecord::TestCase assert_equal [nil, pirate.id], pirate.previous_changes["id"] assert_includes pirate.previous_changes, "updated_on" assert_includes pirate.previous_changes, "created_on" - assert !pirate.previous_changes.key?("parrot_id") + assert_not pirate.previous_changes.key?("parrot_id") pirate.catchphrase = "Yar!!" pirate.reload @@ -519,8 +527,8 @@ class DirtyTest < ActiveRecord::TestCase assert_equal ["arrr", "Me Maties!"], pirate.previous_changes["catchphrase"] assert_not_nil pirate.previous_changes["updated_on"][0] assert_not_nil pirate.previous_changes["updated_on"][1] - assert !pirate.previous_changes.key?("parrot_id") - assert !pirate.previous_changes.key?("created_on") + assert_not pirate.previous_changes.key?("parrot_id") + assert_not pirate.previous_changes.key?("created_on") pirate = Pirate.find_by_catchphrase("Me Maties!") @@ -533,8 +541,8 @@ class DirtyTest < ActiveRecord::TestCase assert_equal ["Me Maties!", "Thar She Blows!"], pirate.previous_changes["catchphrase"] assert_not_nil pirate.previous_changes["updated_on"][0] assert_not_nil pirate.previous_changes["updated_on"][1] - assert !pirate.previous_changes.key?("parrot_id") - assert !pirate.previous_changes.key?("created_on") + assert_not pirate.previous_changes.key?("parrot_id") + assert_not pirate.previous_changes.key?("created_on") travel(1.second) @@ -545,8 +553,8 @@ class DirtyTest < ActiveRecord::TestCase assert_equal ["Thar She Blows!", "Ahoy!"], pirate.previous_changes["catchphrase"] assert_not_nil pirate.previous_changes["updated_on"][0] assert_not_nil pirate.previous_changes["updated_on"][1] - assert !pirate.previous_changes.key?("parrot_id") - assert !pirate.previous_changes.key?("created_on") + assert_not pirate.previous_changes.key?("parrot_id") + assert_not pirate.previous_changes.key?("created_on") travel(1.second) @@ -557,8 +565,8 @@ class DirtyTest < ActiveRecord::TestCase assert_equal ["Ahoy!", "Ninjas suck!"], pirate.previous_changes["catchphrase"] assert_not_nil pirate.previous_changes["updated_on"][0] assert_not_nil pirate.previous_changes["updated_on"][1] - assert !pirate.previous_changes.key?("parrot_id") - assert !pirate.previous_changes.key?("created_on") + assert_not pirate.previous_changes.key?("parrot_id") + assert_not pirate.previous_changes.key?("created_on") ensure travel_back end diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb index 9e33c3110c..387a0b1fdd 100644 --- a/activerecord/test/cases/dup_test.rb +++ b/activerecord/test/cases/dup_test.rb @@ -171,7 +171,7 @@ module ActiveRecord end end - assert !movie.persisted? + assert_not movie.persisted? end end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index d85910d9c6..04150f4d57 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -727,8 +727,8 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ActiveModel::MissingAttributeError) { topic.title? } assert_nil topic.read_attribute("title") assert_equal "David", topic.author_name - assert !topic.attribute_present?("title") - assert !topic.attribute_present?(:title) + assert_not topic.attribute_present?("title") + assert_not topic.attribute_present?(:title) assert topic.attribute_present?("author_name") assert_respond_to topic, "author_name" end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 184b750161..a4fa3c285b 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -592,10 +592,10 @@ class FixturesWithoutInstantiationTest < ActiveRecord::TestCase fixtures :topics, :developers, :accounts def test_without_complete_instantiation - assert !defined?(@first) - assert !defined?(@topics) - assert !defined?(@developers) - assert !defined?(@accounts) + assert_not defined?(@first) + assert_not defined?(@topics) + assert_not defined?(@developers) + assert_not defined?(@accounts) end def test_fixtures_from_root_yml_without_instantiation @@ -1082,13 +1082,13 @@ class FoxyFixturesTest < ActiveRecord::TestCase def test_supports_inline_habtm assert(parrots(:george).treasures.include?(treasures(:diamond))) assert(parrots(:george).treasures.include?(treasures(:sapphire))) - assert(!parrots(:george).treasures.include?(treasures(:ruby))) + assert_not(parrots(:george).treasures.include?(treasures(:ruby))) end def test_supports_inline_habtm_with_specified_id assert(parrots(:polly).treasures.include?(treasures(:ruby))) assert(parrots(:polly).treasures.include?(treasures(:sapphire))) - assert(!parrots(:polly).treasures.include?(treasures(:diamond))) + assert_not(parrots(:polly).treasures.include?(treasures(:diamond))) end def test_supports_yaml_arrays diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 6ea02ac191..66f11fe5bd 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -184,4 +184,4 @@ module InTimeZone end end -require "mocha/setup" # FIXME: stop using mocha +require "mocha/minitest" # FIXME: stop using mocha diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index e3ca79af99..7a5c06b894 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -174,17 +174,26 @@ class InheritanceTest < ActiveRecord::TestCase def test_inheritance_base_class assert_equal Post, Post.base_class + assert_predicate Post, :base_class? assert_equal Post, SpecialPost.base_class + assert_not_predicate SpecialPost, :base_class? assert_equal Post, StiPost.base_class + assert_not_predicate StiPost, :base_class? assert_equal Post, SubStiPost.base_class + assert_not_predicate SubStiPost, :base_class? assert_equal SubAbstractStiPost, SubAbstractStiPost.base_class + assert_predicate SubAbstractStiPost, :base_class? end def test_abstract_inheritance_base_class assert_equal LoosePerson, LoosePerson.base_class + assert_predicate LoosePerson, :base_class? assert_equal LooseDescendant, LooseDescendant.base_class + assert_predicate LooseDescendant, :base_class? assert_equal TightPerson, TightPerson.base_class + assert_predicate TightPerson, :base_class? assert_equal TightPerson, TightDescendant.base_class + assert_not_predicate TightDescendant, :base_class? end def test_base_class_activerecord_error diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index ebe0b0aa87..9c85543b9b 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -215,7 +215,7 @@ module ActiveRecord migration = InvertibleMigration.new migration.migrate :up migration.migrate :down - assert !migration.connection.table_exists?("horses") + assert_not migration.connection.table_exists?("horses") end def test_migrate_revert @@ -223,11 +223,11 @@ module ActiveRecord revert = InvertibleRevertMigration.new migration.migrate :up revert.migrate :up - assert !migration.connection.table_exists?("horses") + assert_not migration.connection.table_exists?("horses") revert.migrate :down assert migration.connection.table_exists?("horses") migration.migrate :down - assert !migration.connection.table_exists?("horses") + assert_not migration.connection.table_exists?("horses") end def test_migrate_revert_by_part @@ -241,12 +241,12 @@ module ActiveRecord } migration.migrate :up assert_equal [:both, :up], received - assert !migration.connection.table_exists?("horses") + assert_not migration.connection.table_exists?("horses") assert migration.connection.table_exists?("new_horses") migration.migrate :down assert_equal [:both, :up, :both, :down], received assert migration.connection.table_exists?("horses") - assert !migration.connection.table_exists?("new_horses") + assert_not migration.connection.table_exists?("new_horses") end def test_migrate_revert_whole_migration @@ -255,11 +255,11 @@ module ActiveRecord revert = RevertWholeMigration.new(klass) migration.migrate :up revert.migrate :up - assert !migration.connection.table_exists?("horses") + assert_not migration.connection.table_exists?("horses") revert.migrate :down assert migration.connection.table_exists?("horses") migration.migrate :down - assert !migration.connection.table_exists?("horses") + assert_not migration.connection.table_exists?("horses") end end @@ -268,7 +268,7 @@ module ActiveRecord revert.migrate :down assert revert.connection.table_exists?("horses") revert.migrate :up - assert !revert.connection.table_exists?("horses") + assert_not revert.connection.table_exists?("horses") end def test_migrate_revert_change_column_default @@ -402,7 +402,7 @@ module ActiveRecord UpOnlyMigration.new.migrate(:down) # should be no error connection = ActiveRecord::Base.connection - assert !connection.column_exists?(:horses, :oldie) + assert_not connection.column_exists?(:horses, :oldie) Horse.reset_column_information end end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 9d04750712..8513edb0ab 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -15,6 +15,7 @@ require "models/bulb" require "models/engine" require "models/wheel" require "models/treasure" +require "models/frog" class LockWithoutDefault < ActiveRecord::Base; end @@ -653,6 +654,16 @@ unless in_memory_db? end end + def test_locking_in_after_save_callback + assert_nothing_raised do + frog = ::Frog.create(name: "Old Frog") + frog.name = "New Frog" + assert_not_deprecated do + frog.save! + end + end + end + def test_with_lock_commits_transaction person = Person.find 1 person.with_lock do diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 1494027182..f4d16cb093 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -205,8 +205,8 @@ module ActiveRecord created_at_column = created_columns.detect { |c| c.name == "created_at" } updated_at_column = created_columns.detect { |c| c.name == "updated_at" } - assert !created_at_column.null - assert !updated_at_column.null + assert_not created_at_column.null + assert_not updated_at_column.null end def test_create_table_with_timestamps_should_create_datetime_columns_with_options @@ -408,7 +408,7 @@ module ActiveRecord end connection.change_table :testings do |t| assert t.column_exists?(:foo) - assert !(t.column_exists?(:bar)) + assert_not (t.column_exists?(:bar)) end end diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb index 83fb4f9385..a1e5fb1115 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -95,42 +95,42 @@ module ActiveRecord connection.create_join_table :artists, :musics connection.drop_join_table :artists, :musics - assert !connection.table_exists?("artists_musics") + assert_not connection.table_exists?("artists_musics") end def test_drop_join_table_with_strings connection.create_join_table :artists, :musics connection.drop_join_table "artists", "musics" - assert !connection.table_exists?("artists_musics") + assert_not connection.table_exists?("artists_musics") end def test_drop_join_table_with_the_proper_order connection.create_join_table :videos, :musics connection.drop_join_table :videos, :musics - assert !connection.table_exists?("musics_videos") + assert_not connection.table_exists?("musics_videos") end def test_drop_join_table_with_the_table_name connection.create_join_table :artists, :musics, table_name: :catalog connection.drop_join_table :artists, :musics, table_name: :catalog - assert !connection.table_exists?("catalog") + assert_not connection.table_exists?("catalog") end def test_drop_join_table_with_the_table_name_as_string connection.create_join_table :artists, :musics, table_name: "catalog" connection.drop_join_table :artists, :musics, table_name: "catalog" - assert !connection.table_exists?("catalog") + assert_not connection.table_exists?("catalog") end def test_drop_join_table_with_column_options connection.create_join_table :artists, :musics, column_options: { null: true } connection.drop_join_table :artists, :musics, column_options: { null: true } - assert !connection.table_exists?("artists_musics") + assert_not connection.table_exists?("artists_musics") end def test_create_and_drop_join_table_with_common_prefix diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index de37215e80..50f5696ad1 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -306,6 +306,17 @@ if ActiveRecord::Base.connection.supports_foreign_keys? assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output end + def test_schema_dumping_with_custom_fk_ignore_pattern + original_pattern = ActiveRecord::SchemaDumper.fk_ignore_pattern + ActiveRecord::SchemaDumper.fk_ignore_pattern = /^ignored_/ + @connection.add_foreign_key :astronauts, :rockets, name: :ignored_fk_astronauts_rockets + + output = dump_table_schema "astronauts" + assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output + + ActiveRecord::SchemaDumper.fk_ignore_pattern = original_pattern + end + def test_schema_dumping_on_delete_and_on_update_options @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify, on_update: :cascade diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index b25c6d84bc..f9b2dc0c73 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -99,7 +99,7 @@ module ActiveRecord connection.add_index :testings, :foo assert connection.index_exists?(:testings, :foo) - assert !connection.index_exists?(:testings, :bar) + assert_not connection.index_exists?(:testings, :bar) end def test_index_exists_on_multiple_columns @@ -131,7 +131,7 @@ module ActiveRecord assert connection.index_exists?(:testings, :foo) assert connection.index_exists?(:testings, :foo, name: "custom_index_name") - assert !connection.index_exists?(:testings, :foo, name: "other_index_name") + assert_not connection.index_exists?(:testings, :foo, name: "other_index_name") end def test_remove_named_index @@ -139,7 +139,7 @@ module ActiveRecord assert connection.index_exists?(:testings, :foo) connection.remove_index :testings, :foo - assert !connection.index_exists?(:testings, :foo) + assert_not connection.index_exists?(:testings, :foo) end def test_add_index_attribute_length_limit @@ -203,7 +203,7 @@ module ActiveRecord assert connection.index_exists?("testings", "last_name") connection.remove_index("testings", "last_name") - assert !connection.index_exists?("testings", "last_name") + assert_not connection.index_exists?("testings", "last_name") end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index f77a275544..1fa9a3c34a 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -404,7 +404,7 @@ class MigrationTest < ActiveRecord::TestCase ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo" new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - refute_equal current_env, new_env + assert_not_equal current_env, new_env sleep 1 # mysql by default does not store fractional seconds in the database migrator.up @@ -548,7 +548,7 @@ class MigrationTest < ActiveRecord::TestCase end assert Person.connection.column_exists?(:something, :foo) assert_nothing_raised { Person.connection.remove_column :something, :foo, :bar } - assert !Person.connection.column_exists?(:something, :foo) + assert_not Person.connection.column_exists?(:something, :foo) assert Person.connection.column_exists?(:something, :name) assert Person.connection.column_exists?(:something, :number) ensure @@ -822,7 +822,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? end end - [:qualification, :experience].each { |c| assert ! column(c) } + [:qualification, :experience].each { |c| assert_not column(c) } assert column(:qualification_experience) end @@ -852,7 +852,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? name_age_index = index(:index_delete_me_on_name_and_age) assert_equal ["name", "age"].sort, name_age_index.columns.sort - assert ! name_age_index.unique + assert_not name_age_index.unique assert index(:awesome_username_index).unique end @@ -880,7 +880,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? end end - assert ! index(:index_delete_me_on_name) + assert_not index(:index_delete_me_on_name) new_name_index = index(:new_name_index) assert new_name_index.unique @@ -892,7 +892,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? t.date :birthdate end - assert ! column(:name).default + assert_not column(:name).default assert_equal :date, column(:birthdate).type classname = ActiveRecord::Base.connection.class.name[/[^:]*$/] diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb index a24b173cf5..6f3903eed4 100644 --- a/activerecord/test/cases/multiparameter_attributes_test.rb +++ b/activerecord/test/cases/multiparameter_attributes_test.rb @@ -394,6 +394,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase "written_on(4i)" => "13", "written_on(5i)" => "55", ) - refute_predicate topic, :written_on_came_from_user? + assert_not_predicate topic, :written_on_came_from_user? end end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 1ed3a61bbb..32af90caef 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -83,7 +83,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase def test_a_model_should_respond_to_underscore_destroy_and_return_if_it_is_marked_for_destruction ship = Ship.create!(name: "Nights Dirty Lightning") - assert !ship._destroy + assert_not ship._destroy ship.mark_for_destruction assert ship._destroy end @@ -835,7 +835,7 @@ module NestedAttributesOnACollectionAssociationTests man = Man.create(name: "John") interest = man.interests.create(topic: "bar", zine_id: 0) assert interest.save - assert !man.update(interests_attributes: { id: interest.id, zine_id: "foo" }) + assert_not man.update(interests_attributes: { id: interest.id, zine_id: "foo" }) end end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index e4a65a48ca..e0c5725944 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -48,7 +48,7 @@ class PersistenceTest < ActiveRecord::TestCase end if test_update_with_order_succeeds.call("id DESC") - assert !test_update_with_order_succeeds.call("id ASC") # test that this wasn't a fluke and using an incorrect order results in an exception + assert_not test_update_with_order_succeeds.call("id ASC") # test that this wasn't a fluke and using an incorrect order results in an exception else # test that we're failing because the current Arel's engine doesn't support UPDATE ORDER BY queries is using subselects instead assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\z/i) do diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index d635a47c0e..393f363e37 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -441,8 +441,9 @@ class QueryCacheTest < ActiveRecord::TestCase assert_not ActiveRecord::Base.connection_handler.active_connections? # sanity check middleware { - assert ActiveRecord::Base.connection.query_cache_enabled, "QueryCache did not get lazily enabled" + assert_predicate ActiveRecord::Base.connection, :query_cache_enabled }.call({}) + assert_not_predicate ActiveRecord::Base.connection, :query_cache_enabled end end @@ -517,19 +518,19 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase def test_find assert_called(Task.connection, :clear_query_cache) do - assert !Task.connection.query_cache_enabled + assert_not Task.connection.query_cache_enabled Task.cache do assert Task.connection.query_cache_enabled Task.find(1) Task.uncached do - assert !Task.connection.query_cache_enabled + assert_not Task.connection.query_cache_enabled Task.find(1) end assert Task.connection.query_cache_enabled end - assert !Task.connection.query_cache_enabled + assert_not Task.connection.query_cache_enabled end end diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 6534770c57..92eb0c814f 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -46,27 +46,60 @@ module ActiveRecord assert_equal t.to_s(:db), @quoter.quoted_date(t) end - def test_quoted_time_utc + def test_quoted_timestamp_utc with_timezone_config default: :utc do t = Time.now.change(usec: 0) assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t) end end - def test_quoted_time_local + def test_quoted_timestamp_local with_timezone_config default: :local do t = Time.now.change(usec: 0) assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t) end end - def test_quoted_time_crazy + def test_quoted_timestamp_crazy with_timezone_config default: :asdfasdf do t = Time.now.change(usec: 0) assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t) end end + def test_quoted_time_utc + with_timezone_config default: :utc do + t = Time.now.change(usec: 0) + + expected = t.getutc.change(year: 2000, month: 1, day: 1) + expected = expected.to_s(:db).sub("2000-01-01 ", "") + + assert_equal expected, @quoter.quoted_time(t) + end + end + + def test_quoted_time_local + with_timezone_config default: :local do + t = Time.now.change(usec: 0) + + expected = t.change(year: 2000, month: 1, day: 1) + expected = expected.getlocal.to_s(:db).sub("2000-01-01 ", "") + + assert_equal expected, @quoter.quoted_time(t) + end + end + + def test_quoted_time_crazy + with_timezone_config default: :asdfasdf do + t = Time.now.change(usec: 0) + + expected = t.change(year: 2000, month: 1, day: 1) + expected = expected.getlocal.to_s(:db).sub("2000-01-01 ", "") + + assert_equal expected, @quoter.quoted_time(t) + end + end + def test_quoted_datetime_utc with_timezone_config default: :utc do t = Time.now.change(usec: 0).to_datetime diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index 383e43ed55..059fa76132 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -23,7 +23,7 @@ class ReadOnlyTest < ActiveRecord::TestCase assert_nothing_raised do dev.name = "Luscious forbidden fruit." - assert !dev.save + assert_not dev.save dev.name = "Forbidden." end @@ -38,8 +38,8 @@ class ReadOnlyTest < ActiveRecord::TestCase end def test_find_with_readonly_option - Developer.all.each { |d| assert !d.readonly? } - Developer.readonly(false).each { |d| assert !d.readonly? } + Developer.all.each { |d| assert_not d.readonly? } + Developer.readonly(false).each { |d| assert_not d.readonly? } Developer.readonly(true).each { |d| assert d.readonly? } Developer.readonly.each { |d| assert d.readonly? } end @@ -55,14 +55,14 @@ class ReadOnlyTest < ActiveRecord::TestCase def test_has_many_find_readonly post = Post.find(1) assert_not_empty post.comments - assert !post.comments.any?(&:readonly?) - assert !post.comments.to_a.any?(&:readonly?) + assert_not post.comments.any?(&:readonly?) + assert_not post.comments.to_a.any?(&:readonly?) assert post.comments.readonly(true).all?(&:readonly?) end def test_has_many_with_through_is_not_implicitly_marked_readonly assert people = Post.find(1).people - assert !people.any?(&:readonly?) + assert_not people.any?(&:readonly?) end def test_has_many_with_through_is_not_implicitly_marked_readonly_while_finding_by_id diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb index 61cb0f130d..b034fe3e3b 100644 --- a/activerecord/test/cases/reaper_test.rb +++ b/activerecord/test/cases/reaper_test.rb @@ -36,15 +36,15 @@ module ActiveRecord # A reaper with nil time should never reap connections def test_nil_time fp = FakePool.new - assert !fp.reaped + assert_not fp.reaped reaper = ConnectionPool::Reaper.new(fp, nil) reaper.run - assert !fp.reaped + assert_not fp.reaped end def test_some_time fp = FakePool.new - assert !fp.reaped + assert_not fp.reaped reaper = ConnectionPool::Reaper.new(fp, 0.0001) reaper.run diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index ed19192ad9..abadafbad4 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -75,7 +75,7 @@ class ReflectionTest < ActiveRecord::TestCase def test_column_null_not_null subscriber = Subscriber.first assert subscriber.column_for_attribute("name").null - assert !subscriber.column_for_attribute("nick").null + assert_not subscriber.column_for_attribute("nick").null end def test_human_name_for_column diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 2696d1bb00..3f3d41980c 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -54,4 +54,32 @@ module ActiveRecord Comment.all end end + + class QueryingMethodsDelegationTest < ActiveRecord::TestCase + QUERYING_METHODS = [ + :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :none?, :one?, + :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, + :first_or_create, :first_or_create!, :first_or_initialize, + :find_or_create_by, :find_or_create_by!, :create_or_find_by, :create_or_find_by!, :find_or_initialize_by, + :find_by, :find_by!, + :destroy_all, :delete_all, :update_all, + :find_each, :find_in_batches, :in_batches, + :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or, + :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, + :having, :create_with, :distinct, :references, :none, :unscope, :merge, + :count, :average, :minimum, :maximum, :sum, :calculate, + :pluck, :pick, :ids, + ] + + def test_delegate_querying_methods + klass = Class.new(ActiveRecord::Base) do + self.table_name = "posts" + end + + QUERYING_METHODS.each do |method| + assert_respond_to klass.all, method + assert_respond_to klass, method + end + end + end end diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index 1428b3e132..f82ecd4449 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -137,6 +137,11 @@ module ActiveRecord assert relation.skip_query_cache_value end + test "skip_preloading!" do + relation.skip_preloading! + assert relation.skip_preloading_value + end + private def relation @relation ||= Relation.new(FakeKlass) diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 4e75371147..0f446e06aa 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -315,6 +315,14 @@ module ActiveRecord assert_equal "type cast from database", UpdateAllTestModel.first.body end + def test_skip_preloading_after_arel_has_been_generated + assert_nothing_raised do + relation = Comment.all + relation.arel + relation.skip_preloading! + end + end + private def skip_if_sqlite3_version_includes_quoting_bug diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index cf65789b97..63edd82f7f 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -511,7 +511,7 @@ class RelationTest < ActiveRecord::TestCase end def test_find_with_readonly_option - Developer.all.each { |d| assert !d.readonly? } + Developer.all.each { |d| assert_not d.readonly? } Developer.all.readonly.each { |d| assert d.readonly? } end @@ -1097,7 +1097,7 @@ class RelationTest < ActiveRecord::TestCase assert_not_predicate posts.where(id: nil), :any? assert posts.any? { |p| p.id > 0 } - assert ! posts.any? { |p| p.id <= 0 } + assert_not posts.any? { |p| p.id <= 0 } end assert_predicate posts, :loaded? @@ -1109,7 +1109,7 @@ class RelationTest < ActiveRecord::TestCase assert_queries(2) do assert posts.many? # Uses COUNT() assert posts.many? { |p| p.id > 0 } - assert ! posts.many? { |p| p.id < 2 } + assert_not posts.many? { |p| p.id < 2 } end assert_predicate posts, :loaded? @@ -1125,14 +1125,14 @@ class RelationTest < ActiveRecord::TestCase def test_none? posts = Post.all assert_queries(1) do - assert ! posts.none? # Uses COUNT() + assert_not posts.none? # Uses COUNT() end assert_not_predicate posts, :loaded? assert_queries(1) do assert posts.none? { |p| p.id < 0 } - assert ! posts.none? { |p| p.id == 1 } + assert_not posts.none? { |p| p.id == 1 } end assert_predicate posts, :loaded? @@ -1141,13 +1141,13 @@ class RelationTest < ActiveRecord::TestCase def test_one posts = Post.all assert_queries(1) do - assert ! posts.one? # Uses COUNT() + assert_not posts.one? # Uses COUNT() end assert_not_predicate posts, :loaded? assert_queries(1) do - assert ! posts.one? { |p| p.id < 3 } + assert_not posts.one? { |p| p.id < 3 } assert posts.one? { |p| p.id == 1 } end @@ -1696,7 +1696,7 @@ class RelationTest < ActiveRecord::TestCase # checking if there are topics is used before you actually display them, # thus it shouldn't invoke an extra count query. assert_no_queries { assert topics.present? } - assert_no_queries { assert !topics.blank? } + assert_no_queries { assert_not topics.blank? } # shows count of topics and loops after loading the query should not trigger extra queries either. assert_no_queries { topics.size } diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index a612ce9bb2..31bdf3f357 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -298,7 +298,7 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_expression_indices index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_expression_index/).first.strip - assert_equal 't.index "lower((name)::text)", name: "company_expression_index"', index_definition + assert_match %r{CASE.+lower\(\(name\)::text\)}i, index_definition end def test_schema_dump_interval_type @@ -469,7 +469,7 @@ class SchemaDumperTest < ActiveRecord::TestCase output = ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream).string assert_match %r{create_table "omg_cats"}, output - refute_match %r{create_table "cats"}, output + assert_no_match %r{create_table "cats"}, output ensure migration.migrate(:down) ActiveRecord::Base.table_name_prefix = original_table_name_prefix diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 0804de1fb3..6cda546b08 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -193,7 +193,7 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_order_to_unscope_reordering scope = DeveloperOrderedBySalary.order("salary DESC, name ASC").reverse_order.unscope(:order) - assert !/order/i.match?(scope.to_sql) + assert_not /order/i.match?(scope.to_sql) end def test_unscope_reverse_order diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index ea71a5ce28..6cb252edaf 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -86,7 +86,7 @@ class NamedScopingTest < ActiveRecord::TestCase def test_scopes_are_composable assert_equal((approved = Topic.all.merge!(where: { approved: true }).to_a), Topic.approved) assert_equal((replied = Topic.all.merge!(where: "replies_count > 0").to_a), Topic.replied) - assert !(approved == replied) + assert_not (approved == replied) assert_not_empty (approved & replied) assert_equal approved & replied, Topic.approved.replied @@ -303,6 +303,13 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal "lifo", topic.author_name end + def test_deprecated_delegating_private_method + assert_deprecated do + scope = Topic.all.by_private_lifo + assert_not scope.instance_variable_get(:@delegate_to_klass) + end + end + def test_reserved_scope_names klass = Class.new(ActiveRecord::Base) do self.table_name = "topics" diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index 5c86bc892d..f18f1ed981 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -105,7 +105,7 @@ class RelationScopingTest < ActiveRecord::TestCase Developer.select("id, name").scoping do developer = Developer.where("name = 'David'").first assert_equal "David", developer.name - assert !developer.has_attribute?(:salary) + assert_not developer.has_attribute?(:salary) end end diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index 2d829ad4ba..932780bfef 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -67,8 +67,8 @@ class SerializationTest < ActiveRecord::TestCase klazz.include_root_in_json = false assert ActiveRecord::Base.include_root_in_json - assert !klazz.include_root_in_json - assert !klazz.new.include_root_in_json + assert_not klazz.include_root_in_json + assert_not klazz.new.include_root_in_json ensure ActiveRecord::Base.include_root_in_json = original_root_in_json end diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb index ad6cd198e2..e3c12f68fd 100644 --- a/activerecord/test/cases/statement_cache_test.rb +++ b/activerecord/test/cases/statement_cache_test.rb @@ -102,7 +102,7 @@ module ActiveRecord Book.find_by(name: "my other book") end - refute_equal book, other_book + assert_not_equal book, other_book end def test_find_by_does_not_use_statement_cache_if_table_name_is_changed diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index a30d13632a..3bd480cfbd 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -8,7 +8,12 @@ class StoreTest < ActiveRecord::TestCase fixtures :'admin/users' setup do - @john = Admin::User.create!(name: "John Doe", color: "black", remember_login: true, height: "tall", is_a_good_guy: true) + @john = Admin::User.create!( + name: "John Doe", color: "black", remember_login: true, + height: "tall", is_a_good_guy: true, + parent_name: "Quinn", partner_name: "Dallas", + partner_birthday: "1997-11-1" + ) end test "reading store attributes through accessors" do @@ -24,6 +29,21 @@ class StoreTest < ActiveRecord::TestCase assert_equal "37signals.com", @john.homepage end + test "reading store attributes through accessors with prefix" do + assert_equal "Quinn", @john.parent_name + assert_nil @john.parent_birthday + assert_equal "Dallas", @john.partner_name + assert_equal "1997-11-1", @john.partner_birthday + end + + test "writing store attributes through accessors with prefix" do + @john.partner_name = "River" + @john.partner_birthday = "1999-2-11" + + assert_equal "River", @john.partner_name + assert_equal "1999-2-11", @john.partner_birthday + end + test "accessing attributes not exposed by accessors" do @john.settings[:icecream] = "graeters" @john.save diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index 21226352ff..48d1fc7eb0 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -179,7 +179,7 @@ module ActiveRecord @configurations = { "development" => { "database" => "dev-db" }, "test" => { "database" => "test-db" }, - "production" => { "database" => "prod-db" } + "production" => { "url" => "prod-db-url" } } ActiveRecord::Base.stubs(:configurations).returns(@configurations) @@ -188,7 +188,89 @@ module ActiveRecord def test_creates_current_environment_database ActiveRecord::Tasks::DatabaseTasks.expects(:create). - with("database" => "prod-db") + with("database" => "test-db") + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("test") + ) + end + + def test_creates_current_environment_database_with_url + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("url" => "prod-db-url") + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("production") + ) + end + + def test_creates_test_and_development_databases_when_env_was_not_specified + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "test-db") + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + + def test_creates_test_and_development_databases_when_rails_env_is_development + old_env = ENV["RAILS_ENV"] + ENV["RAILS_ENV"] = "development" + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "test-db") + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + ensure + ENV["RAILS_ENV"] = old_env + end + + def test_establishes_connection_for_the_given_environments + ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true + + ActiveRecord::Base.expects(:establish_connection).with(:development) + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("development") + ) + end + end + + class DatabaseTasksCreateCurrentThreeTierTest < ActiveRecord::TestCase + def setup + @configurations = { + "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } }, + "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, + "production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } } + } + + ActiveRecord::Base.stubs(:configurations).returns(@configurations) + ActiveRecord::Base.stubs(:establish_connection).returns(true) + end + + def test_creates_current_environment_database + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "test-db") + + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "secondary-test-db") + + ActiveRecord::Tasks::DatabaseTasks.create_current( + ActiveSupport::StringInquirer.new("test") + ) + end + + def test_creates_current_environment_database_with_url + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("url" => "prod-db-url") + + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("url" => "secondary-prod-db-url") ActiveRecord::Tasks::DatabaseTasks.create_current( ActiveSupport::StringInquirer.new("production") @@ -199,7 +281,11 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.expects(:create). with("database" => "dev-db") ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "secondary-dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:create). with("database" => "test-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "secondary-test-db") ActiveRecord::Tasks::DatabaseTasks.create_current( ActiveSupport::StringInquirer.new("development") @@ -212,7 +298,11 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.expects(:create). with("database" => "dev-db") ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "secondary-dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:create). with("database" => "test-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:create). + with("database" => "secondary-test-db") ActiveRecord::Tasks::DatabaseTasks.create_current( ActiveSupport::StringInquirer.new("development") @@ -221,7 +311,7 @@ module ActiveRecord ENV["RAILS_ENV"] = old_env end - def test_establishes_connection_for_the_given_environment + def test_establishes_connection_for_the_given_environments_config ActiveRecord::Tasks::DatabaseTasks.stubs(:create).returns true ActiveRecord::Base.expects(:establish_connection).with(:development) @@ -305,7 +395,7 @@ module ActiveRecord @configurations = { "development" => { "database" => "dev-db" }, "test" => { "database" => "test-db" }, - "production" => { "database" => "prod-db" } + "production" => { "url" => "prod-db-url" } } ActiveRecord::Base.stubs(:configurations).returns(@configurations) @@ -313,7 +403,16 @@ module ActiveRecord def test_drops_current_environment_database ActiveRecord::Tasks::DatabaseTasks.expects(:drop). - with("database" => "prod-db") + with("database" => "test-db") + + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("test") + ) + end + + def test_drops_current_environment_database_with_url + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("url" => "prod-db-url") ActiveRecord::Tasks::DatabaseTasks.drop_current( ActiveSupport::StringInquirer.new("production") @@ -347,6 +446,76 @@ module ActiveRecord end end + class DatabaseTasksDropCurrentThreeTierTest < ActiveRecord::TestCase + def setup + @configurations = { + "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } }, + "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, + "production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } } + } + + ActiveRecord::Base.stubs(:configurations).returns(@configurations) + end + + def test_drops_current_environment_database + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "test-db") + + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "secondary-test-db") + + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("test") + ) + end + + def test_drops_current_environment_database_with_url + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("url" => "prod-db-url") + + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("url" => "secondary-prod-db-url") + + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("production") + ) + end + + def test_drops_test_and_development_databases_when_env_was_not_specified + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "secondary-dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "test-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "secondary-test-db") + + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("development") + ) + end + + def test_drops_testand_development_databases_when_rails_env_is_development + old_env = ENV["RAILS_ENV"] + ENV["RAILS_ENV"] = "development" + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "secondary-dev-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "test-db") + ActiveRecord::Tasks::DatabaseTasks.expects(:drop). + with("database" => "secondary-test-db") + + ActiveRecord::Tasks::DatabaseTasks.drop_current( + ActiveSupport::StringInquirer.new("development") + ) + ensure + ENV["RAILS_ENV"] = old_env + end + end + if current_adapter?(:SQLite3Adapter) && !in_memory_db? class DatabaseTasksMigrateTest < ActiveRecord::TestCase self.use_transactional_tests = false diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb index 41455637bb..086500de38 100644 --- a/activerecord/test/cases/time_precision_test.rb +++ b/activerecord/test/cases/time_precision_test.rb @@ -27,6 +27,24 @@ if subsecond_precision_supported? assert_equal 6, Foo.columns_hash["finish"].precision end + def test_time_precision_is_truncated_on_assignment + @connection.create_table(:foos, force: true) + @connection.add_column :foos, :start, :time, precision: 0 + @connection.add_column :foos, :finish, :time, precision: 6 + + time = ::Time.now.change(nsec: 123456789) + foo = Foo.new(start: time, finish: time) + + assert_equal 0, foo.start.nsec + assert_equal 123456000, foo.finish.nsec + + foo.save! + foo.reload + + assert_equal 0, foo.start.nsec + assert_equal 123456000, foo.finish.nsec + end + def test_passing_precision_to_time_does_not_set_limit @connection.create_table(:foos, force: true) do |t| t.time :start, precision: 3 diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index e95446c0a7..d9f7a81ce4 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -96,6 +96,16 @@ class TimestampTest < ActiveRecord::TestCase assert_not_equal @previously_updated_at, @developer.updated_at end + def test_touching_update_at_attribute_as_symbol_updates_timestamp + travel(1.second) do + @developer.touch(:updated_at) + end + + assert_not @developer.updated_at_changed? + assert_not @developer.changed? + assert_not_equal @previously_updated_at, @developer.updated_at + end + def test_touching_an_attribute_updates_it task = Task.first previous_value = task.ending diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index 1c7cec4dad..e89ac53732 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -394,6 +394,28 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end end +class TransactionAfterCommitCallbacksWithOptimisticLockingTest < ActiveRecord::TestCase + class PersonWithCallbacks < ActiveRecord::Base + self.table_name = :people + + after_create_commit { |record| record.history << :commit_on_create } + after_update_commit { |record| record.history << :commit_on_update } + after_destroy_commit { |record| record.history << :commit_on_destroy } + + def history + @history ||= [] + end + end + + def test_after_commit_callbacks_with_optimistic_locking + person = PersonWithCallbacks.create!(first_name: "first name") + person.update!(first_name: "another name") + person.destroy + + assert_equal [:commit_on_create, :commit_on_update, :commit_on_destroy], person.history + end +end + class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase self.use_transactional_tests = false diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index c70286d52a..3fd38b4b60 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -159,7 +159,7 @@ class TransactionTest < ActiveRecord::TestCase def @first.before_save_for_transaction raise ActiveRecord::Rollback end - assert !@first.approved + assert_not @first.approved Topic.transaction do @first.approved = true @@ -194,7 +194,7 @@ class TransactionTest < ActiveRecord::TestCase posts_count = author.posts.size assert posts_count > 0 status = author.update(name: nil, post_ids: []) - assert !status + assert_not status assert_equal posts_count, author.posts.reload.size end @@ -212,7 +212,7 @@ class TransactionTest < ActiveRecord::TestCase add_cancelling_before_destroy_with_db_side_effect_to_topic @first nbooks_before_destroy = Book.count status = @first.destroy - assert !status + assert_not status @first.reload assert_equal nbooks_before_destroy, Book.count end @@ -224,7 +224,7 @@ class TransactionTest < ActiveRecord::TestCase original_author_name = @first.author_name @first.author_name += "_this_should_not_end_up_in_the_db" status = @first.save - assert !status + assert_not status assert_equal original_author_name, @first.reload.author_name assert_equal nbooks_before_save, Book.count end @@ -323,8 +323,8 @@ class TransactionTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end - refute_predicate topic_one, :persisted? - refute_predicate topic_two, :persisted? + assert_not_predicate topic_one, :persisted? + assert_not_predicate topic_two, :persisted? end def test_nested_transaction_without_new_transaction_applies_parent_state_on_rollback @@ -344,8 +344,8 @@ class TransactionTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end - refute_predicate topic_one, :persisted? - refute_predicate topic_two, :persisted? + assert_not_predicate topic_one, :persisted? + assert_not_predicate topic_two, :persisted? end def test_double_nested_transaction_applies_parent_state_on_rollback @@ -371,9 +371,9 @@ class TransactionTest < ActiveRecord::TestCase raise ActiveRecord::Rollback end - refute_predicate topic_one, :persisted? - refute_predicate topic_two, :persisted? - refute_predicate topic_three, :persisted? + assert_not_predicate topic_one, :persisted? + assert_not_predicate topic_two, :persisted? + assert_not_predicate topic_three, :persisted? end def test_manually_rolling_back_a_transaction diff --git a/activerecord/test/cases/unsafe_raw_sql_test.rb b/activerecord/test/cases/unsafe_raw_sql_test.rb index 72d4997d0b..d5d8f2a09a 100644 --- a/activerecord/test/cases/unsafe_raw_sql_test.rb +++ b/activerecord/test/cases/unsafe_raw_sql_test.rb @@ -107,6 +107,26 @@ class UnsafeRawSqlTest < ActiveRecord::TestCase assert_equal ids_expected, ids_disabled end + test "order: allows NULLS FIRST and NULLS LAST too" do + raise "precondition failed" if Post.count < 2 + + # Ensure there are NULL and non-NULL post types. + Post.first.update_column(:type, nil) + Post.last.update_column(:type, "Programming") + + ["asc", "desc", ""].each do |direction| + %w(first last).each do |position| + ids_expected = Post.order(Arel.sql("type #{direction} nulls #{position}")).pluck(:id) + + ids_depr = with_unsafe_raw_sql_deprecated { Post.order("type #{direction} nulls #{position}").pluck(:id) } + ids_disabled = with_unsafe_raw_sql_disabled { Post.order("type #{direction} nulls #{position}").pluck(:id) } + + assert_equal ids_expected, ids_depr + assert_equal ids_expected, ids_disabled + end + end + end if current_adapter?(:PostgreSQLAdapter) + test "order: disallows invalid column name" do with_unsafe_raw_sql_disabled do assert_raises(ActiveRecord::UnknownAttributeReference) do diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb index 62cd89041a..1fbcdc271b 100644 --- a/activerecord/test/cases/validations/length_validation_test.rb +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -17,7 +17,7 @@ class LengthValidationTest < ActiveRecord::TestCase def test_validates_size_of_association assert_nothing_raised { @owner.validates_size_of :pets, minimum: 1 } o = @owner.new("name" => "nopets") - assert !o.save + assert_not o.save assert_predicate o.errors[:pets], :any? o.pets.build("name" => "apet") assert_predicate o, :valid? @@ -26,21 +26,21 @@ class LengthValidationTest < ActiveRecord::TestCase def test_validates_size_of_association_using_within assert_nothing_raised { @owner.validates_size_of :pets, within: 1..2 } o = @owner.new("name" => "nopets") - assert !o.save + assert_not o.save assert_predicate o.errors[:pets], :any? o.pets.build("name" => "apet") assert_predicate o, :valid? 2.times { o.pets.build("name" => "apet") } - assert !o.save + assert_not o.save assert_predicate o.errors[:pets], :any? end def test_validates_size_of_association_utf8 @owner.validates_size_of :pets, minimum: 1 o = @owner.new("name" => "あいうえおかきくけこ") - assert !o.save + assert_not o.save assert_predicate o.errors[:pets], :any? o.pets.build("name" => "あいうえおかきくけこ") assert_predicate o, :valid? diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index 6c83bbd15c..a33877f43a 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -39,7 +39,7 @@ class ValidationsTest < ActiveRecord::TestCase def test_valid_using_special_context r = WrongReply.new(title: "Valid title") - assert !r.valid?(:special_case) + assert_not r.valid?(:special_case) assert_equal "Invalid", r.errors[:author_name].join r.author_name = "secret" @@ -125,7 +125,7 @@ class ValidationsTest < ActiveRecord::TestCase def test_save_without_validation reply = WrongReply.new - assert !reply.save + assert_not reply.save assert reply.save(validate: false) end diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb index abb5cb28e7..3f55364510 100644 --- a/activerecord/test/models/admin/user.rb +++ b/activerecord/test/models/admin/user.rb @@ -19,6 +19,9 @@ class Admin::User < ActiveRecord::Base store :params, accessors: [ :token ], coder: YAML store :settings, accessors: [ :color, :homepage ] store_accessor :settings, :favorite_food + store :parent, accessors: [:birthday, :name], prefix: true + store :spouse, accessors: [:birthday], prefix: :partner + store_accessor :spouse, :name, prefix: :partner store :preferences, accessors: [ :remember_login ] store :json_data, accessors: [ :height, :weight ], coder: Coder.new store :json_data_empty, accessors: [ :is_a_good_guy ], coder: Coder.new diff --git a/activerecord/test/models/frog.rb b/activerecord/test/models/frog.rb new file mode 100644 index 0000000000..73601aacdd --- /dev/null +++ b/activerecord/test/models/frog.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +class Frog < ActiveRecord::Base + after_save do + with_lock do + end + end +end diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 8cd4dc352a..fa50eeb6a4 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -12,9 +12,17 @@ class Topic < ActiveRecord::Base scope :scope_with_lambda, lambda { all } + scope :by_private_lifo, -> { where(author_name: private_lifo) } scope :by_lifo, -> { where(author_name: "lifo") } scope :replied, -> { where "replies_count > 0" } + class << self + private + def private_lifo + "lifo" + end + end + scope "approved_as_string", -> { where(approved: true) } scope :anonymous_extension, -> {} do def one diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 8b0106dbf0..350113eaab 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -21,6 +21,8 @@ ActiveRecord::Schema.define do create_table :admin_users, force: true do |t| t.string :name t.string :settings, null: true, limit: 1024 + t.string :parent, null: true, limit: 1024 + t.string :spouse, null: true, limit: 1024 # MySQL does not allow default values for blobs. Fake it out with a # big varchar below. t.string :preferences, null: true, default: "", limit: 1024 @@ -210,7 +212,7 @@ ActiveRecord::Schema.define do t.index [:firm_id, :type, :rating], name: "company_index", length: { type: 10 }, order: { rating: :desc } t.index [:firm_id, :type], name: "company_partial_index", where: "(rating > 10)" t.index :name, name: "company_name_index", using: :btree - t.index "lower(name)", name: "company_expression_index" if supports_expression_index? + t.index "(CASE WHEN rating > 0 THEN lower(name) END)", name: "company_expression_index" if supports_expression_index? end create_table :content, force: true do |t| @@ -345,6 +347,10 @@ ActiveRecord::Schema.define do t.string :token end + create_table :frogs, force: true do |t| + t.string :name + end + create_table :funny_jokes, force: true do |t| t.string :name end diff --git a/activestorage/CHANGELOG.md b/activestorage/CHANGELOG.md index 2f8b65766d..d794afb0e6 100644 --- a/activestorage/CHANGELOG.md +++ b/activestorage/CHANGELOG.md @@ -1,5 +1,3 @@ -## Rails 6.0.0.alpha (Unreleased) ## - * Rails 6 requires Ruby 2.4.1 or newer. *Jeremy Daer* diff --git a/activestorage/Rakefile b/activestorage/Rakefile index 2aa4d2a76f..7dc69e04ea 100644 --- a/activestorage/Rakefile +++ b/activestorage/Rakefile @@ -8,6 +8,7 @@ Rake::TestTask.new do |test| test.libs << "app/controllers" test.libs << "test" test.test_files = FileList["test/**/*_test.rb"] + test.verbose = true test.warning = false end diff --git a/activestorage/activestorage.gemspec b/activestorage/activestorage.gemspec index df7801a671..cb1bb00a25 100644 --- a/activestorage/activestorage.gemspec +++ b/activestorage/activestorage.gemspec @@ -29,6 +29,4 @@ Gem::Specification.new do |s| s.add_dependency "activerecord", version s.add_dependency "marcel", "~> 0.3.1" - - s.add_development_dependency "webmock", "~> 3.2.1" end diff --git a/activestorage/app/assets/javascripts/activestorage.js b/activestorage/app/assets/javascripts/activestorage.js index 269fc52e64..3e1fbc7b50 100644 --- a/activestorage/app/assets/javascripts/activestorage.js +++ b/activestorage/app/assets/javascripts/activestorage.js @@ -1 +1 @@ -!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ActiveStorage=e():t.ActiveStorage=e()}(this,function(){return function(t){function e(n){if(r[n])return r[n].exports;var i=r[n]={i:n,l:!1,exports:{}};return t[n].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var r={};return e.m=t,e.c=r,e.d=function(t,r,n){e.o(t,r)||Object.defineProperty(t,r,{configurable:!1,enumerable:!0,get:n})},e.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(r,"a",r),r},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=2)}([function(t,e,r){"use strict";function n(t){var e=a(document.head,'meta[name="'+t+'"]');if(e)return e.getAttribute("content")}function i(t,e){return"string"==typeof t&&(e=t,t=document),o(t.querySelectorAll(e))}function a(t,e){return"string"==typeof t&&(e=t,t=document),t.querySelector(e)}function u(t,e){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},n=t.disabled,i=r.bubbles,a=r.cancelable,u=r.detail,o=document.createEvent("Event");o.initEvent(e,i||!0,a||!0),o.detail=u||{};try{t.disabled=!1,t.dispatchEvent(o)}finally{t.disabled=n}return o}function o(t){return Array.isArray(t)?t:Array.from?Array.from(t):[].slice.call(t)}e.d=n,e.c=i,e.b=a,e.a=u,e.e=o},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){if(t&&"function"==typeof t[e]){for(var r=arguments.length,n=Array(r>2?r-2:0),i=2;i<r;i++)n[i-2]=arguments[i];return t[e].apply(t,n)}}r.d(e,"a",function(){return c});var a=r(6),u=r(8),o=r(9),s=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),f=0,c=function(){function t(e,r,i){n(this,t),this.id=++f,this.file=e,this.url=r,this.delegate=i}return s(t,[{key:"create",value:function(t){var e=this;a.a.create(this.file,function(r,n){if(r)return void t(r);var a=new u.a(e.file,n,e.url);i(e.delegate,"directUploadWillCreateBlobWithXHR",a.xhr),a.create(function(r){if(r)t(r);else{var n=new o.a(a);i(e.delegate,"directUploadWillStoreFileWithXHR",n.xhr),n.create(function(e){e?t(e):t(null,a.toJSON())})}})})}}]),t}()},function(t,e,r){"use strict";function n(){window.ActiveStorage&&Object(i.a)()}Object.defineProperty(e,"__esModule",{value:!0});var i=r(3),a=r(1);r.d(e,"start",function(){return i.a}),r.d(e,"DirectUpload",function(){return a.a}),setTimeout(n,1)},function(t,e,r){"use strict";function n(){d||(d=!0,document.addEventListener("submit",i),document.addEventListener("ajax:before",a))}function i(t){u(t)}function a(t){"FORM"==t.target.tagName&&u(t)}function u(t){var e=t.target;if(e.hasAttribute(l))return void t.preventDefault();var r=new c.a(e),n=r.inputs;n.length&&(t.preventDefault(),e.setAttribute(l,""),n.forEach(s),r.start(function(t){e.removeAttribute(l),t?n.forEach(f):o(e)}))}function o(t){var e=Object(h.b)(t,"input[type=submit]");if(e){var r=e,n=r.disabled;e.disabled=!1,e.focus(),e.click(),e.disabled=n}else e=document.createElement("input"),e.type="submit",e.style="display:none",t.appendChild(e),e.click(),t.removeChild(e)}function s(t){t.disabled=!0}function f(t){t.disabled=!1}e.a=n;var c=r(4),h=r(0),l="data-direct-uploads-processing",d=!1},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return s});var i=r(5),a=r(0),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o="input[type=file][data-direct-upload-url]:not([disabled])",s=function(){function t(e){n(this,t),this.form=e,this.inputs=Object(a.c)(e,o).filter(function(t){return t.files.length})}return u(t,[{key:"start",value:function(t){var e=this,r=this.createDirectUploadControllers();this.dispatch("start"),function n(){var i=r.shift();i?i.start(function(r){r?(t(r),e.dispatch("end")):n()}):(t(),e.dispatch("end"))}()}},{key:"createDirectUploadControllers",value:function(){var t=[];return this.inputs.forEach(function(e){Object(a.e)(e.files).forEach(function(r){var n=new i.a(e,r);t.push(n)})}),t}},{key:"dispatch",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return Object(a.a)(this.form,"direct-uploads:"+t,{detail:e})}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return o});var i=r(1),a=r(0),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o=function(){function t(e,r){n(this,t),this.input=e,this.file=r,this.directUpload=new i.a(this.file,this.url,this),this.dispatch("initialize")}return u(t,[{key:"start",value:function(t){var e=this,r=document.createElement("input");r.type="hidden",r.name=this.input.name,this.input.insertAdjacentElement("beforebegin",r),this.dispatch("start"),this.directUpload.create(function(n,i){n?(r.parentNode.removeChild(r),e.dispatchError(n)):r.value=i.signed_id,e.dispatch("end"),t(n)})}},{key:"uploadRequestDidProgress",value:function(t){var e=t.loaded/t.total*100;e&&this.dispatch("progress",{progress:e})}},{key:"dispatch",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.file=this.file,e.id=this.directUpload.id,Object(a.a)(this.input,"direct-upload:"+t,{detail:e})}},{key:"dispatchError",value:function(t){this.dispatch("error",{error:t}).defaultPrevented||alert(t)}},{key:"directUploadWillCreateBlobWithXHR",value:function(t){this.dispatch("before-blob-request",{xhr:t})}},{key:"directUploadWillStoreFileWithXHR",value:function(t){var e=this;this.dispatch("before-storage-request",{xhr:t}),t.upload.addEventListener("progress",function(t){return e.uploadRequestDidProgress(t)})}},{key:"url",get:function(){return this.input.getAttribute("data-direct-upload-url")}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return s});var i=r(7),a=r.n(i),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o=File.prototype.slice||File.prototype.mozSlice||File.prototype.webkitSlice,s=function(){function t(e){n(this,t),this.file=e,this.chunkSize=2097152,this.chunkCount=Math.ceil(this.file.size/this.chunkSize),this.chunkIndex=0}return u(t,null,[{key:"create",value:function(e,r){new t(e).create(r)}}]),u(t,[{key:"create",value:function(t){var e=this;this.callback=t,this.md5Buffer=new a.a.ArrayBuffer,this.fileReader=new FileReader,this.fileReader.addEventListener("load",function(t){return e.fileReaderDidLoad(t)}),this.fileReader.addEventListener("error",function(t){return e.fileReaderDidError(t)}),this.readNextChunk()}},{key:"fileReaderDidLoad",value:function(t){if(this.md5Buffer.append(t.target.result),!this.readNextChunk()){var e=this.md5Buffer.end(!0),r=btoa(e);this.callback(null,r)}}},{key:"fileReaderDidError",value:function(t){this.callback("Error reading "+this.file.name)}},{key:"readNextChunk",value:function(){if(this.chunkIndex<this.chunkCount){var t=this.chunkIndex*this.chunkSize,e=Math.min(t+this.chunkSize,this.file.size),r=o.call(this.file,t,e);return this.fileReader.readAsArrayBuffer(r),this.chunkIndex++,!0}return!1}}]),t}()},function(t,e,r){!function(e){t.exports=e()}(function(t){"use strict";function e(t,e){var r=t[0],n=t[1],i=t[2],a=t[3];r+=(n&i|~n&a)+e[0]-680876936|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[1]-389564586|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[2]+606105819|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[3]-1044525330|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[4]-176418897|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[5]+1200080426|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[6]-1473231341|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[7]-45705983|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[8]+1770035416|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[9]-1958414417|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[10]-42063|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[11]-1990404162|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[12]+1804603682|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[13]-40341101|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[14]-1502002290|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[15]+1236535329|0,n=(n<<22|n>>>10)+i|0,r+=(n&a|i&~a)+e[1]-165796510|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[6]-1069501632|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[11]+643717713|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[0]-373897302|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[5]-701558691|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[10]+38016083|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[15]-660478335|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[4]-405537848|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[9]+568446438|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[14]-1019803690|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[3]-187363961|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[8]+1163531501|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[13]-1444681467|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[2]-51403784|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[7]+1735328473|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[12]-1926607734|0,n=(n<<20|n>>>12)+i|0,r+=(n^i^a)+e[5]-378558|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[8]-2022574463|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[11]+1839030562|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[14]-35309556|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[1]-1530992060|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[4]+1272893353|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[7]-155497632|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[10]-1094730640|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[13]+681279174|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[0]-358537222|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[3]-722521979|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[6]+76029189|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[9]-640364487|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[12]-421815835|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[15]+530742520|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[2]-995338651|0,n=(n<<23|n>>>9)+i|0,r+=(i^(n|~a))+e[0]-198630844|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[7]+1126891415|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[14]-1416354905|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[5]-57434055|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[12]+1700485571|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[3]-1894986606|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[10]-1051523|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[1]-2054922799|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[8]+1873313359|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[15]-30611744|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[6]-1560198380|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[13]+1309151649|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[4]-145523070|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[11]-1120210379|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[2]+718787259|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[9]-343485551|0,n=(n<<21|n>>>11)+i|0,t[0]=r+t[0]|0,t[1]=n+t[1]|0,t[2]=i+t[2]|0,t[3]=a+t[3]|0}function r(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t.charCodeAt(e)+(t.charCodeAt(e+1)<<8)+(t.charCodeAt(e+2)<<16)+(t.charCodeAt(e+3)<<24);return r}function n(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t[e]+(t[e+1]<<8)+(t[e+2]<<16)+(t[e+3]<<24);return r}function i(t){var n,i,a,u,o,s,f=t.length,c=[1732584193,-271733879,-1732584194,271733878];for(n=64;n<=f;n+=64)e(c,r(t.substring(n-64,n)));for(t=t.substring(n-64),i=t.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],n=0;n<i;n+=1)a[n>>2]|=t.charCodeAt(n)<<(n%4<<3);if(a[n>>2]|=128<<(n%4<<3),n>55)for(e(c,a),n=0;n<16;n+=1)a[n]=0;return u=8*f,u=u.toString(16).match(/(.*?)(.{0,8})$/),o=parseInt(u[2],16),s=parseInt(u[1],16)||0,a[14]=o,a[15]=s,e(c,a),c}function a(t){var r,i,a,u,o,s,f=t.length,c=[1732584193,-271733879,-1732584194,271733878];for(r=64;r<=f;r+=64)e(c,n(t.subarray(r-64,r)));for(t=r-64<f?t.subarray(r-64):new Uint8Array(0),i=t.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],r=0;r<i;r+=1)a[r>>2]|=t[r]<<(r%4<<3);if(a[r>>2]|=128<<(r%4<<3),r>55)for(e(c,a),r=0;r<16;r+=1)a[r]=0;return u=8*f,u=u.toString(16).match(/(.*?)(.{0,8})$/),o=parseInt(u[2],16),s=parseInt(u[1],16)||0,a[14]=o,a[15]=s,e(c,a),c}function u(t){var e,r="";for(e=0;e<4;e+=1)r+=p[t>>8*e+4&15]+p[t>>8*e&15];return r}function o(t){var e;for(e=0;e<t.length;e+=1)t[e]=u(t[e]);return t.join("")}function s(t){return/[\u0080-\uFFFF]/.test(t)&&(t=unescape(encodeURIComponent(t))),t}function f(t,e){var r,n=t.length,i=new ArrayBuffer(n),a=new Uint8Array(i);for(r=0;r<n;r+=1)a[r]=t.charCodeAt(r);return e?a:i}function c(t){return String.fromCharCode.apply(null,new Uint8Array(t))}function h(t,e,r){var n=new Uint8Array(t.byteLength+e.byteLength);return n.set(new Uint8Array(t)),n.set(new Uint8Array(e),t.byteLength),r?n:n.buffer}function l(t){var e,r=[],n=t.length;for(e=0;e<n-1;e+=2)r.push(parseInt(t.substr(e,2),16));return String.fromCharCode.apply(String,r)}function d(){this.reset()}var p=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];return"5d41402abc4b2a76b9719d911017c592"!==o(i("hello"))&&function(t,e){var r=(65535&t)+(65535&e);return(t>>16)+(e>>16)+(r>>16)<<16|65535&r},"undefined"==typeof ArrayBuffer||ArrayBuffer.prototype.slice||function(){function e(t,e){return t=0|t||0,t<0?Math.max(t+e,0):Math.min(t,e)}ArrayBuffer.prototype.slice=function(r,n){var i,a,u,o,s=this.byteLength,f=e(r,s),c=s;return n!==t&&(c=e(n,s)),f>c?new ArrayBuffer(0):(i=c-f,a=new ArrayBuffer(i),u=new Uint8Array(a),o=new Uint8Array(this,f,i),u.set(o),a)}}(),d.prototype.append=function(t){return this.appendBinary(s(t)),this},d.prototype.appendBinary=function(t){this._buff+=t,this._length+=t.length;var n,i=this._buff.length;for(n=64;n<=i;n+=64)e(this._hash,r(this._buff.substring(n-64,n)));return this._buff=this._buff.substring(n-64),this},d.prototype.end=function(t){var e,r,n=this._buff,i=n.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<i;e+=1)a[e>>2]|=n.charCodeAt(e)<<(e%4<<3);return this._finish(a,i),r=o(this._hash),t&&(r=l(r)),this.reset(),r},d.prototype.reset=function(){return this._buff="",this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},d.prototype.getState=function(){return{buff:this._buff,length:this._length,hash:this._hash}},d.prototype.setState=function(t){return this._buff=t.buff,this._length=t.length,this._hash=t.hash,this},d.prototype.destroy=function(){delete this._hash,delete this._buff,delete this._length},d.prototype._finish=function(t,r){var n,i,a,u=r;if(t[u>>2]|=128<<(u%4<<3),u>55)for(e(this._hash,t),u=0;u<16;u+=1)t[u]=0;n=8*this._length,n=n.toString(16).match(/(.*?)(.{0,8})$/),i=parseInt(n[2],16),a=parseInt(n[1],16)||0,t[14]=i,t[15]=a,e(this._hash,t)},d.hash=function(t,e){return d.hashBinary(s(t),e)},d.hashBinary=function(t,e){var r=i(t),n=o(r);return e?l(n):n},d.ArrayBuffer=function(){this.reset()},d.ArrayBuffer.prototype.append=function(t){var r,i=h(this._buff.buffer,t,!0),a=i.length;for(this._length+=t.byteLength,r=64;r<=a;r+=64)e(this._hash,n(i.subarray(r-64,r)));return this._buff=r-64<a?new Uint8Array(i.buffer.slice(r-64)):new Uint8Array(0),this},d.ArrayBuffer.prototype.end=function(t){var e,r,n=this._buff,i=n.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<i;e+=1)a[e>>2]|=n[e]<<(e%4<<3);return this._finish(a,i),r=o(this._hash),t&&(r=l(r)),this.reset(),r},d.ArrayBuffer.prototype.reset=function(){return this._buff=new Uint8Array(0),this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},d.ArrayBuffer.prototype.getState=function(){var t=d.prototype.getState.call(this);return t.buff=c(t.buff),t},d.ArrayBuffer.prototype.setState=function(t){return t.buff=f(t.buff,!0),d.prototype.setState.call(this,t)},d.ArrayBuffer.prototype.destroy=d.prototype.destroy,d.ArrayBuffer.prototype._finish=d.prototype._finish,d.ArrayBuffer.hash=function(t,e){var r=a(new Uint8Array(t)),n=o(r);return e?l(n):n},d})},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return u});var i=r(0),a=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),u=function(){function t(e,r,a){var u=this;n(this,t),this.file=e,this.attributes={filename:e.name,content_type:e.type,byte_size:e.size,checksum:r},this.xhr=new XMLHttpRequest,this.xhr.open("POST",a,!0),this.xhr.responseType="json",this.xhr.setRequestHeader("Content-Type","application/json"),this.xhr.setRequestHeader("Accept","application/json"),this.xhr.setRequestHeader("X-Requested-With","XMLHttpRequest"),this.xhr.setRequestHeader("X-CSRF-Token",Object(i.d)("csrf-token")),this.xhr.addEventListener("load",function(t){return u.requestDidLoad(t)}),this.xhr.addEventListener("error",function(t){return u.requestDidError(t)})}return a(t,[{key:"create",value:function(t){this.callback=t,this.xhr.send(JSON.stringify({blob:this.attributes}))}},{key:"requestDidLoad",value:function(t){if(this.status>=200&&this.status<300){var e=this.response,r=e.direct_upload;delete e.direct_upload,this.attributes=e,this.directUploadData=r,this.callback(null,this.toJSON())}else this.requestDidError(t)}},{key:"requestDidError",value:function(t){this.callback('Error creating Blob for "'+this.file.name+'". Status: '+this.status)}},{key:"toJSON",value:function(){var t={};for(var e in this.attributes)t[e]=this.attributes[e];return t}},{key:"status",get:function(){return this.xhr.status}},{key:"response",get:function(){var t=this.xhr,e=t.responseType,r=t.response;return"json"==e?r:JSON.parse(r)}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return a});var i=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),a=function(){function t(e){var r=this;n(this,t),this.blob=e,this.file=e.file;var i=e.directUploadData,a=i.url,u=i.headers;this.xhr=new XMLHttpRequest,this.xhr.open("PUT",a,!0),this.xhr.responseType="text";for(var o in u)this.xhr.setRequestHeader(o,u[o]);this.xhr.addEventListener("load",function(t){return r.requestDidLoad(t)}),this.xhr.addEventListener("error",function(t){return r.requestDidError(t)})}return i(t,[{key:"create",value:function(t){this.callback=t,this.xhr.send(this.file.slice())}},{key:"requestDidLoad",value:function(t){var e=this.xhr,r=e.status,n=e.response;r>=200&&r<300?this.callback(null,n):this.requestDidError(t)}},{key:"requestDidError",value:function(t){this.callback('Error storing "'+this.file.name+'". Status: '+this.xhr.status)}}]),t}()}])});
\ No newline at end of file +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ActiveStorage=e():t.ActiveStorage=e()}(this,function(){return function(t){function e(n){if(r[n])return r[n].exports;var i=r[n]={i:n,l:!1,exports:{}};return t[n].call(i.exports,i,i.exports,e),i.l=!0,i.exports}var r={};return e.m=t,e.c=r,e.d=function(t,r,n){e.o(t,r)||Object.defineProperty(t,r,{configurable:!1,enumerable:!0,get:n})},e.n=function(t){var r=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(r,"a",r),r},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=2)}([function(t,e,r){"use strict";function n(t){var e=a(document.head,'meta[name="'+t+'"]');if(e)return e.getAttribute("content")}function i(t,e){return"string"==typeof t&&(e=t,t=document),o(t.querySelectorAll(e))}function a(t,e){return"string"==typeof t&&(e=t,t=document),t.querySelector(e)}function u(t,e){var r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{},n=t.disabled,i=r.bubbles,a=r.cancelable,u=r.detail,o=document.createEvent("Event");o.initEvent(e,i||!0,a||!0),o.detail=u||{};try{t.disabled=!1,t.dispatchEvent(o)}finally{t.disabled=n}return o}function o(t){return Array.isArray(t)?t:Array.from?Array.from(t):[].slice.call(t)}e.d=n,e.c=i,e.b=a,e.a=u,e.e=o},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){if(t&&"function"==typeof t[e]){for(var r=arguments.length,n=Array(r>2?r-2:0),i=2;i<r;i++)n[i-2]=arguments[i];return t[e].apply(t,n)}}r.d(e,"a",function(){return c});var a=r(6),u=r(8),o=r(9),s=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),f=0,c=function(){function t(e,r,i){n(this,t),this.id=++f,this.file=e,this.url=r,this.delegate=i}return s(t,[{key:"create",value:function(t){var e=this;a.a.create(this.file,function(r,n){if(r)return void t(r);var a=new u.a(e.file,n,e.url);i(e.delegate,"directUploadWillCreateBlobWithXHR",a.xhr),a.create(function(r){if(r)t(r);else{var n=new o.a(a);i(e.delegate,"directUploadWillStoreFileWithXHR",n.xhr),n.create(function(e){e?t(e):t(null,a.toJSON())})}})})}}]),t}()},function(t,e,r){"use strict";function n(){window.ActiveStorage&&Object(i.a)()}Object.defineProperty(e,"__esModule",{value:!0});var i=r(3),a=r(1);r.d(e,"start",function(){return i.a}),r.d(e,"DirectUpload",function(){return a.a}),setTimeout(n,1)},function(t,e,r){"use strict";function n(){d||(d=!0,document.addEventListener("submit",i),document.addEventListener("ajax:before",a))}function i(t){u(t)}function a(t){"FORM"==t.target.tagName&&u(t)}function u(t){var e=t.target;if(e.hasAttribute(l))return void t.preventDefault();var r=new c.a(e),n=r.inputs;n.length&&(t.preventDefault(),e.setAttribute(l,""),n.forEach(s),r.start(function(t){e.removeAttribute(l),t?n.forEach(f):o(e)}))}function o(t){var e=Object(h.b)(t,"input[type=submit]");if(e){var r=e,n=r.disabled;e.disabled=!1,e.focus(),e.click(),e.disabled=n}else e=document.createElement("input"),e.type="submit",e.style.display="none",t.appendChild(e),e.click(),t.removeChild(e)}function s(t){t.disabled=!0}function f(t){t.disabled=!1}e.a=n;var c=r(4),h=r(0),l="data-direct-uploads-processing",d=!1},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return s});var i=r(5),a=r(0),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o="input[type=file][data-direct-upload-url]:not([disabled])",s=function(){function t(e){n(this,t),this.form=e,this.inputs=Object(a.c)(e,o).filter(function(t){return t.files.length})}return u(t,[{key:"start",value:function(t){var e=this,r=this.createDirectUploadControllers();this.dispatch("start"),function n(){var i=r.shift();i?i.start(function(r){r?(t(r),e.dispatch("end")):n()}):(t(),e.dispatch("end"))}()}},{key:"createDirectUploadControllers",value:function(){var t=[];return this.inputs.forEach(function(e){Object(a.e)(e.files).forEach(function(r){var n=new i.a(e,r);t.push(n)})}),t}},{key:"dispatch",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return Object(a.a)(this.form,"direct-uploads:"+t,{detail:e})}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return o});var i=r(1),a=r(0),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o=function(){function t(e,r){n(this,t),this.input=e,this.file=r,this.directUpload=new i.a(this.file,this.url,this),this.dispatch("initialize")}return u(t,[{key:"start",value:function(t){var e=this,r=document.createElement("input");r.type="hidden",r.name=this.input.name,this.input.insertAdjacentElement("beforebegin",r),this.dispatch("start"),this.directUpload.create(function(n,i){n?(r.parentNode.removeChild(r),e.dispatchError(n)):r.value=i.signed_id,e.dispatch("end"),t(n)})}},{key:"uploadRequestDidProgress",value:function(t){var e=t.loaded/t.total*100;e&&this.dispatch("progress",{progress:e})}},{key:"dispatch",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:{};return e.file=this.file,e.id=this.directUpload.id,Object(a.a)(this.input,"direct-upload:"+t,{detail:e})}},{key:"dispatchError",value:function(t){this.dispatch("error",{error:t}).defaultPrevented||alert(t)}},{key:"directUploadWillCreateBlobWithXHR",value:function(t){this.dispatch("before-blob-request",{xhr:t})}},{key:"directUploadWillStoreFileWithXHR",value:function(t){var e=this;this.dispatch("before-storage-request",{xhr:t}),t.upload.addEventListener("progress",function(t){return e.uploadRequestDidProgress(t)})}},{key:"url",get:function(){return this.input.getAttribute("data-direct-upload-url")}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return s});var i=r(7),a=r.n(i),u=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),o=File.prototype.slice||File.prototype.mozSlice||File.prototype.webkitSlice,s=function(){function t(e){n(this,t),this.file=e,this.chunkSize=2097152,this.chunkCount=Math.ceil(this.file.size/this.chunkSize),this.chunkIndex=0}return u(t,null,[{key:"create",value:function(e,r){new t(e).create(r)}}]),u(t,[{key:"create",value:function(t){var e=this;this.callback=t,this.md5Buffer=new a.a.ArrayBuffer,this.fileReader=new FileReader,this.fileReader.addEventListener("load",function(t){return e.fileReaderDidLoad(t)}),this.fileReader.addEventListener("error",function(t){return e.fileReaderDidError(t)}),this.readNextChunk()}},{key:"fileReaderDidLoad",value:function(t){if(this.md5Buffer.append(t.target.result),!this.readNextChunk()){var e=this.md5Buffer.end(!0),r=btoa(e);this.callback(null,r)}}},{key:"fileReaderDidError",value:function(t){this.callback("Error reading "+this.file.name)}},{key:"readNextChunk",value:function(){if(this.chunkIndex<this.chunkCount){var t=this.chunkIndex*this.chunkSize,e=Math.min(t+this.chunkSize,this.file.size),r=o.call(this.file,t,e);return this.fileReader.readAsArrayBuffer(r),this.chunkIndex++,!0}return!1}}]),t}()},function(t,e,r){!function(e){t.exports=e()}(function(t){"use strict";function e(t,e){var r=t[0],n=t[1],i=t[2],a=t[3];r+=(n&i|~n&a)+e[0]-680876936|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[1]-389564586|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[2]+606105819|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[3]-1044525330|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[4]-176418897|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[5]+1200080426|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[6]-1473231341|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[7]-45705983|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[8]+1770035416|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[9]-1958414417|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[10]-42063|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[11]-1990404162|0,n=(n<<22|n>>>10)+i|0,r+=(n&i|~n&a)+e[12]+1804603682|0,r=(r<<7|r>>>25)+n|0,a+=(r&n|~r&i)+e[13]-40341101|0,a=(a<<12|a>>>20)+r|0,i+=(a&r|~a&n)+e[14]-1502002290|0,i=(i<<17|i>>>15)+a|0,n+=(i&a|~i&r)+e[15]+1236535329|0,n=(n<<22|n>>>10)+i|0,r+=(n&a|i&~a)+e[1]-165796510|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[6]-1069501632|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[11]+643717713|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[0]-373897302|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[5]-701558691|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[10]+38016083|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[15]-660478335|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[4]-405537848|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[9]+568446438|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[14]-1019803690|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[3]-187363961|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[8]+1163531501|0,n=(n<<20|n>>>12)+i|0,r+=(n&a|i&~a)+e[13]-1444681467|0,r=(r<<5|r>>>27)+n|0,a+=(r&i|n&~i)+e[2]-51403784|0,a=(a<<9|a>>>23)+r|0,i+=(a&n|r&~n)+e[7]+1735328473|0,i=(i<<14|i>>>18)+a|0,n+=(i&r|a&~r)+e[12]-1926607734|0,n=(n<<20|n>>>12)+i|0,r+=(n^i^a)+e[5]-378558|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[8]-2022574463|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[11]+1839030562|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[14]-35309556|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[1]-1530992060|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[4]+1272893353|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[7]-155497632|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[10]-1094730640|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[13]+681279174|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[0]-358537222|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[3]-722521979|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[6]+76029189|0,n=(n<<23|n>>>9)+i|0,r+=(n^i^a)+e[9]-640364487|0,r=(r<<4|r>>>28)+n|0,a+=(r^n^i)+e[12]-421815835|0,a=(a<<11|a>>>21)+r|0,i+=(a^r^n)+e[15]+530742520|0,i=(i<<16|i>>>16)+a|0,n+=(i^a^r)+e[2]-995338651|0,n=(n<<23|n>>>9)+i|0,r+=(i^(n|~a))+e[0]-198630844|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[7]+1126891415|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[14]-1416354905|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[5]-57434055|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[12]+1700485571|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[3]-1894986606|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[10]-1051523|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[1]-2054922799|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[8]+1873313359|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[15]-30611744|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[6]-1560198380|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[13]+1309151649|0,n=(n<<21|n>>>11)+i|0,r+=(i^(n|~a))+e[4]-145523070|0,r=(r<<6|r>>>26)+n|0,a+=(n^(r|~i))+e[11]-1120210379|0,a=(a<<10|a>>>22)+r|0,i+=(r^(a|~n))+e[2]+718787259|0,i=(i<<15|i>>>17)+a|0,n+=(a^(i|~r))+e[9]-343485551|0,n=(n<<21|n>>>11)+i|0,t[0]=r+t[0]|0,t[1]=n+t[1]|0,t[2]=i+t[2]|0,t[3]=a+t[3]|0}function r(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t.charCodeAt(e)+(t.charCodeAt(e+1)<<8)+(t.charCodeAt(e+2)<<16)+(t.charCodeAt(e+3)<<24);return r}function n(t){var e,r=[];for(e=0;e<64;e+=4)r[e>>2]=t[e]+(t[e+1]<<8)+(t[e+2]<<16)+(t[e+3]<<24);return r}function i(t){var n,i,a,u,o,s,f=t.length,c=[1732584193,-271733879,-1732584194,271733878];for(n=64;n<=f;n+=64)e(c,r(t.substring(n-64,n)));for(t=t.substring(n-64),i=t.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],n=0;n<i;n+=1)a[n>>2]|=t.charCodeAt(n)<<(n%4<<3);if(a[n>>2]|=128<<(n%4<<3),n>55)for(e(c,a),n=0;n<16;n+=1)a[n]=0;return u=8*f,u=u.toString(16).match(/(.*?)(.{0,8})$/),o=parseInt(u[2],16),s=parseInt(u[1],16)||0,a[14]=o,a[15]=s,e(c,a),c}function a(t){var r,i,a,u,o,s,f=t.length,c=[1732584193,-271733879,-1732584194,271733878];for(r=64;r<=f;r+=64)e(c,n(t.subarray(r-64,r)));for(t=r-64<f?t.subarray(r-64):new Uint8Array(0),i=t.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],r=0;r<i;r+=1)a[r>>2]|=t[r]<<(r%4<<3);if(a[r>>2]|=128<<(r%4<<3),r>55)for(e(c,a),r=0;r<16;r+=1)a[r]=0;return u=8*f,u=u.toString(16).match(/(.*?)(.{0,8})$/),o=parseInt(u[2],16),s=parseInt(u[1],16)||0,a[14]=o,a[15]=s,e(c,a),c}function u(t){var e,r="";for(e=0;e<4;e+=1)r+=p[t>>8*e+4&15]+p[t>>8*e&15];return r}function o(t){var e;for(e=0;e<t.length;e+=1)t[e]=u(t[e]);return t.join("")}function s(t){return/[\u0080-\uFFFF]/.test(t)&&(t=unescape(encodeURIComponent(t))),t}function f(t,e){var r,n=t.length,i=new ArrayBuffer(n),a=new Uint8Array(i);for(r=0;r<n;r+=1)a[r]=t.charCodeAt(r);return e?a:i}function c(t){return String.fromCharCode.apply(null,new Uint8Array(t))}function h(t,e,r){var n=new Uint8Array(t.byteLength+e.byteLength);return n.set(new Uint8Array(t)),n.set(new Uint8Array(e),t.byteLength),r?n:n.buffer}function l(t){var e,r=[],n=t.length;for(e=0;e<n-1;e+=2)r.push(parseInt(t.substr(e,2),16));return String.fromCharCode.apply(String,r)}function d(){this.reset()}var p=["0","1","2","3","4","5","6","7","8","9","a","b","c","d","e","f"];return"5d41402abc4b2a76b9719d911017c592"!==o(i("hello"))&&function(t,e){var r=(65535&t)+(65535&e);return(t>>16)+(e>>16)+(r>>16)<<16|65535&r},"undefined"==typeof ArrayBuffer||ArrayBuffer.prototype.slice||function(){function e(t,e){return t=0|t||0,t<0?Math.max(t+e,0):Math.min(t,e)}ArrayBuffer.prototype.slice=function(r,n){var i,a,u,o,s=this.byteLength,f=e(r,s),c=s;return n!==t&&(c=e(n,s)),f>c?new ArrayBuffer(0):(i=c-f,a=new ArrayBuffer(i),u=new Uint8Array(a),o=new Uint8Array(this,f,i),u.set(o),a)}}(),d.prototype.append=function(t){return this.appendBinary(s(t)),this},d.prototype.appendBinary=function(t){this._buff+=t,this._length+=t.length;var n,i=this._buff.length;for(n=64;n<=i;n+=64)e(this._hash,r(this._buff.substring(n-64,n)));return this._buff=this._buff.substring(n-64),this},d.prototype.end=function(t){var e,r,n=this._buff,i=n.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<i;e+=1)a[e>>2]|=n.charCodeAt(e)<<(e%4<<3);return this._finish(a,i),r=o(this._hash),t&&(r=l(r)),this.reset(),r},d.prototype.reset=function(){return this._buff="",this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},d.prototype.getState=function(){return{buff:this._buff,length:this._length,hash:this._hash}},d.prototype.setState=function(t){return this._buff=t.buff,this._length=t.length,this._hash=t.hash,this},d.prototype.destroy=function(){delete this._hash,delete this._buff,delete this._length},d.prototype._finish=function(t,r){var n,i,a,u=r;if(t[u>>2]|=128<<(u%4<<3),u>55)for(e(this._hash,t),u=0;u<16;u+=1)t[u]=0;n=8*this._length,n=n.toString(16).match(/(.*?)(.{0,8})$/),i=parseInt(n[2],16),a=parseInt(n[1],16)||0,t[14]=i,t[15]=a,e(this._hash,t)},d.hash=function(t,e){return d.hashBinary(s(t),e)},d.hashBinary=function(t,e){var r=i(t),n=o(r);return e?l(n):n},d.ArrayBuffer=function(){this.reset()},d.ArrayBuffer.prototype.append=function(t){var r,i=h(this._buff.buffer,t,!0),a=i.length;for(this._length+=t.byteLength,r=64;r<=a;r+=64)e(this._hash,n(i.subarray(r-64,r)));return this._buff=r-64<a?new Uint8Array(i.buffer.slice(r-64)):new Uint8Array(0),this},d.ArrayBuffer.prototype.end=function(t){var e,r,n=this._buff,i=n.length,a=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(e=0;e<i;e+=1)a[e>>2]|=n[e]<<(e%4<<3);return this._finish(a,i),r=o(this._hash),t&&(r=l(r)),this.reset(),r},d.ArrayBuffer.prototype.reset=function(){return this._buff=new Uint8Array(0),this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},d.ArrayBuffer.prototype.getState=function(){var t=d.prototype.getState.call(this);return t.buff=c(t.buff),t},d.ArrayBuffer.prototype.setState=function(t){return t.buff=f(t.buff,!0),d.prototype.setState.call(this,t)},d.ArrayBuffer.prototype.destroy=d.prototype.destroy,d.ArrayBuffer.prototype._finish=d.prototype._finish,d.ArrayBuffer.hash=function(t,e){var r=a(new Uint8Array(t)),n=o(r);return e?l(n):n},d})},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return u});var i=r(0),a=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),u=function(){function t(e,r,a){var u=this;n(this,t),this.file=e,this.attributes={filename:e.name,content_type:e.type,byte_size:e.size,checksum:r},this.xhr=new XMLHttpRequest,this.xhr.open("POST",a,!0),this.xhr.responseType="json",this.xhr.setRequestHeader("Content-Type","application/json"),this.xhr.setRequestHeader("Accept","application/json"),this.xhr.setRequestHeader("X-Requested-With","XMLHttpRequest"),this.xhr.setRequestHeader("X-CSRF-Token",Object(i.d)("csrf-token")),this.xhr.addEventListener("load",function(t){return u.requestDidLoad(t)}),this.xhr.addEventListener("error",function(t){return u.requestDidError(t)})}return a(t,[{key:"create",value:function(t){this.callback=t,this.xhr.send(JSON.stringify({blob:this.attributes}))}},{key:"requestDidLoad",value:function(t){if(this.status>=200&&this.status<300){var e=this.response,r=e.direct_upload;delete e.direct_upload,this.attributes=e,this.directUploadData=r,this.callback(null,this.toJSON())}else this.requestDidError(t)}},{key:"requestDidError",value:function(t){this.callback('Error creating Blob for "'+this.file.name+'". Status: '+this.status)}},{key:"toJSON",value:function(){var t={};for(var e in this.attributes)t[e]=this.attributes[e];return t}},{key:"status",get:function(){return this.xhr.status}},{key:"response",get:function(){var t=this.xhr,e=t.responseType,r=t.response;return"json"==e?r:JSON.parse(r)}}]),t}()},function(t,e,r){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}r.d(e,"a",function(){return a});var i=function(){function t(t,e){for(var r=0;r<e.length;r++){var n=e[r];n.enumerable=n.enumerable||!1,n.configurable=!0,"value"in n&&(n.writable=!0),Object.defineProperty(t,n.key,n)}}return function(e,r,n){return r&&t(e.prototype,r),n&&t(e,n),e}}(),a=function(){function t(e){var r=this;n(this,t),this.blob=e,this.file=e.file;var i=e.directUploadData,a=i.url,u=i.headers;this.xhr=new XMLHttpRequest,this.xhr.open("PUT",a,!0),this.xhr.responseType="text";for(var o in u)this.xhr.setRequestHeader(o,u[o]);this.xhr.addEventListener("load",function(t){return r.requestDidLoad(t)}),this.xhr.addEventListener("error",function(t){return r.requestDidError(t)})}return i(t,[{key:"create",value:function(t){this.callback=t,this.xhr.send(this.file.slice())}},{key:"requestDidLoad",value:function(t){var e=this.xhr,r=e.status,n=e.response;r>=200&&r<300?this.callback(null,n):this.requestDidError(t)}},{key:"requestDidError",value:function(t){this.callback('Error storing "'+this.file.name+'". Status: '+this.xhr.status)}}]),t}()}])}); diff --git a/activestorage/app/controllers/active_storage/base_controller.rb b/activestorage/app/controllers/active_storage/base_controller.rb new file mode 100644 index 0000000000..59312ac8df --- /dev/null +++ b/activestorage/app/controllers/active_storage/base_controller.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# The base controller for all ActiveStorage controllers. +class ActiveStorage::BaseController < ActionController::Base + protect_from_forgery with: :exception + + before_action do + ActiveStorage::Current.host = request.base_url + end +end diff --git a/activestorage/app/controllers/active_storage/blobs_controller.rb b/activestorage/app/controllers/active_storage/blobs_controller.rb index fa44131048..92e54c386d 100644 --- a/activestorage/app/controllers/active_storage/blobs_controller.rb +++ b/activestorage/app/controllers/active_storage/blobs_controller.rb @@ -4,7 +4,7 @@ # Note: These URLs are publicly accessible. If you need to enforce access protection beyond the # security-through-obscurity factor of the signed blob references, you'll need to implement your own # authenticated redirection controller. -class ActiveStorage::BlobsController < ActionController::Base +class ActiveStorage::BlobsController < ActiveStorage::BaseController include ActiveStorage::SetBlob def show diff --git a/activestorage/app/controllers/active_storage/direct_uploads_controller.rb b/activestorage/app/controllers/active_storage/direct_uploads_controller.rb index 205d173648..78b43fc94c 100644 --- a/activestorage/app/controllers/active_storage/direct_uploads_controller.rb +++ b/activestorage/app/controllers/active_storage/direct_uploads_controller.rb @@ -3,7 +3,7 @@ # Creates a new blob on the server side in anticipation of a direct-to-service upload from the client side. # When the client-side upload is completed, the signed_blob_id can be submitted as part of the form to reference # the blob that was created up front. -class ActiveStorage::DirectUploadsController < ActionController::Base +class ActiveStorage::DirectUploadsController < ActiveStorage::BaseController def create blob = ActiveStorage::Blob.create_before_direct_upload!(blob_args) render json: direct_upload_json(blob) @@ -15,7 +15,7 @@ class ActiveStorage::DirectUploadsController < ActionController::Base end def direct_upload_json(blob) - blob.as_json(methods: :signed_id).merge(direct_upload: { + blob.as_json(root: false, methods: :signed_id).merge(direct_upload: { url: blob.service_url_for_direct_upload, headers: blob.service_headers_for_direct_upload }) diff --git a/activestorage/app/controllers/active_storage/disk_controller.rb b/activestorage/app/controllers/active_storage/disk_controller.rb index a7e10c0696..7bc5eb3fdb 100644 --- a/activestorage/app/controllers/active_storage/disk_controller.rb +++ b/activestorage/app/controllers/active_storage/disk_controller.rb @@ -4,8 +4,8 @@ # This means using expiring, signed URLs that are meant for immediate access, not permanent linking. # Always go through the BlobsController, or your own authenticated controller, rather than directly # to the service url. -class ActiveStorage::DiskController < ActionController::Base - skip_forgery_protection if default_protect_from_forgery +class ActiveStorage::DiskController < ActiveStorage::BaseController + skip_forgery_protection def show if key = decode_verified_key diff --git a/activestorage/app/controllers/active_storage/representations_controller.rb b/activestorage/app/controllers/active_storage/representations_controller.rb index e0e944dc9c..ce9286db7d 100644 --- a/activestorage/app/controllers/active_storage/representations_controller.rb +++ b/activestorage/app/controllers/active_storage/representations_controller.rb @@ -4,7 +4,7 @@ # Note: These URLs are publicly accessible. If you need to enforce access protection beyond the # security-through-obscurity factor of the signed blob and variation reference, you'll need to implement your own # authenticated redirection controller. -class ActiveStorage::RepresentationsController < ActionController::Base +class ActiveStorage::RepresentationsController < ActiveStorage::BaseController include ActiveStorage::SetBlob def show diff --git a/activestorage/app/javascript/activestorage/ujs.js b/activestorage/app/javascript/activestorage/ujs.js index 1dda02936f..08c535470d 100644 --- a/activestorage/app/javascript/activestorage/ujs.js +++ b/activestorage/app/javascript/activestorage/ujs.js @@ -59,7 +59,7 @@ function submitForm(form) { } else { button = document.createElement("input") button.type = "submit" - button.style = "display:none" + button.style.display = "none" form.appendChild(button) button.click() form.removeChild(button) diff --git a/activestorage/app/models/active_storage/attachment.rb b/activestorage/app/models/active_storage/attachment.rb index 19f48c57d6..c59877a9a5 100644 --- a/activestorage/app/models/active_storage/attachment.rb +++ b/activestorage/app/models/active_storage/attachment.rb @@ -14,7 +14,7 @@ class ActiveStorage::Attachment < ActiveRecord::Base delegate_missing_to :blob - after_create_commit :identify_blob, :analyze_blob_later + after_create_commit :analyze_blob_later, :identify_blob # Synchronously purges the blob (deletes it from the configured service) and destroys the attachment. def purge diff --git a/activestorage/app/models/active_storage/blob/identifiable.rb b/activestorage/app/models/active_storage/blob/identifiable.rb index dbe03cfa6c..049e45dc3e 100644 --- a/activestorage/app/models/active_storage/blob/identifiable.rb +++ b/activestorage/app/models/active_storage/blob/identifiable.rb @@ -2,7 +2,7 @@ module ActiveStorage::Blob::Identifiable def identify - update!(content_type: identification.content_type, identified: true) unless identified? + update! content_type: identify_content_type, identified: true unless identified? end def identified? @@ -10,7 +10,11 @@ module ActiveStorage::Blob::Identifiable end private - def identification - ActiveStorage::Identification.new self + def identify_content_type + Marcel::MimeType.for download_identifiable_chunk, name: filename.to_s, declared_type: content_type + end + + def download_identifiable_chunk + service.download_chunk key, 0...4.kilobytes end end diff --git a/activestorage/app/models/active_storage/blob/representable.rb b/activestorage/app/models/active_storage/blob/representable.rb index 88fc25b7ae..fea62e62de 100644 --- a/activestorage/app/models/active_storage/blob/representable.rb +++ b/activestorage/app/models/active_storage/blob/representable.rb @@ -20,7 +20,7 @@ module ActiveStorage::Blob::Representable # # <%= image_tag Current.user.avatar.variant(resize: "100x100") %> # - # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController + # This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController # can then produce on-demand. # # Raises ActiveStorage::InvariableError if ImageMagick cannot transform the blob. To determine whether a blob is diff --git a/activestorage/app/models/active_storage/current.rb b/activestorage/app/models/active_storage/current.rb new file mode 100644 index 0000000000..7e431d8462 --- /dev/null +++ b/activestorage/app/models/active_storage/current.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +class ActiveStorage::Current < ActiveSupport::CurrentAttributes #:nodoc: + attribute :host +end diff --git a/activestorage/app/models/active_storage/identification.rb b/activestorage/app/models/active_storage/identification.rb deleted file mode 100644 index 8d334ae1ea..0000000000 --- a/activestorage/app/models/active_storage/identification.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -require "net/http" - -class ActiveStorage::Identification #:nodoc: - attr_reader :blob - - def initialize(blob) - @blob = blob - end - - def content_type - Marcel::MimeType.for(identifiable_chunk, name: filename, declared_type: declared_content_type) - end - - private - def identifiable_chunk - Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |client| - client.get(uri, "Range" => "bytes=0-4095").body - end - end - - def uri - @uri ||= URI.parse(blob.service_url) - end - - - def filename - blob.filename.to_s - end - - def declared_content_type - blob.content_type - end -end diff --git a/activestorage/app/models/active_storage/preview.rb b/activestorage/app/models/active_storage/preview.rb index 45efd26214..2b87897183 100644 --- a/activestorage/app/models/active_storage/preview.rb +++ b/activestorage/app/models/active_storage/preview.rb @@ -21,10 +21,9 @@ # # Outside of a Rails application, modify +ActiveStorage.previewers+ instead. # -# The built-in previewers rely on third-party system libraries: -# -# * {ffmpeg}[https://www.ffmpeg.org] -# * {mupdf}[https://mupdf.com] (version 1.8 or newer) +# The built-in previewers rely on third-party system libraries. Specifically, the built-in video previewer requires +# {ffmpeg}[https://www.ffmpeg.org]. Two PDF previewers are provided: one requires {Poppler}[https://poppler.freedesktop.org], +# and the other requires {mupdf}[https://mupdf.com] (version 1.8 or newer). To preview PDFs, install either Poppler or mupdf. # # These libraries are not provided by Rails. You must install them yourself to use the built-in previewers. Before you # install and use third-party software, make sure you understand the licensing implications of doing so. diff --git a/activestorage/app/models/active_storage/variant.rb b/activestorage/app/models/active_storage/variant.rb index a95a4bfd7c..d84208419c 100644 --- a/activestorage/app/models/active_storage/variant.rb +++ b/activestorage/app/models/active_storage/variant.rb @@ -13,14 +13,14 @@ require "active_storage/downloading" # into memory. The larger the image, the more memory is used. Because of this process, you also want to be # considerate about when the variant is actually processed. You shouldn't be processing variants inline in a # template, for example. Delay the processing to an on-demand controller, like the one provided in -# ActiveStorage::VariantsController. +# ActiveStorage::RepresentationsController. # # To refer to such a delayed on-demand variant, simply link to the variant through the resolved route provided # by Active Storage like so: # # <%= image_tag Current.user.avatar.variant(resize: "100x100") %> # -# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::VariantsController +# This will create a URL for that specific blob with that specific variant, which the ActiveStorage::RepresentationsController # can then produce on-demand. # # When you do want to actually produce the variant needed, call +processed+. This will check that the variant @@ -65,7 +65,7 @@ class ActiveStorage::Variant # it allows permanent URLs that redirect to the +service_url+ to be cached in the view. # # Use <tt>url_for(variant)</tt> (or the implied form, like +link_to variant+ or +redirect_to variant+) to get the stable URL - # for a variant that points to the ActiveStorage::VariantsController, which in turn will use this +service_call+ method + # for a variant that points to the ActiveStorage::RepresentationsController, which in turn will use this +service_call+ method # for its redirection. def service_url(expires_in: service.url_expires_in, disposition: :inline) service.url key, expires_in: expires_in, disposition: disposition, filename: filename, content_type: content_type diff --git a/activestorage/lib/active_storage/attached/macros.rb b/activestorage/lib/active_storage/attached/macros.rb index c51efa9d6b..819f00cc06 100644 --- a/activestorage/lib/active_storage/attached/macros.rb +++ b/activestorage/lib/active_storage/attached/macros.rb @@ -38,13 +38,15 @@ module ActiveStorage end CODE - has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record + has_one :"#{name}_attachment", -> { where(name: name) }, class_name: "ActiveStorage::Attachment", as: :record, inverse_of: :record, dependent: false has_one :"#{name}_blob", through: :"#{name}_attachment", class_name: "ActiveStorage::Blob", source: :blob scope :"with_attached_#{name}", -> { includes("#{name}_attachment": :blob) } if dependent == :purge_later after_destroy_commit { public_send(name).purge_later } + else + before_destroy { public_send(name).detach } end end @@ -83,13 +85,25 @@ module ActiveStorage end CODE - has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record + has_many :"#{name}_attachments", -> { where(name: name) }, as: :record, class_name: "ActiveStorage::Attachment", inverse_of: :record, dependent: false do + def purge + each(&:purge) + reset + end + + def purge_later + each(&:purge_later) + reset + end + end has_many :"#{name}_blobs", through: :"#{name}_attachments", class_name: "ActiveStorage::Blob", source: :blob scope :"with_attached_#{name}", -> { includes("#{name}_attachments": :blob) } if dependent == :purge_later after_destroy_commit { public_send(name).purge_later } + else + before_destroy { public_send(name).detach } end end end diff --git a/activestorage/lib/active_storage/attached/many.rb b/activestorage/lib/active_storage/attached/many.rb index 6eace65b79..d61acb6fad 100644 --- a/activestorage/lib/active_storage/attached/many.rb +++ b/activestorage/lib/active_storage/attached/many.rb @@ -44,20 +44,16 @@ module ActiveStorage attachments.destroy_all if attached? end + ## + # :method: purge + # # Directly purges each associated attachment (i.e. destroys the blobs and # attachments and deletes the files on the service). - def purge - if attached? - attachments.each(&:purge) - attachments.reload - end - end + + ## + # :method: purge_later + # # Purges each associated attachment through the queuing system. - def purge_later - if attached? - attachments.each(&:purge_later) - end - end end end diff --git a/activestorage/lib/active_storage/attached/one.rb b/activestorage/lib/active_storage/attached/one.rb index 008f5a796b..a582ed0137 100644 --- a/activestorage/lib/active_storage/attached/one.rb +++ b/activestorage/lib/active_storage/attached/one.rb @@ -20,10 +20,16 @@ module ActiveStorage # person.avatar.attach(io: File.open("/path/to/face.jpg"), filename: "face.jpg", content_type: "image/jpg") # person.avatar.attach(avatar_blob) # ActiveStorage::Blob object def attach(attachable) - if attached? && dependent == :purge_later - replace attachable - else - write_attachment build_attachment_from(attachable) + blob_was = blob if attached? + blob = create_blob_from(attachable) + + unless blob == blob_was + transaction do + detach + write_attachment build_attachment(blob: blob) + end + + blob_was.purge_later if blob_was && dependent == :purge_later end end @@ -63,23 +69,7 @@ module ActiveStorage end private - def replace(attachable) - blob_was = blob - blob = create_blob_from(attachable) - - unless blob == blob_was - transaction do - detach - write_attachment build_attachment(blob: blob) - end - - blob_was.purge_later - end - end - - def build_attachment_from(attachable) - build_attachment blob: create_blob_from(attachable) - end + delegate :transaction, to: :record def build_attachment(blob:) ActiveStorage::Attachment.new(record: record, name: name, blob: blob) diff --git a/activestorage/lib/active_storage/engine.rb b/activestorage/lib/active_storage/engine.rb index 1e223f9f17..1385e2aa84 100644 --- a/activestorage/lib/active_storage/engine.rb +++ b/activestorage/lib/active_storage/engine.rb @@ -3,7 +3,8 @@ require "rails" require "active_storage" -require "active_storage/previewer/pdf_previewer" +require "active_storage/previewer/poppler_pdf_previewer" +require "active_storage/previewer/mupdf_previewer" require "active_storage/previewer/video_previewer" require "active_storage/analyzer/image_analyzer" @@ -14,7 +15,7 @@ module ActiveStorage isolate_namespace ActiveStorage config.active_storage = ActiveSupport::OrderedOptions.new - config.active_storage.previewers = [ ActiveStorage::Previewer::PDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ] + config.active_storage.previewers = [ ActiveStorage::Previewer::PopplerPDFPreviewer, ActiveStorage::Previewer::MuPDFPreviewer, ActiveStorage::Previewer::VideoPreviewer ] config.active_storage.analyzers = [ ActiveStorage::Analyzer::ImageAnalyzer, ActiveStorage::Analyzer::VideoAnalyzer ] config.active_storage.paths = ActiveSupport::OrderedOptions.new diff --git a/activestorage/lib/active_storage/previewer/mupdf_previewer.rb b/activestorage/lib/active_storage/previewer/mupdf_previewer.rb new file mode 100644 index 0000000000..ae02a4889d --- /dev/null +++ b/activestorage/lib/active_storage/previewer/mupdf_previewer.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module ActiveStorage + class Previewer::MuPDFPreviewer < Previewer + class << self + def accept?(blob) + blob.content_type == "application/pdf" && mutool_exists? + end + + def mutool_path + ActiveStorage.paths[:mutool] || "mutool" + end + + def mutool_exists? + return @mutool_exists unless @mutool_exists.nil? + + system mutool_path, out: File::NULL, err: File::NULL + + @mutool_exists = $?.exitstatus == 1 + end + end + + def preview + download_blob_to_tempfile do |input| + draw_first_page_from input do |output| + yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png" + end + end + end + + private + def draw_first_page_from(file, &block) + draw self.class.mutool_path, "draw", "-F", "png", "-o", "-", file.path, "1", &block + end + end +end diff --git a/activestorage/lib/active_storage/previewer/pdf_previewer.rb b/activestorage/lib/active_storage/previewer/pdf_previewer.rb deleted file mode 100644 index 426ff51eb1..0000000000 --- a/activestorage/lib/active_storage/previewer/pdf_previewer.rb +++ /dev/null @@ -1,26 +0,0 @@ -# frozen_string_literal: true - -module ActiveStorage - class Previewer::PDFPreviewer < Previewer - def self.accept?(blob) - blob.content_type == "application/pdf" - end - - def preview - download_blob_to_tempfile do |input| - draw_first_page_from input do |output| - yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png" - end - end - end - - private - def draw_first_page_from(file, &block) - draw mutool_path, "draw", "-F", "png", "-o", "-", file.path, "1", &block - end - - def mutool_path - ActiveStorage.paths[:mutool] || "mutool" - end - end -end diff --git a/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb b/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb new file mode 100644 index 0000000000..2a787362cf --- /dev/null +++ b/activestorage/lib/active_storage/previewer/poppler_pdf_previewer.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module ActiveStorage + class Previewer::PopplerPDFPreviewer < Previewer + class << self + def accept?(blob) + blob.content_type == "application/pdf" && pdftoppm_exists? + end + + def pdftoppm_path + ActiveStorage.paths[:pdftoppm] || "pdftoppm" + end + + def pdftoppm_exists? + return @pdftoppm_exists unless @pdftoppm_exists.nil? + + @pdftoppm_exists = system(pdftoppm_path, "-v", out: File::NULL, err: File::NULL) + end + end + + def preview + download_blob_to_tempfile do |input| + draw_first_page_from input do |output| + yield io: output, filename: "#{blob.filename.base}.png", content_type: "image/png" + end + end + end + + private + def draw_first_page_from(file, &block) + # use 72 dpi to match thumbnail dimesions of the PDF + draw self.class.pdftoppm_path, "-singlefile", "-r", "72", "-png", file.path, &block + end + end +end diff --git a/activestorage/lib/active_storage/service.rb b/activestorage/lib/active_storage/service.rb index f2e1269f27..949969fc95 100644 --- a/activestorage/lib/active_storage/service.rb +++ b/activestorage/lib/active_storage/service.rb @@ -73,6 +73,11 @@ module ActiveStorage raise NotImplementedError end + # Return the partial content in the byte +range+ of the file at the +key+. + def download_chunk(key, range) + raise NotImplementedError + end + # Delete the file at the +key+. def delete(key) raise NotImplementedError diff --git a/activestorage/lib/active_storage/service/azure_storage_service.rb b/activestorage/lib/active_storage/service/azure_storage_service.rb index 0a9eb7f23d..2867a4e441 100644 --- a/activestorage/lib/active_storage/service/azure_storage_service.rb +++ b/activestorage/lib/active_storage/service/azure_storage_service.rb @@ -8,14 +8,13 @@ module ActiveStorage # Wraps the Microsoft Azure Storage Blob Service as an Active Storage service. # See ActiveStorage::Service for the generic API documentation that applies to all services. class Service::AzureStorageService < Service - attr_reader :client, :path, :blobs, :container, :signer + attr_reader :client, :blobs, :container, :signer - def initialize(path:, storage_account_name:, storage_access_key:, container:) + def initialize(storage_account_name:, storage_access_key:, container:) @client = Azure::Storage::Client.create(storage_account_name: storage_account_name, storage_access_key: storage_access_key) @signer = Azure::Storage::Core::Auth::SharedAccessSignature.new(storage_account_name, storage_access_key) @blobs = client.blob_client @container = container - @path = path end def upload(key, io, checksum: nil) @@ -41,6 +40,13 @@ module ActiveStorage end end + def download_chunk(key, range) + instrument :download_chunk, key: key, range: range do + _, io = blobs.get_blob(container, key, start_range: range.begin, end_range: range.exclude_end? ? range.end - 1 : range.end) + io.force_encoding(Encoding::BINARY) + end + end + def delete(key) instrument :delete, key: key do begin @@ -77,9 +83,9 @@ module ActiveStorage def url(key, expires_in:, filename:, disposition:, content_type:) instrument :url, key: key do |payload| - base_url = url_for(key) generated_url = signer.signed_uri( - URI(base_url), false, + uri_for(key), false, + service: "b", permissions: "r", expiry: format_expiry(expires_in), content_disposition: content_disposition_with(type: disposition, filename: filename), @@ -94,9 +100,12 @@ module ActiveStorage def url_for_direct_upload(key, expires_in:, content_type:, content_length:, checksum:) instrument :url, key: key do |payload| - base_url = url_for(key) - generated_url = signer.signed_uri(URI(base_url), false, permissions: "rw", - expiry: format_expiry(expires_in)).to_s + generated_url = signer.signed_uri( + uri_for(key), false, + service: "b", + permissions: "rw", + expiry: format_expiry(expires_in) + ).to_s payload[:url] = generated_url @@ -109,8 +118,8 @@ module ActiveStorage end private - def url_for(key) - "#{path}/#{container}/#{key}" + def uri_for(key) + blobs.generate_uri("#{container}/#{key}") end def blob_for(key) diff --git a/activestorage/lib/active_storage/service/disk_service.rb b/activestorage/lib/active_storage/service/disk_service.rb index d17eea9046..5b652fe74e 100644 --- a/activestorage/lib/active_storage/service/disk_service.rb +++ b/activestorage/lib/active_storage/service/disk_service.rb @@ -9,10 +9,10 @@ module ActiveStorage # Wraps a local disk path as an Active Storage service. See ActiveStorage::Service for the generic API # documentation that applies to all services. class Service::DiskService < Service - attr_reader :root, :host + attr_reader :root - def initialize(root:, host: "http://localhost:3000") - @root, @host = root, host + def initialize(root:) + @root = root end def upload(key, io, checksum: nil) @@ -38,6 +38,15 @@ module ActiveStorage end end + def download_chunk(key, range) + instrument :download_chunk, key: key, range: range do + File.open(path_for(key), "rb") do |file| + file.seek range.begin + file.read range.size + end + end + end + def delete(key) instrument :delete, key: key do begin @@ -69,12 +78,12 @@ module ActiveStorage verified_key_with_expiration = ActiveStorage.verifier.generate(key, expires_in: expires_in, purpose: :blob_key) generated_url = - Rails.application.routes.url_helpers.rails_disk_service_url( + url_helpers.rails_disk_service_url( verified_key_with_expiration, + host: current_host, filename: filename, disposition: content_disposition_with(type: disposition, filename: filename), - content_type: content_type, - host: host + content_type: content_type ) payload[:url] = generated_url @@ -96,7 +105,7 @@ module ActiveStorage purpose: :blob_token } ) - generated_url = Rails.application.routes.url_helpers.update_rails_disk_service_url(verified_token_with_expiration, host: host) + generated_url = url_helpers.update_rails_disk_service_url(verified_token_with_expiration, host: current_host) payload[:url] = generated_url @@ -127,5 +136,13 @@ module ActiveStorage raise ActiveStorage::IntegrityError end end + + def url_helpers + @url_helpers ||= Rails.application.routes.url_helpers + end + + def current_host + ActiveStorage::Current.host + end end end diff --git a/activestorage/lib/active_storage/service/gcs_service.rb b/activestorage/lib/active_storage/service/gcs_service.rb index d163605754..369c33cbdb 100644 --- a/activestorage/lib/active_storage/service/gcs_service.rb +++ b/activestorage/lib/active_storage/service/gcs_service.rb @@ -3,7 +3,10 @@ gem "google-cloud-storage", "~> 1.8" require "google/cloud/storage" +require "net/http" + require "active_support/core_ext/object/to_query" +require "active_storage/filename" module ActiveStorage # Wraps the Google Cloud Storage as an Active Storage service. See ActiveStorage::Service for the generic API @@ -43,6 +46,16 @@ module ActiveStorage end end + def download_chunk(key, range) + instrument :download_chunk, key: key, range: range do + uri = URI(url(key, expires_in: 30.seconds, filename: ActiveStorage::Filename.new(""), content_type: "application/octet-stream", disposition: "inline")) + + Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |client| + client.get(uri, "Range" => "bytes=#{range.begin}-#{range.exclude_end? ? range.end - 1 : range.end}").body + end + end + end + def delete(key) instrument :delete, key: key do begin @@ -91,7 +104,7 @@ module ActiveStorage end def headers_for_direct_upload(key, checksum:, **) - { "Content-Type" => "", "Content-MD5" => checksum } + { "Content-MD5" => checksum } end private diff --git a/activestorage/lib/active_storage/service/mirror_service.rb b/activestorage/lib/active_storage/service/mirror_service.rb index 7eca8ce7f4..6002ef5a00 100644 --- a/activestorage/lib/active_storage/service/mirror_service.rb +++ b/activestorage/lib/active_storage/service/mirror_service.rb @@ -9,7 +9,7 @@ module ActiveStorage class Service::MirrorService < Service attr_reader :primary, :mirrors - delegate :download, :exist?, :url, to: :primary + delegate :download, :download_chunk, :exist?, :url, to: :primary # Stitch together from named services. def self.build(primary:, mirrors:, configurator:, **options) #:nodoc: diff --git a/activestorage/lib/active_storage/service/s3_service.rb b/activestorage/lib/active_storage/service/s3_service.rb index c95672f338..5e489f4be1 100644 --- a/activestorage/lib/active_storage/service/s3_service.rb +++ b/activestorage/lib/active_storage/service/s3_service.rb @@ -9,8 +9,8 @@ module ActiveStorage class Service::S3Service < Service attr_reader :client, :bucket, :upload_options - def initialize(access_key_id:, secret_access_key:, region:, bucket:, upload: {}, **options) - @client = Aws::S3::Resource.new(access_key_id: access_key_id, secret_access_key: secret_access_key, region: region, **options) + def initialize(bucket:, upload: {}, **options) + @client = Aws::S3::Resource.new(**options) @bucket = @client.bucket(bucket) @upload_options = upload @@ -38,6 +38,12 @@ module ActiveStorage end end + def download_chunk(key, range) + instrument :download_chunk, key: key, range: range do + object_for(key).get(range: "bytes=#{range.begin}-#{range.exclude_end? ? range.end - 1 : range.end}").body.read.force_encoding(Encoding::BINARY) + end + end + def delete(key) instrument :delete, key: key do object_for(key).delete diff --git a/activestorage/test/controllers/direct_uploads_controller_test.rb b/activestorage/test/controllers/direct_uploads_controller_test.rb index 5d57e37688..1b16da17d9 100644 --- a/activestorage/test/controllers/direct_uploads_controller_test.rb +++ b/activestorage/test/controllers/direct_uploads_controller_test.rb @@ -62,7 +62,7 @@ if SERVICE_CONFIGURATIONS[:gcs] assert_equal checksum, details["checksum"] assert_equal "text/plain", details["content_type"] assert_match %r{storage\.googleapis\.com/#{@config[:bucket]}}, details["direct_upload"]["url"] - assert_equal({ "Content-Type" => "", "Content-MD5" => checksum }, details["direct_upload"]["headers"]) + assert_equal({ "Content-MD5" => checksum }, details["direct_upload"]["headers"]) end end end @@ -121,4 +121,27 @@ class ActiveStorage::DiskDirectUploadsControllerTest < ActionDispatch::Integrati assert_equal({ "Content-Type" => "text/plain" }, details["direct_upload"]["headers"]) end end + + test "creating new direct upload does not include root in json" do + checksum = Digest::MD5.base64digest("Hello") + + set_include_root_in_json(true) do + post rails_direct_uploads_url, params: { blob: { + filename: "hello.txt", byte_size: 6, checksum: checksum, content_type: "text/plain" } } + end + + @response.parsed_body.tap do |details| + assert_nil details["blob"] + assert_not_nil details["id"] + end + end + + private + def set_include_root_in_json(value) + original = ActiveRecord::Base.include_root_in_json + ActiveRecord::Base.include_root_in_json = value + yield + ensure + ActiveRecord::Base.include_root_in_json = original + end end diff --git a/activestorage/test/models/attachments_test.rb b/activestorage/test/models/attachments_test.rb index 25e0352eca..ce83ec27d2 100644 --- a/activestorage/test/models/attachments_test.rb +++ b/activestorage/test/models/attachments_test.rb @@ -80,6 +80,20 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase end end + test "replace independent attached blob" do + @user.cover_photo.attach create_blob(filename: "funky.jpg") + + perform_enqueued_jobs do + assert_difference -> { ActiveStorage::Blob.count }, +1 do + assert_no_difference -> { ActiveStorage::Attachment.count } do + @user.cover_photo.attach create_blob(filename: "town.jpg") + end + end + end + + assert_equal "town.jpg", @user.cover_photo.filename.to_s + end + test "attach blob to new record" do user = User.new(name: "Jason") @@ -122,17 +136,26 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase end test "identify newly-attached, directly-uploaded blob" do - # Simulate a direct upload. - blob = create_blob_before_direct_upload(filename: "racecar.jpg", content_type: "application/octet-stream", byte_size: 1124062, checksum: "7GjDDNEQb4mzMzsW+MS0JQ==") - ActiveStorage::Blob.service.upload(blob.key, file_fixture("racecar.jpg").open) + blob = directly_upload_file_blob(content_type: "application/octet-stream") - stub_request(:get, %r{localhost:3000/rails/active_storage/disk/.*}).to_return(body: file_fixture("racecar.jpg")) @user.avatar.attach(blob) assert_equal "image/jpeg", @user.avatar.reload.content_type assert_predicate @user.avatar, :identified? end + test "identify and analyze newly-attached, directly-uploaded blob" do + blob = directly_upload_file_blob(content_type: "application/octet-stream") + + perform_enqueued_jobs do + @user.avatar.attach blob + end + + assert_equal true, @user.avatar.reload.metadata[:identified] + assert_equal 4104, @user.avatar.metadata[:width] + assert_equal 2736, @user.avatar.metadata[:height] + end + test "identify newly-attached blob only once" do blob = create_file_blob assert_predicate blob, :identified? @@ -204,13 +227,20 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase avatar_key = @user.avatar.key perform_enqueued_jobs do - @user.destroy + @user.reload.destroy assert_nil ActiveStorage::Blob.find_by(key: avatar_key) assert_not ActiveStorage::Blob.service.exist?(avatar_key) end end + test "delete attachment for independent blob when record is destroyed" do + @user.cover_photo.attach create_blob(filename: "funky.jpg") + + @user.destroy + assert_not ActiveStorage::Attachment.exists?(record: @user, name: "cover_photo") + end + test "find with attached blob" do records = %w[alice bob].map do |name| User.create!(name: name).tap do |user| @@ -382,7 +412,7 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase highlight_keys = @user.highlights.collect(&:key) perform_enqueued_jobs do - @user.destroy + @user.reload.destroy assert_nil ActiveStorage::Blob.find_by(key: highlight_keys.first) assert_not ActiveStorage::Blob.service.exist?(highlight_keys.first) @@ -391,4 +421,39 @@ class ActiveStorage::AttachmentsTest < ActiveSupport::TestCase assert_not ActiveStorage::Blob.service.exist?(highlight_keys.second) end end + + test "delete attachments for independent blobs when the record is destroyed" do + @user.vlogs.attach create_blob(filename: "funky.mp4"), create_blob(filename: "wonky.mp4") + + @user.destroy + assert_not ActiveStorage::Attachment.exists?(record: @user, name: "vlogs") + end + + test "selectively purge one attached blob of many" do + first_blob = create_blob(filename: "funky.jpg") + second_blob = create_blob(filename: "wonky.jpg") + attachments = @user.highlights.attach(first_blob, second_blob) + + assert_difference -> { ActiveStorage::Blob.count }, -1 do + @user.highlights.where(id: attachments.first.id).purge + end + + assert_not ActiveStorage::Blob.exists?(key: first_blob.key) + assert ActiveStorage::Blob.exists?(key: second_blob.key) + end + + test "selectively purge one attached blob of many later" do + first_blob = create_blob(filename: "funky.jpg") + second_blob = create_blob(filename: "wonky.jpg") + attachments = @user.highlights.attach(first_blob, second_blob) + + perform_enqueued_jobs do + assert_difference -> { ActiveStorage::Blob.count }, -1 do + @user.highlights.where(id: attachments.first.id).purge_later + end + end + + assert_not ActiveStorage::Blob.exists?(key: first_blob.key) + assert ActiveStorage::Blob.exists?(key: second_blob.key) + end end diff --git a/activestorage/test/models/blob_test.rb b/activestorage/test/models/blob_test.rb index 779e47ffb6..fead17d33a 100644 --- a/activestorage/test/models/blob_test.rb +++ b/activestorage/test/models/blob_test.rb @@ -140,6 +140,6 @@ class ActiveStorage::BlobTest < ActiveSupport::TestCase def expected_url_for(blob, disposition: :inline, filename: nil) filename ||= blob.filename query_string = { content_type: blob.content_type, disposition: "#{disposition}; #{filename.parameters}" }.to_param - "http://localhost:3000/rails/active_storage/disk/#{ActiveStorage.verifier.generate(blob.key, expires_in: 5.minutes, purpose: :blob_key)}/#{filename}?#{query_string}" + "https://example.com/rails/active_storage/disk/#{ActiveStorage.verifier.generate(blob.key, expires_in: 5.minutes, purpose: :blob_key)}/#{filename}?#{query_string}" end end diff --git a/activestorage/test/previewer/pdf_previewer_test.rb b/activestorage/test/previewer/mupdf_previewer_test.rb index fe32f39be4..6c2db6fcbf 100644 --- a/activestorage/test/previewer/pdf_previewer_test.rb +++ b/activestorage/test/previewer/mupdf_previewer_test.rb @@ -3,15 +3,15 @@ require "test_helper" require "database/setup" -require "active_storage/previewer/pdf_previewer" +require "active_storage/previewer/mupdf_previewer" -class ActiveStorage::Previewer::PDFPreviewerTest < ActiveSupport::TestCase +class ActiveStorage::Previewer::MuPDFPreviewerTest < ActiveSupport::TestCase setup do @blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf") end test "previewing a PDF document" do - ActiveStorage::Previewer::PDFPreviewer.new(@blob).preview do |attachable| + ActiveStorage::Previewer::MuPDFPreviewer.new(@blob).preview do |attachable| assert_equal "image/png", attachable[:content_type] assert_equal "report.png", attachable[:filename] diff --git a/activestorage/test/previewer/poppler_pdf_previewer_test.rb b/activestorage/test/previewer/poppler_pdf_previewer_test.rb new file mode 100644 index 0000000000..2b41c8b642 --- /dev/null +++ b/activestorage/test/previewer/poppler_pdf_previewer_test.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +require "test_helper" +require "database/setup" + +require "active_storage/previewer/poppler_pdf_previewer" + +class ActiveStorage::Previewer::PopplerPDFPreviewerTest < ActiveSupport::TestCase + setup do + @blob = create_file_blob(filename: "report.pdf", content_type: "application/pdf") + end + + test "previewing a PDF document" do + ActiveStorage::Previewer::PopplerPDFPreviewer.new(@blob).preview do |attachable| + assert_equal "image/png", attachable[:content_type] + assert_equal "report.png", attachable[:filename] + + image = MiniMagick::Image.read(attachable[:io]) + assert_equal 612, image.width + assert_equal 792, image.height + end + end +end diff --git a/activestorage/test/service/configurations.example.yml b/activestorage/test/service/configurations.example.yml index 43cc013bc8..a63aa33302 100644 --- a/activestorage/test/service/configurations.example.yml +++ b/activestorage/test/service/configurations.example.yml @@ -24,7 +24,6 @@ # # azure: # service: AzureStorage -# path: "" # storage_account_name: "" # storage_access_key: "" # container: "" diff --git a/activestorage/test/service/configurations.yml.enc b/activestorage/test/service/configurations.yml.enc Binary files differindex df11aac161..648924a562 100644 --- a/activestorage/test/service/configurations.yml.enc +++ b/activestorage/test/service/configurations.yml.enc diff --git a/activestorage/test/service/configurator_test.rb b/activestorage/test/service/configurator_test.rb index fe8a637ad0..1c9c5c3aa0 100644 --- a/activestorage/test/service/configurator_test.rb +++ b/activestorage/test/service/configurator_test.rb @@ -5,10 +5,8 @@ require "service/shared_service_tests" class ActiveStorage::Service::ConfiguratorTest < ActiveSupport::TestCase test "builds correct service instance based on service name" do service = ActiveStorage::Service::Configurator.build(:foo, foo: { service: "Disk", root: "path" }) - assert_instance_of ActiveStorage::Service::DiskService, service assert_equal "path", service.root - assert_equal "http://localhost:3000", service.host end test "raises error when passing non-existent service name" do diff --git a/activestorage/test/service/disk_service_test.rb b/activestorage/test/service/disk_service_test.rb index 4a6361b920..d7142de458 100644 --- a/activestorage/test/service/disk_service_test.rb +++ b/activestorage/test/service/disk_service_test.rb @@ -8,7 +8,7 @@ class ActiveStorage::Service::DiskServiceTest < ActiveSupport::TestCase include ActiveStorage::Service::SharedServiceTests test "url generation" do - assert_match(/rails\/active_storage\/disk\/.*\/avatar\.png\?content_type=image%2Fpng&disposition=inline/, + assert_match(/^https:\/\/example.com\/rails\/active_storage\/disk\/.*\/avatar\.png\?content_type=image%2Fpng&disposition=inline/, @service.url(FIXTURE_KEY, expires_in: 5.minutes, disposition: :inline, filename: ActiveStorage::Filename.new("avatar.png"), content_type: "image/png")) end end diff --git a/activestorage/test/service/s3_service_test.rb b/activestorage/test/service/s3_service_test.rb index d6996209d2..7833e51122 100644 --- a/activestorage/test/service/s3_service_test.rb +++ b/activestorage/test/service/s3_service_test.rb @@ -3,7 +3,7 @@ require "service/shared_service_tests" require "net/http" -if SERVICE_CONFIGURATIONS[:s3] && SERVICE_CONFIGURATIONS[:s3][:access_key_id].present? +if SERVICE_CONFIGURATIONS[:s3] class ActiveStorage::Service::S3ServiceTest < ActiveSupport::TestCase SERVICE = ActiveStorage::Service.configure(:s3, SERVICE_CONFIGURATIONS) diff --git a/activestorage/test/service/shared_service_tests.rb b/activestorage/test/service/shared_service_tests.rb index ce28c4393a..24debe7f47 100644 --- a/activestorage/test/service/shared_service_tests.rb +++ b/activestorage/test/service/shared_service_tests.rb @@ -60,6 +60,11 @@ module ActiveStorage::Service::SharedServiceTests assert_equal [ FIXTURE_DATA ], chunks end + test "downloading partially" do + assert_equal "\x10\x00\x00", @service.download_chunk(FIXTURE_KEY, 19..21) + assert_equal "\x10\x00\x00", @service.download_chunk(FIXTURE_KEY, 19...22) + end + test "existing" do assert @service.exist?(FIXTURE_KEY) assert_not @service.exist?(FIXTURE_KEY + "nonsense") diff --git a/activestorage/test/test_helper.rb b/activestorage/test/test_helper.rb index 98fa44a604..028874f374 100644 --- a/activestorage/test/test_helper.rb +++ b/activestorage/test/test_helper.rb @@ -7,7 +7,6 @@ require "bundler/setup" require "active_support" require "active_support/test_case" require "active_support/testing/autorun" -require "webmock/minitest" require "mini_magick" begin @@ -42,7 +41,13 @@ ActiveStorage.verifier = ActiveSupport::MessageVerifier.new("Testing") class ActiveSupport::TestCase self.file_fixture_path = File.expand_path("fixtures/files", __dir__) - setup { WebMock.allow_net_connect! } + setup do + ActiveStorage::Current.host = "https://example.com" + end + + teardown do + ActiveStorage::Current.reset + end private def create_blob(data: "Hello world!", filename: "hello.txt", content_type: "text/plain") @@ -57,6 +62,16 @@ class ActiveSupport::TestCase ActiveStorage::Blob.create_before_direct_upload! filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type end + def directly_upload_file_blob(filename: "racecar.jpg", content_type: "image/jpeg") + file = file_fixture(filename) + byte_size = file.size + checksum = Digest::MD5.file(file).base64digest + + create_blob_before_direct_upload(filename: filename, byte_size: byte_size, checksum: checksum, content_type: content_type).tap do |blob| + ActiveStorage::Blob.service.upload(blob.key, file.open) + end + end + def read_image(blob_or_variant) MiniMagick::Image.open blob_or_variant.service.send(:path_for, blob_or_variant.key) end @@ -68,5 +83,8 @@ ActiveRecord::Base.send :include, GlobalID::Identification class User < ActiveRecord::Base has_one_attached :avatar + has_one_attached :cover_photo, dependent: false + has_many_attached :highlights + has_many_attached :vlogs, dependent: false end diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index cbbb8597c2..3cbaa65dea 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,4 +1,32 @@ -## Rails 6.0.0.alpha (Unreleased) ## +* Fix bug where `ActiveSupport::Timezone.all` would fail when tzinfo data for + any timezone defined in `ActiveSupport::MAPPING` is missing. + + *Dominik Sander* + +* Redis cache store: `delete_matched` no longer blocks the Redis server. + (Switches from evaled Lua to a batched SCAN + DEL loop.) + + *Gleb Mazovetskiy* + +* Fix bug where `ActiveSupport::Cache` will massively inflate the storage + size when compression is enabled (which is true by default). This patch + does not attempt to repair existing data: please manually flush the cache + to clear out the problematic entries. + + *Godfrey Chan* + +* Fix bug where `URI.unscape` would fail with mixed Unicode/escaped character input: + + URI.unescape("\xe3\x83\x90") # => "バ" + URI.unescape("%E3%83%90") # => "バ" + URI.unescape("\xe3\x83\x90%E3%83%90") # => Encoding::CompatibilityError + + *Ashe Connor*, *Aaron Patterson* + +* Add `before?` and `after?` methods to `Date`, `DateTime`, + `Time`, and `TimeWithZone`. + + *Nick Holden* * `ActiveSupport::Inflector#ordinal` and `ActiveSupport::Inflector#ordinalize` now support translations through I18n. @@ -71,7 +99,7 @@ *Jeremy Daer* -* Adds parallel testing to Rails +* Adds parallel testing to Rails. Parallelize your test suite with forked processes or threads. diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb index 1ea2d0bbf2..d769e2c8ea 100644 --- a/activesupport/lib/active_support/cache.rb +++ b/activesupport/lib/active_support/cache.rb @@ -333,8 +333,9 @@ module ActiveSupport # the cache with the given key, then that data is returned. Otherwise, # +nil+ is returned. # - # Note, if data was written with the <tt>:expires_in<tt> or <tt>:version</tt> options, - # both of these conditions are applied before the data is returned. + # Note, if data was written with the <tt>:expires_in</tt> or + # <tt>:version</tt> options, both of these conditions are applied before + # the data is returned. # # Options are passed to the underlying cache implementation. def read(name, options = nil) @@ -712,19 +713,14 @@ module ActiveSupport DEFAULT_COMPRESS_LIMIT = 1.kilobyte # Creates a new cache entry for the specified value. Options supported are - # +:compress+, +:compress_threshold+, and +:expires_in+. - def initialize(value, options = {}) - if should_compress?(value, options) - @value = compress(value) - @compressed = true - else - @value = value - end - - @version = options[:version] + # +:compress+, +:compress_threshold+, +:version+ and +:expires_in+. + def initialize(value, compress: true, compress_threshold: DEFAULT_COMPRESS_LIMIT, version: nil, expires_in: nil, **) + @value = value + @version = version @created_at = Time.now.to_f - @expires_in = options[:expires_in] - @expires_in = @expires_in.to_f if @expires_in + @expires_in = expires_in && expires_in.to_f + + compress!(compress_threshold) if compress end def value @@ -756,17 +752,13 @@ module ActiveSupport # Returns the size of the cached value. This could be less than # <tt>value.size</tt> if the data is compressed. def size - if defined?(@s) - @s + case value + when NilClass + 0 + when String + @value.bytesize else - case value - when NilClass - 0 - when String - @value.bytesize - else - @s = Marshal.dump(@value).bytesize - end + @s ||= Marshal.dump(@value).bytesize end end @@ -783,23 +775,30 @@ module ActiveSupport end private - def should_compress?(value, options) - if value && options.fetch(:compress, true) - compress_threshold = options.fetch(:compress_threshold, DEFAULT_COMPRESS_LIMIT) - serialized_value_size = (value.is_a?(String) ? value : Marshal.dump(value)).bytesize - - return true if serialized_value_size >= compress_threshold + def compress!(compress_threshold) + case @value + when nil, true, false, Numeric + uncompressed_size = 0 + when String + uncompressed_size = @value.bytesize + else + serialized = Marshal.dump(@value) + uncompressed_size = serialized.bytesize end - false - end + if uncompressed_size >= compress_threshold + serialized ||= Marshal.dump(@value) + compressed = Zlib::Deflate.deflate(serialized) - def compressed? - defined?(@compressed) ? @compressed : false + if compressed.bytesize < uncompressed_size + @value = compressed + @compressed = true + end + end end - def compress(value) - Zlib::Deflate.deflate(Marshal.dump(value)) + def compressed? + defined?(@compressed) end def uncompress(value) diff --git a/activesupport/lib/active_support/cache/redis_cache_store.rb b/activesupport/lib/active_support/cache/redis_cache_store.rb index 64bc77cf22..11c574258f 100644 --- a/activesupport/lib/active_support/cache/redis_cache_store.rb +++ b/activesupport/lib/active_support/cache/redis_cache_store.rb @@ -62,8 +62,9 @@ module ActiveSupport end end - DELETE_GLOB_LUA = "for i, name in ipairs(redis.call('KEYS', ARGV[1])) do redis.call('DEL', name); end" - private_constant :DELETE_GLOB_LUA + # The maximum number of entries to receive per SCAN call. + SCAN_BATCH_SIZE = 1000 + private_constant :SCAN_BATCH_SIZE # Support raw values in the local cache strategy. module LocalCacheWithRaw # :nodoc: @@ -118,7 +119,7 @@ module ActiveSupport def build_redis(redis: nil, url: nil, **redis_options) #:nodoc: urls = Array(url) - if redis.respond_to?(:call) + if redis.is_a?(Proc) redis.call elsif redis redis @@ -154,7 +155,7 @@ module ActiveSupport # :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …]) # # No namespace is set by default. Provide one if the Redis cache - # server is shared with other apps: <tt>namespace: 'myapp-cache'<tt>. + # server is shared with other apps: <tt>namespace: 'myapp-cache'</tt>. # # Compression is enabled by default with a 1kB threshold, so cached # values larger than 1kB are automatically compressed. Disable by @@ -231,12 +232,18 @@ module ActiveSupport # Failsafe: Raises errors. def delete_matched(matcher, options = nil) instrument :delete_matched, matcher do - case matcher - when String - redis.with { |c| c.eval DELETE_GLOB_LUA, [], [namespace_key(matcher, options)] } - else + unless String === matcher raise ArgumentError, "Only Redis glob strings are supported: #{matcher.inspect}" end + redis.with do |c| + pattern = namespace_key(matcher, options) + cursor = "0" + # Fetch keys in batches using SCAN to avoid blocking the Redis server. + begin + cursor, keys = c.scan(cursor, match: pattern, count: SCAN_BATCH_SIZE) + c.del(*keys) unless keys.empty? + end until cursor == "0" + end end end @@ -286,7 +293,7 @@ module ActiveSupport # Failsafe: Raises errors. def clear(options = nil) failsafe :clear do - if namespace = merged_options(options)[namespace] + if namespace = merged_options(options)[:namespace] delete_matched "*", namespace: namespace else redis.with { |c| c.flushdb } diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb index e17308f83e..39b32fc7f6 100644 --- a/activesupport/lib/active_support/cache/strategy/local_cache.rb +++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb @@ -55,7 +55,14 @@ module ActiveSupport end def read_multi_entries(keys, options) - Hash[keys.map { |name| [name, read_entry(name, options)] }.keep_if { |_name, value| value }] + values = {} + + keys.each do |name| + entry = read_entry(name, options) + values[name] = entry.value if entry + end + + values end def write_entry(key, value, options) diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 0ed4681b7d..a1b841ec3d 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -749,8 +749,8 @@ module ActiveSupport # * <tt>:skip_after_callbacks_if_terminated</tt> - Determines if after # callbacks should be terminated by the <tt>:terminator</tt> option. By # default after callbacks are executed no matter if callback chain was - # terminated or not. This option makes sense only when <tt>:terminator</tt> - # option is specified. + # terminated or not. This option has no effect if <tt>:terminator</tt> + # option is set to +nil+. # # * <tt>:scope</tt> - Indicates which methods should be executed when an # object is used as a callback. @@ -809,7 +809,9 @@ module ActiveSupport names.each do |name| name = name.to_sym - set_callbacks name, CallbackChain.new(name, options) + ([self] + ActiveSupport::DescendantsTracker.descendants(self)).each do |target| + target.set_callbacks name, CallbackChain.new(name, options) + end module_eval <<-RUBY, __FILE__, __LINE__ + 1 def _run_#{name}_callbacks(&block) diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb index f6cb1a384c..de13f00e60 100644 --- a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb +++ b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb @@ -60,6 +60,16 @@ module DateAndTime !WEEKEND_DAYS.include?(wday) end + # Returns true if the date/time before <tt>date_or_time</tt>. + def before?(date_or_time) + self < date_or_time + end + + # Returns true if the date/time after <tt>date_or_time</tt>. + def after?(date_or_time) + self > date_or_time + end + # Returns a new date/time the specified number of days ago. def days_ago(days) advance(days: -days) diff --git a/activesupport/lib/active_support/core_ext/enumerable.rb b/activesupport/lib/active_support/core_ext/enumerable.rb index f01d01e6aa..edde4f46b9 100644 --- a/activesupport/lib/active_support/core_ext/enumerable.rb +++ b/activesupport/lib/active_support/core_ext/enumerable.rb @@ -6,7 +6,7 @@ module Enumerable # We can't use Refinements here because Refinements with Module which will be prepended # doesn't work well https://bugs.ruby-lang.org/issues/13446 - alias :_original_sum_with_required_identity :sum + alias :_original_sum_with_required_identity :sum # :nodoc: private :_original_sum_with_required_identity # Calculates a sum from the elements. diff --git a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb index 580baffa2b..01fee0fb74 100644 --- a/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb +++ b/activesupport/lib/active_support/core_ext/module/attribute_accessors.rb @@ -163,10 +163,10 @@ class Module # parent class. Similarly if parent class changes the value then that would # change the value of subclasses too. # - # class Male < Person + # class Citizen < Person # end # - # Male.new.hair_colors << :blue + # Citizen.new.hair_colors << :blue # Person.new.hair_colors # => [:brown, :black, :blonde, :red, :blue] # # To opt out of the instance writer method, pass <tt>instance_writer: false</tt>. diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index ec3497173f..7f42f44efb 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -22,8 +22,9 @@ class Module # ==== Options # * <tt>:to</tt> - Specifies the target object # * <tt>:prefix</tt> - Prefixes the new method with the target name or a custom prefix - # * <tt>:allow_nil</tt> - if set to true, prevents a +Module::DelegationError+ + # * <tt>:allow_nil</tt> - If set to true, prevents a +Module::DelegationError+ # from being raised + # * <tt>:private</tt> - If set to true, changes method visibility to private # # The macro receives one or more method names (specified as symbols or # strings) and the name of the target object via the <tt>:to</tt> option diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb index f7c623fe13..416059d17b 100644 --- a/activesupport/lib/active_support/core_ext/object/json.rb +++ b/activesupport/lib/active_support/core_ext/object/json.rb @@ -14,6 +14,7 @@ require "active_support/core_ext/time/conversions" require "active_support/core_ext/date_time/conversions" require "active_support/core_ext/date/conversions" +#-- # The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting # their default behavior. That said, we need to define the basic to_json method in all of them, # otherwise they will always use to_json gem implementation, which is backwards incompatible in diff --git a/activesupport/lib/active_support/core_ext/uri.rb b/activesupport/lib/active_support/core_ext/uri.rb index c93c0b5c2d..cdd81ae562 100644 --- a/activesupport/lib/active_support/core_ext/uri.rb +++ b/activesupport/lib/active_support/core_ext/uri.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true require "uri" -str = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" # Ni-ho-nn-go in UTF-8, means Japanese. -parser = URI::Parser.new -unless str == parser.unescape(parser.escape(str)) +if RUBY_VERSION < "2.6.0" require "active_support/core_ext/module/redefine_method" URI::Parser.class_eval do silence_redefinition_of_method :unescape @@ -13,7 +11,7 @@ unless str == parser.unescape(parser.escape(str)) # YK: My initial experiments say yes, but let's be sure please enc = str.encoding enc = Encoding::UTF_8 if enc == Encoding::US_ASCII - str.gsub(escaped) { |match| [match[1, 2].hex].pack("C") }.force_encoding(enc) + str.dup.force_encoding(Encoding::ASCII_8BIT).gsub(escaped) { |match| [match[1, 2].hex].pack("C") }.force_encoding(enc) end end end diff --git a/activesupport/lib/active_support/encrypted_configuration.rb b/activesupport/lib/active_support/encrypted_configuration.rb index dab953d5d5..3c6da10548 100644 --- a/activesupport/lib/active_support/encrypted_configuration.rb +++ b/activesupport/lib/active_support/encrypted_configuration.rb @@ -38,10 +38,6 @@ module ActiveSupport @options ||= ActiveSupport::InheritableOptions.new(config) end - def serialize(config) - config.present? ? YAML.dump(config) : "" - end - def deserialize(config) config.present? ? YAML.load(config, content_path) : {} end diff --git a/activesupport/lib/active_support/encrypted_file.rb b/activesupport/lib/active_support/encrypted_file.rb index 671b6b6a69..c66f1b557e 100644 --- a/activesupport/lib/active_support/encrypted_file.rb +++ b/activesupport/lib/active_support/encrypted_file.rb @@ -57,7 +57,7 @@ module ActiveSupport private def writing(contents) - tmp_file = "#{content_path.basename}.#{Process.pid}" + tmp_file = "#{Process.pid}.#{content_path.basename.to_s.chomp('.enc')}" tmp_path = Pathname.new File.join(Dir.tmpdir, tmp_file) tmp_path.binwrite contents diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 5236c776dd..8b73270894 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -3,6 +3,7 @@ require "openssl" require "base64" require "active_support/core_ext/array/extract_options" +require "active_support/core_ext/module/attribute_accessors" require "active_support/message_verifier" require "active_support/messages/metadata" diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index a698b4e61e..4e42db4500 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -43,23 +43,23 @@ module ActiveSupport # Parallelizes the test suite. # - # Takes a `workers` argument that controls how many times the process + # Takes a +workers+ argument that controls how many times the process # is forked. For each process a new database will be created suffixed # with the worker number. # # test-database-0 # test-database-1 # - # If `ENV["PARALLEL_WORKERS"]` is set the workers argument will be ignored + # If <tt>ENV["PARALLEL_WORKERS"]</tt> is set the workers argument will be ignored # and the environment variable will be used instead. This is useful for CI # environments, or other environments where you may need more workers than # you do for local testing. # - # If the number of workers is set to `1` or fewer, the tests will not be + # If the number of workers is set to +1+ or fewer, the tests will not be # parallelized. # # The default parallelization method is to fork processes. If you'd like to - # use threads instead you can pass `with: :threads` to the `parallelize` + # use threads instead you can pass <tt>with: :threads</tt> to the +parallelize+ # method. Note the threaded parallelization does not create multiple # database and will not work with system tests at this time. # diff --git a/activesupport/lib/active_support/testing/setup_and_teardown.rb b/activesupport/lib/active_support/testing/setup_and_teardown.rb index 1dbf3c5da0..35236f1401 100644 --- a/activesupport/lib/active_support/testing/setup_and_teardown.rb +++ b/activesupport/lib/active_support/testing/setup_and_teardown.rb @@ -44,8 +44,15 @@ module ActiveSupport end def after_teardown # :nodoc: - run_callbacks :teardown + begin + run_callbacks :teardown + rescue => e + error = e + end + super + ensure + raise error if error end end end diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb index 20650ce714..7e71318404 100644 --- a/activesupport/lib/active_support/time_with_zone.rb +++ b/activesupport/lib/active_support/time_with_zone.rb @@ -225,6 +225,8 @@ module ActiveSupport def <=>(other) utc <=> other end + alias_method :before?, :< + alias_method :after?, :> # Returns true if the current object's time is within the specified # +min+ and +max+ time. diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb index 9dfaddb825..5f709c5fd9 100644 --- a/activesupport/lib/active_support/values/time_zone.rb +++ b/activesupport/lib/active_support/values/time_zone.rb @@ -279,7 +279,8 @@ module ActiveSupport def zones_map @zones_map ||= MAPPING.each_with_object({}) do |(name, _), zones| - zones[name] = self[name] + timezone = self[name] + zones[name] = timezone if timezone end end end diff --git a/activesupport/test/cache/behaviors/cache_store_behavior.rb b/activesupport/test/cache/behaviors/cache_store_behavior.rb index efb57d34a2..f6763d195a 100644 --- a/activesupport/test/cache/behaviors/cache_store_behavior.rb +++ b/activesupport/test/cache/behaviors/cache_store_behavior.rb @@ -141,29 +141,111 @@ module CacheStoreBehavior end end - def test_read_and_write_compressed_small_data - @cache.write("foo", "bar", compress: true) - assert_equal "bar", @cache.read("foo") + # Use strings that are guarenteed to compress well, so we can easily tell if + # the compression kicked in or not. + SMALL_STRING = "0" * 100 + LARGE_STRING = "0" * 2.kilobytes + + SMALL_OBJECT = { data: SMALL_STRING } + LARGE_OBJECT = { data: LARGE_STRING } + + def test_nil_with_default_compression_settings + assert_uncompressed(nil) end - def test_read_and_write_compressed_large_data - @cache.write("foo", "bar", compress: true, compress_threshold: 2) - assert_equal "bar", @cache.read("foo") + def test_nil_with_compress_true + assert_uncompressed(nil, compress: true) end - def test_read_and_write_compressed_nil - @cache.write("foo", nil, compress: true) - assert_nil @cache.read("foo") + def test_nil_with_compress_false + assert_uncompressed(nil, compress: false) end - def test_read_and_write_uncompressed_small_data - @cache.write("foo", "bar", compress: false) - assert_equal "bar", @cache.read("foo") + def test_nil_with_compress_low_compress_threshold + assert_uncompressed(nil, compress: true, compress_threshold: 1) end - def test_read_and_write_uncompressed_nil - @cache.write("foo", nil, compress: false) - assert_nil @cache.read("foo") + def test_small_string_with_default_compression_settings + assert_uncompressed(SMALL_STRING) + end + + def test_small_string_with_compress_true + assert_uncompressed(SMALL_STRING, compress: true) + end + + def test_small_string_with_compress_false + assert_uncompressed(SMALL_STRING, compress: false) + end + + def test_small_string_with_low_compress_threshold + assert_compressed(SMALL_STRING, compress: true, compress_threshold: 1) + end + + def test_small_object_with_default_compression_settings + assert_uncompressed(SMALL_OBJECT) + end + + def test_small_object_with_compress_true + assert_uncompressed(SMALL_OBJECT, compress: true) + end + + def test_small_object_with_compress_false + assert_uncompressed(SMALL_OBJECT, compress: false) + end + + def test_small_object_with_low_compress_threshold + assert_compressed(SMALL_OBJECT, compress: true, compress_threshold: 1) + end + + def test_large_string_with_default_compression_settings + assert_compressed(LARGE_STRING) + end + + def test_large_string_with_compress_true + assert_compressed(LARGE_STRING, compress: true) + end + + def test_large_string_with_compress_false + assert_uncompressed(LARGE_STRING, compress: false) + end + + def test_large_string_with_high_compress_threshold + assert_uncompressed(LARGE_STRING, compress: true, compress_threshold: 1.megabyte) + end + + def test_large_object_with_default_compression_settings + assert_compressed(LARGE_OBJECT) + end + + def test_large_object_with_compress_true + assert_compressed(LARGE_OBJECT, compress: true) + end + + def test_large_object_with_compress_false + assert_uncompressed(LARGE_OBJECT, compress: false) + end + + def test_large_object_with_high_compress_threshold + assert_uncompressed(LARGE_OBJECT, compress: true, compress_threshold: 1.megabyte) + end + + def test_incompressable_data + assert_uncompressed(nil, compress: true, compress_threshold: 1) + assert_uncompressed(true, compress: true, compress_threshold: 1) + assert_uncompressed(false, compress: true, compress_threshold: 1) + assert_uncompressed(0, compress: true, compress_threshold: 1) + assert_uncompressed(1.2345, compress: true, compress_threshold: 1) + assert_uncompressed("", compress: true, compress_threshold: 1) + + incompressible = nil + + # generate an incompressible string + loop do + incompressible = SecureRandom.random_bytes(1.kilobyte) + break if incompressible.bytesize < Zlib::Deflate.deflate(incompressible).bytesize + end + + assert_uncompressed(incompressible, compress: true, compress_threshold: 1) end def test_cache_key @@ -359,4 +441,41 @@ module CacheStoreBehavior ensure ActiveSupport::Notifications.unsubscribe "cache_read.active_support" end + + private + + def assert_compressed(value, **options) + assert_compression(true, value, **options) + end + + def assert_uncompressed(value, **options) + assert_compression(false, value, **options) + end + + def assert_compression(should_compress, value, **options) + freeze_time do + @cache.write("actual", value, options) + @cache.write("uncompressed", value, options.merge(compress: false)) + end + + if value.nil? + assert_nil @cache.read("actual") + assert_nil @cache.read("uncompressed") + else + assert_equal value, @cache.read("actual") + assert_equal value, @cache.read("uncompressed") + end + + actual_entry = @cache.send(:read_entry, @cache.send(:normalize_key, "actual", {}), {}) + uncompressed_entry = @cache.send(:read_entry, @cache.send(:normalize_key, "uncompressed", {}), {}) + + actual_size = Marshal.dump(actual_entry).bytesize + uncompressed_size = Marshal.dump(uncompressed_entry).bytesize + + if should_compress + assert_operator actual_size, :<, uncompressed_size, "value should be compressed" + else + assert_equal uncompressed_size, actual_size, "value should not be compressed" + end + end end diff --git a/activesupport/test/cache/behaviors/local_cache_behavior.rb b/activesupport/test/cache/behaviors/local_cache_behavior.rb index 363f2d1084..baa38ba6ac 100644 --- a/activesupport/test/cache/behaviors/local_cache_behavior.rb +++ b/activesupport/test/cache/behaviors/local_cache_behavior.rb @@ -129,6 +129,18 @@ module LocalCacheBehavior end end + def test_local_cache_of_read_multi + @cache.with_local_cache do + @cache.write("foo", "foo", raw: true) + @cache.write("bar", "bar", raw: true) + values = @cache.read_multi("foo", "bar") + assert_equal "foo", @cache.read("foo") + assert_equal "bar", @cache.read("bar") + assert_equal "foo", values["foo"] + assert_equal "bar", values["bar"] + end + end + def test_middleware app = lambda { |env| result = @cache.write("foo", "bar") diff --git a/activesupport/test/cache/cache_entry_test.rb b/activesupport/test/cache/cache_entry_test.rb index 80ff7ad564..d7baaa5c72 100644 --- a/activesupport/test/cache/cache_entry_test.rb +++ b/activesupport/test/cache/cache_entry_test.rb @@ -13,25 +13,4 @@ class CacheEntryTest < ActiveSupport::TestCase assert entry.expired?, "entry is expired" end end - - def test_compressed_values - value = "value" * 100 - entry = ActiveSupport::Cache::Entry.new(value, compress: true, compress_threshold: 1) - assert_equal value, entry.value - assert(value.bytesize > entry.size, "value is compressed") - end - - def test_compressed_by_default - value = "value" * 100 - entry = ActiveSupport::Cache::Entry.new(value, compress_threshold: 1) - assert_equal value, entry.value - assert(value.bytesize > entry.size, "value is compressed") - end - - def test_uncompressed_values - value = "value" * 100 - entry = ActiveSupport::Cache::Entry.new(value, compress: false) - assert_equal value, entry.value - assert_equal value.bytesize, entry.size - end end diff --git a/activesupport/test/cache/cache_store_namespace_test.rb b/activesupport/test/cache/cache_store_namespace_test.rb index b52a61c500..dfdb3262f2 100644 --- a/activesupport/test/cache/cache_store_namespace_test.rb +++ b/activesupport/test/cache/cache_store_namespace_test.rb @@ -25,7 +25,7 @@ class CacheStoreNamespaceTest < ActiveSupport::TestCase cache.write("foo", "bar") cache.write("fu", "baz") cache.delete_matched(/^fo/) - assert !cache.exist?("foo") + assert_not cache.exist?("foo") assert cache.exist?("fu") end @@ -34,7 +34,7 @@ class CacheStoreNamespaceTest < ActiveSupport::TestCase cache.write("foo", "bar") cache.write("fu", "baz") cache.delete_matched(/OO/i) - assert !cache.exist?("foo") + assert_not cache.exist?("foo") assert cache.exist?("fu") end end diff --git a/activesupport/test/cache/stores/file_store_test.rb b/activesupport/test/cache/stores/file_store_test.rb index c3c35a7bcc..f6855bb308 100644 --- a/activesupport/test/cache/stores/file_store_test.rb +++ b/activesupport/test/cache/stores/file_store_test.rb @@ -68,7 +68,9 @@ class FileStoreTest < ActiveSupport::TestCase def test_filename_max_size key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}" path = @cache.send(:normalize_key, key, {}) - Dir::Tmpname.create(path) do |tmpname, n, opts| + basename = File.basename(path) + dirname = File.dirname(path) + Dir::Tmpname.create(basename, Dir.tmpdir + dirname) do |tmpname, n, opts| assert File.basename(tmpname + ".lock").length <= 255, "Temp filename too long: #{File.basename(tmpname + '.lock').length}" end end diff --git a/activesupport/test/cache/stores/memory_store_test.rb b/activesupport/test/cache/stores/memory_store_test.rb index 72fafc187b..340fb517cb 100644 --- a/activesupport/test/cache/stores/memory_store_test.rb +++ b/activesupport/test/cache/stores/memory_store_test.rb @@ -6,8 +6,7 @@ require_relative "../behaviors" class MemoryStoreTest < ActiveSupport::TestCase def setup - @record_size = ActiveSupport::Cache.lookup_store(:memory_store).send(:cached_size, 1, ActiveSupport::Cache::Entry.new("aaaaaaaaaa")) - @cache = ActiveSupport::Cache.lookup_store(:memory_store, expires_in: 60, size: @record_size * 10 + 1) + @cache = ActiveSupport::Cache.lookup_store(:memory_store, expires_in: 60) end include CacheStoreBehavior @@ -15,6 +14,13 @@ class MemoryStoreTest < ActiveSupport::TestCase include CacheDeleteMatchedBehavior include CacheIncrementDecrementBehavior include CacheInstrumentationBehavior +end + +class MemoryStorePruningTest < ActiveSupport::TestCase + def setup + @record_size = ActiveSupport::Cache.lookup_store(:memory_store).send(:cached_size, 1, ActiveSupport::Cache::Entry.new("aaaaaaaaaa")) + @cache = ActiveSupport::Cache.lookup_store(:memory_store, expires_in: 60, size: @record_size * 10 + 1) + end def test_prune_size @cache.write(1, "aaaaaaaaaa") && sleep(0.001) @@ -98,7 +104,7 @@ class MemoryStoreTest < ActiveSupport::TestCase assert @cache.exist?(4) assert @cache.exist?(3) assert @cache.exist?(2) - assert !@cache.exist?(1) + assert_not @cache.exist?(1) end def test_write_with_unless_exist diff --git a/activesupport/test/cache/stores/redis_cache_store_test.rb b/activesupport/test/cache/stores/redis_cache_store_test.rb index 375993a632..24c4c5c481 100644 --- a/activesupport/test/cache/stores/redis_cache_store_test.rb +++ b/activesupport/test/cache/stores/redis_cache_store_test.rb @@ -95,6 +95,12 @@ module ActiveSupport::Cache::RedisCacheStoreTests end end + test "instance of Redis uses given instance" do + redis_instance = Redis.new + @cache = build(redis: redis_instance) + assert_same @cache.redis, redis_instance + end + private def build(**kwargs) ActiveSupport::Cache::RedisCacheStore.new(driver: DRIVER, **kwargs).tap do |cache| @@ -205,7 +211,7 @@ module ActiveSupport::Cache::RedisCacheStoreTests @cache.write("foo", "bar") @cache.write("fu", "baz") @cache.delete_matched("foo*") - assert !@cache.exist?("foo") + assert_not @cache.exist?("foo") assert @cache.exist?("fu") end @@ -215,4 +221,22 @@ module ActiveSupport::Cache::RedisCacheStoreTests end end end + + class ClearTest < StoreTest + test "clear all cache key" do + @cache.write("foo", "bar") + @cache.write("fu", "baz") + @cache.clear + assert_not @cache.exist?("foo") + assert_not @cache.exist?("fu") + end + + test "only clear namespace cache key" do + @cache.write("foo", "bar") + @cache.redis.set("fu", "baz") + @cache.clear + assert_not @cache.exist?("foo") + assert @cache.redis.exists("fu") + end + end end diff --git a/activesupport/test/callback_inheritance_test.rb b/activesupport/test/callback_inheritance_test.rb index 015e17deb9..5633b6e2b8 100644 --- a/activesupport/test/callback_inheritance_test.rb +++ b/activesupport/test/callback_inheritance_test.rb @@ -176,3 +176,13 @@ class DynamicInheritedCallbacks < ActiveSupport::TestCase assert_equal 1, child.count end end + +class DynamicDefinedCallbacks < ActiveSupport::TestCase + def test_callbacks_should_be_performed_once_in_child_class_after_dynamic_define + GrandParent.define_callbacks(:foo) + GrandParent.set_callback(:foo, :before, :before1) + parent = Parent.new("foo") + parent.run_callbacks(:foo) + assert_equal %w(before1), parent.log + end +end diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb index 6c7643ed72..5c9a3b29e7 100644 --- a/activesupport/test/callbacks_test.rb +++ b/activesupport/test/callbacks_test.rb @@ -829,7 +829,7 @@ module CallbacksTest def test_block_never_called_if_terminated obj = CallbackTerminator.new obj.save - assert !obj.saved + assert_not obj.saved end end @@ -857,7 +857,7 @@ module CallbacksTest def test_block_never_called_if_abort_is_thrown obj = CallbackDefaultTerminator.new obj.save - assert !obj.saved + assert_not obj.saved end end diff --git a/activesupport/test/class_cache_test.rb b/activesupport/test/class_cache_test.rb index 8cfcaedafd..1ef1939b4b 100644 --- a/activesupport/test/class_cache_test.rb +++ b/activesupport/test/class_cache_test.rb @@ -68,7 +68,7 @@ module ActiveSupport def test_new_rejects_strings @cache.store ClassCacheTest.name - assert !@cache.key?(ClassCacheTest.name) + assert_not @cache.key?(ClassCacheTest.name) end def test_store_returns_self diff --git a/activesupport/test/core_ext/date_and_time_behavior.rb b/activesupport/test/core_ext/date_and_time_behavior.rb index 1422f135a8..b77ea22701 100644 --- a/activesupport/test/core_ext/date_and_time_behavior.rb +++ b/activesupport/test/core_ext/date_and_time_behavior.rb @@ -385,6 +385,18 @@ module DateAndTimeBehavior assert_predicate date_time_init(2015, 1, 5, 15, 15, 10), :on_weekday? end + def test_before + assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).before?(date_time_init(2017, 3, 5, 12, 0, 0)) + assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).before?(date_time_init(2017, 3, 6, 12, 0, 0)) + assert_equal true, date_time_init(2017, 3, 6, 12, 0, 0).before?(date_time_init(2017, 3, 7, 12, 0, 0)) + end + + def test_after + assert_equal true, date_time_init(2017, 3, 6, 12, 0, 0).after?(date_time_init(2017, 3, 5, 12, 0, 0)) + assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).after?(date_time_init(2017, 3, 6, 12, 0, 0)) + assert_equal false, date_time_init(2017, 3, 6, 12, 0, 0).after?(date_time_init(2017, 3, 7, 12, 0, 0)) + end + def with_bw_default(bw = :monday) old_bw = Date.beginning_of_week Date.beginning_of_week = bw diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb index 8f6befe809..240ae3bde0 100644 --- a/activesupport/test/core_ext/duration_test.rb +++ b/activesupport/test/core_ext/duration_test.rb @@ -16,30 +16,30 @@ class DurationTest < ActiveSupport::TestCase assert_kind_of ActiveSupport::Duration, d assert_kind_of Numeric, d assert_kind_of Integer, d - assert !d.is_a?(Hash) + assert_not d.is_a?(Hash) k = Class.new class << k; undef_method :== end - assert !d.is_a?(k) + assert_not d.is_a?(k) end def test_instance_of assert 1.minute.instance_of?(Integer) assert 2.days.instance_of?(ActiveSupport::Duration) - assert !3.second.instance_of?(Numeric) + assert_not 3.second.instance_of?(Numeric) end def test_threequals assert ActiveSupport::Duration === 1.day - assert !(ActiveSupport::Duration === 1.day.to_i) - assert !(ActiveSupport::Duration === "foo") + assert_not (ActiveSupport::Duration === 1.day.to_i) + assert_not (ActiveSupport::Duration === "foo") end def test_equals assert 1.day == 1.day assert 1.day == 1.day.to_i assert 1.day.to_i == 1.day - assert !(1.day == "foo") + assert_not (1.day == "foo") end def test_to_s @@ -53,11 +53,11 @@ class DurationTest < ActiveSupport::TestCase assert 1.minute.eql?(1.minute) assert 1.minute.eql?(60.seconds) assert 2.days.eql?(48.hours) - assert !1.second.eql?(1) - assert !1.eql?(1.second) + assert_not 1.second.eql?(1) + assert_not 1.eql?(1.second) assert 1.minute.eql?(180.seconds - 2.minutes) - assert !1.minute.eql?(60) - assert !1.minute.eql?("foo") + assert_not 1.minute.eql?(60) + assert_not 1.minute.eql?("foo") end def test_inspect diff --git a/activesupport/test/core_ext/file_test.rb b/activesupport/test/core_ext/file_test.rb index 23e3c277cc..9c97700e5d 100644 --- a/activesupport/test/core_ext/file_test.rb +++ b/activesupport/test/core_ext/file_test.rb @@ -8,7 +8,7 @@ class AtomicWriteTest < ActiveSupport::TestCase contents = "Atomic Text" File.atomic_write(file_name, Dir.pwd) do |file| file.write(contents) - assert !File.exist?(file_name) + assert_not File.exist?(file_name) end assert File.exist?(file_name) assert_equal contents, File.read(file_name) @@ -22,7 +22,7 @@ class AtomicWriteTest < ActiveSupport::TestCase raise "something bad" end rescue - assert !File.exist?(file_name) + assert_not File.exist?(file_name) end def test_atomic_write_preserves_file_permissions @@ -50,7 +50,7 @@ class AtomicWriteTest < ActiveSupport::TestCase contents = "Atomic Text" File.atomic_write(file_name, Dir.pwd) do |file| file.write(contents) - assert !File.exist?(file_name) + assert_not File.exist?(file_name) end assert File.exist?(file_name) assert_equal File.probe_stat_in(Dir.pwd).mode, file_mode diff --git a/activesupport/test/core_ext/integer_ext_test.rb b/activesupport/test/core_ext/integer_ext_test.rb index 14169b084d..5691dc5341 100644 --- a/activesupport/test/core_ext/integer_ext_test.rb +++ b/activesupport/test/core_ext/integer_ext_test.rb @@ -8,14 +8,14 @@ class IntegerExtTest < ActiveSupport::TestCase def test_multiple_of [ -7, 0, 7, 14 ].each { |i| assert i.multiple_of?(7) } - [ -7, 7, 14 ].each { |i| assert ! i.multiple_of?(6) } + [ -7, 7, 14 ].each { |i| assert_not i.multiple_of?(6) } # test the 0 edge case assert 0.multiple_of?(0) - assert !5.multiple_of?(0) + assert_not 5.multiple_of?(0) # test with a prime - [2, 3, 5, 7].each { |i| assert !PRIME.multiple_of?(i) } + [2, 3, 5, 7].each { |i| assert_not PRIME.multiple_of?(i) } end def test_ordinalize diff --git a/activesupport/test/core_ext/module/attr_internal_test.rb b/activesupport/test/core_ext/module/attr_internal_test.rb index c2a28eced4..9a65f75497 100644 --- a/activesupport/test/core_ext/module/attr_internal_test.rb +++ b/activesupport/test/core_ext/module/attr_internal_test.rb @@ -12,7 +12,7 @@ class AttrInternalTest < ActiveSupport::TestCase def test_reader assert_nothing_raised { @target.attr_internal_reader :foo } - assert !@instance.instance_variable_defined?("@_foo") + assert_not @instance.instance_variable_defined?("@_foo") assert_raise(NoMethodError) { @instance.foo = 1 } @instance.instance_variable_set("@_foo", 1) @@ -22,7 +22,7 @@ class AttrInternalTest < ActiveSupport::TestCase def test_writer assert_nothing_raised { @target.attr_internal_writer :foo } - assert !@instance.instance_variable_defined?("@_foo") + assert_not @instance.instance_variable_defined?("@_foo") assert_nothing_raised { assert_equal 1, @instance.foo = 1 } assert_equal 1, @instance.instance_variable_get("@_foo") @@ -32,7 +32,7 @@ class AttrInternalTest < ActiveSupport::TestCase def test_accessor assert_nothing_raised { @target.attr_internal :foo } - assert !@instance.instance_variable_defined?("@_foo") + assert_not @instance.instance_variable_defined?("@_foo") assert_nothing_raised { assert_equal 1, @instance.foo = 1 } assert_equal 1, @instance.instance_variable_get("@_foo") @@ -44,10 +44,10 @@ class AttrInternalTest < ActiveSupport::TestCase assert_nothing_raised { Module.attr_internal_naming_format = "@abc%sdef" } @target.attr_internal :foo - assert !@instance.instance_variable_defined?("@_foo") - assert !@instance.instance_variable_defined?("@abcfoodef") + assert_not @instance.instance_variable_defined?("@_foo") + assert_not @instance.instance_variable_defined?("@abcfoodef") assert_nothing_raised { @instance.foo = 1 } - assert !@instance.instance_variable_defined?("@_foo") + assert_not @instance.instance_variable_defined?("@_foo") assert @instance.instance_variable_defined?("@abcfoodef") ensure Module.attr_internal_naming_format = "@_%s" diff --git a/activesupport/test/core_ext/module/concerning_test.rb b/activesupport/test/core_ext/module/concerning_test.rb index 969434766f..374114c11b 100644 --- a/activesupport/test/core_ext/module/concerning_test.rb +++ b/activesupport/test/core_ext/module/concerning_test.rb @@ -21,7 +21,7 @@ class ModuleConcernTest < ActiveSupport::TestCase # Declares a concern but doesn't include it assert klass.const_defined?(:Baz, false) - assert !ModuleConcernTest.const_defined?(:Baz) + assert_not ModuleConcernTest.const_defined?(:Baz) assert_kind_of ActiveSupport::Concern, klass::Baz assert_not_includes klass.ancestors, klass::Baz, klass.ancestors.inspect diff --git a/activesupport/test/core_ext/name_error_test.rb b/activesupport/test/core_ext/name_error_test.rb index d1dace3713..5c6c12ffc7 100644 --- a/activesupport/test/core_ext/name_error_test.rb +++ b/activesupport/test/core_ext/name_error_test.rb @@ -17,7 +17,7 @@ class NameErrorTest < ActiveSupport::TestCase exc = assert_raise NameError do some_method_that_does_not_exist end - assert !exc.missing_name?(:Foo) + assert_not exc.missing_name?(:Foo) assert_nil exc.missing_name end end diff --git a/activesupport/test/core_ext/object/acts_like_test.rb b/activesupport/test/core_ext/object/acts_like_test.rb index 9f7b81f7fc..31241caf0a 100644 --- a/activesupport/test/core_ext/object/acts_like_test.rb +++ b/activesupport/test/core_ext/object/acts_like_test.rb @@ -17,19 +17,19 @@ class ObjectTests < ActiveSupport::TestCase dt = DateTime.new duck = DuckTime.new - assert !object.acts_like?(:time) - assert !object.acts_like?(:date) + assert_not object.acts_like?(:time) + assert_not object.acts_like?(:date) assert time.acts_like?(:time) - assert !time.acts_like?(:date) + assert_not time.acts_like?(:date) - assert !date.acts_like?(:time) + assert_not date.acts_like?(:time) assert date.acts_like?(:date) assert dt.acts_like?(:time) assert dt.acts_like?(:date) assert duck.acts_like?(:time) - assert !duck.acts_like?(:date) + assert_not duck.acts_like?(:date) end end diff --git a/activesupport/test/core_ext/object/deep_dup_test.rb b/activesupport/test/core_ext/object/deep_dup_test.rb index 2486592441..1fb26ebac7 100644 --- a/activesupport/test/core_ext/object/deep_dup_test.rb +++ b/activesupport/test/core_ext/object/deep_dup_test.rb @@ -47,7 +47,7 @@ class DeepDupTest < ActiveSupport::TestCase object = Object.new dup = object.deep_dup dup.instance_variable_set(:@a, 1) - assert !object.instance_variable_defined?(:@a) + assert_not object.instance_variable_defined?(:@a) assert dup.instance_variable_defined?(:@a) end diff --git a/activesupport/test/core_ext/object/inclusion_test.rb b/activesupport/test/core_ext/object/inclusion_test.rb index 52c21f2e8e..8cbb4f848f 100644 --- a/activesupport/test/core_ext/object/inclusion_test.rb +++ b/activesupport/test/core_ext/object/inclusion_test.rb @@ -6,30 +6,30 @@ require "active_support/core_ext/object/inclusion" class InTest < ActiveSupport::TestCase def test_in_array assert 1.in?([1, 2]) - assert !3.in?([1, 2]) + assert_not 3.in?([1, 2]) end def test_in_hash h = { "a" => 100, "b" => 200 } assert "a".in?(h) - assert !"z".in?(h) + assert_not "z".in?(h) end def test_in_string assert "lo".in?("hello") - assert !"ol".in?("hello") + assert_not "ol".in?("hello") assert ?h.in?("hello") end def test_in_range assert 25.in?(1..50) - assert !75.in?(1..50) + assert_not 75.in?(1..50) end def test_in_set s = Set.new([1, 2]) assert 1.in?(s) - assert !3.in?(s) + assert_not 3.in?(s) end module A @@ -45,8 +45,8 @@ class InTest < ActiveSupport::TestCase def test_in_module assert A.in?(B) assert A.in?(C) - assert !A.in?(A) - assert !A.in?(D) + assert_not A.in?(A) + assert_not A.in?(D) end def test_no_method_catching diff --git a/activesupport/test/core_ext/object/try_test.rb b/activesupport/test/core_ext/object/try_test.rb index 40d6cdd28e..a838334034 100644 --- a/activesupport/test/core_ext/object/try_test.rb +++ b/activesupport/test/core_ext/object/try_test.rb @@ -78,7 +78,6 @@ class ObjectTryTest < ActiveSupport::TestCase def test_try_with_private_method_bang klass = Class.new do private - def private_method "private method" end @@ -90,7 +89,6 @@ class ObjectTryTest < ActiveSupport::TestCase def test_try_with_private_method klass = Class.new do private - def private_method "private method" end @@ -109,7 +107,6 @@ class ObjectTryTest < ActiveSupport::TestCase end private - def private_delegator_method "private delegator method" end @@ -120,11 +117,11 @@ class ObjectTryTest < ActiveSupport::TestCase end def test_try_with_method_on_delegator_target - assert_equal 5, Decorator.new(@string).size + assert_equal 5, Decorator.new(@string).try(:size) end def test_try_with_overridden_method_on_delegator - assert_equal "overridden reverse", Decorator.new(@string).reverse + assert_equal "overridden reverse", Decorator.new(@string).try(:reverse) end def test_try_with_private_method_on_delegator @@ -140,7 +137,6 @@ class ObjectTryTest < ActiveSupport::TestCase def test_try_with_private_method_on_delegator_target klass = Class.new do private - def private_method "private method" end @@ -152,7 +148,6 @@ class ObjectTryTest < ActiveSupport::TestCase def test_try_with_private_method_on_delegator_target_bang klass = Class.new do private - def private_method "private method" end diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb index 903c173e59..7c7a78f461 100644 --- a/activesupport/test/core_ext/range_ext_test.rb +++ b/activesupport/test/core_ext/range_ext_test.rb @@ -37,7 +37,7 @@ class RangeTest < ActiveSupport::TestCase end def test_overlaps_last_exclusive - assert !(1...5).overlaps?(5..10) + assert_not (1...5).overlaps?(5..10) end def test_overlaps_first_inclusive @@ -45,7 +45,7 @@ class RangeTest < ActiveSupport::TestCase end def test_overlaps_first_exclusive - assert !(5..10).overlaps?(1...5) + assert_not (5..10).overlaps?(1...5) end def test_should_include_identical_inclusive @@ -102,7 +102,7 @@ class RangeTest < ActiveSupport::TestCase def test_no_overlaps_on_time time_range_1 = Time.utc(2005, 12, 10, 15, 30)..Time.utc(2005, 12, 10, 17, 30) time_range_2 = Time.utc(2005, 12, 10, 17, 31)..Time.utc(2005, 12, 10, 18, 00) - assert !time_range_1.overlaps?(time_range_2) + assert_not time_range_1.overlaps?(time_range_2) end def test_each_on_time_with_zone diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb index ccaa5dc786..b8de16cc5e 100644 --- a/activesupport/test/core_ext/string_ext_test.rb +++ b/activesupport/test/core_ext/string_ext_test.rb @@ -237,11 +237,11 @@ class StringInflectionsTest < ActiveSupport::TestCase s = "hello" assert s.starts_with?("h") assert s.starts_with?("hel") - assert !s.starts_with?("el") + assert_not s.starts_with?("el") assert s.ends_with?("o") assert s.ends_with?("lo") - assert !s.ends_with?("el") + assert_not s.ends_with?("el") end def test_string_squish diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb index 2ea5f0921c..e650209268 100644 --- a/activesupport/test/core_ext/time_with_zone_test.rb +++ b/activesupport/test/core_ext/time_with_zone_test.rb @@ -289,6 +289,20 @@ class TimeWithZoneTest < ActiveSupport::TestCase end end + def test_before + twz = ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 0, 0), @time_zone) + assert_equal false, twz.before?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 11, 59, 59), @time_zone)) + assert_equal false, twz.before?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 0, 0), @time_zone)) + assert_equal true, twz.before?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 00, 1), @time_zone)) + end + + def test_after + twz = ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 0, 0), @time_zone) + assert_equal true, twz.after?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 11, 59, 59), @time_zone)) + assert_equal false, twz.after?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 0, 0), @time_zone)) + assert_equal false, twz.after?(ActiveSupport::TimeWithZone.new(Time.utc(2017, 3, 6, 12, 00, 1), @time_zone)) + end + def test_eql? assert_equal true, @twz.eql?(@twz.dup) assert_equal true, @twz.eql?(Time.utc(2000)) diff --git a/activesupport/test/core_ext/uri_ext_test.rb b/activesupport/test/core_ext/uri_ext_test.rb index 8816b0d392..c0686bc720 100644 --- a/activesupport/test/core_ext/uri_ext_test.rb +++ b/activesupport/test/core_ext/uri_ext_test.rb @@ -9,6 +9,6 @@ class URIExtTest < ActiveSupport::TestCase str = "\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E" # Ni-ho-nn-go in UTF-8, means Japanese. parser = URI.parser - assert_equal str, parser.unescape(parser.escape(str)) + assert_equal str + str, parser.unescape(str + parser.escape(str).encode(Encoding::UTF_8)) end end diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb index c0c4c4cac5..a4fbab7b55 100644 --- a/activesupport/test/dependencies_test.rb +++ b/activesupport/test/dependencies_test.rb @@ -223,7 +223,7 @@ class DependenciesTest < ActiveSupport::TestCase Timeout.timeout(0.1) do # Remove the constant, as if Rails development middleware is reloading changed files: ActiveSupport::Dependencies.remove_unloadable_constants! - refute defined?(AnotherConstant::ReloadError) + assert_not defined?(AnotherConstant::ReloadError) end # Change the file, so that it is **correct** this time: @@ -231,7 +231,7 @@ class DependenciesTest < ActiveSupport::TestCase # Again: Remove the constant, as if Rails development middleware is reloading changed files: ActiveSupport::Dependencies.remove_unloadable_constants! - refute defined?(AnotherConstant::ReloadError) + assert_not defined?(AnotherConstant::ReloadError) # Now, reload the _fixed_ constant: assert ConstantReloadError @@ -535,9 +535,9 @@ class DependenciesTest < ActiveSupport::TestCase def test_qualified_const_defined_should_not_call_const_missing ModuleWithMissing.missing_count = 0 - assert ! ActiveSupport::Dependencies.qualified_const_defined?("ModuleWithMissing::A") + assert_not ActiveSupport::Dependencies.qualified_const_defined?("ModuleWithMissing::A") assert_equal 0, ModuleWithMissing.missing_count - assert ! ActiveSupport::Dependencies.qualified_const_defined?("ModuleWithMissing::A::B") + assert_not ActiveSupport::Dependencies.qualified_const_defined?("ModuleWithMissing::A::B") assert_equal 0, ModuleWithMissing.missing_count end @@ -547,13 +547,13 @@ class DependenciesTest < ActiveSupport::TestCase def test_autoloaded? with_autoloading_fixtures do - assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder") - assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") + assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder") + assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") assert ActiveSupport::Dependencies.autoloaded?(ModuleFolder) assert ActiveSupport::Dependencies.autoloaded?("ModuleFolder") - assert ! ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") + assert_not ActiveSupport::Dependencies.autoloaded?("ModuleFolder::NestedClass") assert ActiveSupport::Dependencies.autoloaded?(ModuleFolder::NestedClass) @@ -564,11 +564,11 @@ class DependenciesTest < ActiveSupport::TestCase assert ActiveSupport::Dependencies.autoloaded?(:ModuleFolder) # Anonymous modules aren't autoloaded. - assert !ActiveSupport::Dependencies.autoloaded?(Module.new) + assert_not ActiveSupport::Dependencies.autoloaded?(Module.new) nil_name = Module.new def nil_name.name() nil end - assert !ActiveSupport::Dependencies.autoloaded?(nil_name) + assert_not ActiveSupport::Dependencies.autoloaded?(nil_name) end ensure remove_constants(:ModuleFolder) @@ -778,7 +778,7 @@ class DependenciesTest < ActiveSupport::TestCase M.unloadable ActiveSupport::Dependencies.clear - assert ! defined?(M) + assert_not defined?(M) Object.const_set :M, Module.new ActiveSupport::Dependencies.clear @@ -809,7 +809,7 @@ class DependenciesTest < ActiveSupport::TestCase assert_called(C, :before_remove_const, times: 1) do assert_respond_to C, :before_remove_const ActiveSupport::Dependencies.clear - assert !defined?(C) + assert_not defined?(C) end ensure remove_constants(:C) @@ -1023,7 +1023,7 @@ class DependenciesTest < ActiveSupport::TestCase assert !defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!" end - assert !defined?(::RaisesNameError) + assert_not defined?(::RaisesNameError) 2.times do assert_raise(NameError) { ::RaisesNameError } assert !defined?(::RaisesNameError), "::RaisesNameError is defined but it should have failed!" diff --git a/activesupport/test/deprecation/proxy_wrappers_test.rb b/activesupport/test/deprecation/proxy_wrappers_test.rb index 2f866775f6..9e26052fb4 100644 --- a/activesupport/test/deprecation/proxy_wrappers_test.rb +++ b/activesupport/test/deprecation/proxy_wrappers_test.rb @@ -9,16 +9,16 @@ class ProxyWrappersTest < ActiveSupport::TestCase def test_deprecated_object_proxy_doesnt_wrap_falsy_objects proxy = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(nil, "message") - assert !proxy + assert_not proxy end def test_deprecated_instance_variable_proxy_doesnt_wrap_falsy_objects proxy = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(nil, :waffles) - assert !proxy + assert_not proxy end def test_deprecated_constant_proxy_doesnt_wrap_falsy_objects proxy = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(Waffles, NewWaffles) - assert !proxy + assert_not proxy end end diff --git a/activesupport/test/descendants_tracker_without_autoloading_test.rb b/activesupport/test/descendants_tracker_without_autoloading_test.rb index f5c6a3045d..c65f69cba3 100644 --- a/activesupport/test/descendants_tracker_without_autoloading_test.rb +++ b/activesupport/test/descendants_tracker_without_autoloading_test.rb @@ -13,7 +13,7 @@ class DescendantsTrackerWithoutAutoloadingTest < ActiveSupport::TestCase parent_instance = Parent.new parent_instance.singleton_class.descendants ActiveSupport::DescendantsTracker.clear - assert !ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants).key?(parent_instance.singleton_class) + assert_not ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants).key?(parent_instance.singleton_class) end end end diff --git a/activesupport/test/hash_with_indifferent_access_test.rb b/activesupport/test/hash_with_indifferent_access_test.rb index b06250baf8..a20c428bf8 100644 --- a/activesupport/test/hash_with_indifferent_access_test.rb +++ b/activesupport/test/hash_with_indifferent_access_test.rb @@ -280,7 +280,7 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase replaced = hash.replace(b: 12) assert hash.key?("b") - assert !hash.key?(:a) + assert_not hash.key?(:a) assert_equal 12, hash[:b] assert_same hash, replaced end @@ -292,7 +292,7 @@ class HashWithIndifferentAccessTest < ActiveSupport::TestCase replaced = hash.replace(HashByConversion.new(b: 12)) assert hash.key?("b") - assert !hash.key?(:a) + assert_not hash.key?(:a) assert_equal 12, hash[:b] assert_same hash, replaced end diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb index 05d5c1cbc3..0fa53695e0 100644 --- a/activesupport/test/message_verifier_test.rb +++ b/activesupport/test/message_verifier_test.rb @@ -25,12 +25,12 @@ class MessageVerifierTest < ActiveSupport::TestCase def test_valid_message data, hash = @verifier.generate(@data).split("--") - assert !@verifier.valid_message?(nil) - assert !@verifier.valid_message?("") - assert !@verifier.valid_message?("\xff") # invalid encoding - assert !@verifier.valid_message?("#{data.reverse}--#{hash}") - assert !@verifier.valid_message?("#{data}--#{hash.reverse}") - assert !@verifier.valid_message?("purejunk") + assert_not @verifier.valid_message?(nil) + assert_not @verifier.valid_message?("") + assert_not @verifier.valid_message?("\xff") # invalid encoding + assert_not @verifier.valid_message?("#{data.reverse}--#{hash}") + assert_not @verifier.valid_message?("#{data}--#{hash.reverse}") + assert_not @verifier.valid_message?("purejunk") end def test_simple_round_tripping @@ -40,7 +40,7 @@ class MessageVerifierTest < ActiveSupport::TestCase end def test_verified_returns_false_on_invalid_message - assert !@verifier.verified("purejunk") + assert_not @verifier.verified("purejunk") end def test_verify_exception_on_invalid_message diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb index 560b86b1a3..061446c782 100644 --- a/activesupport/test/multibyte_chars_test.rb +++ b/activesupport/test/multibyte_chars_test.rb @@ -75,7 +75,7 @@ class MultibyteCharsTest < ActiveSupport::TestCase def test_consumes_utf8_strings assert @proxy_class.consumes?(UNICODE_STRING) assert @proxy_class.consumes?(ASCII_STRING) - assert !@proxy_class.consumes?(BYTE_STRING) + assert_not @proxy_class.consumes?(BYTE_STRING) end def test_concatenation_should_return_a_proxy_class_instance @@ -148,7 +148,7 @@ class MultibyteCharsUTF8BehaviourTest < ActiveSupport::TestCase def test_identity assert_equal @chars, @chars assert @chars.eql?(@chars) - assert !@chars.eql?(UNICODE_STRING) + assert_not @chars.eql?(UNICODE_STRING) end def test_string_methods_are_chainable diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb index 28a02aafc8..d035f993f7 100644 --- a/activesupport/test/notifications_test.rb +++ b/activesupport/test/notifications_test.rb @@ -270,9 +270,9 @@ module Notifications parent.children << child assert parent.parent_of?(child) - assert !child.parent_of?(parent) - assert !parent.parent_of?(not_child) - assert !not_child.parent_of?(parent) + assert_not child.parent_of?(parent) + assert_not parent.parent_of?(not_child) + assert_not not_child.parent_of?(parent) end private diff --git a/activesupport/test/ordered_options_test.rb b/activesupport/test/ordered_options_test.rb index cba5b8a8de..90394fee0a 100644 --- a/activesupport/test/ordered_options_test.rb +++ b/activesupport/test/ordered_options_test.rb @@ -15,7 +15,7 @@ class OrderedOptionsTest < ActiveSupport::TestCase a[:allow_concurrency] = false assert_equal 1, a.size - assert !a[:allow_concurrency] + assert_not a[:allow_concurrency] a["else_where"] = 56 assert_equal 2, a.size @@ -47,7 +47,7 @@ class OrderedOptionsTest < ActiveSupport::TestCase a.allow_concurrency = false assert_equal 1, a.size - assert !a.allow_concurrency + assert_not a.allow_concurrency a.else_where = 56 assert_equal 2, a.size diff --git a/activesupport/test/reloader_test.rb b/activesupport/test/reloader_test.rb index 3e4229eaf7..976917c1a1 100644 --- a/activesupport/test/reloader_test.rb +++ b/activesupport/test/reloader_test.rb @@ -8,18 +8,18 @@ class ReloaderTest < ActiveSupport::TestCase reloader.to_prepare { prepared = true } reloader.to_complete { completed = true } - assert !prepared - assert !completed + assert_not prepared + assert_not completed reloader.prepare! assert prepared - assert !completed + assert_not completed prepared = false reloader.wrap do assert prepared prepared = false end - assert !prepared + assert_not prepared end def test_prepend_prepare_callback @@ -42,7 +42,7 @@ class ReloaderTest < ActiveSupport::TestCase invoked = false r.to_run { invoked = true } r.wrap {} - assert !invoked + assert_not invoked end def test_full_reload_sequence diff --git a/activesupport/test/security_utils_test.rb b/activesupport/test/security_utils_test.rb index 0a607594a2..fff9cc2a8d 100644 --- a/activesupport/test/security_utils_test.rb +++ b/activesupport/test/security_utils_test.rb @@ -11,7 +11,7 @@ class SecurityUtilsTest < ActiveSupport::TestCase def test_fixed_length_secure_compare_should_perform_string_comparison assert ActiveSupport::SecurityUtils.fixed_length_secure_compare("a", "a") - assert !ActiveSupport::SecurityUtils.fixed_length_secure_compare("a", "b") + assert_not ActiveSupport::SecurityUtils.fixed_length_secure_compare("a", "b") end def test_fixed_length_secure_compare_raise_on_length_mismatch diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb index eced622137..8a1ecb6b33 100644 --- a/activesupport/test/test_case_test.rb +++ b/activesupport/test/test_case_test.rb @@ -2,7 +2,7 @@ require "abstract_unit" -class AssertDifferenceTest < ActiveSupport::TestCase +class AssertionsTest < ActiveSupport::TestCase def setup @object = Class.new do attr_accessor :num diff --git a/activesupport/test/testing/after_teardown_test.rb b/activesupport/test/testing/after_teardown_test.rb new file mode 100644 index 0000000000..68c368909c --- /dev/null +++ b/activesupport/test/testing/after_teardown_test.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require "abstract_unit" + +module OtherAfterTeardown + def after_teardown + @witness = true + end +end + +class AfterTeardownTest < Minitest::Test + include OtherAfterTeardown + include ActiveSupport::Testing::SetupAndTeardown + + attr_writer :witness + + MyError = Class.new(StandardError) + + teardown do + raise MyError, "Test raises an error, all after_teardown should still get called" + end + + def after_teardown + assert_raises MyError do + super + end + + assert_equal true, @witness + end + + def test_teardown_raise_but_all_after_teardown_method_are_called + assert true + end +end diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb index 63ca22efb5..120afa61f2 100644 --- a/activesupport/test/time_zone_test.rb +++ b/activesupport/test/time_zone_test.rb @@ -725,6 +725,21 @@ class TimeZoneTest < ActiveSupport::TestCase assert_not_includes all_zones, galapagos end + def test_all_not_raises_exception_with_mizzing_tzinfo_data + mappings = { + "Puerto Rico" => "America/Unknown", + "Pittsburgh" => "America/New_York" + } + + with_tz_mappings(mappings) do + assert_nil ActiveSupport::TimeZone["Puerto Rico"] + assert_nil ActiveSupport::TimeZone[-9] + assert_nothing_raised do + ActiveSupport::TimeZone.all + end + end + end + def test_index assert_nil ActiveSupport::TimeZone["bogus"] assert_instance_of ActiveSupport::TimeZone, ActiveSupport::TimeZone["Central Time (US & Canada)"] diff --git a/activesupport/test/time_zone_test_helpers.rb b/activesupport/test/time_zone_test_helpers.rb index 051703a781..85ed727c9b 100644 --- a/activesupport/test/time_zone_test_helpers.rb +++ b/activesupport/test/time_zone_test_helpers.rb @@ -23,4 +23,17 @@ module TimeZoneTestHelpers ensure ActiveSupport.to_time_preserves_timezone = old_preserve_tz end + + def with_tz_mappings(mappings) + old_mappings = ActiveSupport::TimeZone::MAPPING.dup + ActiveSupport::TimeZone.clear + ActiveSupport::TimeZone::MAPPING.clear + ActiveSupport::TimeZone::MAPPING.merge!(mappings) + + yield + ensure + ActiveSupport::TimeZone.clear + ActiveSupport::TimeZone::MAPPING.clear + ActiveSupport::TimeZone::MAPPING.merge!(old_mappings) + end end diff --git a/ci/custom_cops/bin/test b/ci/custom_cops/bin/test new file mode 100755 index 0000000000..495ffec83a --- /dev/null +++ b/ci/custom_cops/bin/test @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby +# frozen_string_literal: true + +COMPONENT_ROOT = File.expand_path("..", __dir__) +require_relative "../../../tools/test" diff --git a/ci/custom_cops/lib/custom_cops.rb b/ci/custom_cops/lib/custom_cops.rb new file mode 100644 index 0000000000..157b8247e4 --- /dev/null +++ b/ci/custom_cops/lib/custom_cops.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +require_relative "custom_cops/refute_not" +require_relative "custom_cops/assert_not" diff --git a/ci/custom_cops/lib/custom_cops/assert_not.rb b/ci/custom_cops/lib/custom_cops/assert_not.rb new file mode 100644 index 0000000000..e722448e21 --- /dev/null +++ b/ci/custom_cops/lib/custom_cops/assert_not.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module CustomCops + # Enforces the use of `assert_not` over `assert !`. + # + # @example + # # bad + # assert !x + # assert ! x + # + # # good + # assert_not x + # + class AssertNot < RuboCop::Cop::Cop + MSG = "Prefer `assert_not` over `assert !`" + + def_node_matcher :offensive?, "(send nil? :assert (send ... :!))" + + def on_send(node) + add_offense(node) if offensive?(node) + end + + def autocorrect(node) + expression = node.loc.expression + + ->(corrector) do + corrector.replace( + expression, + corrected_source(expression.source) + ) + end + end + + private + + def corrected_source(source) + source.gsub(/^assert(\(| ) *! */, "assert_not\\1") + end + end +end diff --git a/ci/custom_cops/lib/custom_cops/refute_not.rb b/ci/custom_cops/lib/custom_cops/refute_not.rb new file mode 100644 index 0000000000..3e89e0fd32 --- /dev/null +++ b/ci/custom_cops/lib/custom_cops/refute_not.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module CustomCops + # Enforces the use of `#assert_not` methods over `#refute` methods. + # + # @example + # # bad + # refute false + # refute_empty [1, 2, 3] + # refute_equal true, false + # + # # good + # assert_not false + # assert_not_empty [1, 2, 3] + # assert_not_equal true, false + # + class RefuteNot < RuboCop::Cop::Cop + MSG = "Prefer `%<assert_method>s` over `%<refute_method>s`" + + CORRECTIONS = { + refute: "assert_not", + refute_empty: "assert_not_empty", + refute_equal: "assert_not_equal", + refute_in_delta: "assert_not_in_delta", + refute_in_epsilon: "assert_not_in_epsilon", + refute_includes: "assert_not_includes", + refute_instance_of: "assert_not_instance_of", + refute_kind_of: "assert_not_kind_of", + refute_nil: "assert_not_nil", + refute_operator: "assert_not_operator", + refute_predicate: "assert_not_predicate", + refute_respond_to: "assert_not_respond_to", + refute_same: "assert_not_same", + refute_match: "assert_no_match" + }.freeze + + OFFENSIVE_METHODS = CORRECTIONS.keys.freeze + + def_node_matcher :offensive?, "(send nil? #offensive_method? ...)" + + def on_send(node) + return unless offensive?(node) + + message = offense_message(node.method_name) + add_offense(node, location: :selector, message: message) + end + + def autocorrect(node) + ->(corrector) do + corrector.replace( + node.loc.selector, + CORRECTIONS[node.method_name] + ) + end + end + + private + + def offensive_method?(method_name) + OFFENSIVE_METHODS.include?(method_name) + end + + def offense_message(method_name) + format( + MSG, + refute_method: method_name, + assert_method: CORRECTIONS[method_name] + ) + end + end +end diff --git a/ci/custom_cops/test/custom_cops/assert_not_test.rb b/ci/custom_cops/test/custom_cops/assert_not_test.rb new file mode 100644 index 0000000000..abb151aeb4 --- /dev/null +++ b/ci/custom_cops/test/custom_cops/assert_not_test.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require "support/cop_helper" +require_relative "../../lib/custom_cops/assert_not" + +class AssertNotTest < ActiveSupport::TestCase + include CopHelper + + setup do + @cop = CustomCops::AssertNot.new + end + + test "rejects 'assert !'" do + inspect_source @cop, "assert !x" + assert_offense @cop, "^^^^^^^^^ Prefer `assert_not` over `assert !`" + end + + test "rejects 'assert !' with a complex value" do + inspect_source @cop, "assert !a.b(c)" + assert_offense @cop, "^^^^^^^^^^^^^^ Prefer `assert_not` over `assert !`" + end + + test "autocorrects `assert !`" do + corrected = autocorrect_source(@cop, "assert !false") + assert_equal "assert_not false", corrected + end + + test "autocorrects `assert !` with extra spaces" do + corrected = autocorrect_source(@cop, "assert ! false") + assert_equal "assert_not false", corrected + end + + test "autocorrects `assert !` with parentheses" do + corrected = autocorrect_source(@cop, "assert(!false)") + assert_equal "assert_not(false)", corrected + end + + test "accepts `assert_not`" do + inspect_source @cop, "assert_not x" + assert_empty @cop.offenses + end +end diff --git a/ci/custom_cops/test/custom_cops/refute_not_test.rb b/ci/custom_cops/test/custom_cops/refute_not_test.rb new file mode 100644 index 0000000000..f0f6eaeda0 --- /dev/null +++ b/ci/custom_cops/test/custom_cops/refute_not_test.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require "support/cop_helper" +require_relative "../../lib/custom_cops/refute_not" + +class RefuteNotTest < ActiveSupport::TestCase + include CopHelper + + setup do + @cop = CustomCops::RefuteNot.new + end + + { + refute: :assert_not, + refute_empty: :assert_not_empty, + refute_equal: :assert_not_equal, + refute_in_delta: :assert_not_in_delta, + refute_in_epsilon: :assert_not_in_epsilon, + refute_includes: :assert_not_includes, + refute_instance_of: :assert_not_instance_of, + refute_kind_of: :assert_not_kind_of, + refute_nil: :assert_not_nil, + refute_operator: :assert_not_operator, + refute_predicate: :assert_not_predicate, + refute_respond_to: :assert_not_respond_to, + refute_same: :assert_not_same, + refute_match: :assert_no_match + }.each do |refute_method, assert_method| + test "rejects `#{refute_method}` with a single argument" do + inspect_source(@cop, "#{refute_method} a") + assert_offense @cop, offense_message(refute_method, assert_method) + end + + test "rejects `#{refute_method}` with multiple arguments" do + inspect_source(@cop, "#{refute_method} a, b, c") + assert_offense @cop, offense_message(refute_method, assert_method) + end + + test "autocorrects `#{refute_method}` with a single argument" do + corrected = autocorrect_source(@cop, "#{refute_method} a") + assert_equal "#{assert_method} a", corrected + end + + test "autocorrects `#{refute_method}` with multiple arguments" do + corrected = autocorrect_source(@cop, "#{refute_method} a, b, c") + assert_equal "#{assert_method} a, b, c", corrected + end + + test "accepts `#{assert_method}` with a single argument" do + inspect_source(@cop, "#{assert_method} a") + assert_empty @cop.offenses + end + + test "accepts `#{assert_method}` with multiple arguments" do + inspect_source(@cop, "#{assert_method} a, b, c") + assert_empty @cop.offenses + end + end + + private + + def offense_message(refute_method, assert_method) + carets = "^" * refute_method.to_s.length + "#{carets} Prefer `#{assert_method}` over `#{refute_method}`" + end +end diff --git a/ci/custom_cops/test/support/cop_helper.rb b/ci/custom_cops/test/support/cop_helper.rb new file mode 100644 index 0000000000..c2c6b969dd --- /dev/null +++ b/ci/custom_cops/test/support/cop_helper.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +require "rubocop" + +module CopHelper + def inspect_source(cop, source) + processed_source = parse_source(source) + raise "Error parsing example code" unless processed_source.valid_syntax? + investigate(cop, processed_source) + processed_source + end + + def autocorrect_source(cop, source) + cop.instance_variable_get(:@options)[:auto_correct] = true + processed_source = inspect_source(cop, source) + rewrite(cop, processed_source) + end + + def assert_offense(cop, expected_message) + assert_not_empty( + cop.offenses, + "Expected offense with message \"#{expected_message}\", but got no offense" + ) + + offense = cop.offenses.first + carets = "^" * offense.column_length + + assert_equal expected_message, "#{carets} #{offense.message}" + end + + private + TARGET_RUBY_VERSION = 2.4 + + def parse_source(source) + RuboCop::ProcessedSource.new(source, TARGET_RUBY_VERSION) + end + + def rewrite(cop, processed_source) + RuboCop::Cop::Corrector.new(processed_source.buffer, cop.corrections) + .rewrite + end + + def investigate(cop, processed_source) + RuboCop::Cop::Commissioner.new([cop], [], raise_error: true) + .investigate(processed_source) + end +end diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md index 21b4ed4b9f..0307e06fd9 100644 --- a/guides/CHANGELOG.md +++ b/guides/CHANGELOG.md @@ -1,5 +1,3 @@ -## Rails 6.0.0.alpha (Unreleased) ## - * Rails 6 requires Ruby 2.4.1 or newer. *Jeremy Daer* diff --git a/guides/assets/images/rails4_features.png b/guides/assets/images/4_0_release_notes/rails4_features.png Binary files differindex ac73f05cf7..ac73f05cf7 100644 --- a/guides/assets/images/rails4_features.png +++ b/guides/assets/images/4_0_release_notes/rails4_features.png diff --git a/guides/assets/images/akshaysurve.jpg b/guides/assets/images/akshaysurve.jpg Binary files differdeleted file mode 100644 index cfc3333958..0000000000 --- a/guides/assets/images/akshaysurve.jpg +++ /dev/null diff --git a/guides/assets/images/belongs_to.png b/guides/assets/images/association_basics/belongs_to.png Binary files differindex 2b8c1d52ea..2b8c1d52ea 100644 --- a/guides/assets/images/belongs_to.png +++ b/guides/assets/images/association_basics/belongs_to.png diff --git a/guides/assets/images/habtm.png b/guides/assets/images/association_basics/habtm.png Binary files differindex 7e508cc1a6..7e508cc1a6 100644 --- a/guides/assets/images/habtm.png +++ b/guides/assets/images/association_basics/habtm.png diff --git a/guides/assets/images/has_many.png b/guides/assets/images/association_basics/has_many.png Binary files differindex 36ccf9f0f6..36ccf9f0f6 100644 --- a/guides/assets/images/has_many.png +++ b/guides/assets/images/association_basics/has_many.png diff --git a/guides/assets/images/has_many_through.png b/guides/assets/images/association_basics/has_many_through.png Binary files differindex 9e9caabd73..9e9caabd73 100644 --- a/guides/assets/images/has_many_through.png +++ b/guides/assets/images/association_basics/has_many_through.png diff --git a/guides/assets/images/has_one.png b/guides/assets/images/association_basics/has_one.png Binary files differindex c29c6b9c59..c29c6b9c59 100644 --- a/guides/assets/images/has_one.png +++ b/guides/assets/images/association_basics/has_one.png diff --git a/guides/assets/images/has_one_through.png b/guides/assets/images/association_basics/has_one_through.png Binary files differindex fdf13286c4..fdf13286c4 100644 --- a/guides/assets/images/has_one_through.png +++ b/guides/assets/images/association_basics/has_one_through.png diff --git a/guides/assets/images/polymorphic.png b/guides/assets/images/association_basics/polymorphic.png Binary files differindex d630db9e01..d630db9e01 100644 --- a/guides/assets/images/polymorphic.png +++ b/guides/assets/images/association_basics/polymorphic.png diff --git a/guides/assets/images/credits_pic_blank.gif b/guides/assets/images/credits_pic_blank.gif Binary files differdeleted file mode 100644 index a6b335d0c9..0000000000 --- a/guides/assets/images/credits_pic_blank.gif +++ /dev/null diff --git a/guides/assets/images/fxn.png b/guides/assets/images/fxn.png Binary files differdeleted file mode 100644 index 733d380cba..0000000000 --- a/guides/assets/images/fxn.png +++ /dev/null diff --git a/guides/assets/images/getting_started/routing_error_no_route_matches.png b/guides/assets/images/getting_started/routing_error_no_route_matches.png Binary files differdeleted file mode 100644 index 08c54f921f..0000000000 --- a/guides/assets/images/getting_started/routing_error_no_route_matches.png +++ /dev/null diff --git a/guides/assets/images/header_backdrop.png b/guides/assets/images/header_backdrop.png Binary files differdeleted file mode 100644 index 81f4d91774..0000000000 --- a/guides/assets/images/header_backdrop.png +++ /dev/null diff --git a/guides/assets/images/icons/README b/guides/assets/images/icons/README deleted file mode 100644 index 09da77fc86..0000000000 --- a/guides/assets/images/icons/README +++ /dev/null @@ -1,5 +0,0 @@ -Replaced the plain DocBook XSL admonition icons with Jimmac's DocBook -icons (http://jimmac.musichall.cz/ikony.php3). I dropped transparency -from the Jimmac icons to get round MS IE and FOP PNG incompatibilities. - -Stuart Rackham diff --git a/guides/assets/images/icons/callouts/1.png b/guides/assets/images/icons/callouts/1.png Binary files differdeleted file mode 100644 index c5d02adcf4..0000000000 --- a/guides/assets/images/icons/callouts/1.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/10.png b/guides/assets/images/icons/callouts/10.png Binary files differdeleted file mode 100644 index fe89f9ef83..0000000000 --- a/guides/assets/images/icons/callouts/10.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/11.png b/guides/assets/images/icons/callouts/11.png Binary files differdeleted file mode 100644 index 3b7b9318e7..0000000000 --- a/guides/assets/images/icons/callouts/11.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/12.png b/guides/assets/images/icons/callouts/12.png Binary files differdeleted file mode 100644 index 7b95925e9d..0000000000 --- a/guides/assets/images/icons/callouts/12.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/13.png b/guides/assets/images/icons/callouts/13.png Binary files differdeleted file mode 100644 index 4b99fe8efc..0000000000 --- a/guides/assets/images/icons/callouts/13.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/14.png b/guides/assets/images/icons/callouts/14.png Binary files differdeleted file mode 100644 index dbde9ca749..0000000000 --- a/guides/assets/images/icons/callouts/14.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/15.png b/guides/assets/images/icons/callouts/15.png Binary files differdeleted file mode 100644 index 70e4bba615..0000000000 --- a/guides/assets/images/icons/callouts/15.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/2.png b/guides/assets/images/icons/callouts/2.png Binary files differdeleted file mode 100644 index 8c57970ba9..0000000000 --- a/guides/assets/images/icons/callouts/2.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/3.png b/guides/assets/images/icons/callouts/3.png Binary files differdeleted file mode 100644 index 57a33d15b4..0000000000 --- a/guides/assets/images/icons/callouts/3.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/4.png b/guides/assets/images/icons/callouts/4.png Binary files differdeleted file mode 100644 index f061ab02b8..0000000000 --- a/guides/assets/images/icons/callouts/4.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/5.png b/guides/assets/images/icons/callouts/5.png Binary files differdeleted file mode 100644 index b4de02da11..0000000000 --- a/guides/assets/images/icons/callouts/5.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/6.png b/guides/assets/images/icons/callouts/6.png Binary files differdeleted file mode 100644 index 0e055eec1e..0000000000 --- a/guides/assets/images/icons/callouts/6.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/7.png b/guides/assets/images/icons/callouts/7.png Binary files differdeleted file mode 100644 index 5ead87d040..0000000000 --- a/guides/assets/images/icons/callouts/7.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/8.png b/guides/assets/images/icons/callouts/8.png Binary files differdeleted file mode 100644 index cb99545eb6..0000000000 --- a/guides/assets/images/icons/callouts/8.png +++ /dev/null diff --git a/guides/assets/images/icons/callouts/9.png b/guides/assets/images/icons/callouts/9.png Binary files differdeleted file mode 100644 index 0ac03602f6..0000000000 --- a/guides/assets/images/icons/callouts/9.png +++ /dev/null diff --git a/guides/assets/images/icons/caution.png b/guides/assets/images/icons/caution.png Binary files differdeleted file mode 100644 index 7227b54b32..0000000000 --- a/guides/assets/images/icons/caution.png +++ /dev/null diff --git a/guides/assets/images/icons/example.png b/guides/assets/images/icons/example.png Binary files differdeleted file mode 100644 index a0e855befa..0000000000 --- a/guides/assets/images/icons/example.png +++ /dev/null diff --git a/guides/assets/images/icons/home.png b/guides/assets/images/icons/home.png Binary files differdeleted file mode 100644 index e70e164522..0000000000 --- a/guides/assets/images/icons/home.png +++ /dev/null diff --git a/guides/assets/images/icons/important.png b/guides/assets/images/icons/important.png Binary files differdeleted file mode 100644 index bab53bf3aa..0000000000 --- a/guides/assets/images/icons/important.png +++ /dev/null diff --git a/guides/assets/images/icons/next.png b/guides/assets/images/icons/next.png Binary files differdeleted file mode 100644 index a158832725..0000000000 --- a/guides/assets/images/icons/next.png +++ /dev/null diff --git a/guides/assets/images/icons/note.png b/guides/assets/images/icons/note.png Binary files differdeleted file mode 100644 index 62eec7845f..0000000000 --- a/guides/assets/images/icons/note.png +++ /dev/null diff --git a/guides/assets/images/icons/prev.png b/guides/assets/images/icons/prev.png Binary files differdeleted file mode 100644 index 8a96960422..0000000000 --- a/guides/assets/images/icons/prev.png +++ /dev/null diff --git a/guides/assets/images/icons/tip.png b/guides/assets/images/icons/tip.png Binary files differdeleted file mode 100644 index a5316d318f..0000000000 --- a/guides/assets/images/icons/tip.png +++ /dev/null diff --git a/guides/assets/images/icons/up.png b/guides/assets/images/icons/up.png Binary files differdeleted file mode 100644 index 6cac818170..0000000000 --- a/guides/assets/images/icons/up.png +++ /dev/null diff --git a/guides/assets/images/icons/warning.png b/guides/assets/images/icons/warning.png Binary files differdeleted file mode 100644 index 72a8a5d873..0000000000 --- a/guides/assets/images/icons/warning.png +++ /dev/null diff --git a/guides/assets/images/oscardelben.jpg b/guides/assets/images/oscardelben.jpg Binary files differdeleted file mode 100644 index 9f3f67c2c7..0000000000 --- a/guides/assets/images/oscardelben.jpg +++ /dev/null diff --git a/guides/assets/images/radar.png b/guides/assets/images/radar.png Binary files differdeleted file mode 100644 index 421b62b623..0000000000 --- a/guides/assets/images/radar.png +++ /dev/null diff --git a/guides/assets/images/rails_logo_remix.gif b/guides/assets/images/rails_logo_remix.gif Binary files differdeleted file mode 100644 index 58960ee4f9..0000000000 --- a/guides/assets/images/rails_logo_remix.gif +++ /dev/null diff --git a/guides/assets/images/csrf.png b/guides/assets/images/security/csrf.png Binary files differindex a8123d47c3..a8123d47c3 100644 --- a/guides/assets/images/csrf.png +++ b/guides/assets/images/security/csrf.png diff --git a/guides/assets/images/session_fixation.png b/guides/assets/images/security/session_fixation.png Binary files differindex e009484f09..e009484f09 100644 --- a/guides/assets/images/session_fixation.png +++ b/guides/assets/images/security/session_fixation.png diff --git a/guides/assets/images/tab_yellow.png b/guides/assets/images/tab_yellow.png Binary files differdeleted file mode 100644 index 053c807d28..0000000000 --- a/guides/assets/images/tab_yellow.png +++ /dev/null diff --git a/guides/assets/images/vijaydev.jpg b/guides/assets/images/vijaydev.jpg Binary files differdeleted file mode 100644 index fe5e4f1cb4..0000000000 --- a/guides/assets/images/vijaydev.jpg +++ /dev/null diff --git a/guides/assets/stylesheets/main.css b/guides/assets/stylesheets/main.css index b27776745a..a3ab745234 100644 --- a/guides/assets/stylesheets/main.css +++ b/guides/assets/stylesheets/main.css @@ -70,7 +70,7 @@ table { } table th, table td { - padding: 0.25em 1em; + padding: 9px 10px; border: 1px solid #CCC; border-collapse: collapse; } @@ -79,7 +79,6 @@ table th { border-bottom: 2px solid #CCC; background: #EEE; font-weight: bold; - padding: 0.5em 1em; } img { @@ -265,8 +264,6 @@ body { } } -#extraCol {display: none;} - #footer { padding: 2em 0; background: #222 url(../images/footer_tile.gif) repeat-x; @@ -555,8 +552,6 @@ h6 { font-size: 1.2857em; padding: 0.125em 0 0.25em 0; margin-bottom: 0; - /*background: url(../images/book_icon.gif) no-repeat left top; - padding: 0.125em 0 0.25em 28px;*/ } @media screen and (max-width: 480px) { @@ -665,10 +660,8 @@ div.code_container { visibility: hidden; } -.clearfix {display: inline-block;} * html .clearfix {height: 1%;} .clearfix {display: block;} -.clear { clear:both; } /* Same bottom margin for special boxes than for regular paragraphs, this way intermediate whitespace looks uniform. */ @@ -696,9 +689,6 @@ div.important p, div.caution p, div.warning p, div.note p, div.info p { /* Foundation v2.1.4 http://foundation.zurb.com */ /* Artfully masterminded by ZURB */ -table th { font-weight: bold; } -table td, table th { padding: 9px 10px; text-align: left; } - /* Mobile */ @media only screen and (max-width: 767px) { table.responsive { margin-bottom: 0; } diff --git a/guides/assets/stylesheets/print.css b/guides/assets/stylesheets/print.css index bdc8ec948d..6280422469 100644 --- a/guides/assets/stylesheets/print.css +++ b/guides/assets/stylesheets/print.css @@ -4,7 +4,7 @@ /* Modified January 31, 2009 --------------------------------------- */ -body, .wrapper, .note, .info, code, #topNav, .L, .R, #frame, #container, #header, #navigation, #footer, #feature, #mainCol, #subCol, #extraCol, .content {position: static; text-align: left; text-indent: 0; background: White; color: Black; border-color: Black; width: auto; height: auto; display: block; float: none; min-height: 0; margin: 0; padding: 0;} +body, .wrapper, .note, .info, code, #topNav, .L, .R, #frame, #container, #header, #navigation, #footer, #feature, #mainCol, #subCol, .content {position: static; text-align: left; text-indent: 0; background: White; color: Black; border-color: Black; width: auto; height: auto; display: block; float: none; min-height: 0; margin: 0; padding: 0;} body { background: #FFF; diff --git a/guides/assets/stylesheets/responsive-tables.css b/guides/assets/stylesheets/responsive-tables.css deleted file mode 100644 index f5fbcbf948..0000000000 --- a/guides/assets/stylesheets/responsive-tables.css +++ /dev/null @@ -1,50 +0,0 @@ -/* Foundation v2.1.4 http://foundation.zurb.com */ -/* Artfully masterminded by ZURB */ - -/* -------------------------------------------------- - Table of Contents ------------------------------------------------------ -:: Shared Styles -:: Page Name 1 -:: Page Name 2 -*/ - - -/* ----------------------------------------- - Shared Styles ------------------------------------------ */ - -table th { font-weight: bold; } -table td, table th { padding: 9px 10px; text-align: left; } - -/* Mobile */ -@media only screen and (max-width: 767px) { - - table { margin-bottom: 0; } - - .pinned { position: absolute; left: 0; top: 0; background: #fff; width: 35%; overflow: hidden; overflow-x: scroll; border-right: 1px solid #ccc; border-left: 1px solid #ccc; } - .pinned table { border-right: none; border-left: none; width: 100%; } - .pinned table th, .pinned table td { white-space: nowrap; } - .pinned td:last-child { border-bottom: 0; } - - div.table-wrapper { position: relative; margin-bottom: 20px; overflow: hidden; border-right: 1px solid #ccc; } - div.table-wrapper div.scrollable table { margin-left: 35%; } - div.table-wrapper div.scrollable { overflow: scroll; overflow-y: hidden; } - - table td, table th { position: relative; white-space: nowrap; overflow: hidden; } - table th:first-child, table td:first-child, table td:first-child, table.pinned td { display: none; } - -} - -/* ----------------------------------------- - Page Name 1 ------------------------------------------ */ - - - - -/* ----------------------------------------- - Page Name 2 ------------------------------------------ */ - - diff --git a/guides/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb index 7fc85e636a..e8b6ad19dd 100644 --- a/guides/bug_report_templates/action_controller_gem.rb +++ b/guides/bug_report_templates/action_controller_gem.rb @@ -13,7 +13,7 @@ gemfile(true) do git_source(:github) { |repo| "https://github.com/#{repo}.git" } # Activate the gem you are reporting the issue against. - gem "rails", "5.2.0.rc1" + gem "rails", "5.2.0" end require "rack/test" diff --git a/guides/bug_report_templates/active_job_gem.rb b/guides/bug_report_templates/active_job_gem.rb index 6b30a7d446..720b7e9c51 100644 --- a/guides/bug_report_templates/active_job_gem.rb +++ b/guides/bug_report_templates/active_job_gem.rb @@ -13,7 +13,7 @@ gemfile(true) do git_source(:github) { |repo| "https://github.com/#{repo}.git" } # Activate the gem you are reporting the issue against. - gem "activejob", "5.2.0.rc1" + gem "activejob", "5.2.0" end require "minitest/autorun" diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb index fabc2a2382..c0d705239b 100644 --- a/guides/bug_report_templates/active_record_gem.rb +++ b/guides/bug_report_templates/active_record_gem.rb @@ -13,7 +13,7 @@ gemfile(true) do git_source(:github) { |repo| "https://github.com/#{repo}.git" } # Activate the gem you are reporting the issue against. - gem "activerecord", "5.2.0.rc1" + gem "activerecord", "5.2.0" gem "sqlite3" end diff --git a/guides/bug_report_templates/active_record_migrations_gem.rb b/guides/bug_report_templates/active_record_migrations_gem.rb index ca9987f956..f47cf08766 100644 --- a/guides/bug_report_templates/active_record_migrations_gem.rb +++ b/guides/bug_report_templates/active_record_migrations_gem.rb @@ -13,7 +13,7 @@ gemfile(true) do git_source(:github) { |repo| "https://github.com/#{repo}.git" } # Activate the gem you are reporting the issue against. - gem "activerecord", "5.2.0.rc1" + gem "activerecord", "5.2.0" gem "sqlite3" end diff --git a/guides/bug_report_templates/generic_gem.rb b/guides/bug_report_templates/generic_gem.rb index 7a55d7c660..0935354bf4 100644 --- a/guides/bug_report_templates/generic_gem.rb +++ b/guides/bug_report_templates/generic_gem.rb @@ -13,7 +13,7 @@ gemfile(true) do git_source(:github) { |repo| "https://github.com/#{repo}.git" } # Activate the gem you are reporting the issue against. - gem "activesupport", "5.2.0.rc1" + gem "activesupport", "5.2.0" end require "active_support" diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb index 7205f37be7..c83538ad48 100644 --- a/guides/rails_guides/generator.rb +++ b/guides/rails_guides/generator.rb @@ -141,32 +141,34 @@ module RailsGuides puts "Generating #{guide} as #{output_file}" layout = @kindle ? "kindle/layout" : "layout" - File.open(output_path, "w") do |f| - view = ActionView::Base.new( - @source_dir, - edge: @edge, - version: @version, - mobi: "kindle/#{mobi}", - language: @language - ) - view.extend(Helpers) - - if guide =~ /\.(\w+)\.erb$/ - # Generate the special pages like the home. - # Passing a template handler in the template name is deprecated. So pass the file name without the extension. - result = view.render(layout: layout, formats: [$1], file: $`) - else - body = File.read("#{@source_dir}/#{guide}") - result = RailsGuides::Markdown.new( - view: view, - layout: layout, - edge: @edge, - version: @version - ).render(body) - - warn_about_broken_links(result) - end + view = ActionView::Base.new( + @source_dir, + edge: @edge, + version: @version, + mobi: "kindle/#{mobi}", + language: @language + ) + view.extend(Helpers) + + if guide =~ /\.(\w+)\.erb$/ + return if %w[_license _welcome layout].include?($`) + + # Generate the special pages like the home. + # Passing a template handler in the template name is deprecated. So pass the file name without the extension. + result = view.render(layout: layout, formats: [$1], file: $`) + else + body = File.read("#{@source_dir}/#{guide}") + result = RailsGuides::Markdown.new( + view: view, + layout: layout, + edge: @edge, + version: @version + ).render(body) + + warn_about_broken_links(result) + end + File.open(output_path, "w") do |f| f.write(result) end end diff --git a/guides/rails_guides/helpers.rb b/guides/rails_guides/helpers.rb index a6970fb90c..5ab1388c29 100644 --- a/guides/rails_guides/helpers.rb +++ b/guides/rails_guides/helpers.rb @@ -38,15 +38,6 @@ module RailsGuides end end - def author(name, nick, image = "credits_pic_blank.gif", &block) - image = "images/#{image}" - - result = tag(:img, src: image, class: "left pic", alt: name, width: 91, height: 91) - result << content_tag(:h3, name) - result << content_tag(:p, capture(&block)) - content_tag(:div, result, class: "clearfix", id: nick) - end - def code(&block) c = capture(&block) content_tag(:code, c) diff --git a/guides/rails_guides/kindle.rb b/guides/rails_guides/kindle.rb index 5c4f7d159c..d370541d2e 100644 --- a/guides/rails_guides/kindle.rb +++ b/guides/rails_guides/kindle.rb @@ -35,7 +35,7 @@ module Kindle def generate_front_matter(html_pages) frontmatter = [] html_pages.delete_if { |x| - if x =~ /(toc|welcome|credits|copyright).html/ + if x =~ /(toc|welcome|copyright).html/ frontmatter << x unless x =~ /toc/ true end diff --git a/guides/source/2_2_release_notes.md b/guides/source/2_2_release_notes.md index afe0550a17..8b91b4853f 100644 --- a/guides/source/2_2_release_notes.md +++ b/guides/source/2_2_release_notes.md @@ -57,7 +57,6 @@ rake doc:guides This will put the guides inside `Rails.root/doc/guides` and you may start surfing straight away by opening `Rails.root/doc/guides/index.html` in your favourite browser. -* Lead Contributors: [Rails Documentation Team](credits.html) * Major contributions from [Xavier Noria](http://advogato.org/person/fxn/diary.html) and [Hongli Lai](http://izumi.plan99.net/blog/). * More information: * [Rails Guides hackfest](http://hackfest.rubyonrails.org/guide) diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md index 0921cd1979..a1a6a225b2 100644 --- a/guides/source/4_0_release_notes.md +++ b/guides/source/4_0_release_notes.md @@ -55,7 +55,7 @@ $ ruby /path/to/rails/railties/bin/rails new myapp --dev Major Features -------------- -[![Rails 4.0](images/rails4_features.png)](http://guides.rubyonrails.org/images/rails4_features.png) +[![Rails 4.0](images/4_0_release_notes/rails4_features.png)](http://guides.rubyonrails.org/images/4_0_release_notes/rails4_features.png) ### Upgrade diff --git a/guides/source/5_2_release_notes.md b/guides/source/5_2_release_notes.md index 7b5c4b87e3..ab24c7e590 100644 --- a/guides/source/5_2_release_notes.md +++ b/guides/source/5_2_release_notes.md @@ -7,9 +7,9 @@ Highlights in Rails 5.2: * Active Storage * Redis Cache Store -* HTTP/2 Early hints support +* HTTP/2 Early Hints * Credentials -* Default Content Security Policy +* Content Security Policy These release notes cover only the major changes. To learn about various bug fixes and changes, please refer to the change logs or check out the [list of @@ -24,39 +24,73 @@ Upgrading to Rails 5.2 If you're upgrading an existing application, it's a great idea to have good test coverage before going in. You should also first upgrade to Rails 5.1 in case you haven't and make sure your application still runs as expected before attempting -an update to Rails 5.2. - +an update to Rails 5.2. A list of things to watch out for when upgrading is +available in the +[Upgrading Ruby on Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-5-1-to-rails-5-2) +guide. Major Features -------------- ### Active Storage -[README](https://github.com/rails/rails/blob/d3893ec38ec61282c2598b01a298124356d6b35a/activestorage/README.md) +[Pull Request](https://github.com/rails/rails/pull/30020) + +[Active Storage](https://github.com/rails/rails/tree/5-2-stable/activestorage) +facilitates uploading files to a cloud storage service like +Amazon S3, Google Cloud Storage, or Microsoft Azure Storage and attaching +those files to Active Record objects. It comes with a local disk-based service +for development and testing and supports mirroring files to subordinate +services for backups and migrations. +You can read more about Active Storage in the +[Active Storage Overview](active_storage_overview.html) guide. ### Redis Cache Store [Pull Request](https://github.com/rails/rails/pull/31134) +Rails 5.2 ships with built-in Redis cache store. +You can read more about this in the +[Caching with Rails: An Overview](caching_with_rails.html#activesupport-cache-rediscachestore) +guide. -### HTTP/2 Early hints support +### HTTP/2 Early Hints [Pull Request](https://github.com/rails/rails/pull/30744) +Rails 5.2 supports [HTTP/2 Early Hints](https://tools.ietf.org/html/rfc8297). +To start the server with Early Hints enabled pass `--early-hints` +to `bin/rails server`. ### Credentials [Pull Request](https://github.com/rails/rails/pull/30067) - -### Default Content Security Policy +Added `config/credentials.yml.enc` file to store production app secrets. +It allows saving any authentication credentials for third-party services +directly in repository encrypted with a key in the `config/master.key` file or +the `RAILS_MASTER_KEY` environment variable. +This will eventually replace `Rails.application.secrets` and the encrypted +secrets introduced in Rails 5.1. +Furthermore, Rails 5.2 +[opens API underlying Credentials](https://github.com/rails/rails/pull/30940), +so you can easily deal with other encrypted configurations, keys, and files. +You can read more about this in the +[Securing Rails Applications](security.html#custom-credentials) +guide. + +### Content Security Policy [Pull Request](https://github.com/rails/rails/pull/31162) -Incompatibilities ------------------ - -ToDo +Rails 5.2 ships with a new DSL that allows you to configure a +[Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) +for your application. You can configure a global default policy and then +override it on a per-resource basis and even use lambdas to inject per-request +values into the header such as account subdomains in a multi-tenant application. +You can read more about this in the +[Securing Rails Applications](security.html#content-security-policy) +guide. Railties -------- @@ -68,37 +102,105 @@ Please refer to the [Changelog][railties] for detailed changes. * Deprecate `capify!` method in generators and templates. ([Pull Request](https://github.com/rails/rails/pull/29493)) -* Deprecated passing the environment's name as a regular argument to the - `rails dbconsole` and `rails console` commands. - ([Pull Request](https://github.com/rails/rails/pull/29358)) +* Passing the environment's name as a regular argument to the + `rails dbconsole` and `rails console` commands is deprecated. + The `-e` option should be used instead. + ([Commit](https://github.com/rails/rails/commit/48b249927375465a7102acc71c2dfb8d49af8309)) -* Deprecated using subclass of `Rails::Application` to start the Rails server. +* Deprecate using subclass of `Rails::Application` to start the Rails server. ([Pull Request](https://github.com/rails/rails/pull/30127)) -* Deprecated `after_bundle` callback in Rails plugin templates. +* Deprecate `after_bundle` callback in Rails plugin templates. ([Pull Request](https://github.com/rails/rails/pull/29446)) ### Notable changes -ToDo +* Added a shared section to `config/database.yml` that will be loaded for + all environments. + ([Pull Request](https://github.com/rails/rails/pull/28896)) + +* Add `railtie.rb` to the plugin generator. + ([Pull Request](https://github.com/rails/rails/pull/29576)) + +* Clear screenshot files in `tmp:clear` task. + ([Pull Request](https://github.com/rails/rails/pull/29534)) + +* Skip unused components when running `bin/rails app:update`. + If the initial app generation skipped Action Cable, Active Record etc., + the update task honors those skips too. + ([Pull Request](https://github.com/rails/rails/pull/29645)) + +* Allow passing a custom connection name to the `rails dbconsole` + command when using a 3-level database configuration. + Example: `bin/rails dbconsole -c replica`. + ([Commit](https://github.com/rails/rails/commit/1acd9a6464668d4d54ab30d016829f60b70dbbeb)) + +* Properly expand shortcuts for environment's name running the `console` + and `dbconsole` commands. + ([Commit](https://github.com/rails/rails/commit/3777701f1380f3814bd5313b225586dec64d4104)) + +* Add `bootsnap` to default `Gemfile`. + ([Pull Request](https://github.com/rails/rails/pull/29313)) + +* Support `-` as a platform-agnostic way to run a script from stdin with + `rails runner` + ([Pull Request](https://github.com/rails/rails/pull/26343)) + +* Add `ruby x.x.x` version to `Gemfile` and create `.ruby-version` + root file containing the current Ruby version when new Rails applications + are created. + ([Pull Request](https://github.com/rails/rails/pull/30016)) + +* Add `--skip-action-cable` option to the plugin generator. + ([Pull Request](https://github.com/rails/rails/pull/30164)) + +* Add `git_source` to `Gemfile` for plugin generator. + ([Pull Request](https://github.com/rails/rails/pull/30110)) + +* Skip unused components when running `bin/rails` in Rails plugin. + ([Commit](https://github.com/rails/rails/commit/62499cb6e088c3bc32a9396322c7473a17a28640)) + +* Optimize indentation for generator actions. + ([Pull Request](https://github.com/rails/rails/pull/30166)) + +* Optimize routes indentation. + ([Pull Request](https://github.com/rails/rails/pull/30241)) + +* Add `--skip-yarn` option to the plugin generator. + ([Pull Request](https://github.com/rails/rails/pull/30238)) + +* Support multiple versions arguments for `gem` method of Generators. + ([Pull Request](https://github.com/rails/rails/pull/30323)) + +* Derive `secret_key_base` from the app name in development and test + environments. + ([Pull Request](https://github.com/rails/rails/pull/30067)) + +* Add `mini_magick` to default `Gemfile` as comment. + ([Pull Request](https://github.com/rails/rails/pull/30633)) + +* `rails new` and `rails plugin new` get `Active Storage` by default. + Add ability to skip `Active Storage` with `--skip-active-storage` + and do so automatically when `--skip-active-record` is used. + ([Pull Request](https://github.com/rails/rails/pull/30101)) Action Cable ------------ +------------ Please refer to the [Changelog][action-cable] for detailed changes. ### Removals * Removed deprecated evented redis adapter. - ([Commit](https://github.com/rails/rails/commit/48766e32d31)) + ([Commit](https://github.com/rails/rails/commit/48766e32d31651606b9f68a16015ad05c3b0de2c)) ### Notable changes -* Added support for `host`, `port`, `db` and `password` options in cable.yml +* Add support for `host`, `port`, `db` and `password` options in cable.yml ([Pull Request](https://github.com/rails/rails/pull/29528)) -* Added support for compatibility with redis-rb gem for 4.0 version. - ([Pull Request](https://github.com/rails/rails/pull/30748)) +* Hash long stream identifiers when using PostgreSQL adapter. + ([Pull Request](https://github.com/rails/rails/pull/29297)) Action Pack ----------- @@ -107,38 +209,131 @@ Please refer to the [Changelog][action-pack] for detailed changes. ### Removals -* Removed deprecated `ActionController::ParamsParser::ParseError`. - ([Commit](https://github.com/rails/rails/commit/e16c765ac6d)) +* Remove deprecated `ActionController::ParamsParser::ParseError`. + ([Commit](https://github.com/rails/rails/commit/e16c765ac6dcff068ff2e5554d69ff345c003de1)) ### Deprecations -* Deprecated `#success?`, `#missing?` and `#error?` aliases of +* Deprecate `#success?`, `#missing?` and `#error?` aliases of `ActionDispatch::TestResponse`. ([Pull Request](https://github.com/rails/rails/pull/30104)) ### Notable changes -ToDo +* Add support for recyclable cache keys with fragment caching. + ([Pull Request](https://github.com/rails/rails/pull/29092)) + +* Change the cache key format for fragments to make it easier to debug key + churn. + ([Pull Request](https://github.com/rails/rails/pull/29092)) + +* AEAD encrypted cookies and sessions with GCM. + ([Pull Request](https://github.com/rails/rails/pull/28132)) + +* Protect from forgery by default. + ([Pull Request](https://github.com/rails/rails/pull/29742)) + +* Enforce signed/encrypted cookie expiry server side. + ([Pull Request](https://github.com/rails/rails/pull/30121)) + +* Cookies `:expires` option supports `ActiveSupport::Duration` object. + ([Pull Request](https://github.com/rails/rails/pull/30121)) + +* Use Capybara registered `:puma` server config. + ([Pull Request](https://github.com/rails/rails/pull/30638)) + +* Simplify cookies middleware with key rotation support. + ([Pull Request](https://github.com/rails/rails/pull/29716)) + +* Add ability to enable Early Hints for HTTP/2. + ([Pull Request](https://github.com/rails/rails/pull/30744)) + +* Add headless chrome support to System Tests. + ([Pull Request](https://github.com/rails/rails/pull/30876)) + +* Add `:allow_other_host` option to `redirect_back` method. + ([Pull Request](https://github.com/rails/rails/pull/30850)) + +* Make `assert_recognizes` to traverse mounted engines. + ([Pull Request](https://github.com/rails/rails/pull/22435)) + +* Add DSL for configuring Content-Security-Policy header. + ([Pull Request](https://github.com/rails/rails/pull/31162), + [Commit](https://github.com/rails/rails/commit/619b1b6353a65e1635d10b8f8c6630723a5a6f1a), + [Commit](https://github.com/rails/rails/commit/4ec8bf68ff92f35e79232fbd605012ce1f4e1e6e)) + +* Register most popular audio/video/font mime types supported by modern + browsers. + ([Pull Request](https://github.com/rails/rails/pull/31251)) + +* Changed the default system test screenshot output from `inline` to `simple`. + ([Commit](https://github.com/rails/rails/commit/9d6e288ee96d6241f864dbf90211c37b14a57632)) + +* Add headless firefox support to System Tests. + ([Pull Request](https://github.com/rails/rails/pull/31365)) + +* Add secure `X-Download-Options` and `X-Permitted-Cross-Domain-Policies` to + default headers set. + ([Commit](https://github.com/rails/rails/commit/5d7b70f4336d42eabfc403e9f6efceb88b3eff44)) + +* Changed the system tests to set Puma as default server only when the + user haven't specified manually another server. + ([Pull Request](https://github.com/rails/rails/pull/31384)) + +* Add `Referrer-Policy` header to default headers set. + ([Commit](https://github.com/rails/rails/commit/428939be9f954d39b0c41bc53d85d0d106b9d1a1)) + +* Matches behavior of `Hash#each` in `ActionController::Parameters#each`. + ([Pull Request](https://github.com/rails/rails/pull/27790)) + +* Add support for automatic nonce generation for Rails UJS. + ([Commit](https://github.com/rails/rails/commit/b2f0a8945956cd92dec71ec4e44715d764990a49)) + +* Update the default HSTS max-age value to 31536000 seconds (1 year) + to meet the minimum max-age requirement for https://hstspreload.org/. + ([Commit](https://github.com/rails/rails/commit/30b5f469a1d30c60d1fb0605e84c50568ff7ed37)) + +* Add alias method `to_hash` to `to_h` for `cookies`. + Add alias method `to_h` to `to_hash` for `session`. + ([Commit](https://github.com/rails/rails/commit/50a62499e41dfffc2903d468e8b47acebaf9b500)) Action View -------------- +----------- Please refer to the [Changelog][action-view] for detailed changes. ### Removals -* Removed deprecated Erubis ERB handler. - ([Commit](https://github.com/rails/rails/commit/7de7f12fd14)) +* Remove deprecated Erubis ERB handler. + ([Commit](https://github.com/rails/rails/commit/7de7f12fd140a60134defe7dc55b5a20b2372d06)) ### Deprecations -* Deprecated `image_alt` helper which used to add default alt text to +* Deprecate `image_alt` helper which used to add default alt text to the images generated by `image_tag`. ([Pull Request](https://github.com/rails/rails/pull/30213)) ### Notable changes -ToDo +* Add `:json` type to `auto_discovery_link_tag` to support + [JSON Feeds](https://jsonfeed.org/version/1). + ([Pull Request](https://github.com/rails/rails/pull/29158)) + +* Add `srcset` option to `image_tag` helper. + ([Pull Request](https://github.com/rails/rails/pull/29349)) + +* Fix issues with `field_error_proc` wrapping `optgroup` and + select divider `option`. + ([Pull Request](https://github.com/rails/rails/pull/31088)) + +* Change `form_with` to generates ids by default. + ([Commit](https://github.com/rails/rails/commit/260d6f112a0ffdbe03e6f5051504cb441c1e94cd)) + +* Add `preload_link_tag` helper. + ([Pull Request](https://github.com/rails/rails/pull/31251)) + +* Allow the use of callable objects as group methods for grouped selects. + ([Pull Request](https://github.com/rails/rails/pull/31578)) Action Mailer ------------- @@ -147,35 +342,306 @@ Please refer to the [Changelog][action-mailer] for detailed changes. ### Notable changes -ToDo +* Allow Action Mailer classes to configure their delivery job. + ([Pull Request](https://github.com/rails/rails/pull/29457)) + +* Add `assert_enqueued_email_with` test helper. + ([Pull Request](https://github.com/rails/rails/pull/30695)) Active Record ------------- Please refer to the [Changelog][active-record] for detailed changes. -ToDo +### Removals + +* Remove deprecated `#migration_keys`. + ([Pull Request](https://github.com/rails/rails/pull/30337)) + +* Remove deprecated support to `quoted_id` when typecasting + an Active Record object. + ([Commit](https://github.com/rails/rails/commit/82472b3922bda2f337a79cef961b4760d04f9689)) + +* Remove deprecated argument `default` from `index_name_exists?`. + ([Commit](https://github.com/rails/rails/commit/8f5b34df81175e30f68879479243fbce966122d7)) + +* Remove deprecated support to passing a class to `:class_name` + on associations. + ([Commit](https://github.com/rails/rails/commit/e65aff70696be52b46ebe57207ebd8bb2cfcdbb6)) + +* Remove deprecated methods `initialize_schema_migrations_table` and + `initialize_internal_metadata_table`. + ([Commit](https://github.com/rails/rails/commit/c9660b5777707658c414b430753029cd9bc39934)) + +* Remove deprecated method `supports_migrations?`. + ([Commit](https://github.com/rails/rails/commit/9438c144b1893f2a59ec0924afe4d46bd8d5ffdd)) + +* Remove deprecated method `supports_primary_key?`. + ([Commit](https://github.com/rails/rails/commit/c56ff22fc6e97df4656ddc22909d9bf8b0c2cbb1)) + +* Remove deprecated method + `ActiveRecord::Migrator.schema_migrations_table_name`. + ([Commit](https://github.com/rails/rails/commit/7df6e3f3cbdea9a0460ddbab445c81fbb1cfd012)) + +* Remove deprecated argument `name` from `#indexes`. + ([Commit](https://github.com/rails/rails/commit/d6b779ecebe57f6629352c34bfd6c442ac8fba0e)) + +* Remove deprecated arguments from `#verify!`. + ([Commit](https://github.com/rails/rails/commit/9c6ee1bed0292fc32c23dc1c68951ae64fc510be)) + +* Remove deprecated configuration `.error_on_ignored_order_or_limit`. + ([Commit](https://github.com/rails/rails/commit/e1066f450d1a99c9a0b4d786b202e2ca82a4c3b3)) + +* Remove deprecated method `#scope_chain`. + ([Commit](https://github.com/rails/rails/commit/ef7784752c5c5efbe23f62d2bbcc62d4fd8aacab)) + +* Remove deprecated method `#sanitize_conditions`. + ([Commit](https://github.com/rails/rails/commit/8f5413b896099f80ef46a97819fe47a820417bc2)) ### Deprecations -ToDo +* Deprecate `supports_statement_cache?`. + ([Pull Request](https://github.com/rails/rails/pull/28938)) + +* Deprecate passing arguments and block at the same time to + `count` and `sum` in `ActiveRecord::Calculations`. + ([Pull Request](https://github.com/rails/rails/pull/29262)) + +* Deprecate delegating to `arel` in `Relation`. + ([Pull Request](https://github.com/rails/rails/pull/29619)) + +* Deprecate `set_state` method in `TransactionState`. + ([Commit](https://github.com/rails/rails/commit/608ebccf8f6314c945444b400a37c2d07f21b253)) + +* Deprecate `expand_hash_conditions_for_aggregates` without replacement. + ([Commit](https://github.com/rails/rails/commit/7ae26885d96daee3809d0bd50b1a440c2f5ffb69)) ### Notable changes -ToDo +* When calling the dynamic fixture accessor method with no arguments, it now + returns all fixtures of this type. Previously this method always returned + an empty array. + ([Pull Request](https://github.com/rails/rails/pull/28692)) + +* Fix inconsistency with changed attributes when overriding + Active Record attribute reader. + ([Pull Request](https://github.com/rails/rails/pull/28661)) + +* Support Descending Indexes for MySQL. + ([Pull Request](https://github.com/rails/rails/pull/28773)) + +* Fix `bin/rails db:forward` first migration. + ([Commit](https://github.com/rails/rails/commit/b77d2aa0c336492ba33cbfade4964ba0eda3ef84)) + +* Raise error `UnknownMigrationVersionError` on the movement of migrations + when the current migration does not exist. + ([Commit](https://github.com/rails/rails/commit/bb9d6eb094f29bb94ef1f26aa44f145f17b973fe)) + +* Respect `SchemaDumper.ignore_tables` in rake tasks for + databases structure dump. + ([Pull Request](https://github.com/rails/rails/pull/29077)) + +* Add `ActiveRecord::Base#cache_version` to support recyclable cache keys via + the new versioned entries in `ActiveSupport::Cache`. This also means that + `ActiveRecord::Base#cache_key` will now return a stable key that + does not include a timestamp any more. + ([Pull Request](https://github.com/rails/rails/pull/29092)) + +* Prevent creation of bind param if casted value is nil. + ([Pull Request](https://github.com/rails/rails/pull/29282)) + +* Use bulk INSERT to insert fixtures for better performance. + ([Pull Request](https://github.com/rails/rails/pull/29504)) + +* Merging two relations representing nested joins no longer transforms + the joins of the merged relation into LEFT OUTER JOIN. + ([Pull Request](https://github.com/rails/rails/pull/27063)) + +* Fix transactions to apply state to child transactions. + Previously, if you had a nested transaction and the outer transaction was + rolledback, the record from the inner transaction would still be marked + as persisted. It was fixed by applying the state of the parent + transaction to the child transaction when the parent transaction is + rolledback. This will correctly mark records from the inner transaction + as not persisted. + ([Commit](https://github.com/rails/rails/commit/0237da287eb4c507d10a0c6d94150093acc52b03)) + +* Fix eager loading/preloading association with scope including joins. + ([Pull Request](https://github.com/rails/rails/pull/29413)) + +* Prevent errors raised by `sql.active_record` notification subscribers + from being converted into `ActiveRecord::StatementInvalid` exceptions. + ([Pull Request](https://github.com/rails/rails/pull/29692)) + +* Skip query caching when working with batches of records + (`find_each`, `find_in_batches`, `in_batches`). + ([Commit](https://github.com/rails/rails/commit/b83852e6eed5789b23b13bac40228e87e8822b4d)) + +* Change sqlite3 boolean serialization to use 1 and 0. + SQLite natively recognizes 1 and 0 as true and false, but does not natively + recognize 't' and 'f' as was previously serialized. + ([Pull Request](https://github.com/rails/rails/pull/29699)) + +* Values constructed using multi-parameter assignment will now use the + post-type-cast value for rendering in single-field form inputs. + ([Commit](https://github.com/rails/rails/commit/1519e976b224871c7f7dd476351930d5d0d7faf6)) + +* `ApplicationRecord` is no longer generated when generating models. If you + need to generate it, it can be created with `rails g application_record`. + ([Pull Request](https://github.com/rails/rails/pull/29916)) + +* `Relation#or` now accepts two relations who have different values for + `references` only, as `references` can be implicitly called by `where`. + ([Commit](https://github.com/rails/rails/commit/ea6139101ccaf8be03b536b1293a9f36bc12f2f7)) + +* When using `Relation#or`, extract the common conditions and + put them before the OR condition. + ([Pull Request](https://github.com/rails/rails/pull/29950)) + +* Add `binary` fixture helper method. + ([Pull Request](https://github.com/rails/rails/pull/30073)) + +* Automatically guess the inverse associations for STI. + ([Pull Request](https://github.com/rails/rails/pull/23425)) + +* Add new error class `LockWaitTimeout` which will be raised + when lock wait timeout exceeded. + ([Pull Request](https://github.com/rails/rails/pull/30360)) + +* Update payload names for `sql.active_record` instrumentation to be + more descriptive. + ([Pull Request](https://github.com/rails/rails/pull/30619)) + +* Use given algorithm while removing index from database. + ([Pull Request](https://github.com/rails/rails/pull/24199)) + +* Passing a `Set` to `Relation#where` now behaves the same as passing + an array. + ([Commit](https://github.com/rails/rails/commit/9cf7e3494f5bd34f1382c1ff4ea3d811a4972ae2)) + +* PostgreSQL `tsrange` now preserves subsecond precision. + ([Pull Request](https://github.com/rails/rails/pull/30725)) + +* Raises when calling `lock!` in a dirty record. + ([Commit](https://github.com/rails/rails/commit/63cf15877bae859ff7b4ebaf05186f3ca79c1863)) + +* Fixed a bug where column orders for an index weren't written to + `db/schema.rb` when using the sqlite adapter. + ([Pull Request](https://github.com/rails/rails/pull/30970)) + +* Fix `bin/rails db:migrate` with specified `VERSION`. + `bin/rails db:migrate` with empty VERSION behaves as without `VERSION`. + Check a format of `VERSION`: Allow a migration version number + or name of a migration file. Raise error if format of `VERSION` is invalid. + Raise error if target migration doesn't exist. + ([Pull Request](https://github.com/rails/rails/pull/30714)) + +* Add new error class `StatementTimeout` which will be raised + when statement timeout exceeded. + ([Pull Request](https://github.com/rails/rails/pull/31129)) + +* `update_all` will now pass its values to `Type#cast` before passing them to + `Type#serialize`. This means that `update_all(foo: 'true')` will properly + persist a boolean. + ([Commit](https://github.com/rails/rails/commit/68fe6b08ee72cc47263e0d2c9ff07f75c4b42761)) + +* Require raw SQL fragments to be explicitly marked when used in + relation query methods. + ([Commit](https://github.com/rails/rails/commit/a1ee43d2170dd6adf5a9f390df2b1dde45018a48), + [Commit](https://github.com/rails/rails/commit/e4a921a75f8702a7dbaf41e31130fe884dea93f9)) + +* Add `#up_only` to database migrations for code that is only relevant when + migrating up, e.g. populating a new column. + ([Pull Request](https://github.com/rails/rails/pull/31082)) + +* Add new error class `QueryCanceled` which will be raised + when canceling statement due to user request. + ([Pull Request](https://github.com/rails/rails/pull/31235)) + +* Don't allow scopes to be defined which conflict with instance methods + on `Relation`. + ([Pull Request](https://github.com/rails/rails/pull/31179)) + +* Add support for PostgreSQL operator classes to `add_index`. + ([Pull Request](https://github.com/rails/rails/pull/19090)) + +* Log database query callers. + ([Pull Request](https://github.com/rails/rails/pull/26815), + [Pull Request](https://github.com/rails/rails/pull/31519), + [Pull Request](https://github.com/rails/rails/pull/31690)) + +* Undefine attribute methods on descendants when resetting column information. + ([Pull Request](https://github.com/rails/rails/pull/31475)) + +* Using subselect for `delete_all` with `limit` or `offset`. + ([Commit](https://github.com/rails/rails/commit/9e7260da1bdc0770cf4ac547120c85ab93ff3d48)) + +* Fixed inconsistency with `first(n)` when used with `limit()`. + The `first(n)` finder now respects the `limit()`, making it consistent + with `relation.to_a.first(n)`, and also with the behavior of `last(n)`. + ([Pull Request](https://github.com/rails/rails/pull/27597)) + +* Fix nested `has_many :through` associations on unpersisted parent instances. + ([Commit](https://github.com/rails/rails/commit/027f865fc8b262d9ba3ee51da3483e94a5489b66)) + +* Take into account association conditions when deleting through records. + ([Commit](https://github.com/rails/rails/commit/ae48c65e411e01c1045056562319666384bb1b63)) + +* Don't allow destroyed object mutation after `save` or `save!` is called. + ([Commit](https://github.com/rails/rails/commit/562dd0494a90d9d47849f052e8913f0050f3e494)) + +* Fix relation merger issue with `left_outer_joins`. + ([Pull Request](https://github.com/rails/rails/pull/27860)) + +* Support for PostgreSQL foreign tables. + ([Pull Request](https://github.com/rails/rails/pull/31549)) + +* Clear the transaction state when an Active Record object is duped. + ([Pull Request](https://github.com/rails/rails/pull/31751)) + +* Fix not expanded problem when passing an Array object as argument + to the where method using `composed_of` column. + ([Pull Request](https://github.com/rails/rails/pull/31724)) + +* Make `reflection.klass` raise if `polymorphic?` not to be misused. + ([Commit](https://github.com/rails/rails/commit/63fc1100ce054e3e11c04a547cdb9387cd79571a)) + +* Fix `#columns_for_distinct` of MySQL and PostgreSQL to make + `ActiveRecord::FinderMethods#limited_ids_for` use correct primary key values + even if `ORDER BY` columns include other table's primary key. + ([Commit](https://github.com/rails/rails/commit/851618c15750979a75635530200665b543561a44)) + +* Fix `dependent: :destroy` issue for has_one/belongs_to relationship where + the parent class was getting deleted when the child was not. + ([Commit](https://github.com/rails/rails/commit/b0fc04aa3af338d5a90608bf37248668d59fc881)) Active Model ------------ Please refer to the [Changelog][active-model] for detailed changes. -### Removals +### Notable changes -ToDo +* Fix methods `#keys`, `#values` in `ActiveModel::Errors`. + Change `#keys` to only return the keys that don't have empty messages. + Change `#values` to only return the not empty values. + ([Pull Request](https://github.com/rails/rails/pull/28584)) -### Notable changes +* Add method `#merge!` for `ActiveModel::Errors`. + ([Pull Request](https://github.com/rails/rails/pull/29714)) + +* Allow passing a Proc or Symbol to length validator options. + ([Pull Request](https://github.com/rails/rails/pull/30674)) + +* Execute `ConfirmationValidator` validation when `_confirmation`'s value + is `false`. + ([Pull Request](https://github.com/rails/rails/pull/31058)) -ToDo +* Models using the attributes API with a proc default can now be marshalled. + ([Commit](https://github.com/rails/rails/commit/0af36c62a5710e023402e37b019ad9982e69de4b)) + +* Do not lose all multiple `:includes` with options in serialization. + ([Commit](https://github.com/rails/rails/commit/853054bcc7a043eea78c97e7705a46abb603cc44)) Active Support -------------- @@ -184,35 +650,203 @@ Please refer to the [Changelog][active-support] for detailed changes. ### Removals -ToDo +* Remove deprecated `:if` and `:unless` string filter for callbacks. + ([Commit](https://github.com/rails/rails/commit/c792354adcbf8c966f274915c605c6713b840548)) + +* Remove deprecated `halt_callback_chains_on_return_false` option. + ([Commit](https://github.com/rails/rails/commit/19fbbebb1665e482d76cae30166b46e74ceafe29)) ### Deprecations -ToDo +* Deprecate `Module#reachable?` method. + ([Pull Request](https://github.com/rails/rails/pull/30624)) + +* Deprecate `secrets.secret_token`. + ([Commit](https://github.com/rails/rails/commit/fbcc4bfe9a211e219da5d0bb01d894fcdaef0a0e)) ### Notable changes -ToDo +* Add `fetch_values` for `HashWithIndifferentAccess`. + ([Pull Request](https://github.com/rails/rails/pull/28316)) + +* Add support for `:offset` to `Time#change`. + ([Commit](https://github.com/rails/rails/commit/851b7f866e13518d900407c78dcd6eb477afad06)) + +* Add support for `:offset` and `:zone` + to `ActiveSupport::TimeWithZone#change`. + ([Commit](https://github.com/rails/rails/commit/851b7f866e13518d900407c78dcd6eb477afad06)) + +* Pass gem name and deprecation horizon to deprecation notifications. + ([Pull Request](https://github.com/rails/rails/pull/28800)) + +* Add support for versioned cache entries. This enables the cache stores to + recycle cache keys, greatly saving on storage in cases with frequent churn. + Works together with the separation of `#cache_key` and `#cache_version` + in Active Record and its use in Action Pack's fragment caching. + ([Pull Request](https://github.com/rails/rails/pull/29092)) + +* Add `ActiveSupport::CurrentAttributes` to provide a thread-isolated + attributes singleton. Primary use case is keeping all the per-request + attributes easily available to the whole system. + ([Pull Request](https://github.com/rails/rails/pull/29180)) + +* `#singularize` and `#pluralize` now respect uncountables for + the specified locale. + ([Commit](https://github.com/rails/rails/commit/352865d0f835c24daa9a2e9863dcc9dde9e5371a)) + +* Add default option to `class_attribute`. + ([Pull Request](https://github.com/rails/rails/pull/29270)) + +* Add `Date#prev_occurring` and `Date#next_occurring` to return + specified next/previous occurring day of week. + ([Pull Request](https://github.com/rails/rails/pull/26600)) + +* Add default option to module and class attribute accessors. + ([Pull Request](https://github.com/rails/rails/pull/29294)) + +* Cache: `write_multi`. + ([Pull Request](https://github.com/rails/rails/pull/29366)) + +* Default `ActiveSupport::MessageEncryptor` to use AES 256 GCM encryption. + ([Pull Request](https://github.com/rails/rails/pull/29263)) + +* Add `freeze_time` helper which freezes time to `Time.now` in tests. + ([Pull Request](https://github.com/rails/rails/pull/29681)) + +* Make the order of `Hash#reverse_merge!` consistent + with `HashWithIndifferentAccess`. + ([Pull Request](https://github.com/rails/rails/pull/28077)) + +* Add purpose and expiry support to `ActiveSupport::MessageVerifier` and + `ActiveSupport::MessageEncryptor`. + ([Pull Request](https://github.com/rails/rails/pull/29892)) + +* Update `String#camelize` to provide feedback when wrong option is passed. + ([Pull Request](https://github.com/rails/rails/pull/30039)) + +* `Module#delegate_missing_to` now raises `DelegationError` if target is nil, + similar to `Module#delegate`. + ([Pull Request](https://github.com/rails/rails/pull/30191)) + +* Add `ActiveSupport::EncryptedFile` and + `ActiveSupport::EncryptedConfiguration`. + ([Pull Request](https://github.com/rails/rails/pull/30067)) + +* Add `config/credentials.yml.enc` to store production app secrets. + ([Pull Request](https://github.com/rails/rails/pull/30067)) + +* Add key rotation support to `MessageEncryptor` and `MessageVerifier`. + ([Pull Request](https://github.com/rails/rails/pull/29716)) + +* Return an instance of `HashWithIndifferentAccess` from + `HashWithIndifferentAccess#transform_keys`. + ([Pull Request](https://github.com/rails/rails/pull/30728)) + +* `Hash#slice` now falls back to Ruby 2.5+'s built-in definition if defined. + ([Commit](https://github.com/rails/rails/commit/01ae39660243bc5f0a986e20f9c9bff312b1b5f8)) + +* `IO#to_json` now returns the `to_s` representation, rather than + attempting to convert to an array. This fixes a bug where `IO#to_json` + would raise an `IOError` when called on an unreadable object. + ([Pull Request](https://github.com/rails/rails/pull/30953)) + +* Add same method signature for `Time#prev_day` and `Time#next_day` + in accordance with `Date#prev_day`, `Date#next_day`. + Allows pass argument for `Time#prev_day` and `Time#next_day`. + ([Commit](https://github.com/rails/rails/commit/61ac2167eff741bffb44aec231f4ea13d004134e)) + +* Add same method signature for `Time#prev_month` and `Time#next_month` + in accordance with `Date#prev_month`, `Date#next_month`. + Allows pass argument for `Time#prev_month` and `Time#next_month`. + ([Commit](https://github.com/rails/rails/commit/f2c1e3a793570584d9708aaee387214bc3543530)) + +* Add same method signature for `Time#prev_year` and `Time#next_year` + in accordance with `Date#prev_year`, `Date#next_year`. + Allows pass argument for `Time#prev_year` and `Time#next_year`. + ([Commit](https://github.com/rails/rails/commit/ee9d81837b5eba9d5ec869ae7601d7ffce763e3e)) + +* Fix acronym support in `humanize`. + ([Commit](https://github.com/rails/rails/commit/0ddde0a8fca6a0ca3158e3329713959acd65605d)) + +* Allow `Range#include?` on TWZ ranges. + ([Pull Request](https://github.com/rails/rails/pull/31081)) + +* Cache: Enable compression by default for values > 1kB. + ([Pull Request](https://github.com/rails/rails/pull/31147)) + +* Redis cache store. + ([Pull Request](https://github.com/rails/rails/pull/31134), + [Pull Request](https://github.com/rails/rails/pull/31866)) + +* Handle `TZInfo::AmbiguousTime` errors. + ([Pull Request](https://github.com/rails/rails/pull/31128)) + +* MemCacheStore: Support expiring counters. + ([Commit](https://github.com/rails/rails/commit/b22ee64b5b30c6d5039c292235e10b24b1057f6d)) + +* Make `ActiveSupport::TimeZone.all` return only time zones that are in + `ActiveSupport::TimeZone::MAPPING`. + ([Pull Request](https://github.com/rails/rails/pull/31176)) + +* Changed default behaviour of `ActiveSupport::SecurityUtils.secure_compare`, + to make it not leak length information even for variable length string. + Renamed old `ActiveSupport::SecurityUtils.secure_compare` to + `fixed_length_secure_compare`, and started raising `ArgumentError` in + case of length mismatch of passed strings. + ([Pull Request](https://github.com/rails/rails/pull/24510)) + +* Use SHA-1 to generate non-sensitive digests, such as the ETag header. + ([Pull Request](https://github.com/rails/rails/pull/31289), + [Pull Request](https://github.com/rails/rails/pull/31651)) + +* `assert_changes` will always assert that the expression changes, + regardless of `from:` and `to:` argument combinations. + ([Pull Request](https://github.com/rails/rails/pull/31011)) + +* Add missing instrumentation for `read_multi` + in `ActiveSupport::Cache::Store`. + ([Pull Request](https://github.com/rails/rails/pull/30268)) + +* Support hash as first argument in `assert_difference`. + This allows to specify multiple numeric differences in the same assertion. + ([Pull Request](https://github.com/rails/rails/pull/31600)) + +* Caching: MemCache and Redis `read_multi` and `fetch_multi` speedup. + Read from the local in-memory cache before consulting the backend. + ([Commit](https://github.com/rails/rails/commit/a2b97e4ffef971607a1be8fc7909f099b6840f36)) Active Job ------------ +---------- Please refer to the [Changelog][active-job] for detailed changes. -### Removals +### Notable changes + +* Allow block to be passed to `ActiveJob::Base.discard_on` to allow custom + handling of discard jobs. + ([Pull Request](https://github.com/rails/rails/pull/30622)) -ToDo +Ruby on Rails Guides +-------------------- + +Please refer to the [Changelog][guides] for detailed changes. ### Notable changes -ToDo +* Add + [Threading and Code Execution in Rails](threading_and_code_execution.html) + Guide. + ([Pull Request](https://github.com/rails/rails/pull/27494)) + +* Add [Active Storage Overview](active_storage_overview.html) Guide. + ([Pull Request](https://github.com/rails/rails/pull/31037)) Credits ------- See the -[full list of contributors to Rails](http://contributors.rubyonrails.org/) for -the many people who spent many hours making Rails, the stable and robust +[full list of contributors to Rails](http://contributors.rubyonrails.org/) +for the many people who spent many hours making Rails, the stable and robust framework it is. Kudos to all of them. [railties]: https://github.com/rails/rails/blob/5-2-stable/railties/CHANGELOG.md @@ -224,3 +858,4 @@ framework it is. Kudos to all of them. [active-model]: https://github.com/rails/rails/blob/5-2-stable/activemodel/CHANGELOG.md [active-support]: https://github.com/rails/rails/blob/5-2-stable/activesupport/CHANGELOG.md [active-job]: https://github.com/rails/rails/blob/5-2-stable/activejob/CHANGELOG.md +[guides]: https://github.com/rails/rails/blob/5-2-stable/guides/CHANGELOG.md diff --git a/guides/source/_welcome.html.erb b/guides/source/_welcome.html.erb index 6959f992aa..5dd6bfdd23 100644 --- a/guides/source/_welcome.html.erb +++ b/guides/source/_welcome.html.erb @@ -10,17 +10,20 @@ </p> <% else %> <p> - These are the new guides for Rails 5.1 based on <a href="https://github.com/rails/rails/tree/<%= @version %>"><%= @version %></a>. + These are the new guides for Rails 5.2 based on <a href="https://github.com/rails/rails/tree/<%= @version %>"><%= @version %></a>. These guides are designed to make you immediately productive with Rails, and to help you understand how all of the pieces fit together. </p> <% end %> <p> The guides for earlier releases: +<a href="http://guides.rubyonrails.org/v5.2/">Rails 5.2</a>, <a href="http://guides.rubyonrails.org/v5.1/">Rails 5.1</a>, <a href="http://guides.rubyonrails.org/v5.0/">Rails 5.0</a>, <a href="http://guides.rubyonrails.org/v4.2/">Rails 4.2</a>, <a href="http://guides.rubyonrails.org/v4.1/">Rails 4.1</a>, <a href="http://guides.rubyonrails.org/v4.0/">Rails 4.0</a>, -<a href="http://guides.rubyonrails.org/v3.2/">Rails 3.2</a>, and +<a href="http://guides.rubyonrails.org/v3.2/">Rails 3.2</a>, +<a href="http://guides.rubyonrails.org/v3.1/">Rails 3.1</a>, +<a href="http://guides.rubyonrails.org/v3.0/">Rails 3.0</a>, and <a href="http://guides.rubyonrails.org/v2.3/">Rails 2.3</a>. </p> diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index 6ecfb57db3..e0e85588a0 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -51,7 +51,7 @@ class ClientsController < ApplicationController end ``` -As an example, if a user goes to `/clients/new` in your application to add a new client, Rails will create an instance of `ClientsController` and call its `new` method. Note that the empty method from the example above would work just fine because Rails will by default render the `new.html.erb` view unless the action says otherwise. The `new` method could make available to the view a `@client` instance variable by creating a new `Client`: +As an example, if a user goes to `/clients/new` in your application to add a new client, Rails will create an instance of `ClientsController` and call its `new` method. Note that the empty method from the example above would work just fine because Rails will by default render the `new.html.erb` view unless the action says otherwise. By creating a new `Client`, the `new` method can make a `@client` instance variable accessible in the view: ```ruby def new @@ -855,7 +855,7 @@ If you want to set custom headers for a response then `response.headers` is the response.headers["Content-Type"] = "application/pdf" ``` -Note: in the above case it would make more sense to use the `content_type` setter directly. +NOTE: In the above case it would make more sense to use the `content_type` setter directly. HTTP Authentications -------------------- @@ -1181,22 +1181,6 @@ NOTE: Certain exceptions are only rescuable from the `ApplicationController` cla Force HTTPS protocol -------------------- -Sometime you might want to force a particular controller to only be accessible via an HTTPS protocol for security reasons. You can use the `force_ssl` method in your controller to enforce that: - -```ruby -class DinnerController - force_ssl -end -``` - -Just like the filter, you could also pass `:only` and `:except` to enforce the secure connection only to specific actions: - -```ruby -class DinnerController - force_ssl only: :cheeseburger - # or - force_ssl except: :cheeseburger -end -``` - -Please note that if you find yourself adding `force_ssl` to many controllers, you may want to force the whole application to use HTTPS instead. In that case, you can set the `config.force_ssl` in your environment file. +If you'd like to ensure that communication to your controller is only possible +via HTTPS, you should do so by enabling the `ActionDispatch::SSL` middleware via +`config.force_ssl` in your environment configuration. diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md index fe31e3403f..662f9ea38a 100644 --- a/guides/source/action_mailer_basics.md +++ b/guides/source/action_mailer_basics.md @@ -20,9 +20,18 @@ Introduction ------------ Action Mailer allows you to send emails from your application using mailer classes -and views. Mailers work very similarly to controllers. They inherit from -`ActionMailer::Base` and live in `app/mailers`, and they have associated views -that appear in `app/views`. +and views. + +#### Mailers are similar to controllers + +They inherit from `ActionMailer::Base` and live in `app/mailers`. Mailers also work +very similarly to controllers. Some examples of similarities are enumerated below. +Mailers have: + +* Actions, and also, associated views that appear in `app/views`. +* Instance variables that are accessible in views. +* The ability to utilise layouts and partials. +* The ability to access a params hash. Sending Emails -------------- @@ -60,8 +69,7 @@ end ``` As you can see, you can generate mailers just like you use other generators with -Rails. Mailers are conceptually similar to controllers, and so we get a mailer, -a directory for views, and a test. +Rails. If you didn't want to use a generator, you could create your own file inside of `app/mailers`, just make sure that it inherits from `ActionMailer::Base`: @@ -73,10 +81,9 @@ end #### Edit the Mailer -Mailers are very similar to Rails controllers. They also have methods called -"actions" and use views to structure the content. Where a controller generates -content like HTML to send back to the client, a Mailer creates a message to be -delivered via email. +Mailers have methods called "actions" and they use views to structure their content. +Where a controller generates content like HTML to send back to the client, a Mailer +creates a message to be delivered via email. `app/mailers/user_mailer.rb` contains an empty mailer: @@ -110,9 +117,6 @@ messages in this class. This can be overridden on a per-email basis. * `mail` - The actual email message, we are passing the `:to` and `:subject` headers in. -Just like controllers, any instance variables we define in the method become -available for use in the views. - #### Create a Mailer View Create a file called `welcome_email.html.erb` in `app/views/user_mailer/`. This @@ -807,7 +811,7 @@ config.action_mailer.smtp_settings = { authentication: 'plain', enable_starttls_auto: true } ``` -Note: As of July 15, 2014, Google increased [its security measures](https://support.google.com/accounts/answer/6010255) and now blocks attempts from apps it deems less secure. +NOTE: As of July 15, 2014, Google increased [its security measures](https://support.google.com/accounts/answer/6010255) and now blocks attempts from apps it deems less secure. You can change your Gmail settings [here](https://www.google.com/settings/security/lesssecureapps) to allow the attempts. If your Gmail account has 2-factor authentication enabled, then you will need to set an [app password](https://myaccount.google.com/apppasswords) and use that instead of your regular password. Alternatively, you can use another ESP to send email by replacing 'smtp.gmail.com' above with the address of your provider. diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md index 1cba5c6fb6..37b8843d1e 100644 --- a/guides/source/action_view_overview.md +++ b/guides/source/action_view_overview.md @@ -1102,7 +1102,7 @@ Possible output: </optgroup> ``` -Note: Only the `optgroup` and `option` tags are returned, so you still have to wrap the output in an appropriate `select` tag. +NOTE: Only the `optgroup` and `option` tags are returned, so you still have to wrap the output in an appropriate `select` tag. #### options_for_select @@ -1113,7 +1113,7 @@ options_for_select([ "VISA", "MasterCard" ]) # => <option>VISA</option> <option>MasterCard</option> ``` -Note: Only the `option` tags are returned, you have to wrap this call in a regular HTML `select` tag. +NOTE: Only the `option` tags are returned, you have to wrap this call in a regular HTML `select` tag. #### options_from_collection_for_select @@ -1130,7 +1130,7 @@ options_from_collection_for_select(@project.people, "id", "name") # => <option value="#{person.id}">#{person.name}</option> ``` -Note: Only the `option` tags are returned, you have to wrap this call in a regular HTML `select` tag. +NOTE: Only the `option` tags are returned, you have to wrap this call in a regular HTML `select` tag. #### select @@ -1267,8 +1267,8 @@ password_field_tag 'pass' Creates a radio button; use groups of radio buttons named the same to allow users to select from a group of options. ```ruby -radio_button_tag 'gender', 'male' -# => <input id="gender_male" name="gender" type="radio" value="male" /> +radio_button_tag 'favorite_color', 'maroon' +# => <input id="favorite_color_maroon" name="favorite_color" type="radio" value="maroon" /> ``` #### select_tag diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md index 97d98efba0..6d52ac0a99 100644 --- a/guides/source/active_job_basics.md +++ b/guides/source/active_job_basics.md @@ -147,7 +147,7 @@ class GuestsCleanupJob < ApplicationJob #.... end -# Now your job will use `resque` as it's backend queue adapter overriding what +# Now your job will use `resque` as its backend queue adapter overriding what # was configured in `config.active_job.queue_adapter`. ``` @@ -290,7 +290,7 @@ For example, you could send metrics for every job enqueued: ```ruby class ApplicationJob - before_enqueue { |job| $statsd.increment "#{job.name.underscore}.enqueue" } + before_enqueue { |job| $statsd.increment "#{job.class.name.underscore}.enqueue" } end ``` diff --git a/guides/source/active_record_migrations.md b/guides/source/active_record_migrations.md index ab3af438f5..5be514c786 100644 --- a/guides/source/active_record_migrations.md +++ b/guides/source/active_record_migrations.md @@ -442,7 +442,7 @@ change_column_default :products, :approved, from: true, to: false This sets `:name` field on products to a `NOT NULL` column and the default value of the `:approved` field from true to false. -Note: You could also write the above `change_column_default` migration as +NOTE: You could also write the above `change_column_default` migration as `change_column_default :products, :approved, false`, but unlike the previous example, this would make your migration irreversible. diff --git a/guides/source/active_storage_overview.md b/guides/source/active_storage_overview.md index a7cb14c52a..d67f65e88a 100644 --- a/guides/source/active_storage_overview.md +++ b/guides/source/active_storage_overview.md @@ -93,15 +93,6 @@ local: root: <%= Rails.root.join("storage") %> ``` -Optionally specify a host for generating URLs (the default is `http://localhost:3000`): - -```yaml -local: - service: Disk - root: <%= Rails.root.join("storage") %> - host: http://myapp.test -``` - ### Amazon S3 Service Declare an S3 service in `config/storage.yml`: @@ -123,6 +114,13 @@ gem "aws-sdk-s3", require: false NOTE: The core features of Active Storage require the following permissions: `s3:ListBucket`, `s3:PutObject`, `s3:GetObject`, and `s3:DeleteObject`. If you have additional upload options configured such as setting ACLs then additional permissions may be required. +NOTE: If you want to use environment variables, standard SDK configuration files, profiles, +IAM instance profiles or task roles, you can omit the `access_key_id`, `secret_access_key`, +and `region` keys in the example above. The Amazon S3 Service supports all of the +authentication options described in the [AWS SDK documentation] +(https://docs.aws.amazon.com/sdk-for-ruby/v3/developer-guide/setup-config.html). + + ### Microsoft Azure Storage Service Declare an Azure Storage service in `config/storage.yml`: @@ -130,7 +128,6 @@ Declare an Azure Storage service in `config/storage.yml`: ```yaml azure: service: AzureStorage - path: "" storage_account_name: "" storage_access_key: "" container: "" diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index 75ad343613..ae2e1faf14 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -779,6 +779,14 @@ delegate :size, to: :attachment, prefix: :avatar In the previous example the macro generates `avatar_size` rather than `size`. +The option `:private` changes methods scope: + +```ruby +delegate :date_of_birth, to: :profile, private: true +``` + +The delegated methods are public by default. Pass `private: true` to change that. + NOTE: Defined in `active_support/core_ext/module/delegation.rb` #### `delegate_missing_to` diff --git a/guides/source/api_app.md b/guides/source/api_app.md index b4d90d31de..c2df6c45ad 100644 --- a/guides/source/api_app.md +++ b/guides/source/api_app.md @@ -24,7 +24,7 @@ With the advent of client-side frameworks, more developers are using Rails to build a back-end that is shared between their web application and other native applications. -For example, Twitter uses its [public API](https://dev.twitter.com) in its web +For example, Twitter uses its [public API](https://developer.twitter.com/) in its web application, which is built as a static site that consumes JSON resources. Instead of using Rails to generate HTML that communicates with the server @@ -375,7 +375,6 @@ controller modules by default: - `ActionController::ConditionalGet`: Support for `stale?`. - `ActionController::BasicImplicitRender`: Makes sure to return an empty response, if there isn't an explicit one. - `ActionController::StrongParameters`: Support for parameters white-listing in combination with Active Model mass assignment. -- `ActionController::ForceSSL`: Support for `force_ssl`. - `ActionController::DataStreaming`: Support for `send_file` and `send_data`. - `AbstractController::Callbacks`: Support for `before_action` and similar helpers. diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md index 618896d458..88b87b78d2 100644 --- a/guides/source/asset_pipeline.md +++ b/guides/source/asset_pipeline.md @@ -728,8 +728,8 @@ Rails.application.config.assets.precompile += %w( admin.js admin.css ) NOTE. Always specify an expected compiled filename that ends with `.js` or `.css`, even if you want to add Sass or CoffeeScript files to the precompile array. -The task also generates a `.sprockets-manifest-md5hash.json` (where `md5hash` is -an MD5 hash) that contains a list with all your assets and their respective +The task also generates a `.sprockets-manifest-randomhex.json` (where `randomhex` is +a 16-byte random hex string) that contains a list with all your assets and their respective fingerprints. This is used by the Rails helper methods to avoid handing the mapping requests back to Sprockets. A typical manifest file looks like: @@ -917,7 +917,7 @@ config.action_controller.asset_host = ENV['CDN_HOST'] -Note: You would need to set `CDN_HOST` on your server to `mycdnsubdomain +NOTE: You would need to set `CDN_HOST` on your server to `mycdnsubdomain .fictional-cdn.com` for this to work. Once you have configured your server and your CDN when you serve a webpage that diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md index f895cadea5..860a1e1cba 100644 --- a/guides/source/association_basics.md +++ b/guides/source/association_basics.md @@ -94,7 +94,7 @@ class Book < ApplicationRecord end ``` -![belongs_to Association Diagram](images/belongs_to.png) +![belongs_to Association Diagram](images/association_basics/belongs_to.png) NOTE: `belongs_to` associations _must_ use the singular term. If you used the pluralized form in the above example for the `author` association in the `Book` model, you would be told that there was an "uninitialized constant Book::Authors". This is because Rails automatically infers the class name from the association name. If the association name is wrongly pluralized, then the inferred class will be wrongly pluralized too. @@ -127,7 +127,7 @@ class Supplier < ApplicationRecord end ``` -![has_one Association Diagram](images/has_one.png) +![has_one Association Diagram](images/association_basics/has_one.png) The corresponding migration might look like this: @@ -171,7 +171,7 @@ end NOTE: The name of the other model is pluralized when declaring a `has_many` association. -![has_many Association Diagram](images/has_many.png) +![has_many Association Diagram](images/association_basics/has_many.png) The corresponding migration might look like this: @@ -213,7 +213,7 @@ class Patient < ApplicationRecord end ``` -![has_many :through Association Diagram](images/has_many_through.png) +![has_many :through Association Diagram](images/association_basics/has_many_through.png) The corresponding migration might look like this: @@ -299,7 +299,7 @@ class AccountHistory < ApplicationRecord end ``` -![has_one :through Association Diagram](images/has_one_through.png) +![has_one :through Association Diagram](images/association_basics/has_one_through.png) The corresponding migration might look like this: @@ -340,7 +340,7 @@ class Part < ApplicationRecord end ``` -![has_and_belongs_to_many Association Diagram](images/habtm.png) +![has_and_belongs_to_many Association Diagram](images/association_basics/habtm.png) The corresponding migration might look like this: @@ -494,7 +494,7 @@ class CreatePictures < ActiveRecord::Migration[5.0] end ``` -![Polymorphic Association Diagram](images/polymorphic.png) +![Polymorphic Association Diagram](images/association_basics/polymorphic.png) ### Self Joins diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md index 5dde6f34fa..3f357b532b 100644 --- a/guides/source/caching_with_rails.md +++ b/guides/source/caching_with_rails.md @@ -446,30 +446,28 @@ config.cache_store = :mem_cache_store, "cache-1.example.com", "cache-2.example.c ### ActiveSupport::Cache::RedisCacheStore -The Redis cache store takes advantage of Redis support for least-recently-used -and least-frequently-used key eviction when it reaches max memory, allowing it -to behave much like a Memcached cache server. +The Redis cache store takes advantage of Redis support for automatic eviction +when it reaches max memory, allowing it to behave much like a Memcached cache server. Deployment note: Redis doesn't expire keys by default, so take care to use a dedicated Redis cache server. Don't fill up your persistent-Redis server with volatile cache data! Read the [Redis cache server setup guide](https://redis.io/topics/lru-cache) in detail. -For an all-cache Redis server, set `maxmemory-policy` to an `allkeys` policy. -Redis 4+ support least-frequently-used (`allkeys-lfu`) eviction, an excellent -default choice. Redis 3 and earlier should use `allkeys-lru` for -least-recently-used eviction. +For a cache-only Redis server, set `maxmemory-policy` to one of the variants of allkeys. +Redis 4+ supports least-frequently-used eviction (`allkeys-lfu`), an excellent +default choice. Redis 3 and earlier should use least-recently-used eviction (`allkeys-lru`). Set cache read and write timeouts relatively low. Regenerating a cached value is often faster than waiting more than a second to retrieve it. Both read and write timeouts default to 1 second, but may be set lower if your network is -consistently low latency. +consistently low-latency. By default, the cache store will not attempt to reconnect to Redis if the connection fails during a request. If you experience frequent disconnects you may wish to enable reconnect attempts. -Cache reads and writes never raise exceptions. They just return `nil` instead, +Cache reads and writes never raise exceptions; they just return `nil` instead, behaving as if there was nothing in the cache. To gauge whether your cache is hitting exceptions, you may provide an `error_handler` to report to an exception gathering service. It must accept three keyword arguments: `method`, @@ -477,12 +475,33 @@ the cache store method that was originally called; `returning`, the value that was returned to the user, typically `nil`; and `exception`, the exception that was rescued. -Putting it all together, a production Redis cache store may look something -like this: +To get started, add the redis gem to your Gemfile: ```ruby -cache_servers = %w[ "redis://cache-01:6379/0", "redis://cache-02:6379/0", … ], -config.cache_store = :redis_cache_store, url: cache_servers, +gem 'redis' +``` + +You can enable support for the faster [hiredis](https://github.com/redis/hiredis) +connection library by additionally adding its ruby wrapper to your Gemfile: + +```ruby +gem 'hiredis' +``` + +Redis cache store will automatically require & use hiredis if available. No further +configuration is needed. + +Finally, add the configuration in the relevant `config/environments/*.rb` file: + +```ruby +config.cache_store = :redis_cache_store, { url: ENV['REDIS_URL'] } +``` + +A more complex, production Redis cache store may look something like this: + +```ruby +cache_servers = %w(redis://cache-01:6379/0 redis://cache-02:6379/0) +config.cache_store = :redis_cache_store, { url: cache_servers, connect_timeout: 30, # Defaults to 20 seconds read_timeout: 0.2, # Defaults to 1 second @@ -491,9 +510,10 @@ config.cache_store = :redis_cache_store, url: cache_servers, error_handler: -> (method:, returning:, exception:) { # Report errors to Sentry as warnings - Raven.capture_exception exception, level: 'warning", + Raven.capture_exception exception, level: 'warning', tags: { method: method, returning: returning } } +} ``` ### ActiveSupport::Cache::NullStore diff --git a/guides/source/configuring.md b/guides/source/configuring.md index a87b8a2f48..7d5ca4b8a7 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -400,10 +400,16 @@ by adding the following to your `application.rb` file: Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true ``` -The schema dumper adds one additional configuration option: +The schema dumper adds two additional configuration options: * `ActiveRecord::SchemaDumper.ignore_tables` accepts an array of tables that should _not_ be included in any generated schema file. +* `ActiveRecord::SchemaDumper.fk_ignore_pattern` allows setting a different regular + expression that will be used to decide whether a foreign key's name should be + dumped to db/schema.rb or not. By default, foreign key names starting with + `fk_rails_` are not exported to the database schema dump. + Defaults to `/^fk_rails_[0-9a-f]{10}$/`. + ### Configuring Action Controller `config.action_controller` includes a number of configuration settings: @@ -502,6 +508,10 @@ Defaults to `'signed cookie'`. * `config.action_dispatch.cookies_rotations` allows rotating secrets, ciphers, and digests for encrypted and signed cookies. +* `config.action_dispatch.use_authenticated_cookie_encryption` controls whether + signed and encrypted cookies use the AES-256-GCM cipher or + the older AES-256-CBC cipher. It defaults to `true`. + * `config.action_dispatch.perform_deep_munge` configures whether `deep_munge` method should be performed on the parameters. See [Security Guide](security.html#unsafe-query-generation) for more information. It defaults to `true`. @@ -590,6 +600,13 @@ Defaults to `'signed cookie'`. * `config.action_view.default_enforce_utf8` determines whether forms are generated with a hidden tag that forces older versions of Internet Explorer to submit forms encoded in UTF-8. This defaults to `false`. +* `config.action_view.finalize_compiled_template_methods` determines + whether the methods on `ActionView::CompiledTemplates` that templates + compile themselves to are removed when template instances are + destroyed by the garbage collector. This helps prevent memory leaks in + development mode, but for large test suites, disabling this option in + the test environment can improve performance. This defaults to `true`. + ### Configuring Action Mailer There are a number of settings available on `config.action_mailer`: diff --git a/guides/source/credits.html.erb b/guides/source/credits.html.erb deleted file mode 100644 index 5adbd12ac0..0000000000 --- a/guides/source/credits.html.erb +++ /dev/null @@ -1,80 +0,0 @@ -<% content_for :page_title do %> -Ruby on Rails Guides: Credits -<% end %> - -<% content_for :header_section do %> -<h2>Credits</h2> - -<p>We'd like to thank the following people for their tireless contributions to this project.</p> - -<% end %> - -<h3 class="section">Rails Guides Reviewers</h3> - -<%= author('Vijay Dev', 'vijaydev', 'vijaydev.jpg') do %> - Vijayakumar, found as Vijay Dev on the web, is a web applications developer and an open source enthusiast who lives in Chennai, India. He started using Rails in 2009 and began actively contributing to Rails documentation in late 2010. He <a href="https://twitter.com/vijay_dev">tweets</a> a lot and also <a href="http://vijaydev.wordpress.com">blogs</a>. -<% end %> - -<%= author('Xavier Noria', 'fxn', 'fxn.png') do %> - Xavier Noria has been into Ruby on Rails since 2005. He is a Rails core team member and enjoys combining his passion for Rails and his past life as a proofreader of math textbooks. Xavier is currently an independent Ruby on Rails consultant. Oh, he also <a href="http://twitter.com/fxn">tweets</a> and can be found everywhere as "fxn". -<% end %> - -<h3 class="section">Rails Guides Designers</h3> - -<%= author('Jason Zimdars', 'jz') do %> - Jason Zimdars is an experienced creative director and web designer who has lead UI and UX design for numerous websites and web applications. You can see more of his design and writing at <a href="http://www.thinkcage.com/">Thinkcage.com</a> or follow him on <a href="https://twitter.com/jasonzimdars">Twitter</a>. -<% end %> - -<h3 class="section">Rails Guides Authors</h3> - -<%= author('Ryan Bigg', 'radar', 'radar.png') do %> - Ryan Bigg works as a Rails developer at <a href="http://marketplacer.com">Marketplacer</a> and has been working with Rails since 2006. He's the author of <a href="https://leanpub.com/multi-tenancy-rails">Multi Tenancy With Rails</a> and co-author of <a href="http://manning.com/bigg2">Rails 4 in Action</a>. He's written many gems which can be seen on <a href="https://github.com/radar">his GitHub page</a> and he also tweets prolifically as <a href="http://twitter.com/ryanbigg">@ryanbigg</a>. -<% end %> - -<%= author('Oscar Del Ben', 'oscardelben', 'oscardelben.jpg') do %> -Oscar Del Ben is a software engineer at <a href="http://www.businessinsider.com/google-buys-wildfire-2012-8">Wildfire</a>. He's a regular open source contributor (<a href="https://github.com/oscardelben">GitHub account</a>) and tweets regularly at <a href="https://twitter.com/oscardelben">@oscardelben</a>. - <% end %> - -<%= author('Frederick Cheung', 'fcheung') do %> - Frederick Cheung is Chief Wizard at Texperts where he has been using Rails since 2006. He is based in Cambridge (UK) and when not consuming fine ales he blogs at <a href="http://www.spacevatican.org">spacevatican.org</a>. -<% end %> - -<%= author('Tore Darell', 'toretore') do %> - Tore Darell is an independent developer based in Menton, France who specialises in cruft-free web applications using Ruby, Rails and unobtrusive JavaScript. You can follow him on <a href="http://twitter.com/toretore">Twitter</a>. -<% end %> - -<%= author('Jeff Dean', 'zilkey') do %> - Jeff Dean is a software engineer with <a href="http://pivotallabs.com">Pivotal Labs</a>. -<% end %> - -<%= author('Mike Gunderloy', 'mgunderloy') do %> - Mike Gunderloy is a consultant with <a href="http://www.actionrails.com">ActionRails</a>. He brings 25 years of experience in a variety of languages to bear on his current work with Rails. His near-daily links and other blogging can be found at <a href="http://afreshcup.com">A Fresh Cup</a> and he <a href="http://twitter.com/MikeG1">twitters</a> too much. -<% end %> - -<%= author('Mikel Lindsaar', 'raasdnil') do %> - Mikel Lindsaar has been working with Rails since 2006 and is the author of the Ruby <a href="https://github.com/mikel/mail">Mail gem</a> and core contributor (he helped re-write Action Mailer's API). Mikel is the founder of <a href="http://rubyx.com/">RubyX</a>, has a <a href="http://lindsaar.net/">blog</a> and <a href="http://twitter.com/raasdnil">tweets</a>. -<% end %> - -<%= author('Cássio Marques', 'cmarques') do %> - Cássio Marques is a Brazilian software developer working with different programming languages such as Ruby, JavaScript, CPP and Java, as an independent consultant. He blogs at <a href="http://cassiomarques.wordpress.com">/* CODIFICANDO */</a>, which is mainly written in Portuguese, but will soon get a new section for posts with English translation. -<% end %> - -<%= author('James Miller', 'bensie') do %> - James Miller is a software developer for <a href="http://www.jk-tech.com">JK Tech</a> in San Diego, CA. You can find James on GitHub, Gmail, Twitter, and Freenode as "bensie". -<% end %> - -<%= author('Pratik Naik', 'lifo') do %> - Pratik Naik is a Ruby on Rails developer at <a href="https://basecamp.com/">Basecamp</a> and maintains a blog at <a href="http://m.onkey.org">has_many :bugs, :through => :rails</a>. He also has a semi-active <a href="http://twitter.com/lifo">twitter account</a>. -<% end %> - -<%= author('Emilio Tagua', 'miloops') do %> - Emilio Tagua —a.k.a. miloops— is an Argentinian entrepreneur, developer, open source contributor and Rails evangelist. Cofounder of <a href="http://eventioz.com">Eventioz</a>. He has been using Rails since 2006 and contributing since early 2008. Can be found at gmail, twitter, freenode, everywhere as "miloops". -<% end %> - -<%= author('Heiko Webers', 'hawe') do %> - Heiko Webers is the founder of <a href="http://www.bauland42.de">bauland42</a>, a German web application security consulting and development company focused on Ruby on Rails. He blogs at the <a href="http://www.rorsecurity.info">Ruby on Rails Security Project</a>. After 10 years of desktop application development, Heiko has rarely looked back. -<% end %> - -<%= author('Akshay Surve', 'startupjockey', 'akshaysurve.jpg') do %> - Akshay Surve is the Founder at <a href="http://www.deltax.com">DeltaX</a>, hackathon specialist, a midnight code junkie and occasionally writes prose. You can connect with him on <a href="https://twitter.com/akshaysurve">Twitter</a>, <a href="http://www.linkedin.com/in/akshaysurve">Linkedin</a>, <a href="http://www.akshaysurve.com/">Personal Blog</a> or <a href="http://www.quora.com/Akshay-Surve">Quora</a>. -<% end %> diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index f545b90103..5b6cfe6659 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -1125,7 +1125,7 @@ TIP: Rails automatically wraps fields that contain an error with a div with class `field_with_errors`. You can define a CSS rule to make them standout. -Now you'll get a nice error message when saving an article without title when +Now you'll get a nice error message when saving an article without a title when you attempt to do just that on the new article form <http://localhost:3000/articles/new>: @@ -1522,7 +1522,7 @@ comments on articles. ### Generating a Model We're going to see the same generator that we used before when creating -the `Article` model. This time we'll create a `Comment` model to hold +the `Article` model. This time we'll create a `Comment` model to hold a reference to an article. Run this command in your terminal: ```bash @@ -1857,7 +1857,7 @@ This will now render the partial in `app/views/comments/_comment.html.erb` once for each comment that is in the `@article.comments` collection. As the `render` method iterates over the `@article.comments` collection, it assigns each comment to a local variable named the same as the partial, in this case -`comment` which is then available in the partial for us to show. +`comment`, which is then available in the partial for us to show. ### Rendering a Partial Form @@ -2060,7 +2060,7 @@ What's Next? Now that you've seen your first Rails application, you should feel free to update it and experiment on your own. -Remember you don't have to do everything without help. As you need assistance +Remember, you don't have to do everything without help. As you need assistance getting up and running with Rails, feel free to consult these support resources: diff --git a/guides/source/i18n.md b/guides/source/i18n.md index 2b545e6b82..f42ab15b8b 100644 --- a/guides/source/i18n.md +++ b/guides/source/i18n.md @@ -42,6 +42,8 @@ Internationalization is a complex problem. Natural languages differ in so many w As part of this solution, **every static string in the Rails framework** - e.g. Active Record validation messages, time and date formats - **has been internationalized**. _Localization_ of a Rails application means defining translated values for these strings in desired languages. +To localize store and update _content_ in your application (e.g. translate blog posts), see the [Translating model content](#translating-model-content) section. + ### The Overall Architecture of the Library Thus, the Ruby I18n gem is split into two parts: @@ -105,7 +107,7 @@ This means, that in the `:en` locale, the key _hello_ will map to the _Hello wor The I18n library will use **English** as a **default locale**, i.e. if a different locale is not set, `:en` will be used for looking up translations. -NOTE: The i18n library takes a **pragmatic approach** to locale keys (after [some discussion](https://groups.google.com/forum/#!topic/rails-i18n/FN7eLH2-lHA)), including only the _locale_ ("language") part, like `:en`, `:pl`, not the _region_ part, like `:en-US` or `:en-GB`, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as `:cs`, `:th` or `:es` (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the `:en-US` locale you would have $ as a currency symbol, while in `:en-GB`, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a `:en-GB` dictionary. Few gems such as [Globalize3](https://github.com/globalize/globalize) may help you implement it. +NOTE: The i18n library takes a **pragmatic approach** to locale keys (after [some discussion](https://groups.google.com/forum/#!topic/rails-i18n/FN7eLH2-lHA)), including only the _locale_ ("language") part, like `:en`, `:pl`, not the _region_ part, like `:en-US` or `:en-GB`, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as `:cs`, `:th` or `:es` (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the `:en-US` locale you would have $ as a currency symbol, while in `:en-GB`, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a `:en-GB` dictionary. The **translations load path** (`I18n.load_path`) is an array of paths to files that will be loaded automatically. Configuring this path allows for customization of translations directory structure and file naming scheme. @@ -827,14 +829,14 @@ For example when you add the following translations: en: activerecord: models: - user: Dude + user: Customer attributes: user: login: "Handle" # will translate User attribute "login" as "Handle" ``` -Then `User.model_name.human` will return "Dude" and `User.human_attribute_name("login")` will return "Handle". +Then `User.model_name.human` will return "Customer" and `User.human_attribute_name("login")` will return "Handle". You can also set a plural form for model names, adding as following: @@ -843,11 +845,11 @@ en: activerecord: models: user: - one: Dude - other: Dudes + one: Customer + other: Customers ``` -Then `User.model_name.human(count: 2)` will return "Dudes". With `count: 1` or without params will return "Dude". +Then `User.model_name.human(count: 2)` will return "Customers". With `count: 1` or without params will return "Customer". In the event you need to access nested attributes within a given model, you should nest these under `model/attribute` at the model level of your translation file: @@ -855,12 +857,12 @@ In the event you need to access nested attributes within a given model, you shou en: activerecord: attributes: - user/gender: - female: "Female" - male: "Male" + user/role: + admin: "Admin" + contributor: "Contributor" ``` -Then `User.human_attribute_name("gender.female")` will return "Female". +Then `User.human_attribute_name("role.admin")` will return "Admin". NOTE: If you are using a class which includes `ActiveModel` and does not inherit from `ActiveRecord::Base`, replace `activerecord` with `activemodel` in the above key paths. @@ -1099,13 +1101,11 @@ Customize your I18n Setup For several reasons the Simple backend shipped with Active Support only does the "simplest thing that could possibly work" _for Ruby on Rails_[^3] ... which means that it is only guaranteed to work for English and, as a side effect, languages that are very similar to English. Also, the simple backend is only capable of reading translations but cannot dynamically store them to any format. -That does not mean you're stuck with these limitations, though. The Ruby I18n gem makes it very easy to exchange the Simple backend implementation with something else that fits better for your needs. E.g. you could exchange it with Globalize's Static backend: +That does not mean you're stuck with these limitations, though. The Ruby I18n gem makes it very easy to exchange the Simple backend implementation with something else that fits better for your needs, by passing a backend instance to the `I18n.backend=` setter. -```ruby -I18n.backend = Globalize::Backend::Static.new -``` +For example, you can replace the Simple backend with the the Chain backend to chain multiple backends together. This is useful when you want to use standard translations with a Simple backend but store custom application translations in a database or other backends. -You can also use the Chain backend to chain multiple backends together. This is useful when you want to use standard translations with a Simple backend but store custom application translations in a database or other backends. For example, you could use the Active Record backend and fall back to the (default) Simple backend: +With the Chain backend, you could use the Active Record backend and fall back to the (default) Simple backend: ```ruby I18n.backend = I18n::Backend::Chain.new(I18n::Backend::ActiveRecord.new, I18n.backend) @@ -1166,13 +1166,22 @@ To do so, the helper forces `I18n#translate` to raise exceptions no matter what I18n.t :foo, raise: true # always re-raises exceptions from the backend ``` +Translating Model Content +------------------------- + +The I18n API described in this guide is primarily intended for translating interface strings. If you are looking to translate model content (e.g. blog posts), you will need a different solution to help with this. + +Several gems can help with this: + +* [Globalize](https://github.com/globalize/globalize): Store translations on separate translation tables, one for each translated model +* [Mobility](https://github.com/shioyama/mobility): Provides support for storing translations in many formats, including translation tables, json columns (Postgres), etc. +* [Traco](https://github.com/barsoom/traco): Translatable columns for Rails 3 and 4, stored in the model table itself + Conclusion ---------- At this point you should have a good overview about how I18n support in Ruby on Rails works and are ready to start translating your project. -If you want to discuss certain portions or have questions, please sign up to the [rails-i18n mailing list](https://groups.google.com/forum/#!forum/rails-i18n). - Contributing to Rails I18n -------------------------- diff --git a/guides/source/index.html.erb b/guides/source/index.html.erb index 2fdf18a2e9..76f01fea0a 100644 --- a/guides/source/index.html.erb +++ b/guides/source/index.html.erb @@ -10,7 +10,9 @@ Ruby on Rails Guides <div id="subCol"> <dl> <dt></dt> - <dd class="kindle">Rails Guides are also available for <%= link_to 'Kindle', @mobi %>.</dd> + <% unless @edge -%> + <dd class="kindle">Rails Guides are also available for <%= link_to 'Kindle', @mobi %>.</dd> + <% end -%> <dd class="work-in-progress">Guides marked with this icon are currently being worked on and will not be available in the Guides Index menu. While still useful, they may contain incomplete information and even errors. You can help by reviewing them and posting your comments and corrections.</dd> </dl> </div> diff --git a/guides/source/kindle/rails_guides.opf.erb b/guides/source/kindle/rails_guides.opf.erb index 63eeb007d7..1882ec1005 100644 --- a/guides/source/kindle/rails_guides.opf.erb +++ b/guides/source/kindle/rails_guides.opf.erb @@ -26,7 +26,7 @@ <item id="<%= document['url'] %>" media-type="text/html" href="<%= document['url'] %>" /> <% end %> - <% %w{toc.html credits.html welcome.html copyright.html}.each do |url| %> + <% %w{toc.html welcome.html copyright.html}.each do |url| %> <item id="<%= url %>" media-type="text/html" href="<%= url %>" /> <% end %> @@ -38,7 +38,6 @@ <spine toc="toc"> <itemref idref="toc.html" /> <itemref idref="welcome.html" /> - <itemref idref="credits.html" /> <itemref idref="copyright.html" /> <% documents_flat.each do |document| %> <itemref idref="<%= document['url'] %>" /> diff --git a/guides/source/kindle/toc.html.erb b/guides/source/kindle/toc.html.erb index 0f4228ed6b..b77ac2e99d 100644 --- a/guides/source/kindle/toc.html.erb +++ b/guides/source/kindle/toc.html.erb @@ -18,7 +18,6 @@ Ruby on Rails Guides <% end %> <hr /> <ul> - <li><a href="credits.html">Credits</a></li> <li><a href="copyright.html">Copyright & License</a></li> </ul> </div> diff --git a/guides/source/kindle/toc.ncx.erb b/guides/source/kindle/toc.ncx.erb index 5094fea4ca..9b73bc9bea 100644 --- a/guides/source/kindle/toc.ncx.erb +++ b/guides/source/kindle/toc.ncx.erb @@ -30,10 +30,6 @@ </navLabel> <content src="welcome.html"/> </navPoint> - <navPoint class="article" id="credits" playOrder="3"> - <navLabel><text>Credits</text></navLabel> - <content src="credits.html"/> - </navPoint> <navPoint class="article" id="copyright" playOrder="4"> <navLabel><text>Copyright & License</text></navLabel> <content src="copyright.html"/> diff --git a/guides/source/layout.html.erb b/guides/source/layout.html.erb index 3981199e95..6e3aa9207e 100644 --- a/guides/source/layout.html.erb +++ b/guides/source/layout.html.erb @@ -59,7 +59,6 @@ </div> </li> <li><a class="nav-item" href="contributing_to_ruby_on_rails.html">Contribute</a></li> - <li><a class="nav-item" href="credits.html">Credits</a></li> <li class="guides-index guides-index-small"> <select class="guides-index-item nav-item"> <option value="index.html">Guides Index</option> diff --git a/guides/source/maintenance_policy.md b/guides/source/maintenance_policy.md index 1d6a4edb5b..2604d289e9 100644 --- a/guides/source/maintenance_policy.md +++ b/guides/source/maintenance_policy.md @@ -44,7 +44,7 @@ from. In special situations, where someone from the Core Team agrees to support more series, they are included in the list of supported series. -**Currently included series:** `5.1.Z`. +**Currently included series:** `5.2.Z`. Security Issues --------------- @@ -59,16 +59,16 @@ be built from 1.2.2, and then added to the end of 1-2-stable. This means that security releases are easy to upgrade to if you're running the latest version of Rails. -**Currently included series:** `5.1.Z`, `5.0.Z`. +**Currently included series:** `5.2.Z`, `5.1.Z`. Severe Security Issues ---------------------- -For severe security issues we will provide new versions as above, and also the +For severe security issues all releases in the current major series, and also the last major release series will receive patches and new versions. The classification of the security issue is judged by the core team. -**Currently included series:** `5.1.Z`, `5.0.Z`, `4.2.Z`. +**Currently included series:** `5.2.Z`, `5.1.Z`, `5.0.Z`, `4.2.Z`. Unsupported Release Series -------------------------- diff --git a/guides/source/routing.md b/guides/source/routing.md index 98aa841938..3cf5b56340 100644 --- a/guides/source/routing.md +++ b/guides/source/routing.md @@ -58,6 +58,26 @@ and this in the corresponding view: then the router will generate the path `/patients/17`. This reduces the brittleness of your view and makes your code easier to understand. Note that the id does not need to be specified in the route helper. +### Configuring the Rails Router + +The routes for your application or engine live in the file `config/routes.rb` and typically looks like this: + +```ruby +Rails.application.routes.draw do + resources :brands, only: [:index, :show] do + resources :products, only: [:index, :show] + end + + resource :basket, only: [:show, :update, :destroy] + + resolve("Basket") { route_for(:basket) } +end +``` + +Since this is a regular Ruby source file you can use all of its features to help you define your routes but be careful with variable names as they can clash with the DSL methods of the router. + +NOTE: The `Rails.application.routes.draw do ... end` block that wraps your route definitions is required to establish the scope for the router DSL and must not be deleted. + Resource Routing: the Rails Default ----------------------------------- diff --git a/guides/source/security.md b/guides/source/security.md index 28ddbdc26a..3ac50fb147 100644 --- a/guides/source/security.md +++ b/guides/source/security.md @@ -74,7 +74,7 @@ Hence, the cookie serves as temporary authentication for the web application. An * Instead of stealing a cookie unknown to the attacker, they fix a user's session identifier (in the cookie) known to them. Read more about this so-called session fixation later. -The main objective of most attackers is to make money. The underground prices for stolen bank login accounts range from $10-$1000 (depending on the available amount of funds), $0.40-$20 for credit card numbers, $1-$8 for online auction site accounts and $4-$30 for email passwords, according to the [Symantec Global Internet Security Threat Report](http://eval.symantec.com/mktginfo/enterprise/white_papers/b-whitepaper_internet_security_threat_report_xiii_04-2008.en-us.pdf). +The main objective of most attackers is to make money. The underground prices for stolen bank login accounts range from 0.5%-10% of account balance, $0.5-$30 for credit card numbers ($20-$60 with full details), $0.1-$1.5 for identities (Name, SSN & DOB), $20-$50 for retailer accounts, and $6-$10 for cloud service provider accounts, according to the [Symantec Internet Security Threat Report (2017)](https://www.symantec.com/content/dam/symantec/docs/reports/istr-22-2017-en.pdf). ### Session Guidelines @@ -217,7 +217,7 @@ The best _solution against it is not to store this kind of data in a session, bu NOTE: _Apart from stealing a user's session ID, the attacker may fix a session ID known to them. This is called session fixation._ -![Session fixation](images/session_fixation.png) +![Session fixation](images/security/session_fixation.png) This attack focuses on fixing a user's session ID known to the attacker, and forcing the user's browser into using this ID. It is therefore not necessary for the attacker to steal the session ID afterwards. Here is how this attack works: @@ -272,7 +272,7 @@ Cross-Site Request Forgery (CSRF) This attack method works by including malicious code or a link in a page that accesses a web application that the user is believed to have authenticated. If the session for that web application has not timed out, an attacker may execute unauthorized commands. -![](images/csrf.png) +![](images/security/csrf.png) In the [session chapter](#sessions) you have learned that most Rails applications use cookie-based sessions. Either they store the session ID in the cookie and have a server-side session hash, or the entire session hash is on the client-side. In either case the browser will automatically send along the cookie on every request to a domain, if it can find a cookie for that domain. The controversial point is that if the request comes from a site of a different domain, it will also send the cookie. Let's start with an example: @@ -325,7 +325,7 @@ Or the attacker places the code into the onmouseover event handler of an image: There are many other possibilities, like using a `<script>` tag to make a cross-site request to a URL with a JSONP or JavaScript response. The response is executable code that the attacker can find a way to run, possibly extracting sensitive data. To protect against this data leakage, we must disallow cross-site `<script>` tags. Ajax requests, however, obey the browser's same-origin policy (only your own site is allowed to initiate `XmlHttpRequest`) so we can safely allow them to return JavaScript responses. -Note: We can't distinguish a `<script>` tag's origin—whether it's a tag on your own site or on some other malicious site—so we must block all `<script>` across the board, even if it's actually a safe same-origin script served from your own site. In these cases, explicitly skip CSRF protection on actions that serve JavaScript meant for a `<script>` tag. +NOTE: We can't distinguish a `<script>` tag's origin—whether it's a tag on your own site or on some other malicious site—so we must block all `<script>` across the board, even if it's actually a safe same-origin script served from your own site. In these cases, explicitly skip CSRF protection on actions that serve JavaScript meant for a `<script>` tag. To protect against all other forged requests, we introduce a _required security token_ that our site knows but other sites don't know. We include the security token in requests and verify it on the server. This is a one-liner in your application controller, and is the default for newly created Rails applications: @@ -862,7 +862,7 @@ In December 2006, 34,000 actual user names and passwords were stolen in a [MySpa INFO: _CSS Injection is actually JavaScript injection, because some browsers (IE, some versions of Safari and others) allow JavaScript in CSS. Think twice about allowing custom CSS in your web application._ -CSS Injection is explained best by the well-known [MySpace Samy worm](https://samy.pl/popular/tech.html). This worm automatically sent a friend request to Samy (the attacker) simply by visiting his profile. Within several hours he had over 1 million friend requests, which created so much traffic that MySpace went offline. The following is a technical explanation of that worm. +CSS Injection is explained best by the well-known [MySpace Samy worm](https://samy.pl/myspace/tech.html). This worm automatically sent a friend request to Samy (the attacker) simply by visiting his profile. Within several hours he had over 1 million friend requests, which created so much traffic that MySpace went offline. The following is a technical explanation of that worm. MySpace blocked many tags, but allowed CSS. So the worm's author put JavaScript into CSS like this: @@ -1089,6 +1089,118 @@ Here is a list of common headers: * **Access-Control-Allow-Origin:** Used to control which sites are allowed to bypass same origin policies and send cross-origin requests. * **Strict-Transport-Security:** [Used to control if the browser is allowed to only access a site over a secure connection](https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security) +### Content Security Policy + +Rails provides a DSL that allows you to configure a +[Content Security Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) +for your application. You can configure a global default policy and then +override it on a per-resource basis and even use lambdas to inject per-request +values into the header such as account subdomains in a multi-tenant application. + +Example global policy: + +```ruby +# config/initializers/content_security_policy.rb +Rails.application.config.content_security_policy do |policy| + policy.default_src :self, :https + policy.font_src :self, :https, :data + policy.img_src :self, :https, :data + policy.object_src :none + policy.script_src :self, :https + policy.style_src :self, :https + + # Specify URI for violation reports + policy.report_uri "/csp-violation-report-endpoint" +end +``` + +Example controller overrides: + +```ruby +# Override policy inline +class PostsController < ApplicationController + content_security_policy do |p| + p.upgrade_insecure_requests true + end +end + +# Using literal values +class PostsController < ApplicationController + content_security_policy do |p| + p.base_uri "https://www.example.com" + end +end + +# Using mixed static and dynamic values +class PostsController < ApplicationController + content_security_policy do |p| + p.base_uri :self, -> { "https://#{current_user.domain}.example.com" } + end +end + +# Disabling the global CSP +class LegacyPagesController < ApplicationController + content_security_policy false, only: :index +end +``` + +Use the `content_security_policy_report_only` +configuration attribute to set +[Content-Security-Policy-Report-Only](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only) +in order to report only content violations for migrating +legacy content + +```ruby +# config/initializers/content_security_policy.rb +Rails.application.config.content_security_policy_report_only = true +``` + +```ruby +# Controller override +class PostsController < ApplicationController + content_security_policy_report_only only: :index +end +``` + +You can enable automatic nonce generation: + +```ruby +# config/initializers/content_security_policy.rb +Rails.application.config.content_security_policy do |policy| + policy.script_src :self, :https +end + +Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } +``` + +Then you can add an automatic nonce value by passing `nonce: true` +as part of `html_options`. Example: + +```html+erb +<%= javascript_tag nonce: true do -%> + alert('Hello, World!'); +<% end -%> +``` + +The same works with `javascript_include_tag`: + +```html+erb +<%= javascript_include_tag "script", nonce: true %> +``` + +Use [`csp_meta_tag`](http://api.rubyonrails.org/classes/ActionView/Helpers/CspHelper.html#method-i-csp_meta_tag) +helper to create a meta tag "csp-nonce" with the per-session nonce value +for allowing inline `<script>` tags. + +```html+erb +<head> + <%= csp_meta_tag %> +</head> +``` + +This is used by the Rails UJS helper to create dynamically +loaded inline `<script>` elements. + Environmental Security ---------------------- diff --git a/guides/source/testing.md b/guides/source/testing.md index b9b310cbba..47a3b26447 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -690,7 +690,7 @@ System Testing -------------- System tests allow you to test user interactions with your application, running tests -in either a real or a headless browser. System tests uses Capybara under the hood. +in either a real or a headless browser. System tests use Capybara under the hood. For creating Rails system tests, you use the `test/system` directory in your application. Rails provides a generator to create a system test skeleton for you. diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index ff7ccfd47d..c2fe012eeb 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -66,6 +66,17 @@ Overwrite /myapp/config/application.rb? (enter "h" for help) [Ynaqdh] Don't forget to review the difference, to see if there were any unexpected changes. +Upgrading from Rails 5.2 to Rails 6.0 +------------------------------------- + +### Force SSL + +The `force_ssl` method on controllers has been deprecated and will be removed in +Rails 6.1. You are encouraged to enable `config.force_ssl` to enforce HTTPS +connections throughout your application. If you need to exempt certain endpoints +from redirection, you can use `config.ssl_options` to configure that behavior. + + Upgrading from Rails 5.1 to Rails 5.2 ------------------------------------- @@ -77,6 +88,16 @@ Rails 5.2 adds bootsnap gem in the [newly generated app's Gemfile](https://githu The `app:update` task sets it up in `boot.rb`. If you want to use it, then add it in the Gemfile, otherwise change the `boot.rb` to not use bootsnap. +### Expiry in signed or encrypted cookie is now embedded in the cookies values + +To improve security, Rails now embeds the expiry information also in encrypted or signed cookies value. + +This new embed information make those cookies incompatible with versions of Rails older than 5.2. + +If you require your cookies to be read by 5.1 and older, or you are still validating your 5.2 deploy and want +to allow you to rollback set +`Rails.application.config.action_dispatch.use_authenticated_cookie_encryption` to `false`. + Upgrading from Rails 5.0 to Rails 5.1 ------------------------------------- @@ -660,7 +681,7 @@ xhr :get, :index, format: :js to explicitly test an `XmlHttpRequest`. -Note: Your own `<script>` tags are treated as cross-origin and blocked by +NOTE: Your own `<script>` tags are treated as cross-origin and blocked by default, too. If you really mean to load JavaScript from `<script>` tags, you must now explicitly skip CSRF protection on those actions. diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index 335eb6c4e3..a4d4a87a8b 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,4 +1,27 @@ -## Rails 6.0.0.alpha (Unreleased) ## +* Make the master.key file read-only for the owner upon generation on + POSIX-compliant systems. + + Previously: + + $ ls -l config/master.key + -rw-r--r-- 1 owner group 32 Jan 1 00:00 master.key + + Now: + + $ ls -l config/master.key + -rw------- 1 owner group 32 Jan 1 00:00 master.key + + Fixes #32604. + + *Jose Luis Duran* + +* Deprecate support for using the `HOST` environment to specify the server IP. + + The `BINDING` environment should be used instead. + + Fixes #29516. + + *Yuji Yaginuma* * Deprecate passing Rack server name as a regular argument to `rails server`. diff --git a/railties/RDOC_MAIN.rdoc b/railties/RDOC_MAIN.rdoc index c70a9f0ba0..a4a4b6b235 100644 --- a/railties/RDOC_MAIN.rdoc +++ b/railties/RDOC_MAIN.rdoc @@ -1,4 +1,6 @@ -== Welcome to \Rails += Welcome to \Rails + +== What's \Rails \Rails is a web-application framework that includes everything needed to create database-backed web applications according to the @@ -6,43 +8,48 @@ create database-backed web applications according to the pattern. Understanding the MVC pattern is key to understanding \Rails. MVC divides your -application into three layers, each with a specific responsibility. +application into three layers: Model, View, and Controller, each with a specific responsibility. + +== Model layer -The <em>Model layer</em> represents your domain model (such as Account, Product, -Person, Post, etc.) and encapsulates the business logic that is specific to +The <em><b>Model layer</b></em> represents the domain model (such as Account, Product, +Person, Post, etc.) and encapsulates the business logic specific to your application. In \Rails, database-backed model classes are derived from -ActiveRecord::Base. Active Record allows you to present the data from +<tt>ActiveRecord::Base</tt>. {Active Record}[link:files/activerecord/README_rdoc.html] allows you to present the data from database rows as objects and embellish these data objects with business logic -methods. You can read more about Active Record in its {README}[link:files/activerecord/README_rdoc.html]. -Although most \Rails models are backed by a database, models can also be ordinary +methods. Although most \Rails models are backed by a database, models can also be ordinary Ruby classes, or Ruby classes that implement a set of interfaces as provided by -the Active Model module. You can read more about Active Model in its {README}[link:files/activemodel/README_rdoc.html]. +the {Active Model}[link:files/activemodel/README_rdoc.html] module. + +== Controller layer -The <em>Controller layer</em> is responsible for handling incoming HTTP requests and +The <em><b>Controller layer</b></em> is responsible for handling incoming HTTP requests and providing a suitable response. Usually this means returning \HTML, but \Rails controllers can also generate XML, JSON, PDFs, mobile-specific views, and more. Controllers load and manipulate models, and render view templates in order to generate the appropriate HTTP response. In \Rails, incoming requests are routed by Action Dispatch to an appropriate controller, and -controller classes are derived from ActionController::Base. Action Dispatch and Action Controller -are bundled together in Action Pack. You can read more about Action Pack in its -{README}[link:files/actionpack/README_rdoc.html]. +controller classes are derived from <tt>ActionController::Base</tt>. Action Dispatch and Action Controller +are bundled together in {Action Pack}[link:files/actionpack/README_rdoc.html]. -The <em>View layer</em> is composed of "templates" that are responsible for providing +== View layer + +The <em><b>View layer</b></em> is composed of "templates" that are responsible for providing appropriate representations of your application's resources. Templates can come in a variety of formats, but most view templates are \HTML with embedded Ruby code (ERB files). Views are typically rendered to generate a controller response, -or to generate the body of an email. In \Rails, View generation is handled by Action View. -You can read more about Action View in its {README}[link:files/actionview/README_rdoc.html]. +or to generate the body of an email. In \Rails, View generation is handled by {Action View}[link:files/actionview/README_rdoc.html]. + +== Frameworks and libraries -Active Record, Active Model, Action Pack, and Action View can each be used independently outside \Rails. -In addition to that, \Rails also comes with Action Mailer ({README}[link:files/actionmailer/README_rdoc.html]), a library -to generate and send emails; Active Job ({README}[link:files/activejob/README_md.html]), a +{Active Record}[link:files/activerecord/README_rdoc.html], {Active Model}[link:files/activemodel/README_rdoc.html], +{Action Pack}[link:files/actionpack/README_rdoc.html], and {Action View}[link:files/actionview/README_rdoc.html] can each be used independently outside \Rails. +In addition to that, \Rails also comes with {Action Mailer}[link:files/actionmailer/README_rdoc.html], a library +to generate and send emails; {Active Job}[link:files/activejob/README_md.html], a framework for declaring jobs and making them run on a variety of queueing -backends; Action Cable ({README}[link:files/actioncable/README_md.html]), a framework to -integrate WebSockets with a \Rails application; -Active Storage ({README}[link:files/activestorage/README_md.html]), a library to attach cloud -and local files to \Rails applications; -and Active Support ({README}[link:files/activesupport/README_rdoc.html]), a collection +backends; {Action Cable}[link:files/actioncable/README_md.html], a framework to +integrate WebSockets with a \Rails application; {Active Storage}[link:files/activestorage/README_md.html], +a library to attach cloud and local files to \Rails applications; +and {Active Support}[link:files/activesupport/README_rdoc.html], a collection of utility classes and standard library extensions that are useful for \Rails, and may also be used independently outside \Rails. diff --git a/railties/lib/minitest/rails_plugin.rb b/railties/lib/minitest/rails_plugin.rb index 7193abbc33..6486fa1798 100644 --- a/railties/lib/minitest/rails_plugin.rb +++ b/railties/lib/minitest/rails_plugin.rb @@ -13,7 +13,7 @@ module Minitest end def self.plugin_rails_options(opts, options) - Rails::TestUnit::Runner.attach_before_load_options(opts) + ::Rails::TestUnit::Runner.attach_before_load_options(opts) opts.on("-b", "--backtrace", "Show the complete backtrace") do options[:full_backtrace] = true diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index a9dee10981..e346d5cc3a 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -427,7 +427,7 @@ module Rails # the correct place to store it is in the encrypted credentials file. def secret_key_base if Rails.env.test? || Rails.env.development? - Digest::MD5.hexdigest self.class.name + secrets.secret_key_base || Digest::MD5.hexdigest(self.class.name) else validate_secret_key_base( ENV["SECRET_KEY_BASE"] || credentials.secret_key_base || secrets.secret_key_base diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index 912faed3e4..bba573499d 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -166,6 +166,18 @@ module Rails end end + # Loads the database YAML without evaluating ERB. People seem to + # write ERB that makes the database configuration depend on + # Rails configuration. But we want Rails configuration (specifically + # `rake` and `rails` tasks) to be generated based on information in + # the database yaml, so we need a method that loads the database + # yaml *without* the context of the Rails application. + def load_database_yaml # :nodoc: + path = paths["config/database"].existent.first + return {} unless path + YAML.load_file(path.to_s) + end + # Loads and returns the entire raw configuration of database from # values stored in <tt>config/database.yml</tt>. def database_configuration @@ -241,7 +253,7 @@ module Rails end def annotations - SourceAnnotationExtractor::Annotation + Rails::SourceAnnotationExtractor::Annotation end def content_security_policy(&block) diff --git a/railties/lib/rails/application_controller.rb b/railties/lib/rails/application_controller.rb index fa8793d81a..b3fe822218 100644 --- a/railties/lib/rails/application_controller.rb +++ b/railties/lib/rails/application_controller.rb @@ -4,6 +4,13 @@ class Rails::ApplicationController < ActionController::Base # :nodoc: self.view_paths = File.expand_path("templates", __dir__) layout "application" + before_action :disable_content_security_policy_nonce! + + content_security_policy do |policy| + policy.script_src :unsafe_inline + policy.style_src :unsafe_inline + end + private def require_local! @@ -15,4 +22,8 @@ class Rails::ApplicationController < ActionController::Base # :nodoc: def local_request? Rails.application.config.consider_all_requests_local || request.local? end + + def disable_content_security_policy_nonce! + request.content_security_policy_nonce_generator = nil + end end diff --git a/railties/lib/rails/command/spellchecker.rb b/railties/lib/rails/command/spellchecker.rb index 59ccab4ea2..154358cd45 100644 --- a/railties/lib/rails/command/spellchecker.rb +++ b/railties/lib/rails/command/spellchecker.rb @@ -3,50 +3,8 @@ module Rails module Command module Spellchecker # :nodoc: - class << self - def suggest(word, from:, count: 3) - from.sort_by { |w| levenshtein_distance(word, w) }.take(count) - end - - private - # This code is based directly on the Text gem implementation. - # Copyright (c) 2006-2013 Paul Battley, Michael Neumann, Tim Fletcher. - # - # Returns a value representing the "cost" of transforming str1 into str2. - def levenshtein_distance(str1, str2) # :doc: - s = str1 - t = str2 - n = s.length - m = t.length - - return m if (0 == n) - return n if (0 == m) - - d = (0..m).to_a - x = nil - - # avoid duplicating an enumerable object in the loop - str2_codepoint_enumerable = str2.each_codepoint - - str1.each_codepoint.with_index do |char1, i| - e = i + 1 - - str2_codepoint_enumerable.with_index do |char2, j| - cost = (char1 == char2) ? 0 : 1 - x = [ - d[j + 1] + 1, # insertion - e + 1, # deletion - d[j] + cost # substitution - ].min - d[j] = e - e = x - end - - d[m] = x - end - - x - end + def self.suggest(word, from:) + DidYouMean::SpellChecker.new(dictionary: from.map(&:to_s)).correct(word).first end end end diff --git a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb index 8df548b5de..806b7de6d6 100644 --- a/railties/lib/rails/commands/dbconsole/dbconsole_command.rb +++ b/railties/lib/rails/commands/dbconsole/dbconsole_command.rb @@ -97,7 +97,7 @@ module Rails elsif configurations[environment].blank? && configurations[connection].blank? raise ActiveRecord::AdapterNotSpecified, "'#{environment}' database is not configured. Available configuration: #{configurations.inspect}" else - configurations[environment].presence || configurations[connection] + configurations[connection] || configurations[environment].presence end end end diff --git a/railties/lib/rails/commands/routes/routes_command.rb b/railties/lib/rails/commands/routes/routes_command.rb index c4f3717095..b592a5212f 100644 --- a/railties/lib/rails/commands/routes/routes_command.rb +++ b/railties/lib/rails/commands/routes/routes_command.rb @@ -5,45 +5,33 @@ require "rails/command" module Rails module Command class RoutesCommand < Base # :nodoc: - class_option :controller, aliases: "-c", type: :string, desc: "Specifies the controller." - class_option :grep_pattern, aliases: "-g", type: :string, desc: "Specifies grep pattern." - class_option :expanded_format, aliases: "--expanded", type: :string, desc: "Turn on expanded format mode." - - no_commands do - def help - say "Usage: Print out all defined routes in match order, with names." - say "" - say "Target specific controller with -c option, or grep routes using -g option" - say "Use expanded format with --expanded option" - say "" - end - end + class_option :controller, aliases: "-c", desc: "Filter by a specific controller, e.g. PostsController or Admin::PostsController." + class_option :grep, aliases: "-g", desc: "Grep routes by a specific pattern." + class_option :expanded, type: :boolean, aliases: "-E", desc: "Print routes expanded vertically with parts explained." def perform(*) require_application_and_environment! require "action_dispatch/routing/inspector" - all_routes = Rails.application.routes.routes - inspector = ActionDispatch::Routing::RoutesInspector.new(all_routes) - - if options.has_key?("expanded_format") - say inspector.format(ActionDispatch::Routing::ConsoleFormatter::Expanded.new, routes_filter) - else - say inspector.format(ActionDispatch::Routing::ConsoleFormatter::Sheet.new, routes_filter) - end + say inspector.format(formatter, routes_filter) end private + def inspector + ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes) + end - def routes_filter - if options.has_key?("controller") - { controller: options["controller"] } - elsif options.has_key?("grep_pattern") - options["grep_pattern"] + def formatter + if options.key?("expanded") + ActionDispatch::Routing::ConsoleFormatter::Expanded.new else - nil + ActionDispatch::Routing::ConsoleFormatter::Sheet.new end end + + def routes_filter + options.symbolize_keys.slice(:controller, :grep) + end end end end diff --git a/railties/lib/rails/commands/server/server_command.rb b/railties/lib/rails/commands/server/server_command.rb index 64fb912b51..77b6c1f65d 100644 --- a/railties/lib/rails/commands/server/server_command.rb +++ b/railties/lib/rails/commands/server/server_command.rb @@ -43,15 +43,15 @@ module Rails ENV["RAILS_ENV"] ||= options[:environment] end - def start + def start(after_stop_callback = nil) trap(:INT) { exit } create_tmp_directories setup_dev_caching log_to_stdout if options[:log_stdout] - super + super() ensure - yield + after_stop_callback.call if after_stop_callback end def serveable? # :nodoc: @@ -157,9 +157,8 @@ module Rails if server.serveable? print_boot_information(server.server, server.served_url) - server.start do - say "Exiting" unless options[:daemon] - end + after_stop_callback = -> { say "Exiting" unless options[:daemon] } + server.start(after_stop_callback) else say rack_server_suggestion(using) end @@ -220,7 +219,7 @@ module Rails user_supplied_options << name end end - user_supplied_options << :Host if ENV["HOST"] + user_supplied_options << :Host if ENV["HOST"] || ENV["BINDING"] user_supplied_options << :Port if ENV["PORT"] user_supplied_options.uniq end @@ -235,7 +234,17 @@ module Rails options[:binding] else default_host = environment == "development" ? "localhost" : "0.0.0.0" - ENV.fetch("HOST", default_host) + + if ENV["HOST"] && !ENV["BINDING"] + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Using the `HOST` environment to specify the IP is deprecated and will be removed in Rails 6.1. + Please use `BINDING` environment instead. + MSG + + return ENV["HOST"] + end + + ENV.fetch("BINDING", default_host) end end @@ -284,10 +293,10 @@ module Rails Run `rails server --help` for more options. MSG else - suggestions = Rails::Command::Spellchecker.suggest(server, from: RACK_SERVERS).map(&:inspect) + suggestions = Rails::Command::Spellchecker.suggest(server, from: RACK_SERVERS) <<~MSG - Could not find server "#{server}". Maybe you meant #{suggestions.first} or #{suggestions.second}? + Could not find server "#{server}". Maybe you meant #{suggestions.inspect}? Run `rails server --help` for more options. MSG end diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb index 7248fbbc94..f8460bd4ee 100644 --- a/railties/lib/rails/generators.rb +++ b/railties/lib/rails/generators.rb @@ -276,12 +276,11 @@ module Rails klass.start(args, config) else options = sorted_groups.flat_map(&:last) - suggestions = Rails::Command::Spellchecker.suggest(namespace.to_s, from: options, count: 3) - suggestions.map! { |s| "'#{s}'" } - msg = "Could not find generator '#{namespace}'. ".dup - msg << "Maybe you meant #{ suggestions[0...-1].join(', ')} or #{suggestions[-1]}\n" - msg << "Run `rails generate --help` for more options." - puts msg + suggestion = Rails::Command::Spellchecker.suggest(namespace.to_s, from: options) + puts <<~MSG + Could not find generator '#{namespace}'. Maybe you meant #{suggestion.inspect}? + Run `rails generate --help` for more options. + MSG end end diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index e1889979d7..8c5d872573 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -299,7 +299,7 @@ module Rails def gem_for_database # %w( mysql postgresql sqlite3 oracle frontbase ibm_db sqlserver jdbcmysql jdbcsqlite3 jdbcpostgresql ) case options[:database] - when "mysql" then ["mysql2", ["~> 0.4.4"]] + when "mysql" then ["mysql2", [">= 0.4.4", "< 0.6.0"]] when "postgresql" then ["pg", [">= 0.18", "< 2.0"]] when "oracle" then ["activerecord-oracle_enhanced-adapter", nil] when "frontbase" then ["ruby-frontbase", nil] diff --git a/railties/lib/rails/generators/migration.rb b/railties/lib/rails/generators/migration.rb index 1cbccfe461..5081060895 100644 --- a/railties/lib/rails/generators/migration.rb +++ b/railties/lib/rails/generators/migration.rb @@ -63,7 +63,12 @@ module Rails numbered_destination = File.join(dir, ["%migration_number%", base].join("_")) create_migration numbered_destination, nil, config do - ERB.new(::File.binread(source), nil, "-", "@output_buffer").result(context) + match = ERB.version.match(/\Aerb\.rb \[(?<version>[^ ]+) /) + if match && match[:version] >= "2.2.0" # Ruby 2.6+ + ERB.new(::File.binread(source), trim_mode: "-", eoutvar: "@output_buffer").result(context) + else + ERB.new(::File.binread(source), nil, "-", "@output_buffer").result(context) + end end end end diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt index 23bb89f4ce..5e7455cdc7 100644 --- a/railties/lib/rails/generators/rails/app/templates/Gemfile.tt +++ b/railties/lib/rails/generators/rails/app/templates/Gemfile.tt @@ -69,7 +69,7 @@ end <%- if depends_on_system_test? -%> group :test do # Adds support for Capybara system testing and selenium driver - gem 'capybara', '~> 2.15' + gem 'capybara', '>= 2.15' gem 'selenium-webdriver' # Easy installation and use of chromedriver to run system tests with Chrome gem 'chromedriver-helper' diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt index a87649b50f..3807c8a9aa 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt @@ -28,7 +28,7 @@ Rails.application.configure do end <%- unless skip_active_storage? -%> - # Store uploaded files on the local file system (see config/storage.yml for options) + # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local <%- end -%> <%- unless options.skip_action_mailer? -%> @@ -60,7 +60,7 @@ Rails.application.configure do config.assets.quiet = true <%- end -%> - # Raises error for missing translations + # Raises error for missing translations. # config.action_view.raise_on_missing_translations = true # Use an evented file watcher to asynchronously detect changes in source code, diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt index 926326b5bb..d646694477 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt @@ -43,12 +43,12 @@ Rails.application.configure do # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX <%- unless skip_active_storage? -%> - # Store uploaded files on the local file system (see config/storage.yml for options) + # Store uploaded files on the local file system (see config/storage.yml for options). config.active_storage.service = :local <%- end -%> <%- unless options[:skip_action_cable] -%> - # Mount Action Cable outside main process or domain + # Mount Action Cable outside main process or domain. # config.action_cable.mount_path = nil # config.action_cable.url = 'wss://example.com/cable' # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] @@ -67,7 +67,7 @@ Rails.application.configure do # Use a different cache store in production. # config.cache_store = :mem_cache_store - # Use a real queuing backend for Active Job (and separate queues per environment) + # Use a real queuing backend for Active Job (and separate queues per environment). # config.active_job.queue_adapter = :resque # config.active_job.queue_name_prefix = "<%= app_name %>_#{Rails.env}" diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt index ff4c89219a..82f2a8aebe 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt @@ -29,7 +29,7 @@ Rails.application.configure do config.action_controller.allow_forgery_protection = false <%- unless skip_active_storage? -%> - # Store uploaded files on the local file system in a temporary directory + # Store uploaded files on the local file system in a temporary directory. config.active_storage.service = :test <%- end -%> @@ -45,6 +45,9 @@ Rails.application.configure do # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr - # Raises error for missing translations + # Raises error for missing translations. # config.action_view.raise_on_missing_translations = true + + # Prevent expensive template finalization at end of test suite runs. + config.action_view.finalize_compiled_template_methods = false end diff --git a/railties/lib/rails/generators/rails/app/templates/config/storage.yml.tt b/railties/lib/rails/generators/rails/app/templates/config/storage.yml.tt index 1c0cde0b09..7207c75086 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/storage.yml.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/storage.yml.tt @@ -24,7 +24,6 @@ local: # Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) # microsoft: # service: AzureStorage -# path: your_azure_storage_path # storage_account_name: your_account_name # storage_access_key: <%%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> # container: your_container_name diff --git a/railties/lib/rails/generators/rails/app/templates/gitignore.tt b/railties/lib/rails/generators/rails/app/templates/gitignore.tt index 2cd8335aba..4e114fb1d9 100644 --- a/railties/lib/rails/generators/rails/app/templates/gitignore.tt +++ b/railties/lib/rails/generators/rails/app/templates/gitignore.tt @@ -24,8 +24,11 @@ <% unless skip_active_storage? -%> # Ignore uploaded files in development /storage/* - +<% if keeps? -%> +!/storage/.keep <% end -%> +<% end -%> + <% unless options.skip_yarn? -%> /node_modules /yarn-error.log diff --git a/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb b/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb index 90068c678d..e2359e9ded 100644 --- a/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb +++ b/railties/lib/rails/generators/rails/encryption_key_file/encryption_key_file_generator.rb @@ -27,6 +27,7 @@ module Rails def add_key_file_silently(key_path, key = nil) create_file key_path, key || ActiveSupport::EncryptedFile.generate_key + key_path.chmod 0600 end def ignore_key_file(key_path, ignore: key_ignore(key_path)) diff --git a/railties/lib/rails/rack/logger.rb b/railties/lib/rails/rack/logger.rb index ec5212ee76..4ea7e40319 100644 --- a/railties/lib/rails/rack/logger.rb +++ b/railties/lib/rails/rack/logger.rb @@ -35,9 +35,9 @@ module Rails instrumenter = ActiveSupport::Notifications.instrumenter instrumenter.start "request.action_dispatch", request: request logger.info { started_request_message(request) } - resp = @app.call(env) - resp[2] = ::Rack::BodyProxy.new(resp[2]) { finish(request) } - resp + status, headers, body = @app.call(env) + body = ::Rack::BodyProxy.new(body) { finish(request) } + [status, headers, body] rescue Exception finish(request) raise diff --git a/railties/lib/rails/ruby_version_check.rb b/railties/lib/rails/ruby_version_check.rb index f8d3311156..b2d44d9b8e 100644 --- a/railties/lib/rails/ruby_version_check.rb +++ b/railties/lib/rails/ruby_version_check.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -if RUBY_VERSION < "2.4.1" && RUBY_ENGINE == "ruby" +if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.4.1") && RUBY_ENGINE == "ruby" desc = defined?(RUBY_DESCRIPTION) ? RUBY_DESCRIPTION : "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE})" abort <<-end_message diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb index 1db6c98537..73bee5bcbf 100644 --- a/railties/lib/rails/source_annotation_extractor.rb +++ b/railties/lib/rails/source_annotation_extractor.rb @@ -1,141 +1,150 @@ # frozen_string_literal: true -# Implements the logic behind the rake tasks for annotations like -# -# rails notes -# rails notes:optimize -# -# and friends. See <tt>rails -T notes</tt> and <tt>railties/lib/rails/tasks/annotations.rake</tt>. -# -# Annotation objects are triplets <tt>:line</tt>, <tt>:tag</tt>, <tt>:text</tt> that -# represent the line where the annotation lives, its tag, and its text. Note -# the filename is not stored. -# -# Annotations are looked for in comments and modulus whitespace they have to -# start with the tag optionally followed by a colon. Everything up to the end -# of the line (or closing ERB comment tag) is considered to be their text. -class SourceAnnotationExtractor - Annotation = Struct.new(:line, :tag, :text) do - def self.directories - @@directories ||= %w(app config db lib test) + (ENV["SOURCE_ANNOTATION_DIRECTORIES"] || "").split(",") - end +require "active_support/deprecation" - # Registers additional directories to be included - # SourceAnnotationExtractor::Annotation.register_directories("spec", "another") - def self.register_directories(*dirs) - directories.push(*dirs) - end +# Remove this deprecated class in the next minor version +#:nodoc: +SourceAnnotationExtractor = ActiveSupport::Deprecation::DeprecatedConstantProxy. + new("SourceAnnotationExtractor", "Rails::SourceAnnotationExtractor") - def self.extensions - @@extensions ||= {} - end +module Rails + # Implements the logic behind the rake tasks for annotations like + # + # rails notes + # rails notes:optimize + # + # and friends. See <tt>rails -T notes</tt> and <tt>railties/lib/rails/tasks/annotations.rake</tt>. + # + # Annotation objects are triplets <tt>:line</tt>, <tt>:tag</tt>, <tt>:text</tt> that + # represent the line where the annotation lives, its tag, and its text. Note + # the filename is not stored. + # + # Annotations are looked for in comments and modulus whitespace they have to + # start with the tag optionally followed by a colon. Everything up to the end + # of the line (or closing ERB comment tag) is considered to be their text. + class SourceAnnotationExtractor + class Annotation < Struct.new(:line, :tag, :text) + def self.directories + @@directories ||= %w(app config db lib test) + (ENV["SOURCE_ANNOTATION_DIRECTORIES"] || "").split(",") + end - # Registers new Annotations File Extensions - # SourceAnnotationExtractor::Annotation.register_extensions("css", "scss", "sass", "less", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ } - def self.register_extensions(*exts, &block) - extensions[/\.(#{exts.join("|")})$/] = block - end + # Registers additional directories to be included + # SourceAnnotationExtractor::Annotation.register_directories("spec", "another") + def self.register_directories(*dirs) + directories.push(*dirs) + end - register_extensions("builder", "rb", "rake", "yml", "yaml", "ruby") { |tag| /#\s*(#{tag}):?\s*(.*)$/ } - register_extensions("css", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ } - register_extensions("erb") { |tag| /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/ } + def self.extensions + @@extensions ||= {} + end - # Returns a representation of the annotation that looks like this: + # Registers new Annotations File Extensions + # SourceAnnotationExtractor::Annotation.register_extensions("css", "scss", "sass", "less", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ } + def self.register_extensions(*exts, &block) + extensions[/\.(#{exts.join("|")})$/] = block + end + + register_extensions("builder", "rb", "rake", "yml", "yaml", "ruby") { |tag| /#\s*(#{tag}):?\s*(.*)$/ } + register_extensions("css", "js") { |tag| /\/\/\s*(#{tag}):?\s*(.*)$/ } + register_extensions("erb") { |tag| /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/ } + + # Returns a representation of the annotation that looks like this: + # + # [126] [TODO] This algorithm is simple and clearly correct, make it faster. + # + # If +options+ has a flag <tt>:tag</tt> the tag is shown as in the example above. + # Otherwise the string contains just line and text. + def to_s(options = {}) + s = "[#{line.to_s.rjust(options[:indent])}] ".dup + s << "[#{tag}] " if options[:tag] + s << text + end + end + + # Prints all annotations with tag +tag+ under the root directories +app+, + # +config+, +db+, +lib+, and +test+ (recursively). # - # [126] [TODO] This algorithm is simple and clearly correct, make it faster. + # Additional directories may be added using a comma-delimited list set using + # <tt>ENV['SOURCE_ANNOTATION_DIRECTORIES']</tt>. # - # If +options+ has a flag <tt>:tag</tt> the tag is shown as in the example above. - # Otherwise the string contains just line and text. - def to_s(options = {}) - s = "[#{line.to_s.rjust(options[:indent])}] ".dup - s << "[#{tag}] " if options[:tag] - s << text + # Directories may also be explicitly set using the <tt>:dirs</tt> key in +options+. + # + # SourceAnnotationExtractor.enumerate 'TODO|FIXME', dirs: %w(app lib), tag: true + # + # If +options+ has a <tt>:tag</tt> flag, it will be passed to each annotation's +to_s+. + # + # See <tt>#find_in</tt> for a list of file extensions that will be taken into account. + # + # This class method is the single entry point for the rake tasks. + def self.enumerate(tag, options = {}) + extractor = new(tag) + dirs = options.delete(:dirs) || Annotation.directories + extractor.display(extractor.find(dirs), options) end - end - # Prints all annotations with tag +tag+ under the root directories +app+, - # +config+, +db+, +lib+, and +test+ (recursively). - # - # Additional directories may be added using a comma-delimited list set using - # <tt>ENV['SOURCE_ANNOTATION_DIRECTORIES']</tt>. - # - # Directories may also be explicitly set using the <tt>:dirs</tt> key in +options+. - # - # SourceAnnotationExtractor.enumerate 'TODO|FIXME', dirs: %w(app lib), tag: true - # - # If +options+ has a <tt>:tag</tt> flag, it will be passed to each annotation's +to_s+. - # - # See <tt>#find_in</tt> for a list of file extensions that will be taken into account. - # - # This class method is the single entry point for the rake tasks. - def self.enumerate(tag, options = {}) - extractor = new(tag) - dirs = options.delete(:dirs) || Annotation.directories - extractor.display(extractor.find(dirs), options) - end - - attr_reader :tag - - def initialize(tag) - @tag = tag - end + attr_reader :tag - # Returns a hash that maps filenames under +dirs+ (recursively) to arrays - # with their annotations. - def find(dirs) - dirs.inject({}) { |h, dir| h.update(find_in(dir)) } - end + def initialize(tag) + @tag = tag + end - # Returns a hash that maps filenames under +dir+ (recursively) to arrays - # with their annotations. Only files with annotations are included. Files - # with extension +.builder+, +.rb+, +.rake+, +.yml+, +.yaml+, +.ruby+, - # +.css+, +.js+ and +.erb+ are taken into account. - def find_in(dir) - results = {} - - Dir.glob("#{dir}/*") do |item| - next if File.basename(item)[0] == ?. - - if File.directory?(item) - results.update(find_in(item)) - else - extension = Annotation.extensions.detect do |regexp, _block| - regexp.match(item) - end + # Returns a hash that maps filenames under +dirs+ (recursively) to arrays + # with their annotations. + def find(dirs) + dirs.inject({}) { |h, dir| h.update(find_in(dir)) } + end - if extension - pattern = extension.last.call(tag) - results.update(extract_annotations_from(item, pattern)) if pattern + # Returns a hash that maps filenames under +dir+ (recursively) to arrays + # with their annotations. Only files with annotations are included. Files + # with extension +.builder+, +.rb+, +.rake+, +.yml+, +.yaml+, +.ruby+, + # +.css+, +.js+ and +.erb+ are taken into account. + def find_in(dir) + results = {} + + Dir.glob("#{dir}/*") do |item| + next if File.basename(item)[0] == ?. + + if File.directory?(item) + results.update(find_in(item)) + else + extension = Annotation.extensions.detect do |regexp, _block| + regexp.match(item) + end + + if extension + pattern = extension.last.call(tag) + results.update(extract_annotations_from(item, pattern)) if pattern + end end end - end - results - end + results + end - # If +file+ is the filename of a file that contains annotations this method returns - # a hash with a single entry that maps +file+ to an array of its annotations. - # Otherwise it returns an empty hash. - def extract_annotations_from(file, pattern) - lineno = 0 - result = File.readlines(file, encoding: Encoding::BINARY).inject([]) do |list, line| - lineno += 1 - next list unless line =~ pattern - list << Annotation.new(lineno, $1, $2) + # If +file+ is the filename of a file that contains annotations this method returns + # a hash with a single entry that maps +file+ to an array of its annotations. + # Otherwise it returns an empty hash. + def extract_annotations_from(file, pattern) + lineno = 0 + result = File.readlines(file, encoding: Encoding::BINARY).inject([]) do |list, line| + lineno += 1 + next list unless line =~ pattern + list << Annotation.new(lineno, $1, $2) + end + result.empty? ? {} : { file => result } end - result.empty? ? {} : { file => result } - end - # Prints the mapping from filenames to annotations in +results+ ordered by filename. - # The +options+ hash is passed to each annotation's +to_s+. - def display(results, options = {}) - options[:indent] = results.flat_map { |f, a| a.map(&:line) }.max.to_s.size - results.keys.sort.each do |file| - puts "#{file}:" - results[file].each do |note| - puts " * #{note.to_s(options)}" + # Prints the mapping from filenames to annotations in +results+ ordered by filename. + # The +options+ hash is passed to each annotation's +to_s+. + def display(results, options = {}) + options[:indent] = results.flat_map { |f, a| a.map(&:line) }.max.to_s.size + results.keys.sort.each do |file| + puts "#{file}:" + results[file].each do |note| + puts " * #{note.to_s(options)}" + end + puts end - puts end end end diff --git a/railties/lib/rails/tasks/annotations.rake b/railties/lib/rails/tasks/annotations.rake index 93bc66e2db..60bcdc5e1b 100644 --- a/railties/lib/rails/tasks/annotations.rake +++ b/railties/lib/rails/tasks/annotations.rake @@ -4,19 +4,19 @@ require "rails/source_annotation_extractor" desc "Enumerate all annotations (use notes:optimize, :fixme, :todo for focus)" task :notes do - SourceAnnotationExtractor.enumerate "OPTIMIZE|FIXME|TODO", tag: true + Rails::SourceAnnotationExtractor.enumerate "OPTIMIZE|FIXME|TODO", tag: true end namespace :notes do ["OPTIMIZE", "FIXME", "TODO"].each do |annotation| # desc "Enumerate all #{annotation} annotations" task annotation.downcase.intern do - SourceAnnotationExtractor.enumerate annotation + Rails::SourceAnnotationExtractor.enumerate annotation end end desc "Enumerate a custom annotation, specify with ANNOTATION=CUSTOM" task :custom do - SourceAnnotationExtractor.enumerate ENV["ANNOTATION"] + Rails::SourceAnnotationExtractor.enumerate ENV["ANNOTATION"] end end diff --git a/railties/lib/rails/tasks/yarn.rake b/railties/lib/rails/tasks/yarn.rake index 48da7ffc51..10703a1808 100644 --- a/railties/lib/rails/tasks/yarn.rake +++ b/railties/lib/rails/tasks/yarn.rake @@ -3,7 +3,7 @@ namespace :yarn do desc "Install all JavaScript dependencies as specified via Yarn" task :install do - system("./bin/yarn install --no-progress --production") + system("./bin/yarn install --no-progress --frozen-lockfile --production") end end diff --git a/railties/test/app_loader_test.rb b/railties/test/app_loader_test.rb index bb556f1968..c7a6bdee1b 100644 --- a/railties/test/app_loader_test.rb +++ b/railties/test/app_loader_test.rb @@ -40,13 +40,13 @@ class AppLoaderTest < ActiveSupport::TestCase test "is not in a Rails application if #{exe} is not found in the current or parent directories" do def loader.find_executables; end - assert !loader.exec_app + assert_not loader.exec_app end test "is not in a Rails application if #{exe} exists but is a folder" do FileUtils.mkdir_p(exe) - assert !loader.exec_app + assert_not loader.exec_app end ["APP_PATH", "ENGINE_PATH"].each do |keyword| @@ -61,7 +61,7 @@ class AppLoaderTest < ActiveSupport::TestCase test "is not in a Rails application if #{exe} exists but doesn't contain #{keyword}" do write exe - assert !loader.exec_app + assert_not loader.exec_app end test "is in a Rails application if parent directory has #{exe} containing #{keyword} and chdirs to the root directory" do diff --git a/railties/test/application/asset_debugging_test.rb b/railties/test/application/asset_debugging_test.rb index e56c7b958e..3e0f31860b 100644 --- a/railties/test/application/asset_debugging_test.rb +++ b/railties/test/application/asset_debugging_test.rb @@ -77,7 +77,8 @@ module ApplicationTests stylesheet_link_tag: %r{<link rel="stylesheet" media="screen" href="/stylesheets/#{contents}.css" />}, javascript_include_tag: %r{<script src="/javascripts/#{contents}.js">}, audio_tag: %r{<audio src="/audios/#{contents}"></audio>}, - video_tag: %r{<video src="/videos/#{contents}"></video>} + video_tag: %r{<video src="/videos/#{contents}"></video>}, + image_submit_tag: %r{<input type="image" src="/images/#{contents}" />} } cases.each do |(view_method, tag_match)| diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index 0d3262d6f6..9ef123c5b6 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -76,7 +76,7 @@ module ApplicationTests # Load app env app "production" - assert !defined?(Uglifier) + assert_not defined?(Uglifier) get "/assets/demo.js" assert_match "alert()", last_response.body assert defined?(Uglifier) @@ -270,10 +270,10 @@ module ApplicationTests app "production" # Checking if Uglifier is defined we can know if Sprockets was reached or not - assert !defined?(Uglifier) + assert_not defined?(Uglifier) get "/assets/#{asset_path}" assert_match "alert()", last_response.body - assert !defined?(Uglifier) + assert_not defined?(Uglifier) end test "precompile properly refers files referenced with asset_path" do diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb index bd9b87467c..59b43a7e10 100644 --- a/railties/test/application/configuration_test.rb +++ b/railties/test/application/configuration_test.rb @@ -361,7 +361,7 @@ module ApplicationTests end RUBY - assert !$prepared + assert_not $prepared app "development" @@ -576,6 +576,7 @@ module ApplicationTests app "development" assert_equal "3b7cd727ee24e8444053437c36cc66c3", app.secrets.secret_key_base + assert_equal "3b7cd727ee24e8444053437c36cc66c3", app.secret_key_base end test "secret_key_base is copied from config to secrets when not set" do @@ -1509,7 +1510,7 @@ module ApplicationTests end end - assert_not_nil SourceAnnotationExtractor::Annotation.extensions[/\.(coffee)$/] + assert_not_nil Rails::SourceAnnotationExtractor::Annotation.extensions[/\.(coffee)$/] end test "rake_tasks block works at instance level" do diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb index e631318f82..1530ea82d6 100644 --- a/railties/test/application/initializers/frameworks_test.rb +++ b/railties/test/application/initializers/frameworks_test.rb @@ -226,7 +226,7 @@ module ApplicationTests rails %w(generate model post title:string) rails %w(db:migrate db:schema:cache:dump db:rollback) require "#{app_path}/config/environment" - assert !ActiveRecord::Base.connection.schema_cache.data_sources("posts") + assert_not ActiveRecord::Base.connection.schema_cache.data_sources("posts") end test "active record establish_connection uses Rails.env if DATABASE_URL is not set" do diff --git a/railties/test/application/middleware/sendfile_test.rb b/railties/test/application/middleware/sendfile_test.rb index 9def3a0ce7..818ad61c64 100644 --- a/railties/test/application/middleware/sendfile_test.rb +++ b/railties/test/application/middleware/sendfile_test.rb @@ -29,7 +29,7 @@ module ApplicationTests simple_controller get "/" - assert !last_response.headers["X-Sendfile"] + assert_not last_response.headers["X-Sendfile"] assert_equal File.read(__FILE__), last_response.body end diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb index a17988235a..9182a63ab7 100644 --- a/railties/test/application/middleware/session_test.rb +++ b/railties/test/application/middleware/session_test.rb @@ -31,7 +31,7 @@ module ApplicationTests add_to_config "config.force_ssl = true" add_to_config "config.ssl_options = { secure_cookies: false }" require "#{app_path}/config/environment" - assert !app.config.session_options[:secure] + assert_not app.config.session_options[:secure] end test "session is not loaded if it's not used" do @@ -51,7 +51,7 @@ module ApplicationTests get "/" assert last_request.env["HTTP_COOKIE"] - assert !last_response.headers["Set-Cookie"] + assert_not last_response.headers["Set-Cookie"] end test "session is empty and isn't saved on unverified request when using :null_session protect method" do diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb index 5b4c42c189..0594236b1f 100644 --- a/railties/test/application/rake/dbs_test.rb +++ b/railties/test/application/rake/dbs_test.rb @@ -34,7 +34,7 @@ module ApplicationTests assert_equal expected_database, ActiveRecord::Base.connection_config[:database] if environment_loaded output = rails("db:drop") assert_match(/Dropped database/, output) - assert !File.exist?(expected_database) + assert_not File.exist?(expected_database) end end diff --git a/railties/test/application/rake/multi_dbs_test.rb b/railties/test/application/rake/multi_dbs_test.rb new file mode 100644 index 0000000000..07d96fcb56 --- /dev/null +++ b/railties/test/application/rake/multi_dbs_test.rb @@ -0,0 +1,164 @@ +# frozen_string_literal: true + +require "isolation/abstract_unit" + +module ApplicationTests + module RakeTests + class RakeMultiDbsTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Isolation + + def setup + build_app(multi_db: true) + FileUtils.rm_rf("#{app_path}/config/environments") + end + + def teardown + teardown_app + end + + def db_create_and_drop(namespace, expected_database, environment_loaded: true) + Dir.chdir(app_path) do + output = rails("db:create") + assert_match(/Created database/, output) + assert_match_namespace(namespace, output) + assert File.exist?(expected_database) + + output = rails("db:drop") + assert_match(/Dropped database/, output) + assert_match_namespace(namespace, output) + assert_not File.exist?(expected_database) + end + end + + def db_create_and_drop_namespace(namespace, expected_database, environment_loaded: true) + Dir.chdir(app_path) do + output = rails("db:create:#{namespace}") + assert_match(/Created database/, output) + assert_match_namespace(namespace, output) + assert File.exist?(expected_database) + + output = rails("db:drop:#{namespace}") + assert_match(/Dropped database/, output) + assert_match_namespace(namespace, output) + assert_not File.exist?(expected_database) + end + end + + def assert_match_namespace(namespace, output) + if namespace == "primary" + assert_match(/#{Rails.env}.sqlite3/, output) + else + assert_match(/#{Rails.env}_#{namespace}.sqlite3/, output) + end + end + + def db_migrate_and_schema_dump_and_load(namespace, expected_database, format) + Dir.chdir(app_path) do + rails "generate", "model", "book", "title:string" + rails "generate", "model", "dog", "name:string" + write_models_for_animals + rails "db:migrate", "db:#{format}:dump" + + if format == "schema" + schema_dump = File.read("db/#{format}.rb") + schema_dump_animals = File.read("db/animals_#{format}.rb") + assert_match(/create_table \"books\"/, schema_dump) + assert_match(/create_table \"dogs\"/, schema_dump_animals) + else + schema_dump = File.read("db/#{format}.sql") + schema_dump_animals = File.read("db/animals_#{format}.sql") + assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"books\"/, schema_dump) + assert_match(/CREATE TABLE (?:IF NOT EXISTS )?\"dogs\"/, schema_dump_animals) + end + + rails "db:#{format}:load" + + ar_tables = lambda { rails("runner", "p ActiveRecord::Base.connection.tables").strip } + animals_tables = lambda { rails("runner", "p AnimalsBase.connection.tables").strip } + + assert_equal '["schema_migrations", "ar_internal_metadata", "books"]', ar_tables[] + assert_equal '["schema_migrations", "ar_internal_metadata", "dogs"]', animals_tables[] + end + end + + def db_migrate_namespaced(namespace, expected_database) + Dir.chdir(app_path) do + rails "generate", "model", "book", "title:string" + rails "generate", "model", "dog", "name:string" + write_models_for_animals + output = rails("db:migrate:#{namespace}") + if namespace == "primary" + assert_match(/CreateBooks: migrated/, output) + else + assert_match(/CreateDogs: migrated/, output) + end + end + end + + def write_models_for_animals + # make a directory for the animals migration + FileUtils.mkdir_p("#{app_path}/db/animals_migrate") + # move the dogs migration if it unless it already lives there + FileUtils.mv(Dir.glob("#{app_path}/db/migrate/**/*dogs.rb").first, "db/animals_migrate/") unless Dir.glob("#{app_path}/db/animals_migrate/**/*dogs.rb").first + # delete the dogs migration if it's still present in the + # migrate folder. This is necessary because sometimes + # the code isn't fast enough and an extra migration gets made + FileUtils.rm(Dir.glob("#{app_path}/db/migrate/**/*dogs.rb").first) if Dir.glob("#{app_path}/db/migrate/**/*dogs.rb").first + + # change the base of the dog model + app_path("/app/models/dog.rb") do |file_name| + file = File.read("#{app_path}/app/models/dog.rb") + file.sub!(/ApplicationRecord/, "AnimalsBase") + File.write(file_name, file) + end + + # create the base model for dog to inherit from + File.open("#{app_path}/app/models/animals_base.rb", "w") do |file| + file.write(<<-EOS +class AnimalsBase < ActiveRecord::Base + self.abstract_class = true + + establish_connection :animals +end +EOS +) + end + end + + test "db:create and db:drop works on all databases for env" do + require "#{app_path}/config/environment" + ActiveRecord::Base.configurations[Rails.env].each do |namespace, config| + db_create_and_drop namespace, config["database"] + end + end + + test "db:create:namespace and db:drop:namespace works on specified databases" do + require "#{app_path}/config/environment" + ActiveRecord::Base.configurations[Rails.env].each do |namespace, config| + db_create_and_drop_namespace namespace, config["database"] + end + end + + test "db:migrate and db:schema:dump and db:schema:load works on all databases" do + require "#{app_path}/config/environment" + ActiveRecord::Base.configurations[Rails.env].each do |namespace, config| + db_migrate_and_schema_dump_and_load namespace, config["database"], "schema" + end + end + + test "db:migrate and db:structure:dump and db:structure:load works on all databases" do + require "#{app_path}/config/environment" + ActiveRecord::Base.configurations[Rails.env].each do |namespace, config| + db_migrate_and_schema_dump_and_load namespace, config["database"], "structure" + end + end + + test "db:migrate:namespace works" do + require "#{app_path}/config/environment" + ActiveRecord::Base.configurations[Rails.env].each do |namespace, config| + db_migrate_namespaced namespace, config["database"] + end + end + end + end +end diff --git a/railties/test/application/rake/notes_test.rb b/railties/test/application/rake/notes_test.rb index 8e9fe9b6b4..d73e5cdfa3 100644 --- a/railties/test/application/rake/notes_test.rb +++ b/railties/test/application/rake/notes_test.rb @@ -101,7 +101,7 @@ module ApplicationTests task :notes_custom do tags = 'TODO|FIXME' opts = { dirs: %w(lib test), tag: true } - SourceAnnotationExtractor.enumerate(tags, opts) + Rails::SourceAnnotationExtractor.enumerate(tags, opts) end EOS diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb index 9683230d07..1522a2bbc5 100644 --- a/railties/test/application/rake_test.rb +++ b/railties/test/application/rake_test.rb @@ -41,7 +41,7 @@ module ApplicationTests rails "db:create", "db:migrate" output = rails("db:test:prepare", "test") - refute_match(/ActiveRecord::ProtectedEnvironmentError/, output) + assert_no_match(/ActiveRecord::ProtectedEnvironmentError/, output) end end @@ -229,7 +229,7 @@ module ApplicationTests def test_rake_clear_schema_cache rails "db:schema:cache:dump", "db:schema:cache:clear" - assert !File.exist?(File.join(app_path, "db", "schema_cache.yml")) + assert_not File.exist?(File.join(app_path, "db", "schema_cache.yml")) end def test_copy_templates diff --git a/railties/test/application/rendering_test.rb b/railties/test/application/rendering_test.rb index 3724886c54..ab1591f388 100644 --- a/railties/test/application/rendering_test.rb +++ b/railties/test/application/rendering_test.rb @@ -4,7 +4,7 @@ require "isolation/abstract_unit" require "rack/test" module ApplicationTests - class RoutingTest < ActiveSupport::TestCase + class RenderingTest < ActiveSupport::TestCase include ActiveSupport::Testing::Isolation include Rack::Test::Methods diff --git a/railties/test/command/spellchecker_test.rb b/railties/test/command/spellchecker_test.rb index aff50a3e73..e6a7a3957c 100644 --- a/railties/test/command/spellchecker_test.rb +++ b/railties/test/command/spellchecker_test.rb @@ -5,6 +5,6 @@ require "rails/command/spellchecker" class Rails::Command::SpellcheckerTest < ActiveSupport::TestCase test "suggests a word correction from dictionary" do - assert_equal %w(thin cgi puma), Rails::Command::Spellchecker.suggest("tin", from: %w(puma thin cgi)) + assert_equal "thin", Rails::Command::Spellchecker.suggest("tin", from: %w(puma thin cgi)) end end diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb index 6ad96b28c7..0aea21051a 100644 --- a/railties/test/commands/dbconsole_test.rb +++ b/railties/test/commands/dbconsole_test.rb @@ -123,31 +123,31 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase def test_mysql start(adapter: "mysql2", database: "db") - assert !aborted + assert_not aborted assert_equal [%w[mysql mysql5], "db"], dbconsole.find_cmd_and_exec_args end def test_mysql_full start(adapter: "mysql2", database: "db", host: "locahost", port: 1234, socket: "socket", username: "user", password: "qwerty", encoding: "UTF-8") - assert !aborted + assert_not aborted assert_equal [%w[mysql mysql5], "--host=locahost", "--port=1234", "--socket=socket", "--user=user", "--default-character-set=UTF-8", "-p", "db"], dbconsole.find_cmd_and_exec_args end def test_mysql_include_password start({ adapter: "mysql2", database: "db", username: "user", password: "qwerty" }, ["-p"]) - assert !aborted + assert_not aborted assert_equal [%w[mysql mysql5], "--user=user", "--password=qwerty", "db"], dbconsole.find_cmd_and_exec_args end def test_postgresql start(adapter: "postgresql", database: "db") - assert !aborted + assert_not aborted assert_equal ["psql", "db"], dbconsole.find_cmd_and_exec_args end def test_postgresql_full start(adapter: "postgresql", database: "db", username: "user", password: "q1w2e3", host: "host", port: 5432) - assert !aborted + assert_not aborted assert_equal ["psql", "db"], dbconsole.find_cmd_and_exec_args assert_equal "user", ENV["PGUSER"] assert_equal "host", ENV["PGHOST"] @@ -157,7 +157,7 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase def test_postgresql_include_password start({ adapter: "postgresql", database: "db", username: "user", password: "q1w2e3" }, ["-p"]) - assert !aborted + assert_not aborted assert_equal ["psql", "db"], dbconsole.find_cmd_and_exec_args assert_equal "user", ENV["PGUSER"] assert_equal "q1w2e3", ENV["PGPASSWORD"] @@ -165,13 +165,13 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase def test_sqlite3 start(adapter: "sqlite3", database: "db.sqlite3") - assert !aborted + assert_not aborted assert_equal ["sqlite3", Rails.root.join("db.sqlite3").to_s], dbconsole.find_cmd_and_exec_args end def test_sqlite3_mode start({ adapter: "sqlite3", database: "db.sqlite3" }, ["--mode", "html"]) - assert !aborted + assert_not aborted assert_equal ["sqlite3", "-html", Rails.root.join("db.sqlite3").to_s], dbconsole.find_cmd_and_exec_args end @@ -182,27 +182,27 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase def test_sqlite3_db_absolute_path start(adapter: "sqlite3", database: "/tmp/db.sqlite3") - assert !aborted + assert_not aborted assert_equal ["sqlite3", "/tmp/db.sqlite3"], dbconsole.find_cmd_and_exec_args end def test_sqlite3_db_without_defined_rails_root Rails.stub(:respond_to?, false) do start(adapter: "sqlite3", database: "config/db.sqlite3") - assert !aborted + assert_not aborted assert_equal ["sqlite3", Rails.root.join("../config/db.sqlite3").to_s], dbconsole.find_cmd_and_exec_args end end def test_oracle start(adapter: "oracle", database: "db", username: "user", password: "secret") - assert !aborted + assert_not aborted assert_equal ["sqlplus", "user@db"], dbconsole.find_cmd_and_exec_args end def test_oracle_include_password start({ adapter: "oracle", database: "db", username: "user", password: "secret" }, ["-p"]) - assert !aborted + assert_not aborted assert_equal ["sqlplus", "user/secret@db"], dbconsole.find_cmd_and_exec_args end diff --git a/railties/test/commands/routes_test.rb b/railties/test/commands/routes_test.rb index 409a6f3417..77ed2bda61 100644 --- a/railties/test/commands/routes_test.rb +++ b/railties/test/commands/routes_test.rb @@ -3,6 +3,7 @@ require "isolation/abstract_unit" require "rails/command" require "rails/commands/routes/routes_command" +require "io/console/size" class Rails::Command::RoutesTest < ActiveSupport::TestCase setup :build_app @@ -39,18 +40,18 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase output = run_routes_command(["-g", "show"]) assert_equal <<~MESSAGE, output - Prefix Verb URI Pattern Controller#Action - cart GET /cart(.:format) cart#show - rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show - rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show - rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show + Prefix Verb URI Pattern Controller#Action + cart GET /cart(.:format) cart#show + rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show + rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show + rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show MESSAGE output = run_routes_command(["-g", "POST"]) assert_equal <<~MESSAGE, output - Prefix Verb URI Pattern Controller#Action - POST /cart(.:format) cart#create - rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create + Prefix Verb URI Pattern Controller#Action + POST /cart(.:format) cart#create + rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create MESSAGE output = run_routes_command(["-g", "basketballs"]) @@ -60,16 +61,16 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase test "rails routes with controller search key" do app_file "config/routes.rb", <<-RUBY - Rails.application.routes.draw do - get '/cart', to: 'cart#show' - get '/basketball', to: 'basketball#index' - end + Rails.application.routes.draw do + get '/cart', to: 'cart#show' + get '/basketball', to: 'basketball#index' + end RUBY output = run_routes_command(["-c", "cart"]) assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output - output = run_routes_command(["routes", "-c", "Cart"]) + output = run_routes_command(["-c", "Cart"]) assert_equal "Prefix Verb URI Pattern Controller#Action\n cart GET /cart(.:format) cart#show\n", output output = run_routes_command(["-c", "CartController"]) @@ -78,12 +79,13 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase test "rails routes with namespaced controller search key" do app_file "config/routes.rb", <<-RUBY - Rails.application.routes.draw do - namespace :admin do - resource :post - end + Rails.application.routes.draw do + namespace :admin do + resource :post end + end RUBY + expected_output = [" Prefix Verb URI Pattern Controller#Action", " new_admin_post GET /admin/post/new(.:format) admin/posts#new", "edit_admin_post GET /admin/post/edit(.:format) admin/posts#edit", @@ -102,60 +104,68 @@ class Rails::Command::RoutesTest < ActiveSupport::TestCase test "rails routes displays message when no routes are defined" do app_file "config/routes.rb", <<-RUBY - Rails.application.routes.draw do - end + Rails.application.routes.draw do + end RUBY assert_equal <<~MESSAGE, run_routes_command - Prefix Verb URI Pattern Controller#Action - rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show - rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show - rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show - update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update - rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create + Prefix Verb URI Pattern Controller#Action + rails_service_blob GET /rails/active_storage/blobs/:signed_id/*filename(.:format) active_storage/blobs#show + rails_blob_representation GET /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) active_storage/representations#show + rails_disk_service GET /rails/active_storage/disk/:encoded_key/*filename(.:format) active_storage/disk#show + update_rails_disk_service PUT /rails/active_storage/disk/:encoded_token(.:format) active_storage/disk#update + rails_direct_uploads POST /rails/active_storage/direct_uploads(.:format) active_storage/direct_uploads#create MESSAGE end test "rails routes with expanded option" do - app_file "config/routes.rb", <<-RUBY + begin + previous_console_winsize = IO.console.winsize + IO.console.winsize = [0, 27] + + app_file "config/routes.rb", <<-RUBY Rails.application.routes.draw do get '/cart', to: 'cart#show' end - RUBY - - output = rails("routes", "--expanded") - assert_equal <<~MESSAGE, output - --[ Route 1 ]------------------------------------------------------------ - Prefix | cart - Verb | GET - URI | /cart(.:format) - Controller#Action | cart#show - --[ Route 2 ]------------------------------------------------------------ - Prefix | rails_service_blob - Verb | GET - URI | /rails/active_storage/blobs/:signed_id/*filename(.:format) - Controller#Action | active_storage/blobs#show - --[ Route 3 ]------------------------------------------------------------ - Prefix | rails_blob_representation - Verb | GET - URI | /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) - Controller#Action | active_storage/representations#show - --[ Route 4 ]------------------------------------------------------------ - Prefix | rails_disk_service - Verb | GET - URI | /rails/active_storage/disk/:encoded_key/*filename(.:format) - Controller#Action | active_storage/disk#show - --[ Route 5 ]------------------------------------------------------------ - Prefix | update_rails_disk_service - Verb | PUT - URI | /rails/active_storage/disk/:encoded_token(.:format) - Controller#Action | active_storage/disk#update - --[ Route 6 ]------------------------------------------------------------ - Prefix | rails_direct_uploads - Verb | POST - URI | /rails/active_storage/direct_uploads(.:format) - Controller#Action | active_storage/direct_uploads#create - MESSAGE + RUBY + + output = run_routes_command(["--expanded"]) + + assert_equal <<~MESSAGE, output + --[ Route 1 ]-------------- + Prefix | cart + Verb | GET + URI | /cart(.:format) + Controller#Action | cart#show + --[ Route 2 ]-------------- + Prefix | rails_service_blob + Verb | GET + URI | /rails/active_storage/blobs/:signed_id/*filename(.:format) + Controller#Action | active_storage/blobs#show + --[ Route 3 ]-------------- + Prefix | rails_blob_representation + Verb | GET + URI | /rails/active_storage/representations/:signed_blob_id/:variation_key/*filename(.:format) + Controller#Action | active_storage/representations#show + --[ Route 4 ]-------------- + Prefix | rails_disk_service + Verb | GET + URI | /rails/active_storage/disk/:encoded_key/*filename(.:format) + Controller#Action | active_storage/disk#show + --[ Route 5 ]-------------- + Prefix | update_rails_disk_service + Verb | PUT + URI | /rails/active_storage/disk/:encoded_token(.:format) + Controller#Action | active_storage/disk#update + --[ Route 6 ]-------------- + Prefix | rails_direct_uploads + Verb | POST + URI | /rails/active_storage/direct_uploads(.:format) + Controller#Action | active_storage/direct_uploads#create + MESSAGE + ensure + IO.console.winsize = previous_console_winsize + end end private diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb index 467afe7cea..e7a56b3e6d 100644 --- a/railties/test/commands/server_test.rb +++ b/railties/test/commands/server_test.rb @@ -29,7 +29,7 @@ class Rails::Command::ServerCommandTest < ActiveSupport::TestCase end def test_using_server_mistype - assert_match(/Could not find server "tin". Maybe you meant "thin" or "cgi"/, run_command("--using", "tin")) + assert_match(/Could not find server "tin". Maybe you meant "thin"?/, run_command("--using", "tin")) end def test_using_positional_argument_deprecation @@ -90,6 +90,15 @@ class Rails::Command::ServerCommandTest < ActiveSupport::TestCase def test_environment_with_host switch_env "HOST", "1.2.3.4" do + assert_deprecated do + options = parse_arguments + assert_equal "1.2.3.4", options[:Host] + end + end + end + + def test_environment_with_binding + switch_env "BINDING", "1.2.3.4" do options = parse_arguments assert_equal "1.2.3.4", options[:Host] end @@ -196,7 +205,7 @@ class Rails::Command::ServerCommandTest < ActiveSupport::TestCase assert_equal 3000, options[:Port] end - switch_env "HOST", "1.2.3.4" do + switch_env "BINDING", "1.2.3.4" do args = ["-b", "127.0.0.1"] options = parse_arguments(args) assert_equal "127.0.0.1", options[:Host] @@ -215,6 +224,11 @@ class Rails::Command::ServerCommandTest < ActiveSupport::TestCase server_options = parse_arguments(["--port=3001"]) assert_equal [:Port], server_options[:user_supplied_options] + + switch_env "BINDING", "1.2.3.4" do + server_options = parse_arguments + assert_equal [:Host], server_options[:user_supplied_options] + end end def test_default_options diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index 1d2e0fd354..c3809a912b 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -408,13 +408,13 @@ class AppGeneratorTest < Rails::Generators::TestCase end end - def test_config_another_database + def test_config_mysql_database run_generator([destination_root, "-d", "mysql"]) assert_file "config/database.yml", /mysql/ if defined?(JRUBY_VERSION) assert_gem "activerecord-jdbcmysql-adapter" else - assert_gem "mysql2", "'~> 0.4.4'" + assert_gem "mysql2", "'>= 0.4.4', '< 0.6.0'" end end @@ -941,6 +941,15 @@ class AppGeneratorTest < Rails::Generators::TestCase assert_directory("test/system") end + unless Gem.win_platform? + def test_master_key_is_only_readable_by_the_owner + run_generator + + stat = File.stat("config/master.key") + assert_equal "100600", sprintf("%o", stat.mode) + end + end + private def stub_rails_application(root) Rails.application.config.root = root diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb index 9f1f2a2289..a16a2d3f0a 100644 --- a/railties/test/generators_test.rb +++ b/railties/test/generators_test.rb @@ -33,7 +33,7 @@ class GeneratorsTest < Rails::Generators::TestCase def test_generator_suggestions name = :migrationz output = capture(:stdout) { Rails::Generators.invoke name } - assert_match "Maybe you meant 'migration'", output + assert_match 'Maybe you meant "migration"?', output end def test_generator_suggestions_except_en_locale @@ -43,7 +43,7 @@ class GeneratorsTest < Rails::Generators::TestCase I18n.default_locale = :ja name = :tas output = capture(:stdout) { Rails::Generators.invoke name } - assert_match "Maybe you meant 'task', 'job' or", output + assert_match 'Maybe you meant "task"?', output ensure I18n.available_locales = orig_available_locales I18n.default_locale = orig_default_locale @@ -52,7 +52,7 @@ class GeneratorsTest < Rails::Generators::TestCase def test_generator_multiple_suggestions name = :tas output = capture(:stdout) { Rails::Generators.invoke name } - assert_match "Maybe you meant 'task', 'job' or", output + assert_match 'Maybe you meant "task"?', output end def test_help_when_a_generator_with_required_arguments_is_invoked_without_arguments diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 0a4d2a9167..3cde7c03b0 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -112,22 +112,57 @@ module TestHelpers end end - File.open("#{app_path}/config/database.yml", "w") do |f| - f.puts <<-YAML - default: &default - adapter: sqlite3 - pool: 5 - timeout: 5000 - development: - <<: *default - database: db/development.sqlite3 - test: - <<: *default - database: db/test.sqlite3 - production: - <<: *default - database: db/production.sqlite3 - YAML + if options[:multi_db] + File.open("#{app_path}/config/database.yml", "w") do |f| + f.puts <<-YAML + default: &default + adapter: sqlite3 + pool: 5 + timeout: 5000 + development: + primary: + <<: *default + database: db/development.sqlite3 + animals: + <<: *default + database: db/development_animals.sqlite3 + migrations_paths: db/animals_migrate + test: + primary: + <<: *default + database: db/test.sqlite3 + animals: + <<: *default + database: db/test_animals.sqlite3 + migrations_paths: db/animals_migrate + production: + primary: + <<: *default + database: db/production.sqlite3 + animals: + <<: *default + database: db/production_animals.sqlite3 + migrations_paths: db/animals_migrate + YAML + end + else + File.open("#{app_path}/config/database.yml", "w") do |f| + f.puts <<-YAML + default: &default + adapter: sqlite3 + pool: 5 + timeout: 5000 + development: + <<: *default + database: db/development.sqlite3 + test: + <<: *default + database: db/test.sqlite3 + production: + <<: *default + database: db/production.sqlite3 + YAML + end end add_to_config <<-RUBY diff --git a/railties/test/rack_logger_test.rb b/railties/test/rack_logger_test.rb index e47f30d5b6..6e8f333e1d 100644 --- a/railties/test/rack_logger_test.rb +++ b/railties/test/rack_logger_test.rb @@ -14,14 +14,21 @@ module Rails attr_reader :logger - def initialize(logger = NULL, taggers = nil, &block) - super(->(_) { block.call; [200, {}, []] }, taggers) + def initialize(logger = NULL, app: nil, taggers: nil, &block) + app ||= ->(_) { block.call; [200, {}, []] } + super(app, taggers) @logger = logger end def development?; false; end end + class TestApp < Struct.new(:response) + def call(_env) + response + end + end + Subscriber = Struct.new(:starts, :finishes) do def initialize(starts = [], finishes = []) super @@ -72,6 +79,17 @@ module Rails end end end + + def test_logger_does_not_mutate_app_return + response = [].freeze + app = TestApp.new(response) + logger = TestLogger.new(app: app) + assert_no_changes("response") do + assert_nothing_raised do + logger.call("REQUEST_METHOD" => "GET") + end + end + end end end end diff --git a/railties/test/rails_info_test.rb b/railties/test/rails_info_test.rb index 43b60b9144..50522c1be6 100644 --- a/railties/test/rails_info_test.rb +++ b/railties/test/rails_info_test.rb @@ -9,7 +9,7 @@ class InfoTest < ActiveSupport::TestCase property("Bogus") { raise } end end - assert !property_defined?("Bogus") + assert_not property_defined?("Bogus") end def test_property_with_string diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index a59c63f343..9a3ddc8d5e 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -226,7 +226,7 @@ module RailtiesTest require "rdoc/task" require "rake/testtask" Rails.application.load_tasks - assert !Rake::Task.task_defined?("bukkits:install:migrations") + assert_not Rake::Task.task_defined?("bukkits:install:migrations") end test "puts its lib directory on load path" do @@ -745,7 +745,7 @@ YAML assert_equal "bukkits", Bukkits::Engine.engine_name assert_equal Bukkits.railtie_namespace, Bukkits::Engine assert ::Bukkits::MyMailer.method_defined?(:foo_url) - assert !::Bukkits::MyMailer.method_defined?(:bar_url) + assert_not ::Bukkits::MyMailer.method_defined?(:bar_url) get("/bukkits/from_app") assert_equal "false", last_response.body diff --git a/railties/test/railties/railtie_test.rb b/railties/test/railties/railtie_test.rb index 359ab0fdae..7c3d1e3759 100644 --- a/railties/test/railties/railtie_test.rb +++ b/railties/test/railties/railtie_test.rb @@ -65,7 +65,7 @@ module RailtiesTest test "railtie can add to_prepare callbacks" do $to_prepare = false class Foo < Rails::Railtie ; config.to_prepare { $to_prepare = true } ; end - assert !$to_prepare + assert_not $to_prepare require "#{app_path}/config/environment" require "rack/test" extend Rack::Test::Methods @@ -91,7 +91,7 @@ module RailtiesTest test "railtie can add after_initialize callbacks" do $after_initialize = false class Foo < Rails::Railtie ; config.after_initialize { $after_initialize = true } ; end - assert !$after_initialize + assert_not $after_initialize require "#{app_path}/config/environment" assert $after_initialize end @@ -107,7 +107,7 @@ module RailtiesTest require "#{app_path}/config/environment" - assert !$ran_block + assert_not $ran_block require "rake" require "rake/testtask" require "rdoc/task" @@ -151,7 +151,7 @@ module RailtiesTest require "#{app_path}/config/environment" - assert !$ran_block + assert_not $ran_block Rails.application.load_generators assert $ran_block end @@ -167,7 +167,7 @@ module RailtiesTest require "#{app_path}/config/environment" - assert !$ran_block + assert_not $ran_block Rails.application.load_console assert $ran_block end @@ -183,7 +183,7 @@ module RailtiesTest require "#{app_path}/config/environment" - assert !$ran_block + assert_not $ran_block Rails.application.load_runner assert $ran_block end @@ -197,7 +197,7 @@ module RailtiesTest end end - assert !$ran_block + assert_not $ran_block require "#{app_path}/config/environment" assert $ran_block end diff --git a/tasks/release.rb b/tasks/release.rb index 6ff06f3c4a..e326021dad 100644 --- a/tasks/release.rb +++ b/tasks/release.rb @@ -247,6 +247,12 @@ task :announce do require "erb" template = File.read("../tasks/release_announcement_draft.erb") - puts ERB.new(template, nil, "<>").result(binding) + + match = ERB.version.match(/\Aerb\.rb \[(?<version>[^ ]+) /) + if match && match[:version] >= "2.2.0" # Ruby 2.6+ + puts ERB.new(template, trim_mode: "<>").result(binding) + else + puts ERB.new(template, nil, "<>").result(binding) + end end end |