diff options
160 files changed, 2281 insertions, 847 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml new file mode 100644 index 0000000000..877c67873d --- /dev/null +++ b/.codeclimate.yml @@ -0,0 +1,27 @@ +engines: + rubocop: + enabled: true + +ratings: + paths: + - "**.rb" + +exclude_paths: + - actioncable/lib/rails/generators/ + - actioncable/test/ + - actionmailer/lib/rails/generators/ + - actionmailer/test/ + - actionpack/test/ + - actionview/test/ + - activejob/lib/rails/generators/ + - activejob/test/ + - activemodel/test/ + - activerecord/lib/rails/generators/ + - activerecord/test/ + - activesupport/test/ + - railties/lib/rails/generators/ + - railties/test/ + - ci/ + - guides/ + - tasks/ + - tools/ diff --git a/.rubocop.yml b/.rubocop.yml new file mode 100644 index 0000000000..dd8db6af3a --- /dev/null +++ b/.rubocop.yml @@ -0,0 +1,27 @@ +AllCops: + TargetRubyVersion: 2.3 + DisabledByDefault: true + +# Two spaces, no tabs (for indentation). +Style/IndentationWidth: + enabled: true + +# No trailing whitespace. +Style/TrailingWhitespace: + enabled: true + +# Blank lines should not have any spaces. +Style/TrailingBlankLines: + enabled: true + +# Use Ruby >= 1.9 syntax for hashes. Prefer { a: :b } over { :a => :b }. +Style/HashSyntax: + enabled: true + +# Prefer &&/|| over and/or. +Style/AndOr: + enabled: true + +# Use my_method(my_arg) not my_method( my_arg ) or my_method my_arg. +Lint/RequireParentheses: + enabled: true diff --git a/.travis.yml b/.travis.yml index 461bd172c1..8528a095b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -88,6 +88,3 @@ notifications: on_failure: always rooms: - secure: "YA1alef1ESHWGFNVwvmVGCkMe4cUy4j+UcNvMUESraceiAfVyRMAovlQBGs6\n9kBRm7DHYBUXYC2ABQoJbQRLDr/1B5JPf/M8+Qd7BKu8tcDC03U01SMHFLpO\naOs/HLXcDxtnnpL07tGVsm0zhMc5N8tq4/L3SHxK7Vi+TacwQzI=" - -git: - depth: 1 @@ -11,8 +11,9 @@ gem 'mocha', '~> 0.14', require: false gem 'rack-cache', '~> 1.2' gem 'jquery-rails' -gem 'coffee-rails', github: 'rails/coffee-rails' -gem 'turbolinks', github: 'turbolinks/turbolinks-rails' +gem 'coffee-rails' +gem 'sass-rails' +gem 'turbolinks', '~> 5' # 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) diff --git a/Gemfile.lock b/Gemfile.lock index 8b31934301..a62a71521e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -22,27 +22,12 @@ GIT delayed_job (>= 3.0, < 5) GIT - remote: git://github.com/rails/coffee-rails.git - revision: aa2e623cbda4f3c789a0a15d1f707239e68f5736 - specs: - coffee-rails (4.1.1) - coffee-script (>= 2.2.0) - railties (>= 4.0.0, < 5.2.x) - -GIT remote: git://github.com/sass/sass.git revision: 3fda1cbe70d615e7ef96e28db4fd1f8a3ebb5505 branch: stable specs: sass (3.4.22) -GIT - remote: git://github.com/turbolinks/turbolinks-rails.git - revision: 65884729016dbb4d032f12bb01b7e7c1ddeb68ac - specs: - turbolinks (5.0.0.beta2) - turbolinks-source - PATH remote: . specs: @@ -59,7 +44,7 @@ PATH actionpack (5.1.0.alpha) actionview (= 5.1.0.alpha) activesupport (= 5.1.0.alpha) - rack (~> 2.x) + rack (~> 2.0) rack-test (~> 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) @@ -140,6 +125,9 @@ GEM byebug (8.2.5) childprocess (0.5.9) ffi (~> 1.0, >= 1.0.11) + coffee-rails (4.2.1) + coffee-script (>= 2.2.0) + railties (>= 4.0.0, < 5.2.x) coffee-script (2.4.1) coffee-script-source execjs @@ -233,7 +221,7 @@ GEM pg (0.18.4-x64-mingw32) pg (0.18.4-x86-mingw32) pkg-config (1.1.7) - psych (2.0.17) + psych (2.1.0) puma (3.4.0) qu (0.2.0) multi_json @@ -243,8 +231,7 @@ GEM simple_uuid que (0.11.4) racc (1.4.14) - rack (2.0.0.rc1) - json + rack (2.0.1) rack-cache (1.6.1) rack (>= 0.4) rack-test (0.6.3) @@ -277,6 +264,12 @@ GEM rufus-scheduler (~> 3.2) rubyzip (1.2.0) rufus-scheduler (3.2.1) + sass-rails (5.0.5) + railties (>= 4.0.0, < 6) + sass (~> 3.1) + sprockets (>= 2.8, < 4.0) + sprockets-rails (>= 2.0, < 4.0) + tilt (>= 1.1, < 3) sdoc (0.4.1) json (~> 1.7, >= 1.7.7) rdoc (~> 4.0) @@ -300,11 +293,11 @@ GEM serverengine (~> 1.5.11) thor thread (~> 0.1.7) - sprockets (3.6.0) + sprockets (3.6.1) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-export (0.9.1) - sprockets-rails (3.0.4) + sprockets-rails (3.1.1) actionpack (>= 4.0) activesupport (>= 4.0) sprockets (>= 3.0.0) @@ -321,7 +314,10 @@ GEM thor (0.19.1) thread (0.1.7) thread_safe (0.3.5) - turbolinks-source (5.0.0.beta4) + tilt (2.0.5) + turbolinks (5.0.0) + turbolinks-source (~> 5) + turbolinks-source (5.0.0) tzinfo (1.2.2) thread_safe (~> 0.1) tzinfo-data (1.2016.4) @@ -355,7 +351,7 @@ DEPENDENCIES blade blade-sauce_labs_plugin byebug - coffee-rails! + coffee-rails dalli (>= 2.2.1) delayed_job! delayed_job_active_record! @@ -385,6 +381,7 @@ DEPENDENCIES resque (< 1.26) resque-scheduler sass! + sass-rails sdoc (~> 0.4.0) sequel sidekiq @@ -392,7 +389,7 @@ DEPENDENCIES sqlite3 (~> 1.3.6) stackprof sucker_punch - turbolinks! + turbolinks (~> 5) tzinfo-data uglifier (>= 1.3.0) w3c_validators diff --git a/actioncable/README.md b/actioncable/README.md index 58d23d2834..28e2602cbf 100644 --- a/actioncable/README.md +++ b/actioncable/README.md @@ -299,15 +299,232 @@ The rebroadcast will be received by all connected clients, _including_ the clien See the [rails/actioncable-examples](https://github.com/rails/actioncable-examples) repository for a full example of how to setup Action Cable in a Rails app, and how to add channels. -## Download and installation +## Configuration -The latest version of Action Cable can be installed with RubyGems: +Action Cable has three required configurations: a subscription adapter, allowed request origins, and the cable server URL (which can optionally be set on the client side). - $ gem install actioncable +### Redis -Source code can be downloaded as part of the Rails project on GitHub +By default, `ActionCable::Server::Base` will look for a configuration file in `Rails.root.join('config/cable.yml')`. +This file must specify an adapter and a URL for each Rails environment. It may use the following format: -* https://github.com/rails/rails/tree/master/actioncable +```yaml +production: &production + adapter: redis + url: redis://10.10.3.153:6381 +development: &development + adapter: redis + url: redis://localhost:6379 +test: *development +``` + +You can also change the location of the Action Cable config file in a Rails initializer with something like: + +```ruby +Rails.application.paths.add "config/cable", with: "somewhere/else/cable.yml" +``` + +### Allowed Request Origins + +Action Cable will only accept requests from specified origins, which are passed to the server config as an array. The origins can be instances of strings or regular expressions, against which a check for match will be performed. + +```ruby +Rails.application.config.action_cable.allowed_request_origins = ['http://rubyonrails.com', /http:\/\/ruby.*/] +``` + +When running in the development environment, this defaults to "http://localhost:3000". + +To disable and allow requests from any origin: + +```ruby +Rails.application.config.action_cable.disable_request_forgery_protection = true +``` + +### Consumer Configuration + +Once you have decided how to run your cable server (see below), you must provide the server URL (or path) to your client-side setup. +There are two ways you can do this. + +The first is to simply pass it in when creating your consumer. For a standalone server, +this would be something like: `App.cable = ActionCable.createConsumer("ws://example.com:28080")`, and for an in-app server, +something like: `App.cable = ActionCable.createConsumer("/cable")`. + +The second option is to pass the server URL through the `action_cable_meta_tag` in your layout. +This uses a URL or path typically set via `config.action_cable.url` in the environment configuration files, or defaults to "/cable". + +This method is especially useful if your WebSocket URL might change between environments. If you host your production server via https, you will need to use the wss scheme +for your Action Cable server, but development might remain http and use the ws scheme. You might use localhost in development and your +domain in production. + +In any case, to vary the WebSocket URL between environments, add the following configuration to each environment: + +```ruby +config.action_cable.url = "ws://example.com:28080" +``` + +Then add the following line to your layout before your JavaScript tag: + +```erb +<%= action_cable_meta_tag %> +``` + +And finally, create your consumer like so: + +```coffeescript +App.cable = ActionCable.createConsumer() +``` + +### Other Configurations + +The other common option to configure is the log tags applied to the per-connection logger. Here's an example that uses the user account id if available, else "no-account" while tagging: + +```ruby +config.action_cable.log_tags = [ + -> request { request.env['user_account_id'] || "no-account" }, + :action_cable, + -> request { request.uuid } +] +``` + +For a full list of all configuration options, see the `ActionCable::Server::Configuration` class. + +Also note that your server must provide at least the same number of database connections as you have workers. The default worker pool is set to 4, so that means you have to make at least that available. You can change that in `config/database.yml` through the `pool` attribute. + + +## Running the cable server + +### Standalone +The cable server(s) is separated from your normal application server. It's still a Rack application, but it is its own Rack +application. The recommended basic setup is as follows: + +```ruby +# cable/config.ru +require ::File.expand_path('../../config/environment', __FILE__) +Rails.application.eager_load! + +run ActionCable.server +``` + +Then you start the server using a binstub in bin/cable ala: +```sh +#!/bin/bash +bundle exec puma -p 28080 cable/config.ru +``` + +The above will start a cable server on port 28080. + +### In app + +If you are using a server that supports the [Rack socket hijacking API](http://www.rubydoc.info/github/rack/rack/file/SPEC#Hijacking), Action Cable can run alongside your Rails application. For example, to listen for WebSocket requests on `/websocket`, specify that path to `config.action_cable.mount_path`: + +```ruby +# config/application.rb +class Application < Rails::Application + config.action_cable.mount_path = '/websocket' +end +``` + +For every instance of your server you create and for every worker your server spawns, you will also have a new instance of Action Cable, but the use of Redis keeps messages synced across connections. + +### Notes + +Beware that currently, the cable server will _not_ auto-reload any changes in the framework. As we've discussed, long-running cable connections mean long-running objects. We don't yet have a way of reloading the classes of those objects in a safe manner. So when you change your channels, or the model your channels use, you must restart the cable server. + +We'll get all this abstracted properly when the framework is integrated into Rails. + +The WebSocket server doesn't have access to the session, but it has access to the cookies. This can be used when you need to handle authentication. You can see one way of doing that with Devise in this [article](http://www.rubytutorial.io/actioncable-devise-authentication). + +## Dependencies + +Action Cable provides a subscription adapter interface to process its pubsub internals. By default, asynchronous, inline, PostgreSQL, evented Redis, and non-evented Redis adapters are included. The default adapter in new Rails applications is the asynchronous (`async`) adapter. To create your own adapter, you can look at `ActionCable::SubscriptionAdapter::Base` for all methods that must be implemented, and any of the adapters included within Action Cable as example implementations. + +The Ruby side of things is built on top of [websocket-driver](https://github.com/faye/websocket-driver-ruby), [nio4r](https://github.com/celluloid/nio4r), and [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby). + + +## Deployment + +Action Cable is powered by a combination of WebSockets and threads. All of the +connection management is handled internally by utilizing Ruby’s native thread +support, which means you can use all your regular Rails models with no problems +as long as you haven’t committed any thread-safety sins. + +The Action Cable server does _not_ need to be a multi-threaded application server. +This is because Action Cable uses the [Rack socket hijacking API](http://www.rubydoc.info/github/rack/rack/file/SPEC#Hijacking) +to take over control of connections from the application server. Action Cable +then manages connections internally, in a multithreaded manner, regardless of +whether the application server is multi-threaded or not. So Action Cable works +with all the popular application servers -- Unicorn, Puma and Passenger. + +Action Cable does not work with WEBrick, because WEBrick does not support the +Rack socket hijacking API. + +## Frontend assets + +Action Cable's frontend assets are distributed through two channels: the +official gem and npm package, both titled `actioncable`. + +### Gem usage + +Through the `actioncable` gem, Action Cable's frontend assets are +available through the Rails Asset Pipeline. Create a `cable.js` or +`cable.coffee` file (this is automatically done for you with Rails +generators), and then simply require the assets: + +In JavaScript... + +```javascript +//= require action_cable +``` + +... and in CoffeeScript: + +```coffeescript +#= require action_cable +``` + +### npm usage + +In addition to being available through the `actioncable` gem, Action Cable's +frontend JS assets are also bundled in an officially supported npm module, +intended for usage in standalone frontend applications that communicate with a +Rails application. A common use case for this could be if you have a decoupled +frontend application written in React, Ember.js, etc. and want to add real-time +WebSocket functionality. + +### Installation + +``` +npm install actioncable --save +``` + +### Usage + +The `ActionCable` constant is available as a `require`-able module, so +you only have to require the package to gain access to the API that is +provided. + +In JavaScript... + +```javascript +ActionCable = require('actioncable') + +var cable = ActionCable.createConsumer('wss://RAILS-API-PATH.com/cable') + +cable.subscriptions.create('AppearanceChannel', { + // normal channel code goes here... +}); +``` + +and in CoffeeScript... + +```coffeescript +ActionCable = require('actioncable') + +cable = ActionCable.createConsumer('wss://RAILS-API-PATH.com/cable') + +cable.subscriptions.create 'AppearanceChannel', + # normal channel code goes here... +``` ## License diff --git a/actioncable/lib/action_cable/channel/periodic_timers.rb b/actioncable/lib/action_cable/channel/periodic_timers.rb index dab604440f..41511312fc 100644 --- a/actioncable/lib/action_cable/channel/periodic_timers.rb +++ b/actioncable/lib/action_cable/channel/periodic_timers.rb @@ -64,9 +64,7 @@ module ActionCable def start_periodic_timer(callback, every:) connection.server.event_loop.timer every do - connection.worker_pool.async_invoke connection do - instance_exec(&callback) - end + connection.worker_pool.async_exec self, connection: connection, &callback end end diff --git a/actioncable/lib/action_cable/connection/stream.rb b/actioncable/lib/action_cable/connection/stream.rb index 0cf59091bc..c250cf92fc 100644 --- a/actioncable/lib/action_cable/connection/stream.rb +++ b/actioncable/lib/action_cable/connection/stream.rb @@ -50,6 +50,7 @@ module ActionCable def clean_rack_hijack return unless @rack_hijack_io @event_loop.detach(@rack_hijack_io, self) + @rack_hijack_io.close @rack_hijack_io = nil end end diff --git a/actioncable/lib/action_cable/engine.rb b/actioncable/lib/action_cable/engine.rb index 96176e0014..34f9952c71 100644 --- a/actioncable/lib/action_cable/engine.rb +++ b/actioncable/lib/action_cable/engine.rb @@ -22,7 +22,7 @@ module ActionCable initializer "action_cable.set_configs" do |app| options = app.config.action_cable - options.allowed_request_origins ||= "http://localhost:3000" if ::Rails.env.development? + options.allowed_request_origins ||= /https?:\/\/localhost:\d+/ if ::Rails.env.development? app.paths.add "config/cable", with: "config/cable.yml" diff --git a/actioncable/lib/action_cable/server/worker.rb b/actioncable/lib/action_cable/server/worker.rb index a638ff72e7..f3a4fc5a5b 100644 --- a/actioncable/lib/action_cable/server/worker.rb +++ b/actioncable/lib/action_cable/server/worker.rb @@ -42,16 +42,20 @@ module ActionCable self.connection = nil end - def async_invoke(receiver, method, *args, connection: receiver) + def async_exec(receiver, *args, connection:, &block) + async_invoke receiver, :instance_exec, *args, connection: connection, &block + end + + def async_invoke(receiver, method, *args, connection: receiver, &block) @executor.post do - invoke(receiver, method, *args, connection: connection) + invoke(receiver, method, *args, connection: connection, &block) end end - def invoke(receiver, method, *args, connection:) + def invoke(receiver, method, *args, connection:, &block) work(connection) do begin - receiver.send method, *args + receiver.send method, *args, &block rescue Exception => e logger.error "There was an exception - #{e.class}(#{e.message})" logger.error e.backtrace.join("\n") diff --git a/actioncable/test/client_test.rb b/actioncable/test/client_test.rb index f4b4a53aea..82d9f12fdd 100644 --- a/actioncable/test/client_test.rb +++ b/actioncable/test/client_test.rb @@ -1,12 +1,11 @@ require 'test_helper' require 'concurrent' -require 'active_support/core_ext/hash/indifferent_access' -require 'pathname' - require 'faye/websocket' require 'json' +require 'active_support/hash_with_indifferent_access' + class ClientTest < ActionCable::TestCase WAIT_WHEN_EXPECTING_EVENT = 8 WAIT_WHEN_NOT_EXPECTING_EVENT = 0.5 @@ -39,7 +38,7 @@ class ClientTest < ActionCable::TestCase server = ActionCable.server server.config.logger = Logger.new(StringIO.new).tap { |l| l.level = Logger::UNKNOWN } - server.config.cable = { adapter: 'async' }.with_indifferent_access + server.config.cable = ActiveSupport::HashWithIndifferentAccess.new(adapter: 'async') server.config.use_faye = ENV['FAYE'].present? # and now the "real" setup for our test: diff --git a/actioncable/test/connection/client_socket_test.rb b/actioncable/test/connection/client_socket_test.rb index 4af071b4da..fe9077ae7f 100644 --- a/actioncable/test/connection/client_socket_test.rb +++ b/actioncable/test/connection/client_socket_test.rb @@ -48,6 +48,20 @@ class ActionCable::Connection::ClientSocketTest < ActionCable::TestCase end end + test 'closes hijacked i/o socket at shutdown' do + skip if ENV['FAYE'].present? + + run_in_eventmachine do + connection = open_connection + + client = connection.websocket.send(:websocket) + client.instance_variable_get('@stream') + .instance_variable_get('@rack_hijack_io') + .expects(:close) + connection.close + end + end + private def open_connection env = Rack::MockRequest.env_for '/test', diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec index 965fafff5f..f912a72efe 100644 --- a/actionpack/actionpack.gemspec +++ b/actionpack/actionpack.gemspec @@ -21,7 +21,7 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version - s.add_dependency 'rack', '~> 2.x' + s.add_dependency 'rack', '~> 2.0' s.add_dependency 'rack-test', '~> 0.6.3' s.add_dependency 'rails-html-sanitizer', '~> 1.0', '>= 1.0.2' s.add_dependency 'rails-dom-testing', '~> 2.0' diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb index d546d7260c..251289d4bb 100644 --- a/actionpack/lib/action_controller/base.rb +++ b/actionpack/lib/action_controller/base.rb @@ -32,7 +32,7 @@ module ActionController # new post), it initiates a redirect instead. This redirect works by returning an external # "302 Moved" HTTP response that takes the user to the index action. # - # These two methods represent the two basic action archetypes used in Action Controllers. Get-and-show and do-and-redirect. + # These two methods represent the two basic action archetypes used in Action Controllers: Get-and-show and do-and-redirect. # Most actions are variations on these themes. # # == Requests @@ -51,8 +51,8 @@ module ActionController # == Parameters # # All request parameters, whether they come from a query string in the URL or form data submitted through a POST request are - # available through the params method which returns a hash. For example, an action that was performed through - # <tt>/posts?category=All&limit=5</tt> will include <tt>{ "category" => "All", "limit" => "5" }</tt> in params. + # available through the <tt>params</tt> method which returns a hash. For example, an action that was performed through + # <tt>/posts?category=All&limit=5</tt> will include <tt>{ "category" => "All", "limit" => "5" }</tt> in <tt>params</tt>. # # It's also possible to construct multi-dimensional parameter hashes by specifying keys using brackets, such as: # @@ -60,7 +60,7 @@ module ActionController # <input type="text" name="post[address]" value="hyacintvej"> # # A request stemming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>. - # If the address input had been named <tt>post[address][street]</tt>, the params would have included + # If the address input had been named <tt>post[address][street]</tt>, the <tt>params</tt> would have included # <tt>{ "post" => { "address" => { "street" => "hyacintvej" } } }</tt>. There's no limit to the depth of the nesting. # # == Sessions diff --git a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb index 669cf55bca..75ac996793 100644 --- a/actionpack/lib/action_controller/metal/etag_with_template_digest.rb +++ b/actionpack/lib/action_controller/metal/etag_with_template_digest.rb @@ -39,8 +39,14 @@ module ActionController end end + # Pick the template digest to include in the ETag. If the +:template+ option + # is present, use the named template. If +:template+ is nil or absent, use + # the default controller/action template. If +:template+ is false, omit the + # template digest from the ETag. def pick_template_for_etag(options) - options.fetch(:template) { "#{controller_name}/#{action_name}" } + unless options[:template] == false + options[:template] || "#{controller_name}/#{action_name}" + end end def lookup_and_digest_template(template) diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb index 46589901fd..b326695ce2 100644 --- a/actionpack/lib/action_controller/metal/strong_parameters.rb +++ b/actionpack/lib/action_controller/metal/strong_parameters.rb @@ -58,8 +58,7 @@ module ActionController # }) # # permitted = params.require(:person).permit(:name, :age) - # permitted # => {"name"=>"Francesco", "age"=>22} - # permitted.class # => ActionController::Parameters + # permitted # => <ActionController::Parameters {"name"=>"Francesco", "age"=>22} permitted: true> # permitted.permitted? # => true # # Person.first.update!(permitted) @@ -87,7 +86,7 @@ module ActionController # # params = ActionController::Parameters.new(a: "123", b: "456") # params.permit(:c) - # # => {} + # # => <ActionController::Parameters {} permitted: true> # # ActionController::Parameters.action_on_unpermitted_parameters = :raise # @@ -107,6 +106,8 @@ module ActionController # params["key"] # => "value" class Parameters cattr_accessor :permit_all_parameters, instance_accessor: false + self.permit_all_parameters = false + cattr_accessor :action_on_unpermitted_parameters, instance_accessor: false delegate :keys, :key?, :has_key?, :values, :has_value?, :value?, :empty?, :include?, @@ -255,7 +256,7 @@ module ActionController # either present or the singleton +false+, returns said value: # # ActionController::Parameters.new(person: { name: 'Francesco' }).require(:person) - # # => {"name"=>"Francesco"} + # # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false> # # Otherwise raises <tt>ActionController::ParameterMissing</tt>: # @@ -276,12 +277,12 @@ module ActionController # returned: # # params = ActionController::Parameters.new(user: { ... }, profile: { ... }) - # user_params, profile_params = params.require(:user, :profile) + # user_params, profile_params = params.require([:user, :profile]) # # Otherwise, the method re-raises the first exception found: # # params = ActionController::Parameters.new(user: {}, profile: {}) - # user_params, profile_params = params.require(:user, :profile) + # user_params, profile_params = params.require([:user, :profile]) # # ActionController::ParameterMissing: param is missing or the value is empty: user # # Technically this method can be used to fetch terminal values: @@ -374,13 +375,13 @@ module ActionController # }) # # params.require(:person).permit(:contact) - # # => {} + # # => <ActionController::Parameters {} permitted: true> # # params.require(:person).permit(contact: :phone) - # # => {"contact"=>{"phone"=>"555-1234"}} + # # => <ActionController::Parameters {"contact"=><ActionController::Parameters {"phone"=>"555-1234"} permitted: true>} permitted: true> # # params.require(:person).permit(contact: [ :email, :phone ]) - # # => {"contact"=>{"email"=>"none@test.com", "phone"=>"555-1234"}} + # # => <ActionController::Parameters {"contact"=><ActionController::Parameters {"email"=>"none@test.com", "phone"=>"555-1234"} permitted: true>} permitted: true> def permit(*filters) params = self.class.new @@ -402,7 +403,7 @@ module ActionController # returns +nil+. # # params = ActionController::Parameters.new(person: { name: 'Francesco' }) - # params[:person] # => {"name"=>"Francesco"} + # params[:person] # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false> # params[:none] # => nil def [](key) convert_hashes_to_parameters(key, @parameters[key]) @@ -421,7 +422,7 @@ module ActionController # is given, then that will be run and its result returned. # # params = ActionController::Parameters.new(person: { name: 'Francesco' }) - # params.fetch(:person) # => {"name"=>"Francesco"} + # params.fetch(:person) # => <ActionController::Parameters {"name"=>"Francesco"} permitted: false> # params.fetch(:none) # => ActionController::ParameterMissing: param is missing or the value is empty: none # params.fetch(:none, 'Francesco') # => "Francesco" # params.fetch(:none) { 'Francesco' } # => "Francesco" @@ -457,8 +458,8 @@ module ActionController # don't exist, returns an empty hash. # # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) - # params.slice(:a, :b) # => {"a"=>1, "b"=>2} - # params.slice(:d) # => {} + # params.slice(:a, :b) # => <ActionController::Parameters {"a"=>1, "b"=>2} permitted: false> + # params.slice(:d) # => <ActionController::Parameters {} permitted: false> def slice(*keys) new_instance_with_inherited_permitted_status(@parameters.slice(*keys)) end @@ -474,8 +475,8 @@ module ActionController # filters out the given +keys+. # # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) - # params.except(:a, :b) # => {"c"=>3} - # params.except(:d) # => {"a"=>1,"b"=>2,"c"=>3} + # params.except(:a, :b) # => <ActionController::Parameters {"c"=>3} permitted: false> + # params.except(:d) # => <ActionController::Parameters {"a"=>1, "b"=>2, "c"=>3} permitted: false> def except(*keys) new_instance_with_inherited_permitted_status(@parameters.except(*keys)) end @@ -483,8 +484,8 @@ module ActionController # Removes and returns the key/value pairs matching the given keys. # # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) - # params.extract!(:a, :b) # => {"a"=>1, "b"=>2} - # params # => {"c"=>3} + # params.extract!(:a, :b) # => <ActionController::Parameters {"a"=>1, "b"=>2} permitted: false> + # params # => <ActionController::Parameters {"c"=>3} permitted: false> def extract!(*keys) new_instance_with_inherited_permitted_status(@parameters.extract!(*keys)) end @@ -494,7 +495,7 @@ module ActionController # # params = ActionController::Parameters.new(a: 1, b: 2, c: 3) # params.transform_values { |x| x * 2 } - # # => {"a"=>2, "b"=>4, "c"=>6} + # # => <ActionController::Parameters {"a"=>2, "b"=>4, "c"=>6} permitted: false> def transform_values(&block) if block new_instance_with_inherited_permitted_status( @@ -577,7 +578,7 @@ module ActionController # params = ActionController::Parameters.new(a: 1) # params.permit! # params.permitted? # => true - # copy_params = params.dup # => {"a"=>1} + # copy_params = params.dup # => <ActionController::Parameters {"a"=>1} permitted: true> # copy_params.permitted? # => true def dup super.tap do |duplicate| diff --git a/actionpack/lib/action_controller/renderer.rb b/actionpack/lib/action_controller/renderer.rb index 5ff4a658ad..a8c8d66682 100644 --- a/actionpack/lib/action_controller/renderer.rb +++ b/actionpack/lib/action_controller/renderer.rb @@ -1,7 +1,7 @@ require 'active_support/core_ext/hash/keys' module ActionController - # ActionController::Renderer allows to render arbitrary templates + # ActionController::Renderer allows you to render arbitrary templates # without requirement of being in controller actions. # # You get a concrete renderer class by invoking ActionController::Base#renderer. diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb index 5f758d641a..8974258cf7 100644 --- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb +++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb @@ -36,6 +36,14 @@ module ActionDispatch def debug_hash(object) object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n") end + + def render(*) + if logger = ActionView::Base.logger + logger.silence { super } + else + super + end + end end def initialize(app, routes_app = nil, response_format = :default) diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb index 40b6500553..e2cf75da3a 100644 --- a/actionpack/lib/action_dispatch/routing/mapper.rb +++ b/actionpack/lib/action_dispatch/routing/mapper.rb @@ -1062,6 +1062,10 @@ module ActionDispatch def merge_shallow_scope(parent, child) #:nodoc: child ? true : false end + + def merge_to_scope(parent, child) + child + end end # Resource routing allows you to quickly declare all of the common routes @@ -1582,6 +1586,10 @@ module ActionDispatch raise ArgumentError, "Unknown scope #{on.inspect} given to :on" end + if @scope[:to] + options[:to] ||= @scope[:to] + end + if @scope[:controller] && @scope[:action] options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}" end @@ -2021,7 +2029,7 @@ to this: class Scope # :nodoc: OPTIONS = [:path, :shallow_path, :as, :shallow_prefix, :module, :controller, :action, :path_names, :constraints, - :shallow, :blocks, :defaults, :via, :format, :options] + :shallow, :blocks, :defaults, :via, :format, :options, :to] RESOURCE_SCOPES = [:resource, :resources] RESOURCE_METHOD_SCOPES = [:collection, :member, :new] diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb index 9934f5547a..3156acf615 100644 --- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb +++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb @@ -4,7 +4,7 @@ module ActionDispatch # given an Active Record model instance. They are to be used in combination with # ActionController::Resources. # - # These methods are useful when you want to generate correct URL or path to a RESTful + # These methods are useful when you want to generate the correct URL or path to a RESTful # resource without having to know the exact type of the record in question. # # Nested resources and/or namespaces are also supported, as illustrated in the example: @@ -79,7 +79,7 @@ module ActionDispatch # polymorphic_url([blog, post], anchor: 'my_anchor', script_name: "/my_app") # # => "http://example.com/my_app/blogs/1/posts/1#my_anchor" # - # For all of these options, see the documentation for <tt>url_for</tt>. + # For all of these options, see the documentation for {url_for}[rdoc-ref:ActionDispatch::Routing::UrlFor]. # # ==== Functionality # diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb index 8777666f9f..a7805edaa2 100644 --- a/actionpack/lib/action_dispatch/testing/integration.rb +++ b/actionpack/lib/action_dispatch/testing/integration.rb @@ -327,17 +327,17 @@ module ActionDispatch request_encoder = RequestEncoder.encoder(as) if path =~ %r{://} - location = URI.parse(path) - https! URI::HTTPS === location if location.scheme - if url_host = location.host - default = Rack::Request::DEFAULT_PORTS[location.scheme] - url_host += ":#{location.port}" if default != location.port - host! url_host + path = build_expanded_path(path, request_encoder) do |location| + https! URI::HTTPS === location if location.scheme + + if url_host = location.host + default = Rack::Request::DEFAULT_PORTS[location.scheme] + url_host += ":#{location.port}" if default != location.port + host! url_host + end end - path = request_encoder.append_format_to location.path - path = location.query ? "#{path}?#{location.query}" : path - else - path = request_encoder.append_format_to path + elsif as + path = build_expanded_path(path, request_encoder) end hostname, port = host.split(':') @@ -396,6 +396,13 @@ module ActionDispatch "#{env['rack.url_scheme']}://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{path}" end + def build_expanded_path(path, request_encoder) + location = URI.parse(path) + yield location if block_given? + path = request_encoder.append_format_to location.path + location.query ? "#{path}?#{location.query}" : path + end + class RequestEncoder # :nodoc: @encoders = {} @@ -416,8 +423,11 @@ module ActionDispatch end def append_format_to(path) - path << @path_format unless @url_encoded_form - path + if @url_encoded_form + path + @path_format + else + path + end end def content_type diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb index 34fb3b1003..3b89531e90 100644 --- a/actionpack/test/controller/integration_test.rb +++ b/actionpack/test/controller/integration_test.rb @@ -1210,6 +1210,20 @@ class IntegrationRequestEncodersTest < ActionDispatch::IntegrationTest end end + def test_get_parameters_with_as_option + with_routing do |routes| + routes.draw do + ActiveSupport::Deprecation.silence do + get ':action' => FooController + end + end + + get '/foos_json?foo=heyo', as: :json + + assert_equal({ 'foo' => 'heyo' }, response.parsed_body) + end + end + private def post_to_foos(as:) with_routing do |routes| diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb index b75eb0e3bf..2eed2996f6 100644 --- a/actionpack/test/controller/parameters/parameters_permit_test.rb +++ b/actionpack/test/controller/parameters/parameters_permit_test.rb @@ -369,4 +369,10 @@ class ParametersPermitTest < ActiveSupport::TestCase refute params.permit(foo: [:bar]).has_key?(:foo) refute params.permit(foo: :bar).has_key?(:foo) end + + test '#permitted? is false by default' do + params = ActionController::Parameters.new + + assert_equal false, params.permitted? + end end diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb index 8d3134630d..652c06af19 100644 --- a/actionpack/test/controller/render_test.rb +++ b/actionpack/test/controller/render_test.rb @@ -2,6 +2,9 @@ require 'abstract_unit' require 'controller/fake_models' class TestControllerWithExtraEtags < ActionController::Base + def self.controller_name; 'test'; end + def self.controller_path; 'test'; end + etag { nil } etag { 'ab' } etag { :cde } @@ -17,7 +20,7 @@ class TestControllerWithExtraEtags < ActionController::Base end def strong - render plain: "stale" if stale?(strong_etag: 'strong') + render plain: "stale" if stale?(strong_etag: 'strong', template: false) end def with_template @@ -25,6 +28,10 @@ class TestControllerWithExtraEtags < ActionController::Base render plain: 'stale' end end + + def with_implicit_template + fresh_when(etag: '123') + end end class ImplicitRenderTestController < ActionController::Base @@ -528,20 +535,28 @@ class EtagRenderTest < ActionController::TestCase get :with_template assert_response :not_modified - # Modify the template digest - path = File.expand_path('../../fixtures/test/hello_world.erb', __FILE__) - old = File.read(path) + modify_template(:hello_world) do + request.if_none_match = etag + get :with_template + assert_response :ok + assert_not_equal etag, @response.etag + end + end - begin - File.write path, 'foo' - ActionView::LookupContext::DetailsKey.clear + def test_etag_reflects_implicit_template_digest + get :with_implicit_template + assert_response :ok + assert_not_nil etag = @response.etag + request.if_none_match = etag + get :with_implicit_template + assert_response :not_modified + + modify_template(:with_implicit_template) do request.if_none_match = etag - get :with_template + get :with_implicit_template assert_response :ok assert_not_equal etag, @response.etag - ensure - File.write path, old end end @@ -553,6 +568,16 @@ class EtagRenderTest < ActionController::TestCase def strong_etag(record) %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(record))}") end + + def modify_template(name) + path = File.expand_path("../../fixtures/test/#{name}.erb", __FILE__) + original = File.read(path) + File.write(path, "#{original} Modified!") + ActionView::LookupContext::DetailsKey.clear + yield + ensure + File.write(path, original) + end end class MetalRenderTest < ActionController::TestCase diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb index 5a39db145e..6f3d30ebf9 100644 --- a/actionpack/test/dispatch/debug_exceptions_test.rb +++ b/actionpack/test/dispatch/debug_exceptions_test.rb @@ -362,6 +362,29 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest assert_match(/puke/, output.rewind && output.read) end + test 'logs only what is necessary' do + @app = DevelopmentApp + io = StringIO.new + logger = ActiveSupport::Logger.new(io) + + _old, ActionView::Base.logger = ActionView::Base.logger, logger + begin + get "/", headers: { 'action_dispatch.show_exceptions' => true, 'action_dispatch.logger' => logger } + ensure + ActionView::Base.logger = _old + end + + output = io.rewind && io.read + lines = output.lines + + # Other than the first three... + assert_equal([" \n", "RuntimeError (puke!):\n", " \n"], lines.slice!(0, 3)) + lines.each do |line| + # .. all the remaining lines should be from the backtrace + assert_match(/:\d+:in /, line) + end + end + test 'uses backtrace cleaner from env' do @app = DevelopmentApp backtrace_cleaner = ActiveSupport::BacktraceCleaner.new diff --git a/actionpack/test/dispatch/mapper_test.rb b/actionpack/test/dispatch/mapper_test.rb index 69098326b9..f6dd9272a6 100644 --- a/actionpack/test/dispatch/mapper_test.rb +++ b/actionpack/test/dispatch/mapper_test.rb @@ -102,6 +102,18 @@ module ActionDispatch assert_equal("PUT", fakeset.routes.first.verb) end + def test_to_scope + fakeset = FakeSet.new + mapper = Mapper.new fakeset + mapper.scope(to: "posts#index") do + mapper.get :all + mapper.post :most + end + + assert_equal "posts#index", fakeset.routes.to_a[0].defaults[:to] + assert_equal "posts#index", fakeset.routes.to_a[1].defaults[:to] + end + def test_map_slash fakeset = FakeSet.new mapper = Mapper.new fakeset diff --git a/actionpack/test/dispatch/request/json_params_parsing_test.rb b/actionpack/test/dispatch/request/json_params_parsing_test.rb index a07138b55e..6ab71ebc81 100644 --- a/actionpack/test/dispatch/request/json_params_parsing_test.rb +++ b/actionpack/test/dispatch/request/json_params_parsing_test.rb @@ -155,7 +155,7 @@ class RootLessJSONParamsParsingTest < ActionDispatch::IntegrationTest test "parses json params after custom json mime type registered" do begin Mime::Type.unregister :json - Mime::Type.register "application/json", :json, %w(application/vnd.api+json) + Mime::Type.register "application/json", :json, %w(application/vnd.rails+json) assert_parses( {"user" => {"username" => "meinac"}, "username" => "meinac"}, "{\"username\": \"meinac\"}", { 'CONTENT_TYPE' => 'application/json' } @@ -169,10 +169,10 @@ class RootLessJSONParamsParsingTest < ActionDispatch::IntegrationTest test "parses json params after custom json mime type registered with synonym" do begin Mime::Type.unregister :json - Mime::Type.register "application/json", :json, %w(application/vnd.api+json) + Mime::Type.register "application/json", :json, %w(application/vnd.rails+json) assert_parses( {"user" => {"username" => "meinac"}, "username" => "meinac"}, - "{\"username\": \"meinac\"}", { 'CONTENT_TYPE' => 'application/vnd.api+json' } + "{\"username\": \"meinac\"}", { 'CONTENT_TYPE' => 'application/vnd.rails+json' } ) ensure Mime::Type.unregister :json diff --git a/actionpack/test/fixtures/test/with_implicit_template.erb b/actionpack/test/fixtures/test/with_implicit_template.erb new file mode 100644 index 0000000000..474488cd13 --- /dev/null +++ b/actionpack/test/fixtures/test/with_implicit_template.erb @@ -0,0 +1 @@ +Hello explicitly! diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md index 9d669c7cd8..ab4b46c56e 100644 --- a/actionview/CHANGELOG.md +++ b/actionview/CHANGELOG.md @@ -1,5 +1,49 @@ +* New syntax for tag helpers. Avoid positional parameters and support HTML5 by default. + Example usage of tag helpers before: + + ```ruby + tag(:br, nil, true) + content_tag(:div, content_tag(:p, "Hello world!"), class: "strong") + + <%= content_tag :div, class: "strong" do -%> + Hello world! + <% end -%> + ``` + + Example usage of tag helpers after: + + ```ruby + tag.br + tag.div tag.p("Hello world!"), class: "strong" + + <%= tag.div class: "strong" do %> + Hello world! + <% end %> + ``` + + *Marek Kirejczyk*, *Kasper Timm Hansen* + +* Change `datetime_field` and `datetime_field_tag` to generate `datetime-local` fields. + + As a new specification of the HTML 5 the text field type `datetime` will no longer exist + and it is recomended to use `datetime-local`. + Ref: https://html.spec.whatwg.org/multipage/forms.html#local-date-and-time-state-(type=datetime-local) + + *Herminio Torres* + +* Raw template handler (which is also the default template handler in Rails 5) now outputs + HTML-safe strings. + + In Rails 5 the default template handler was changed to the raw template handler. Because + the ERB template handler escaped strings by default this broke some applications that + expected plain JS or HTML files to be rendered unescaped. This fixes the issue caused + by changing the default handler by changing the Raw template handler to output HTML-safe + strings. + + *Eileen M. Uchitelle* + * `select_tag`'s `include_blank` option for generation for blank option tag, now adds an empty space label, - when the value as well as content for option tag are empty, so that we confirm with html specification. + when the value as well as content for option tag are empty, so that we conform with html specification. Ref: https://www.w3.org/TR/html5/forms.html#the-option-element. Generation of option before: diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb index ad1cb1a4be..0ede884c94 100644 --- a/actionview/lib/action_view/base.rb +++ b/actionview/lib/action_view/base.rb @@ -17,7 +17,7 @@ module ActionView #:nodoc: # # == ERB # - # You trigger ERB by using embeddings such as <% %>, <% -%>, and <%= %>. The <%= %> tag set is used when you want output. Consider the + # You trigger ERB by using embeddings such as <tt><% %></tt>, <tt><% -%></tt>, and <tt><%= %></tt>. The <tt><%= %></tt> tag set is used when you want output. Consider the # following loop for names: # # <b>Names of all the people</b> @@ -25,7 +25,7 @@ module ActionView #:nodoc: # Name: <%= person.name %><br/> # <% end %> # - # The loop is setup in regular embedding tags <% %> and the name is written using the output embedding tag <%= %>. Note that this + # The loop is setup in regular embedding tags <tt><% %></tt>, and the name is written using the output embedding tag <tt><%= %></tt>. Note that this # is not just a usage suggestion. Regular output functions like print or puts won't work with ERB templates. So this would be wrong: # # <%# WRONG %> @@ -33,9 +33,9 @@ module ActionView #:nodoc: # # If you absolutely must write from within a function use +concat+. # - # When on a line that only contains whitespaces except for the tag, <% %> suppress leading and trailing whitespace, - # including the trailing newline. <% %> and <%- -%> are the same. - # Note however that <%= %> and <%= -%> are different: only the latter removes trailing whitespaces. + # When on a line that only contains whitespaces except for the tag, <tt><% %></tt> suppresses leading and trailing whitespace, + # including the trailing newline. <tt><% %></tt> and <tt><%- -%></tt> are the same. + # Note however that <tt><%= %></tt> and <tt><%= -%></tt> are different: only the latter removes trailing whitespaces. # # === Using sub templates # @@ -110,7 +110,7 @@ module ActionView #:nodoc: # <p>A product of Danish Design during the Winter of '79...</p> # </div> # - # A full-length RSS example actually used on Basecamp: + # Here is a full-length RSS example actually used on Basecamp: # # xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do # xml.channel do diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb index b91e61da18..f3c29d663c 100644 --- a/actionview/lib/action_view/digestor.rb +++ b/actionview/lib/action_view/digestor.rb @@ -15,7 +15,7 @@ module ActionView # * <tt>partial</tt> - Specifies whether the template is a partial def digest(name:, finder:, dependencies: []) dependencies ||= [] - cache_key = ([ name ].compact + dependencies).join('.') + cache_key = [ name, finder.rendered_format, dependencies ].flatten.compact.join('.') # this is a correctly done double-checked locking idiom # (Concurrent::Map's lookups have volatile semantics) @@ -39,8 +39,12 @@ module ActionView def tree(name, finder, partial = false, seen = {}) logical_name = name.gsub(%r|/_|, "/") - if finder.disable_cache { finder.exists?(logical_name, [], partial) } - template = finder.disable_cache { finder.find(logical_name, [], partial) } + options = {} + options[:formats] = [finder.rendered_format] if finder.rendered_format + + if finder.disable_cache { finder.exists?(logical_name, [], partial, [], options) } + template = finder.disable_cache { finder.find(logical_name, [], partial, [], options) } + finder.rendered_format ||= template.formats.first if node = seen[template.identifier] # handle cycles in the tree node diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb index 413c35954c..bcbb3db6a9 100644 --- a/actionview/lib/action_view/helpers/asset_tag_helper.rb +++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb @@ -264,8 +264,8 @@ module ActionView # # => <video src="/videos/trailer"></video> # video_tag("trailer.ogg") # # => <video src="/videos/trailer.ogg"></video> - # video_tag("trailer.ogg", controls: true, autobuffer: true) - # # => <video autobuffer="autobuffer" controls="controls" src="/videos/trailer.ogg" ></video> + # video_tag("trailer.ogg", controls: true, preload: 'none') + # # => <video preload="none" controls="controls" src="/videos/trailer.ogg" ></video> # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png") # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png"></video> # video_tag("/trailers/hd.avi", size: "16x16") diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb index 9042b9cffd..a02702bf7a 100644 --- a/actionview/lib/action_view/helpers/date_helper.rb +++ b/actionview/lib/action_view/helpers/date_helper.rb @@ -3,6 +3,7 @@ require 'action_view/helpers/tag_helper' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/date/conversions' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/object/acts_like' require 'active_support/core_ext/object/with_options' module ActionView @@ -1058,7 +1059,7 @@ module ActionView prefix = @options[:prefix] || ActionView::Helpers::DateTimeSelector::DEFAULT_PREFIX prefix += "[#{@options[:index]}]" if @options.has_key?(:index) - field_name = @options[:field_name] || type + field_name = @options[:field_name] || type.to_s if @options[:include_position] field_name += "(#{ActionView::Helpers::DateTimeSelector::POSITION[type]}i)" end diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb index be5010cd9c..3d2ae0cfe0 100644 --- a/actionview/lib/action_view/helpers/form_helper.rb +++ b/actionview/lib/action_view/helpers/form_helper.rb @@ -860,24 +860,6 @@ module ActionView # # file_field(:attachment, :file, class: 'file_input') # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" /> - # - # ==== Gotcha - # - # The HTML specification says that when a file field is empty, web browsers - # do not send any value to the server. Unfortunately this introduces a - # gotcha: if a +User+ model has an +avatar+ field, and no file is selected, - # then the +avatar+ parameter is empty. Thus, any mass-assignment idiom like - # - # @user.update(params[:user]) - # - # wouldn't update the +avatar+ field. - # - # To prevent this, the helper generates an auxiliary hidden field before - # every file field. The hidden field has the same name as the file one and - # a blank value. - # - # In case you don't want the helper to generate this hidden field you can - # specify the <tt>include_hidden: false</tt> option. def file_field(object_name, method, options = {}) Tags::FileField.new(object_name, method, self, options).render end @@ -1092,42 +1074,9 @@ module ActionView Tags::TimeField.new(object_name, method, self, options).render end - # Returns a text_field of type "datetime". - # - # datetime_field("user", "born_on") - # # => <input id="user_born_on" name="user[born_on]" type="datetime" /> - # - # The default value is generated by trying to call +strftime+ with "%Y-%m-%dT%T.%L%z" - # on the object's value, which makes it behave as expected for instances - # of DateTime and ActiveSupport::TimeWithZone. - # - # @user.born_on = Date.new(1984, 1, 12) - # datetime_field("user", "born_on") - # # => <input id="user_born_on" name="user[born_on]" type="datetime" value="1984-01-12T00:00:00.000+0000" /> - # - # You can create values for the "min" and "max" attributes by passing - # instances of Date or Time to the options hash. - # - # datetime_field("user", "born_on", min: Date.today) - # # => <input id="user_born_on" name="user[born_on]" type="datetime" min="2014-05-20T00:00:00.000+0000" /> - # - # Alternatively, you can pass a String formatted as an ISO8601 datetime - # with UTC offset as the values for "min" and "max." - # - # datetime_field("user", "born_on", min: "2014-05-20T00:00:00+0000") - # # => <input id="user_born_on" name="user[born_on]" type="datetime" min="2014-05-20T00:00:00.000+0000" /> - # - def datetime_field(object_name, method, options = {}) - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - datetime_field is deprecated and will be removed in Rails 5.1. - Use datetime_local_field instead. - MESSAGE - Tags::DatetimeField.new(object_name, method, self, options).render - end - # Returns a text_field of type "datetime-local". # - # datetime_local_field("user", "born_on") + # datetime_field("user", "born_on") # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" /> # # The default value is generated by trying to call +strftime+ with "%Y-%m-%dT%T" @@ -1135,25 +1084,27 @@ module ActionView # of DateTime and ActiveSupport::TimeWithZone. # # @user.born_on = Date.new(1984, 1, 12) - # datetime_local_field("user", "born_on") + # datetime_field("user", "born_on") # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" value="1984-01-12T00:00:00" /> # # You can create values for the "min" and "max" attributes by passing # instances of Date or Time to the options hash. # - # datetime_local_field("user", "born_on", min: Date.today) + # datetime_field("user", "born_on", min: Date.today) # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" /> # # Alternatively, you can pass a String formatted as an ISO8601 datetime as # the values for "min" and "max." # - # datetime_local_field("user", "born_on", min: "2014-05-20T00:00:00") + # datetime_field("user", "born_on", min: "2014-05-20T00:00:00") # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" min="2014-05-20T00:00:00.000" /> # - def datetime_local_field(object_name, method, options = {}) + def datetime_field(object_name, method, options = {}) Tags::DatetimeLocalField.new(object_name, method, self, options).render end + alias datetime_local_field datetime_field + # Returns a text_field of type "month". # # month_field("user", "born_on") diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb index b277efd7b6..06b696f281 100644 --- a/actionview/lib/action_view/helpers/form_options_helper.rb +++ b/actionview/lib/action_view/helpers/form_options_helper.rb @@ -363,7 +363,7 @@ module ActionView html_attributes[:disabled] ||= disabled && option_value_selected?(value, disabled) html_attributes[:value] = value - content_tag_string(:option, text, html_attributes) + tag_builder.content_tag_string(:option, text, html_attributes) end.join("\n").html_safe end diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb index 82f2fd30c7..f1375570f2 100644 --- a/actionview/lib/action_view/helpers/form_tag_helper.rb +++ b/actionview/lib/action_view/helpers/form_tag_helper.rb @@ -685,21 +685,6 @@ module ActionView text_field_tag(name, value, options.merge(type: :time)) end - # Creates a text field of type "datetime". - # - # === Options - # * <tt>:min</tt> - The minimum acceptable value. - # * <tt>:max</tt> - The maximum acceptable value. - # * <tt>:step</tt> - The acceptable value granularity. - # * Otherwise accepts the same options as text_field_tag. - def datetime_field_tag(name, value = nil, options = {}) - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - datetime_field_tag is deprecated and will be removed in Rails 5.1. - Use datetime_local_field_tag instead. - MESSAGE - text_field_tag(name, value, options.merge(type: :datetime)) - end - # Creates a text field of type "datetime-local". # # === Options @@ -707,10 +692,12 @@ module ActionView # * <tt>:max</tt> - The maximum acceptable value. # * <tt>:step</tt> - The acceptable value granularity. # * Otherwise accepts the same options as text_field_tag. - def datetime_local_field_tag(name, value = nil, options = {}) + def datetime_field_tag(name, value = nil, options = {}) text_field_tag(name, value, options.merge(type: 'datetime-local')) end + alias datetime_local_field_tag datetime_field_tag + # Creates a text field of type "month". # # === Options diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb index 42e7358a1d..0633cfc2b4 100644 --- a/actionview/lib/action_view/helpers/tag_helper.rb +++ b/actionview/lib/action_view/helpers/tag_helper.rb @@ -1,32 +1,197 @@ +# frozen-string-literal: true + require 'active_support/core_ext/string/output_safety' require 'set' module ActionView # = Action View Tag Helpers module Helpers #:nodoc: - # Provides methods to generate HTML tags programmatically when you can't use - # a Builder. By default, they output XHTML compliant tags. + # Provides methods to generate HTML tags programmatically both as a modern + # HTML5 compliant builder style and legacy XHTML compliant tags. module TagHelper extend ActiveSupport::Concern include CaptureHelper include OutputSafetyHelper - BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple checked autobuffer - autoplay controls loop selected hidden scoped async - defer reversed ismap seamless muted required - autofocus novalidate formnovalidate open pubdate - itemscope allowfullscreen default inert sortable - truespeed typemustmatch).to_set + BOOLEAN_ATTRIBUTES = %w(allowfullscreen async autofocus autoplay checked + compact controls declare default defaultchecked + defaultmuted defaultselected defer disabled + enabled formnovalidate hidden indeterminate inert + ismap itemscope loop multiple muted nohref + noresize noshade novalidate nowrap open + pauseonexit readonly required reversed scoped + seamless selected sortable truespeed typemustmatch + visible).to_set BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map(&:to_sym)) TAG_PREFIXES = ['aria', 'data', :aria, :data].to_set - PRE_CONTENT_STRINGS = Hash.new { "".freeze } + PRE_CONTENT_STRINGS = Hash.new { "" } PRE_CONTENT_STRINGS[:textarea] = "\n" PRE_CONTENT_STRINGS["textarea"] = "\n" + class TagBuilder #:nodoc: + include CaptureHelper + include OutputSafetyHelper + + VOID_ELEMENTS = %i(area base br col embed hr img input keygen link meta param source track wbr).to_set + + def initialize(view_context) + @view_context = view_context + end + + def tag_string(name, content = nil, escape_attributes: true, **options, &block) + content = @view_context.capture(self, &block) if block_given? + if VOID_ELEMENTS.include?(name) && content.nil? + "<#{name.to_s.dasherize}#{tag_options(options, escape_attributes)}>".html_safe + else + content_tag_string(name.to_s.dasherize, content || '', options, escape_attributes) + end + end + + def content_tag_string(name, content, options, escape = true) + tag_options = tag_options(options, escape) if options + content = ERB::Util.unwrapped_html_escape(content) if escape + "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe + end + + def tag_options(options, escape = true) + return if options.blank? + output = "".dup + sep = " " + options.each_pair do |key, value| + if TAG_PREFIXES.include?(key) && value.is_a?(Hash) + value.each_pair do |k, v| + next if v.nil? + output << sep + output << prefix_tag_option(key, k, v, escape) + end + elsif BOOLEAN_ATTRIBUTES.include?(key) + if value + output << sep + output << boolean_tag_option(key) + end + elsif !value.nil? + output << sep + output << tag_option(key, value, escape) + end + end + output unless output.empty? + end + + def boolean_tag_option(key) + %(#{key}="#{key}") + end + + def tag_option(key, value, escape) + if value.is_a?(Array) + value = escape ? safe_join(value, " ") : value.join(" ") + else + value = escape ? ERB::Util.unwrapped_html_escape(value) : value + end + %(#{key}="#{value}") + end + + private + def prefix_tag_option(prefix, key, value, escape) + key = "#{prefix}-#{key.to_s.dasherize}" + unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal) + value = value.to_json + end + tag_option(key, value, escape) + end + + def respond_to_missing?(*args) + true + end + def method_missing(called, *args, &block) + tag_string(called, *args, &block) + end + + end + + # Returns an HTML tag. + # + # === Building HTML tags + # Builds HTML5 compliant tags with a tag proxy. Every tag can be built with: + # + # tag.<tag name>(optional content, options) + # + # where tag name can be e.g. br, div, section, article, or any tag really. + # + # ==== Passing content + # Tags can pass content to embed within it: + # + # tag.h1 'All titles fit to print' # => <h1>All titles fit to print</h1> + # + # tag.div tag.p('Hello world!') # => <div><p>Hello world!</p></div> + # + # Content can also be captured with a block, which is useful in templates: + # + # <%= tag.p do %> + # The next great American novel starts here. + # <% end %> + # # => <p>The next great American novel starts here.</p> + # + # ==== Options + # Any passed options become attributes on the generated tag. + # + # tag.section class: %w( kitties puppies ) + # # => <section class="kitties puppies"></section> + # + # tag.section id: dom_id(@post) + # # => <section id="<generated dom id>"></section> + # + # Pass +true+ for any attributes that can render with no values, like +disabled+ and +readonly+. + # + # tag.input type: 'text', disabled: true + # # => <input type="text" disabled="disabled"> + # + # HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key + # pointing to a hash of sub-attributes. + # + # To play nicely with JavaScript conventions, sub-attributes are dasherized. + # + # tag.article data: { user_id: 123 } + # # => <article data-user-id="123"></article> + # + # Thus <tt>data-user-id</tt> can be accessed as <tt>dataset.userId</tt>. + # + # Data attribute values are encoded to JSON, with the exception of strings, symbols and + # BigDecimals. + # This may come in handy when using jQuery's HTML5-aware <tt>.data()</tt> + # from 1.4.3. + # + # tag.div data: { city_state: %w( Chigaco IL ) } + # # => <div data-city-state="["Chicago","IL"]"></div> + # + # The generated attributes are escaped by default. This can be disabled using + # +escape_attributes+. + # + # tag.img src: 'open & shut.png' + # # => <img src="open & shut.png"> + # + # tag.img src: 'open & shut.png', escape_attributes: false + # # => <img src="open & shut.png"> + # + # The tag builder respects + # [HTML5 void elements](https://www.w3.org/TR/html5/syntax.html#void-elements) + # if no content is passed, and omits closing tags for those elements. + # + # # A standard element: + # tag.div # => <div></div> + # + # # A void element: + # tag.br # => <br> + # + # === Legacy syntax + # The following format is for legacy syntax support. It will be deprecated in future versions of Rails. + # + # tag(tag_name, options) + # + # === Building HTML tags # Returns an empty HTML tag of type +name+ which by default is XHTML # compliant. Set +open+ to true to create an open tag compatible # with HTML 4.0 and below. Add HTML attributes by passing an attributes @@ -42,15 +207,6 @@ module ActionView # HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key # pointing to a hash of sub-attributes. # - # To play nicely with JavaScript conventions sub-attributes are dasherized. - # For example, a key +user_id+ would render as <tt>data-user-id</tt> and - # thus accessed as <tt>dataset.userId</tt>. - # - # Values are encoded to JSON, with the exception of strings, symbols and - # BigDecimals. - # This may come in handy when using jQuery's HTML5-aware <tt>.data()</tt> - # from 1.4.3. - # # ==== Examples # tag("br") # # => <br /> @@ -72,8 +228,12 @@ module ActionView # # tag("div", data: {name: 'Stephen', city_state: %w(Chicago IL)}) # # => <div data-name="Stephen" data-city-state="["Chicago","IL"]" /> - def tag(name, options = nil, open = false, escape = true) - "<#{name}#{tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe + def tag(name = nil, options = nil, open = false, escape = true) + if name.nil? + tag_builder + else + "<#{name}#{tag_builder.tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe + end end # Returns an HTML block tag of type +name+ surrounding the +content+. Add @@ -81,6 +241,7 @@ module ActionView # Instead of passing the content as an argument, you can also use a block # in which case, you pass your +options+ as the second parameter. # Set escape to false to disable attribute value escaping. + # Note: this is legacy syntax, see +tag+ method description for details. # # ==== Options # The +options+ hash can be used with attributes with no value like (<tt>disabled</tt> and @@ -104,9 +265,9 @@ module ActionView def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block) if block_given? options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash) - content_tag_string(name, capture(&block), options, escape) + tag_builder.content_tag_string(name, capture(&block), options, escape) else - content_tag_string(name, content_or_options_with_block, options, escape) + tag_builder.content_tag_string(name, content_or_options_with_block, options, escape) end end @@ -140,56 +301,8 @@ module ActionView end private - - def content_tag_string(name, content, options, escape = true) - tag_options = tag_options(options, escape) if options - content = ERB::Util.unwrapped_html_escape(content) if escape - "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name]}#{content}</#{name}>".html_safe - end - - def tag_options(options, escape = true) - return if options.blank? - output = "" - sep = " ".freeze - options.each_pair do |key, value| - if TAG_PREFIXES.include?(key) && value.is_a?(Hash) - value.each_pair do |k, v| - next if v.nil? - output << sep - output << prefix_tag_option(key, k, v, escape) - end - elsif BOOLEAN_ATTRIBUTES.include?(key) - if value - output << sep - output << boolean_tag_option(key) - end - elsif !value.nil? - output << sep - output << tag_option(key, value, escape) - end - end - output unless output.empty? - end - - def prefix_tag_option(prefix, key, value, escape) - key = "#{prefix}-#{key.to_s.dasherize}" - unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal) - value = value.to_json - end - tag_option(key, value, escape) - end - - def boolean_tag_option(key) - %(#{key}="#{key}") - end - - def tag_option(key, value, escape) - if value.is_a?(Array) - value = escape ? safe_join(value, " ".freeze) : value.join(" ".freeze) - else - value = escape ? ERB::Util.unwrapped_html_escape(value) : value - end - %(#{key}="#{value}") + def tag_builder + @tag_builder ||= TagBuilder.new(self) end end end diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb index d57f26ba4f..086eaa4aab 100644 --- a/actionview/lib/action_view/helpers/tags/base.rb +++ b/actionview/lib/action_view/helpers/tags/base.rb @@ -143,10 +143,10 @@ module ActionView def add_options(option_tags, options, value = nil) if options[:include_blank] - option_tags = content_tag_string('option', options[:include_blank].kind_of?(String) ? options[:include_blank] : nil, :value => '') + "\n" + option_tags + option_tags = tag_builder.content_tag_string('option', options[:include_blank].kind_of?(String) ? options[:include_blank] : nil, :value => '') + "\n" + option_tags end if value.blank? && options[:prompt] - option_tags = content_tag_string('option', prompt_text(options[:prompt]), :value => '') + "\n" + option_tags + option_tags = tag_builder.content_tag_string('option', prompt_text(options[:prompt]), :value => '') + "\n" + option_tags end option_tags end diff --git a/actionview/lib/action_view/helpers/tags/datetime_field.rb b/actionview/lib/action_view/helpers/tags/datetime_field.rb index b2cee9d198..b3940c7e44 100644 --- a/actionview/lib/action_view/helpers/tags/datetime_field.rb +++ b/actionview/lib/action_view/helpers/tags/datetime_field.rb @@ -14,7 +14,7 @@ module ActionView private def format_date(value) - value.try(:strftime, "%Y-%m-%dT%T.%L%z") + raise NotImplementedError end def datetime_value(value) diff --git a/actionview/lib/action_view/helpers/tags/file_field.rb b/actionview/lib/action_view/helpers/tags/file_field.rb index e6a1d9c62d..476b820d84 100644 --- a/actionview/lib/action_view/helpers/tags/file_field.rb +++ b/actionview/lib/action_view/helpers/tags/file_field.rb @@ -2,21 +2,6 @@ module ActionView module Helpers module Tags # :nodoc: class FileField < TextField # :nodoc: - - def render - options = @options.stringify_keys - - if options.fetch("include_hidden", true) - add_default_name_and_id(options) - options[:type] = "file" - tag("input", name: options["name"], type: "hidden", value: "") + tag("input", options) - else - options.delete("include_hidden") - @options = options - - super - end - end end end end diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb index 58ce042f12..fe365fafe1 100644 --- a/actionview/lib/action_view/helpers/text_helper.rb +++ b/actionview/lib/action_view/helpers/text_helper.rb @@ -269,10 +269,11 @@ module ActionView end # Returns +text+ transformed into HTML using simple formatting rules. - # Two or more consecutive newlines(<tt>\n\n</tt>) are considered as a - # paragraph and wrapped in <tt><p></tt> tags. One newline (<tt>\n</tt>) is - # considered as a linebreak and a <tt><br /></tt> tag is appended. This - # method does not remove the newlines from the +text+. + # Two or more consecutive newlines(<tt>\n\n</tt> or <tt>\r\n\r\n</tt>) are + # considered a paragraph and wrapped in <tt><p></tt> tags. One newline + # (<tt>\n</tt> or <tt>\r\n</tt>) is considered a linebreak and a + # <tt><br /></tt> tag is appended. This method does not remove the + # newlines from the +text+. # # You can pass any HTML attributes into <tt>html_options</tt>. These # will be added to all created paragraphs. diff --git a/actionview/lib/action_view/template/handlers/raw.rb b/actionview/lib/action_view/template/handlers/raw.rb index 760f517431..e7519e94f9 100644 --- a/actionview/lib/action_view/template/handlers/raw.rb +++ b/actionview/lib/action_view/template/handlers/raw.rb @@ -2,7 +2,7 @@ module ActionView module Template::Handlers class Raw def call(template) - "#{template.source.inspect};" + "#{template.source.inspect}.html_safe;" end end end diff --git a/actionview/test/fixtures/digestor/api/comments/_comment.json.erb b/actionview/test/fixtures/digestor/api/comments/_comment.json.erb new file mode 100644 index 0000000000..696eb13917 --- /dev/null +++ b/actionview/test/fixtures/digestor/api/comments/_comment.json.erb @@ -0,0 +1 @@ +{"content": "Great story!"} diff --git a/actionview/test/fixtures/digestor/api/comments/_comments.json.erb b/actionview/test/fixtures/digestor/api/comments/_comments.json.erb new file mode 100644 index 0000000000..c28646a283 --- /dev/null +++ b/actionview/test/fixtures/digestor/api/comments/_comments.json.erb @@ -0,0 +1 @@ +<%= render partial: "comments/comment", collection: commentable.comments %> diff --git a/actionview/test/fixtures/digestor/messages/thread.json.erb b/actionview/test/fixtures/digestor/messages/thread.json.erb new file mode 100644 index 0000000000..e4c1ba97cd --- /dev/null +++ b/actionview/test/fixtures/digestor/messages/thread.json.erb @@ -0,0 +1 @@ +<%= render "comments/comments" %> diff --git a/actionview/test/fixtures/test/_builder_tag_nested_in_content_tag.erb b/actionview/test/fixtures/test/_builder_tag_nested_in_content_tag.erb new file mode 100644 index 0000000000..ddad7ec3ac --- /dev/null +++ b/actionview/test/fixtures/test/_builder_tag_nested_in_content_tag.erb @@ -0,0 +1,3 @@ +<%= tag.p do %> + <%= tag.b 'Hello' %> +<% end %> diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb index 8bfd19eb26..1a1b6f5e2d 100644 --- a/actionview/test/template/asset_tag_helper_test.rb +++ b/actionview/test/template/asset_tag_helper_test.rb @@ -238,7 +238,7 @@ class AssetTagHelperTest < ActionView::TestCase VideoLinkToTag = { %(video_tag("xml.ogg")) => %(<video src="/videos/xml.ogg"></video>), %(video_tag("rss.m4v", :autoplay => true, :controls => true)) => %(<video autoplay="autoplay" controls="controls" src="/videos/rss.m4v"></video>), - %(video_tag("rss.m4v", :autobuffer => true)) => %(<video autobuffer="autobuffer" src="/videos/rss.m4v"></video>), + %(video_tag("rss.m4v", :preload => 'none')) => %(<video preload="none" src="/videos/rss.m4v"></video>), %(video_tag("gold.m4v", :size => "160x120")) => %(<video height="120" src="/videos/gold.m4v" width="160"></video>), %(video_tag("gold.m4v", "size" => "320x240")) => %(<video height="240" src="/videos/gold.m4v" width="320"></video>), %(video_tag("trailer.ogg", :poster => "screenshot.png")) => %(<video poster="/images/screenshot.png" src="/videos/trailer.ogg"></video>), @@ -288,7 +288,7 @@ class AssetTagHelperTest < ActionView::TestCase %(audio_tag("//media.rubyonrails.org/audio/rails_blog_2.mov")) => %(<audio src="//media.rubyonrails.org/audio/rails_blog_2.mov"></audio>), %(audio_tag("audio.mp3", "audio.ogg")) => %(<audio><source src="/audios/audio.mp3" /><source src="/audios/audio.ogg" /></audio>), %(audio_tag(["audio.mp3", "audio.ogg"])) => %(<audio><source src="/audios/audio.mp3" /><source src="/audios/audio.ogg" /></audio>), - %(audio_tag(["audio.mp3", "audio.ogg"], :autobuffer => true, :controls => true)) => %(<audio autobuffer="autobuffer" controls="controls"><source src="/audios/audio.mp3" /><source src="/audios/audio.ogg" /></audio>) + %(audio_tag(["audio.mp3", "audio.ogg"], :preload => 'none', :controls => true)) => %(<audio preload="none" controls="controls"><source src="/audios/audio.mp3" /><source src="/audios/audio.ogg" /></audio>) } FontPathToTag = { diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb index e67d5d0e8c..3b4d4f42e5 100644 --- a/actionview/test/template/date_helper_test.rb +++ b/actionview/test/template/date_helper_test.rb @@ -534,6 +534,13 @@ class DateHelperTest < ActionView::TestCase assert_dom_equal expected, select_year(nil, start_year: 2003, end_year: 2005, with_css_classes: { year: 'my-year' }) end + + def test_select_year_with_position + expected = %(<select id="date_year_1i" name="date[year(1i)]">\n) + expected << %(<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n) + expected << "</select>\n" + assert_dom_equal expected, select_year(Date.current, include_position: true, start_year: 2003, end_year: 2005) + end def test_select_hour expected = %(<select id="date_hour" name="date[hour]">\n) @@ -3602,10 +3609,6 @@ class DateHelperTest < ActionView::TestCase assert_equal expected, time_tag(time) end - def test_time_tag_pubdate_option - assert_match(/<time.*pubdate="pubdate">.*<\/time>/, time_tag(Time.now, :pubdate => true)) - end - def test_time_tag_with_given_text assert_match(/<time.*>Right now<\/time>/, time_tag(Time.now, 'Right now')) end diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb index 4750d2a5a3..410f562f07 100644 --- a/actionview/test/template/digestor_test.rb +++ b/actionview/test/template/digestor_test.rb @@ -17,7 +17,14 @@ class FixtureFinder < ActionView::LookupContext FIXTURES_DIR = "#{File.dirname(__FILE__)}/../fixtures/digestor" def initialize(details = {}) - super(ActionView::PathSet.new(['digestor']), details, []) + super(ActionView::PathSet.new(['digestor', 'digestor/api']), details, []) + @rendered_format = :html + end +end + +class ActionView::Digestor::Node + def flatten + [self] + children.flat_map(&:flatten) end end @@ -147,6 +154,21 @@ class TemplateDigestorTest < ActionView::TestCase assert_equal nested_deps, nested_dependencies("messages/show") end + def test_nested_template_deps_with_non_default_rendered_format + finder.rendered_format = nil + nested_deps = [{"comments/comments"=>["comments/comment"]}] + assert_equal nested_deps, nested_dependencies("messages/thread") + end + + def test_template_formats_of_nested_deps_with_non_default_rendered_format + finder.rendered_format = nil + assert_equal [:json], tree_template_formats("messages/thread").uniq + end + + def test_template_formats_of_dependencies_with_same_logical_name_and_different_rendered_format + assert_equal [:html], tree_template_formats("messages/show").uniq + end + def test_recursion_in_renders assert digest("level/recursion") # assert recursion is possible assert_not_nil digest("level/recursion") # assert digest is stored @@ -258,6 +280,13 @@ class TemplateDigestorTest < ActionView::TestCase assert_not_equal digest_phone, digest_fridge_phone end + def test_different_formats_with_same_logical_template_names_results_in_different_digests + html_digest = digest("comments/_comment", format: :html) + json_digest = digest("comments/_comment", format: :json) + + assert_not_equal html_digest, json_digest + end + def test_digest_cache_cleanup_with_recursion first_digest = digest("level/_recursion") second_digest = digest("level/_recursion") @@ -280,7 +309,6 @@ class TemplateDigestorTest < ActionView::TestCase end end - private def assert_logged(message) old_logger = ActionView::Base.logger @@ -309,8 +337,11 @@ class TemplateDigestorTest < ActionView::TestCase def digest(template_name, options = {}) options = options.dup + finder_options = options.extract!(:variants, :format) + + finder.variants = finder_options[:variants] || [] + finder.rendered_format = finder_options[:format] if finder_options[:format] - finder.variants = options.delete(:variants) || [] ActionView::Digestor.digest(name: template_name, finder: finder, dependencies: (options[:dependencies] || [])) end @@ -324,6 +355,11 @@ class TemplateDigestorTest < ActionView::TestCase tree.children.map(&:to_dep_map) end + def tree_template_formats(template_name) + tree = ActionView::Digestor.tree(template_name, finder) + tree.flatten.map(&:template).compact.flat_map(&:formats) + end + def disable_resolver_caching old_caching, ActionView::Resolver.caching = ActionView::Resolver.caching, false yield diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb index 310d0ce514..54da2b0c9c 100644 --- a/actionview/test/template/form_helper_test.rb +++ b/actionview/test/template/form_helper_test.rb @@ -528,33 +528,18 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal expected, text_field(object_name, "title") end - def test_file_field_does_generate_a_hidden_field - expected = '<input name="user[avatar]" type="hidden" value="" /><input id="user_avatar" name="user[avatar]" type="file" />' - assert_dom_equal expected, file_field("user", "avatar") - end - - def test_file_field_does_not_generate_a_hidden_field_if_included_hidden_option_is_false - expected = '<input id="user_avatar" name="user[avatar]" type="file" />' - assert_dom_equal expected, file_field("user", "avatar", include_hidden: false) - end - - def test_file_field_does_not_generate_a_hidden_field_if_included_hidden_option_is_false_with_key_as_string - expected = '<input id="user_avatar" name="user[avatar]" type="file" />' - assert_dom_equal expected, file_field("user", "avatar", "include_hidden" => false) - end - def test_file_field_has_no_size - expected = '<input name="user[avatar]" type="hidden" value="" /><input id="user_avatar" name="user[avatar]" type="file" />' + expected = '<input id="user_avatar" name="user[avatar]" type="file" />' assert_dom_equal expected, file_field("user", "avatar") end def test_file_field_with_multiple_behavior - expected = '<input name="import[file][]" type="hidden" value="" /><input id="import_file" multiple="multiple" name="import[file][]" type="file" />' + expected = '<input id="import_file" multiple="multiple" name="import[file][]" type="file" />' assert_dom_equal expected, file_field("import", "file", :multiple => true) end def test_file_field_with_multiple_behavior_and_explicit_name - expected = '<input name="custom" type="hidden" value="" /><input id="import_file" multiple="multiple" name="custom" type="file" />' + expected = '<input id="import_file" multiple="multiple" name="custom" type="file" />' assert_dom_equal expected, file_field("import", "file", :multiple => true, :name => "custom") end @@ -1138,76 +1123,60 @@ class FormHelperTest < ActionView::TestCase end def test_datetime_field - expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T00:00:00.000+0000" />} - assert_deprecated do - assert_dom_equal(expected, datetime_field("post", "written_on")) - end + expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T00:00:00" />} + assert_dom_equal(expected, datetime_field("post", "written_on")) end def test_datetime_field_with_datetime_value - expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />} + expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />} @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) - assert_deprecated do - assert_dom_equal(expected, datetime_field("post", "written_on")) - end + assert_dom_equal(expected, datetime_field("post", "written_on")) end def test_datetime_field_with_extra_attrs - expected = %{<input id="post_written_on" step="60" max="2010-08-15T10:25:00.000+0000" min="2000-06-15T20:45:30.000+0000" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />} + expected = %{<input id="post_written_on" step="60" max="2010-08-15T10:25:00" min="2000-06-15T20:45:30" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />} @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) min_value = DateTime.new(2000, 6, 15, 20, 45, 30) max_value = DateTime.new(2010, 8, 15, 10, 25, 00) step = 60 - assert_deprecated do - assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value, step: step)) - end + assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value, step: step)) end def test_datetime_field_with_value_attr - expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2013-06-29T13:37:00+00:00" />} + expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2013-06-29T13:37:00+00:00" />} value = DateTime.new(2013,6,29,13,37) - assert_deprecated do - assert_dom_equal(expected, datetime_field("post", "written_on", value: value)) - end + assert_dom_equal(expected, datetime_field("post", "written_on", value: value)) end def test_datetime_field_with_timewithzone_value previous_time_zone, Time.zone = Time.zone, 'UTC' - expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T15:30:45.000+0000" />} + expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T15:30:45" />} @post.written_on = Time.zone.parse('2004-06-15 15:30:45') - assert_deprecated do - assert_dom_equal(expected, datetime_field("post", "written_on")) - end + assert_dom_equal(expected, datetime_field("post", "written_on")) ensure Time.zone = previous_time_zone end def test_datetime_field_with_nil_value - expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" />} + expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" />} @post.written_on = nil - assert_deprecated do - assert_dom_equal(expected, datetime_field("post", "written_on")) - end + assert_dom_equal(expected, datetime_field("post", "written_on")) end def test_datetime_field_with_string_values_for_min_and_max - expected = %{<input id="post_written_on" max="2010-08-15T10:25:00.000+0000" min="2000-06-15T20:45:30.000+0000" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />} + expected = %{<input id="post_written_on" max="2010-08-15T10:25:00" min="2000-06-15T20:45:30" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />} @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) - min_value = "2000-06-15T20:45:30.000+0000" - max_value = "2010-08-15T10:25:00.000+0000" - assert_deprecated do - assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value)) - end + min_value = "2000-06-15T20:45:30" + max_value = "2010-08-15T10:25:00" + assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value)) end def test_datetime_field_with_invalid_string_values_for_min_and_max - expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T01:02:03.000+0000" />} + expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />} @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) min_value = "foo" max_value = "bar" - assert_deprecated do - assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value)) - end + assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value)) end def test_datetime_local_field @@ -1215,52 +1184,6 @@ class FormHelperTest < ActionView::TestCase assert_dom_equal(expected, datetime_local_field("post", "written_on")) end - def test_datetime_local_field_with_datetime_value - expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />} - @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) - assert_dom_equal(expected, datetime_local_field("post", "written_on")) - end - - def test_datetime_local_field_with_extra_attrs - expected = %{<input id="post_written_on" step="60" max="2010-08-15T10:25:00" min="2000-06-15T20:45:30" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />} - @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) - min_value = DateTime.new(2000, 6, 15, 20, 45, 30) - max_value = DateTime.new(2010, 8, 15, 10, 25, 00) - step = 60 - assert_dom_equal(expected, datetime_local_field("post", "written_on", min: min_value, max: max_value, step: step)) - end - - def test_datetime_local_field_with_timewithzone_value - previous_time_zone, Time.zone = Time.zone, 'UTC' - expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T15:30:45" />} - @post.written_on = Time.zone.parse('2004-06-15 15:30:45') - assert_dom_equal(expected, datetime_local_field("post", "written_on")) - ensure - Time.zone = previous_time_zone - end - - def test_datetime_local_field_with_nil_value - expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" />} - @post.written_on = nil - assert_dom_equal(expected, datetime_local_field("post", "written_on")) - end - - def test_datetime_local_field_with_string_values_for_min_and_max - expected = %{<input id="post_written_on" max="2010-08-15T10:25:00" min="2000-06-15T20:45:30" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />} - @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) - min_value = "2000-06-15T20:45:30" - max_value = "2010-08-15T10:25:00" - assert_dom_equal(expected, datetime_local_field("post", "written_on", min: min_value, max: max_value)) - end - - def test_datetime_local_field_with_invalid_string_values_for_min_and_max - expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T01:02:03" />} - @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3) - min_value = "foo" - max_value = "bar" - assert_dom_equal(expected, datetime_local_field("post", "written_on", min: min_value, max: max_value)) - end - def test_month_field expected = %{<input id="post_written_on" name="post[written_on]" type="month" value="2004-06" />} assert_dom_equal(expected, month_field("post", "written_on")) @@ -1838,7 +1761,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch", multipart: true) do - "<input name='post[file]' type='hidden' value='' /><input name='post[file]' type='file' id='post_file' />" + "<input name='post[file]' type='file' id='post_file' />" end assert_dom_equal expected, output_buffer @@ -1854,7 +1777,7 @@ class FormHelperTest < ActionView::TestCase end expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch", multipart: true) do - "<input name='post[comment][file]' type='hidden' value='' /><input name='post[comment][file]' type='file' id='post_comment_file' />" + "<input name='post[comment][file]' type='file' id='post_comment_file' />" end assert_dom_equal expected, output_buffer diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb index 7a5904f151..a85f2da264 100644 --- a/actionview/test/template/form_options_helper_test.rb +++ b/actionview/test/template/form_options_helper_test.rb @@ -738,7 +738,7 @@ class FormOptionsHelperTest < ActionView::TestCase ) end - def test_empty + def test_select_with_empty @post = Post.new @post.category = "" assert_dom_equal( @@ -747,6 +747,15 @@ class FormOptionsHelperTest < ActionView::TestCase ) end + def test_select_with_html_options + @post = Post.new + @post.category = "" + assert_dom_equal( + "<select class=\"disabled\" disabled=\"disabled\" name=\"post[category]\" id=\"post_category\"><option value=\"\">Please select</option>\n<option value=\"\"></option>\n</select>", + select("post", "category", [], { prompt: true, include_blank: true }, { class: 'disabled', disabled: true }) + ) + end + def test_select_with_nil @post = Post.new @post.category = "othervalue" diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb index 5b0b708618..4fdca4976f 100644 --- a/actionview/test/template/form_tag_helper_test.rb +++ b/actionview/test/template/form_tag_helper_test.rb @@ -621,10 +621,8 @@ class FormTagHelperTest < ActionView::TestCase end def test_datetime_field_tag - expected = %{<input id="appointment" name="appointment" type="datetime" />} - assert_deprecated do - assert_dom_equal(expected, datetime_field_tag("appointment")) - end + expected = %{<input id="appointment" name="appointment" type="datetime-local" />} + assert_dom_equal(expected, datetime_field_tag("appointment")) end def test_datetime_local_field_tag diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb index ad93236d32..25b21850b1 100644 --- a/actionview/test/template/render_test.rb +++ b/actionview/test/template/render_test.rb @@ -100,6 +100,13 @@ module RenderTestCases assert_equal %q;Here are some characters: !@#$%^&*()-="'}{`; + "\n", @view.render(:template => "plain_text_with_characters") end + def test_render_raw_is_html_safe_and_does_not_escape_output + buffer = ActiveSupport::SafeBuffer.new + buffer << @view.render(file: "plain_text") + assert_equal true, buffer.html_safe? + assert_equal buffer, "<%= hello_world %>\n" + end + def test_render_ruby_template_with_handlers assert_equal "Hello from Ruby code", @view.render(:template => "ruby_template") end diff --git a/actionview/test/template/tag_helper_test.rb b/actionview/test/template/tag_helper_test.rb index f3956a31f6..4ed3252c63 100644 --- a/actionview/test/template/tag_helper_test.rb +++ b/actionview/test/template/tag_helper_test.rb @@ -11,6 +11,24 @@ class TagHelperTest < ActionView::TestCase assert_equal "<br>", tag("br", nil, true) end + def test_tag_builder + assert_equal "<span></span>", tag.span + assert_equal "<span class=\"bookmark\"></span>", tag.span(class: "bookmark") + end + + def test_tag_builder_void_tag + assert_equal "<br>", tag.br + assert_equal "<br class=\"some_class\">", tag.br(class: 'some_class') + end + + def test_tag_builder_void_tag_with_forced_content + assert_equal "<br>some content</br>", tag.br("some content") + end + + def test_tag_builder_is_singleton + assert_equal tag, tag + end + def test_tag_options str = tag("p", "class" => "show", :class => "elsewhere") assert_match(/class="show"/, str) @@ -21,19 +39,36 @@ class TagHelperTest < ActionView::TestCase assert_equal "<p />", tag("p", :ignored => nil) end + def test_tag_builder_options_rejects_nil_option + assert_equal "<p></p>", tag.p(ignored: nil) + end + def test_tag_options_accepts_false_option assert_equal "<p value=\"false\" />", tag("p", :value => false) end + def test_tag_builder_options_accepts_false_option + assert_equal "<p value=\"false\"></p>", tag.p(value: false) + end + def test_tag_options_accepts_blank_option assert_equal "<p included=\"\" />", tag("p", :included => '') end + def test_tag_builder_options_accepts_blank_option + assert_equal "<p included=\"\"></p>", tag.p(included: '') + end + def test_tag_options_converts_boolean_option assert_dom_equal '<p disabled="disabled" itemscope="itemscope" multiple="multiple" readonly="readonly" allowfullscreen="allowfullscreen" seamless="seamless" typemustmatch="typemustmatch" sortable="sortable" default="default" inert="inert" truespeed="truespeed" />', tag("p", :disabled => true, :itemscope => true, :multiple => true, :readonly => true, :allowfullscreen => true, :seamless => true, :typemustmatch => true, :sortable => true, :default => true, :inert => true, :truespeed => true) end + def test_tag_builder_options_converts_boolean_option + assert_dom_equal '<p disabled="disabled" itemscope="itemscope" multiple="multiple" readonly="readonly" allowfullscreen="allowfullscreen" seamless="seamless" typemustmatch="typemustmatch" sortable="sortable" default="default" inert="inert" truespeed="truespeed" />', + tag.p(disabled: true, itemscope: true, multiple: true, readonly: true, allowfullscreen: true, seamless: true, typemustmatch: true, sortable: true, default: true, inert: true, truespeed: true) + end + def test_content_tag assert_equal "<a href=\"create\">Create</a>", content_tag("a", "Create", "href" => "create") assert content_tag("a", "Create", "href" => "create").html_safe? @@ -45,43 +80,96 @@ class TagHelperTest < ActionView::TestCase content_tag(:p, '<script>evil_js</script>', nil, false) end + def test_tag_builder_with_content + assert_equal "<div id=\"post_1\">Content</div>", tag.div("Content", id: "post_1") + assert tag.div("Content", id: "post_1").html_safe? + assert_equal tag.div("Content", id: "post_1"), + tag.div("Content", "id": "post_1") + assert_equal "<p><script>evil_js</script></p>", + tag.p("<script>evil_js</script>") + assert_equal "<p><script>evil_js</script></p>", + tag.p('<script>evil_js</script>', escape_attributes: false) + end + + def test_tag_builder_nested + assert_equal "<div>content</div>", + tag.div { "content" } + assert_equal "<div id=\"header\"><span>hello</span></div>", + tag.div(id: 'header') { |tag| tag.span 'hello' } + assert_equal "<div id=\"header\"><div class=\"world\"><span>hello</span></div></div>", + tag.div(id: 'header') { |tag| tag.div(class: 'world') { tag.span 'hello' } } + end + def test_content_tag_with_block_in_erb buffer = render_erb("<%= content_tag(:div) do %>Hello world!<% end %>") assert_dom_equal "<div>Hello world!</div>", buffer end + def test_tag_builder_with_block_in_erb + buffer = render_erb("<%= tag.div do %>Hello world!<% end %>") + assert_dom_equal "<div>Hello world!</div>", buffer + end + def test_content_tag_with_block_in_erb_containing_non_displayed_erb buffer = render_erb("<%= content_tag(:p) do %><% 1 %><% end %>") assert_dom_equal "<p></p>", buffer end + def test_tag_builder_with_block_in_erb_containing_non_displayed_erb + buffer = render_erb("<%= tag.p do %><% 1 %><% end %>") + assert_dom_equal "<p></p>", buffer + end + def test_content_tag_with_block_and_options_in_erb buffer = render_erb("<%= content_tag(:div, :class => 'green') do %>Hello world!<% end %>") assert_dom_equal %(<div class="green">Hello world!</div>), buffer end + def test_tag_builder_with_block_and_options_in_erb + buffer = render_erb("<%= tag.div(class: 'green') do %>Hello world!<% end %>") + assert_dom_equal %(<div class="green">Hello world!</div>), buffer + end + def test_content_tag_with_block_and_options_out_of_erb assert_dom_equal %(<div class="green">Hello world!</div>), content_tag(:div, :class => "green") { "Hello world!" } end + def test_tag_builder_with_block_and_options_out_of_erb + assert_dom_equal %(<div class="green">Hello world!</div>), tag.div(class: "green") { "Hello world!" } + end + def test_content_tag_with_block_and_options_outside_out_of_erb assert_equal content_tag("a", "Create", :href => "create"), content_tag("a", "href" => "create") { "Create" } end + def test_tag_builder_with_block_and_options_outside_out_of_erb + assert_equal tag.a("Create", href: "create"), + tag.a("href": "create") { "Create" } + end + def test_content_tag_with_block_and_non_string_outside_out_of_erb assert_equal content_tag("p"), content_tag("p") { 3.times { "do_something" } } end + def test_tag_builder_with_block_and_non_string_outside_out_of_erb + assert_equal tag.p, + tag.p { 3.times { "do_something" } } + end + def test_content_tag_nested_in_content_tag_out_of_erb assert_equal content_tag("p", content_tag("b", "Hello")), content_tag("p") { content_tag("b", "Hello") }, output_buffer + assert_equal tag.p(tag.b("Hello")), + tag.p {tag.b("Hello") }, + output_buffer end def test_content_tag_nested_in_content_tag_in_erb assert_equal "<p>\n <b>Hello</b>\n</p>", view.render("test/content_tag_nested_in_content_tag") + assert_equal "<p>\n <b>Hello</b>\n</p>", view.render("test/builder_tag_nested_in_content_tag") end def test_content_tag_with_escaped_array_class @@ -95,6 +183,17 @@ class TagHelperTest < ActionView::TestCase assert_equal "<p class=\"song play\">limelight</p>", str end + def test_tag_builder_with_escaped_array_class + str = tag.p "limelight", class: ["song", "play>"] + assert_equal "<p class=\"song play>\">limelight</p>", str + + str = tag.p "limelight", class: ["song", "play"] + assert_equal "<p class=\"song play\">limelight</p>", str + + str = tag.p "limelight", class: ["song", ["play"]] + assert_equal "<p class=\"song play\">limelight</p>", str + end + def test_content_tag_with_unescaped_array_class str = content_tag('p', "limelight", {:class => ["song", "play>"]}, false) assert_equal "<p class=\"song play>\">limelight</p>", str @@ -103,21 +202,43 @@ class TagHelperTest < ActionView::TestCase assert_equal "<p class=\"song play>\">limelight</p>", str end + def test_tag_builder_with_unescaped_array_class + str = tag.p "limelight", class: ["song", "play>"], escape_attributes: false + assert_equal "<p class=\"song play>\">limelight</p>", str + + str = tag.p "limelight", class: ["song", ["play>"]], escape_attributes: false + assert_equal "<p class=\"song play>\">limelight</p>", str + end + def test_content_tag_with_empty_array_class str = content_tag('p', 'limelight', {:class => []}) assert_equal '<p class="">limelight</p>', str end + def test_tag_builder_with_empty_array_class + assert_equal '<p class="">limelight</p>', tag.p('limelight', class: []) + end + def test_content_tag_with_unescaped_empty_array_class str = content_tag('p', 'limelight', {:class => []}, false) assert_equal '<p class="">limelight</p>', str end + def test_tag_builder_with_unescaped_empty_array_class + str = tag.p 'limelight', class: [], escape_attributes: false + assert_equal '<p class="">limelight</p>', str + end + def test_content_tag_with_data_attributes assert_dom_equal '<p data-number="1" data-string="hello" data-string-with-quotes="double"quote"party"">limelight</p>', content_tag('p', "limelight", data: { number: 1, string: 'hello', string_with_quotes: 'double"quote"party"' }) end + def test_tag_builder_with_data_attributes + assert_dom_equal '<p data-number="1" data-string="hello" data-string-with-quotes="double"quote"party"">limelight</p>', + tag.p("limelight", data: { number: 1, string: 'hello', string_with_quotes: 'double"quote"party"' }) + end + def test_cdata_section assert_equal "<![CDATA[<hello world>]]>", cdata_section("<hello world>") end @@ -139,20 +260,24 @@ class TagHelperTest < ActionView::TestCase def test_tag_honors_html_safe_for_param_values ['1&2', '1 < 2', '“test“'].each do |escaped| assert_equal %(<a href="#{escaped}" />), tag('a', :href => escaped.html_safe) + assert_equal %(<a href="#{escaped}"></a>), tag.a(href: escaped.html_safe) end end def test_tag_honors_html_safe_with_escaped_array_class - str = tag('p', :class => ['song>', raw('play>')]) - assert_equal '<p class="song> play>" />', str + assert_equal '<p class="song> play>" />', tag('p', :class => ['song>', raw('play>')]) + assert_equal '<p class="song> play>" />', tag('p', :class => [raw('song>'), 'play>']) + end - str = tag('p', :class => [raw('song>'), 'play>']) - assert_equal '<p class="song> play>" />', str + def test_tag_builder_honors_html_safe_with_escaped_array_class + assert_equal '<p class="song> play>"></p>', tag.p(class: ['song>', raw('play>')]) + assert_equal '<p class="song> play>"></p>', tag.p(class: [raw('song>'), 'play>']) end def test_skip_invalid_escaped_attributes ['&1;', 'dfa3;', '& #123;'].each do |escaped| assert_equal %(<a href="#{escaped.gsub(/&/, '&')}" />), tag('a', :href => escaped) + assert_equal %(<a href="#{escaped.gsub(/&/, '&')}"></a>), tag.a(href: escaped) end end @@ -160,10 +285,20 @@ class TagHelperTest < ActionView::TestCase assert_equal '<a href="&" />', tag('a', { :href => '&' }, false, false) end + def test_tag_builder_disable_escaping + assert_equal '<a href="&"></a>', tag.a(href: '&', escape_attributes: false) + assert_equal '<a href="&">cnt</a>', tag.a(href: '&' , escape_attributes: false) { "cnt"} + assert_equal '<br data-hidden="&">', tag.br("data-hidden": '&' , escape_attributes: false) + assert_equal '<a href="&">content</a>', tag.a("content", href: '&', escape_attributes: false) + assert_equal '<a href="&">content</a>', tag.a(href: '&', escape_attributes: false) { "content"} + end + def test_data_attributes ['data', :data].each { |data| assert_dom_equal '<a data-a-float="3.14" data-a-big-decimal="-123.456" data-a-number="1" data-array="[1,2,3]" data-hash="{"key":"value"}" data-string-with-quotes="double"quote"party"" data-string="hello" data-symbol="foo" />', tag('a', { data => { a_float: 3.14, a_big_decimal: BigDecimal.new("-123.456"), a_number: 1, string: 'hello', symbol: :foo, array: [1, 2, 3], hash: { key: 'value'}, string_with_quotes: 'double"quote"party"' } }) + assert_dom_equal '<a data-a-float="3.14" data-a-big-decimal="-123.456" data-a-number="1" data-array="[1,2,3]" data-hash="{"key":"value"}" data-string-with-quotes="double"quote"party"" data-string="hello" data-symbol="foo" />', + tag.a(data: { a_float: 3.14, a_big_decimal: BigDecimal.new("-123.456"), a_number: 1, string: 'hello', symbol: :foo, array: [1, 2, 3], hash: { key: 'value'}, string_with_quotes: 'double"quote"party"' }) } end @@ -171,6 +306,8 @@ class TagHelperTest < ActionView::TestCase ['aria', :aria].each { |aria| assert_dom_equal '<a aria-a-float="3.14" aria-a-big-decimal="-123.456" aria-a-number="1" aria-array="[1,2,3]" aria-hash="{"key":"value"}" aria-string-with-quotes="double"quote"party"" aria-string="hello" aria-symbol="foo" />', tag('a', { aria => { a_float: 3.14, a_big_decimal: BigDecimal.new("-123.456"), a_number: 1, string: 'hello', symbol: :foo, array: [1, 2, 3], hash: { key: 'value'}, string_with_quotes: 'double"quote"party"' } }) + assert_dom_equal '<a aria-a-float="3.14" aria-a-big-decimal="-123.456" aria-a-number="1" aria-array="[1,2,3]" aria-hash="{"key":"value"}" aria-string-with-quotes="double"quote"party"" aria-string="hello" aria-symbol="foo" />', + tag.a(aria: { a_float: 3.14, a_big_decimal: BigDecimal.new("-123.456"), a_number: 1, string: 'hello', symbol: :foo, array: [1, 2, 3], hash: { key: 'value'}, string_with_quotes: 'double"quote"party"' }) } end @@ -179,4 +316,23 @@ class TagHelperTest < ActionView::TestCase div_type2 = content_tag(:div, 'test', { data: {tooltip: nil} }) assert_dom_equal div_type1, div_type2 end + + def test_tag_builder_link_to_data_nil_equal + div_type1 = tag.div 'test', { 'data-tooltip': nil } + div_type2 = tag.div 'test', { data: {tooltip: nil} } + assert_dom_equal div_type1, div_type2 + end + + def test_tag_builder_allow_call_via_method_object + assert_equal "<foo></foo>", tag.method(:foo).call + end + + def test_tag_builder_dasherize_names + assert_equal "<img-slider></img-slider>", tag.img_slider + end + + def test_respond_to + assert_respond_to tag, :any_tag + end + end diff --git a/activejob/lib/active_job/logging.rb b/activejob/lib/active_job/logging.rb index 605057d1e8..d5c7920131 100644 --- a/activejob/lib/active_job/logging.rb +++ b/activejob/lib/active_job/logging.rb @@ -41,7 +41,7 @@ module ActiveJob def tag_logger(*tags) if logger.respond_to?(:tagged) tags.unshift "ActiveJob" unless logger_tagged_by_active_job? - ActiveJob::Base.logger.tagged(*tags){ yield } + logger.tagged(*tags){ yield } else yield end diff --git a/activejob/lib/active_job/test_helper.rb b/activejob/lib/active_job/test_helper.rb index 3feb82d432..e16af1947f 100644 --- a/activejob/lib/active_job/test_helper.rb +++ b/activejob/lib/active_job/test_helper.rb @@ -9,7 +9,7 @@ module ActiveJob to: :queue_adapter def before_setup # :nodoc: - test_adapter = ActiveJob::QueueAdapters::TestAdapter.new + test_adapter = queue_adapter_for_test @old_queue_adapters = (ActiveJob::Base.subclasses << ActiveJob::Base).select do |klass| # only override explicitly set adapters, a quirk of `class_attribute` @@ -32,6 +32,19 @@ module ActiveJob end end + # Specifies the queue adapter to use with all active job test helpers. + # + # Returns an instance of the queue adapter and defaults to + # <tt>ActiveJob::QueueAdapters::TestAdapter</tt>. + # + # Note: The adapter provided by this method must provide some additional + # methods from those expected of a standard <tt>ActiveJob::QueueAdapter</tt> + # in order to be used with the active job test helpers. Refer to + # <tt>ActiveJob::QueueAdapters::TestAdapter</tt>. + def queue_adapter_for_test + ActiveJob::QueueAdapters::TestAdapter.new + end + # Asserts that the number of enqueued jobs matches the given number. # # def test_jobs diff --git a/activejob/test/cases/logging_test.rb b/activejob/test/cases/logging_test.rb index 820e9112de..bf8a66432a 100644 --- a/activejob/test/cases/logging_test.rb +++ b/activejob/test/cases/logging_test.rb @@ -3,6 +3,7 @@ require "active_support/log_subscriber/test_helper" require 'active_support/core_ext/numeric/time' require 'jobs/hello_job' require 'jobs/logging_job' +require 'jobs/overridden_logging_job' require 'jobs/nested_job' require 'models/person' @@ -41,7 +42,6 @@ class LoggingTest < ActiveSupport::TestCase ActiveJob::Base.logger = logger end - def test_uses_active_job_as_tag HelloJob.perform_later "Cristian" assert_match(/\[ActiveJob\]/, @logger.messages) @@ -119,4 +119,9 @@ class LoggingTest < ActiveSupport::TestCase rescue NotImplementedError skip end + + def test_for_tagged_logger_support_is_consistent + set_logger ::Logger.new(nil) + OverriddenLoggingJob.perform_later "Dummy" + end end diff --git a/activejob/test/cases/test_helper_test.rb b/activejob/test/cases/test_helper_test.rb index f7ee763e8a..3d863f5e65 100644 --- a/activejob/test/cases/test_helper_test.rb +++ b/activejob/test/cases/test_helper_test.rb @@ -509,3 +509,15 @@ class PerformedJobsTest < ActiveJob::TestCase assert_equal 2, ActiveJob::Base.queue_adapter.performed_jobs.count end end + +class OverrideQueueAdapterTest < ActiveJob::TestCase + class CustomQueueAdapter < ActiveJob::QueueAdapters::TestAdapter; end + + def queue_adapter_for_test + CustomQueueAdapter.new + end + + def test_assert_job_has_custom_queue_adapter_set + assert_instance_of CustomQueueAdapter, HelloJob.queue_adapter + end +end diff --git a/activejob/test/jobs/overridden_logging_job.rb b/activejob/test/jobs/overridden_logging_job.rb new file mode 100644 index 0000000000..2b17a65142 --- /dev/null +++ b/activejob/test/jobs/overridden_logging_job.rb @@ -0,0 +1,9 @@ +class OverriddenLoggingJob < ActiveJob::Base + def perform(dummy) + logger.info "Dummy, here is it: #{dummy}" + end + + def logger + @logger ||= ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(nil)) + end +end diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb index 6f2c8c1c53..2f883917b0 100644 --- a/activemodel/lib/active_model/errors.rb +++ b/activemodel/lib/active_model/errors.rb @@ -147,9 +147,9 @@ module ActiveModel # Delete messages for +key+. Returns the deleted messages. # - # person.errors[:name] # => ["cannot be nil"] + # person.errors[:name] # => ["cannot be nil"] # person.errors.delete(:name) # => ["cannot be nil"] - # person.errors[:name] # => [] + # person.errors[:name] # => [] def delete(key) details.delete(key) messages.delete(key) @@ -382,10 +382,21 @@ module ActiveModel end # Returns +true+ if an error on the attribute with the given message is - # present, +false+ otherwise. +message+ is treated the same as for +add+. + # present, or +false+ otherwise. +message+ is treated the same as for +add+. # # person.errors.add :name, :blank - # person.errors.added? :name, :blank # => true + # person.errors.added? :name, :blank # => true + # person.errors.added? :name, "can't be blank" # => true + # + # If the error message requires an option, then it returns +true+ with + # the correct option, or +false+ with an incorrect or missing option. + # + # person.errors.add :name, :too_long, { count: 25 } + # person.errors.added? :name, :too_long, count: 25 # => true + # person.errors.added? :name, "is too long (maximum is 25 characters)" # => true + # person.errors.added? :name, :too_long, count: 24 # => false + # person.errors.added? :name, :too_long # => false + # person.errors.added? :name, "is too long" # => false def added?(attribute, message = :invalid, options = {}) message = message.call if message.respond_to?(:call) message = normalize_message(attribute, message, options) diff --git a/activemodel/lib/active_model/type/date.rb b/activemodel/lib/active_model/type/date.rb index f74243a22c..edd07ac38c 100644 --- a/activemodel/lib/active_model/type/date.rb +++ b/activemodel/lib/active_model/type/date.rb @@ -7,6 +7,10 @@ module ActiveModel :date end + def serialize(value) + cast(value) + end + def type_cast_for_schema(value) "'#{value.to_s(:db)}'" end diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb index 8159b9b1d3..a10d2285a2 100644 --- a/activemodel/lib/active_model/validations.rb +++ b/activemodel/lib/active_model/validations.rb @@ -304,8 +304,6 @@ module ActiveModel # Runs all the specified validations and returns +true+ if no errors were # added otherwise +false+. # - # Aliased as validate. - # # class Person # include ActiveModel::Validations # diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb index c90ee7021c..13fa5c76bc 100644 --- a/activemodel/test/cases/errors_test.rb +++ b/activemodel/test/cases/errors_test.rb @@ -219,6 +219,12 @@ class ErrorsTest < ActiveModel::TestCase assert !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) + end + test "size calculates the number of error messages" do person = Person.new person.errors.add(:name, "cannot be blank") diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index a1a4d2646f..8521d3b07b 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,14 @@ +* Inspecting an object with an associated array of over 10 elements no longer + truncates the array, preventing `inspect` from looping infinitely in some + cases. + + *Kevin McPhillips* + +* Removed the unused methods `ActiveRecord::Base.connection_id` and + `ActiveRecord::Base.connection_id=` + + *Sean Griffin* + * Ensure hashes can be assigned to attributes created using `composed_of`. Fixes #25210. @@ -18,4 +29,18 @@ *Erol Fornoles* +* PostgreSQL: Fix db:structure:load silent failure on SQL error + + The command line flag "-v ON_ERROR_STOP=1" should be used + when invoking psql to make sure errors are not suppressed. + + Example: + + psql -v ON_ERROR_STOP=1 -q -f awesome-file.sql my-app-db + + Fixes #23818. + + *Ralin Chimev* + + Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/activerecord/CHANGELOG.md) for previous changes. diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 1fb5eb28cd..78bfcf34a9 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -279,9 +279,8 @@ module ActiveRecord # Returns an <tt>#inspect</tt>-like string for the value of the # attribute +attr_name+. String attributes are truncated up to 50 # characters, Date and Time attributes are returned in the - # <tt>:db</tt> format, Array attributes are truncated up to 10 values. - # Other attributes return the value of <tt>#inspect</tt> without - # modification. + # <tt>:db</tt> format. Other attributes return the value of + # <tt>#inspect</tt> without modification. # # person = Person.create!(name: 'David Heinemeier Hansson ' * 3) # @@ -292,7 +291,7 @@ module ActiveRecord # # => "\"2012-10-22 00:15:07\"" # # person.attribute_for_inspect(:tag_ids) - # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]" + # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]" def attribute_for_inspect(attr_name) value = read_attribute(attr_name) @@ -300,9 +299,6 @@ module ActiveRecord "#{value[0, 50]}...".inspect elsif value.is_a?(Date) || value.is_a?(Time) %("#{value.to_s(:db)}") - elsif value.is_a?(Array) && value.size > 10 - inspected = value.first(10).inspect - %(#{inspected[0...-1]}, ...]) else value.inspect end diff --git a/activerecord/lib/active_record/attribute_set/yaml_encoder.rb b/activerecord/lib/active_record/attribute_set/yaml_encoder.rb index 6208048231..f9d527a5a3 100644 --- a/activerecord/lib/active_record/attribute_set/yaml_encoder.rb +++ b/activerecord/lib/active_record/attribute_set/yaml_encoder.rb @@ -2,7 +2,7 @@ module ActiveRecord class AttributeSet # Attempts to do more intelligent YAML dumping of an # ActiveRecord::AttributeSet to reduce the size of the resulting string - class YAMLEncoder + class YAMLEncoder # :nodoc: def initialize(default_types) @default_types = default_types end 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 5939ee9956..eec0bc8518 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -790,7 +790,7 @@ module ActiveRecord # [<tt>:type</tt>] # The reference column type. Defaults to +:integer+. # [<tt>:index</tt>] - # Add an appropriate index. Defaults to false. + # Add an appropriate index. Defaults to false. # See #add_index for usage of this option. # [<tt>:foreign_key</tt>] # Add an appropriate foreign key constraint. Defaults to false. @@ -1128,7 +1128,6 @@ module ActiveRecord index_type ||= options[:unique] ? "UNIQUE" : "" index_name = options[:name].to_s if options.key?(:name) index_name ||= index_name(table_name, index_name_options(column_names)) - max_index_length = options.fetch(:internal, false) ? index_name_length : allowed_index_name_length if options.key?(:algorithm) algorithm = index_algorithms.fetch(options[:algorithm]) { @@ -1142,9 +1141,8 @@ module ActiveRecord index_options = options[:where] ? " WHERE #{options[:where]}" : "" end - if index_name.length > max_index_length - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters" - end + validate_index_length!(table_name, index_name, options.fetch(:internal, false)) + if data_source_exists?(table_name) && index_name_exists?(table_name, index_name, false) raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" end @@ -1276,8 +1274,10 @@ module ActiveRecord end end - def validate_index_length!(table_name, new_name) # :nodoc: - if new_name.length > allowed_index_name_length + def validate_index_length!(table_name, new_name, internal = false) # :nodoc: + max_index_length = internal ? index_name_length : allowed_index_name_length + + if new_name.length > max_index_length raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 718a6c5b91..c7881a6c6c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -515,19 +515,21 @@ module ActiveRecord schema, name = extract_schema_qualified_name(table_name) - fk_info = select_all <<-SQL.strip_heredoc - SELECT fk.referenced_table_name as 'to_table' - ,fk.referenced_column_name as 'primary_key' - ,fk.column_name as 'column' - ,fk.constraint_name as 'name' + fk_info = select_all(<<-SQL.strip_heredoc, 'SCHEMA') + SELECT fk.referenced_table_name AS 'to_table', + fk.referenced_column_name AS 'primary_key', + fk.column_name AS 'column', + fk.constraint_name AS 'name', + rc.update_rule AS 'on_update', + rc.delete_rule AS 'on_delete' FROM information_schema.key_column_usage fk - WHERE fk.referenced_column_name is not null + JOIN information_schema.referential_constraints rc + USING (constraint_schema, constraint_name) + WHERE fk.referenced_column_name IS NOT NULL AND fk.table_schema = #{quote(schema)} AND fk.table_name = #{quote(name)} SQL - create_table_info = create_table_info(table_name) - fk_info.map do |row| options = { column: row['column'], @@ -535,8 +537,8 @@ module ActiveRecord primary_key: row['primary_key'] } - options[:on_update] = extract_foreign_key_action(create_table_info, row['name'], "UPDATE") - options[:on_delete] = extract_foreign_key_action(create_table_info, row['name'], "DELETE") + options[:on_update] = extract_foreign_key_action(row['on_update']) + options[:on_delete] = extract_foreign_key_action(row['on_delete']) ForeignKeyDefinition.new(table_name, row['to_table'], options) end @@ -891,12 +893,10 @@ module ActiveRecord end end - def extract_foreign_key_action(structure, name, action) # :nodoc: - if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/ - case $1 - when 'CASCADE'; :cascade - when 'SET NULL'; :nullify - end + def extract_foreign_key_action(specifier) # :nodoc: + case specifier + when 'CASCADE'; :cascade + when 'SET NULL'; :nullify end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/column.rb b/activerecord/lib/active_record/connection_adapters/mysql/column.rb index 9c45fdd44a..ea554b188c 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/column.rb @@ -6,7 +6,6 @@ module ActiveRecord def initialize(*) super - assert_valid_default extract_default end @@ -38,12 +37,6 @@ module ActiveRecord @default = null || strict ? nil : '' end end - - def assert_valid_default - if blob_or_text_column? && default.present? - raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" - end - end end end end 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 6318b1c65a..45507e206a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -579,7 +579,7 @@ module ActiveRecord end def foreign_keys(table_name) - fk_info = select_all <<-SQL.strip_heredoc + fk_info = select_all(<<-SQL.strip_heredoc, 'SCHEMA') SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete FROM pg_constraint c JOIN pg_class t1 ON c.conrelid = t1.oid @@ -641,7 +641,7 @@ module ActiveRecord when 1, 2; 'smallint' when nil, 3, 4; 'integer' when 5..8; 'bigint' - else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with precision 0 instead.") + else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with scale 0 instead.") end else super(type, limit, precision, scale) diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 086c678af5..f735bc697b 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -98,14 +98,6 @@ module ActiveRecord @connection_specification_name end - def connection_id - ActiveRecord::RuntimeRegistry.connection_id ||= Thread.current.object_id - end - - def connection_id=(connection_id) - ActiveRecord::RuntimeRegistry.connection_id = connection_id - end - # Returns the configuration of the associated connection as a hash: # # ActiveRecord::Base.connection_config @@ -131,7 +123,7 @@ module ActiveRecord def remove_connection(name = nil) name ||= @connection_specification_name if defined?(@connection_specification_name) - # if removing a connection that have a pool, we reset the + # if removing a connection that has a pool, we reset the # connection_specification_name so it will use the parent # pool. if connection_handler.retrieve_connection_pool(name) diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 7be332fb97..b884edf920 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -105,6 +105,8 @@ module ActiveRecord end class EnumType < Type::Value # :nodoc: + delegate :type, to: :subtype + def initialize(name, mapping, subtype) @name = name @mapping = mapping diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index ed1bbf5dcd..51bf12d0bf 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -66,7 +66,7 @@ module ActiveRecord # By default, +test_helper.rb+ will load all of your fixtures into your test # database, so this test will succeed. # - # The testing environment will automatically load the all fixtures into the database before each + # The testing environment will automatically load all the fixtures into the database before each # test. To ensure consistent data, the environment deletes the fixtures before running the load. # # In addition to being available in the database, the fixture's data may also be accessed by diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 899683ee4f..b5fec57c8c 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -19,7 +19,7 @@ module ActiveRecord # Be aware that because the type column is an attribute on the record every new # subclass will instantly be marked as dirty and the type column will be included # in the list of changed attributes on the record. This is different from non - # STI classes: + # Single Table Inheritance(STI) classes: # # Company.new.changed? # => false # Firm.new.changed? # => true @@ -37,6 +37,7 @@ module ActiveRecord included do # Determines whether to store the full constant name including namespace when using STI. + # This is true, by default. class_attribute :store_full_sti_class, instance_writer: false self.store_full_sti_class = true end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index ca12a603da..387dd8e9bd 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -5,7 +5,7 @@ module ActiveRecord # Enable the query cache within the block if Active Record is configured. # If it's not, it will execute the given block. def cache(&block) - if ActiveRecord::Base.connected? + if connected? connection.cache(&block) else yield @@ -15,7 +15,7 @@ module ActiveRecord # Disable the query cache within the block if Active Record is configured. # If it's not, it will execute the given block. def uncached(&block) - if ActiveRecord::Base.connected? + if connected? connection.uncached(&block) else yield @@ -26,16 +26,12 @@ module ActiveRecord def self.run connection = ActiveRecord::Base.connection enabled = connection.query_cache_enabled - connection_id = ActiveRecord::Base.connection_id connection.enable_query_cache! - [enabled, connection_id] + enabled end - def self.complete(state) - enabled, connection_id = state - - ActiveRecord::Base.connection_id = connection_id + def self.complete(enabled) ActiveRecord::Base.connection.clear_query_cache ActiveRecord::Base.connection.disable_query_cache! unless enabled end @@ -44,7 +40,7 @@ module ActiveRecord executor.register_hook(self) executor.to_complete do - unless ActiveRecord::Base.connection.transaction_open? + unless ActiveRecord::Base.connected? && ActiveRecord::Base.connection.transaction_open? ActiveRecord::Base.clear_active_connections! end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 87eea8277a..d255cad91b 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -333,6 +333,8 @@ module ActiveRecord end connection.select_value(relation, "#{name} Exists", relation.bound_attributes) ? true : false + rescue RangeError + false end # This method is called whenever no records are found with either a single @@ -398,15 +400,7 @@ module ActiveRecord end def construct_relation_for_association_calculations - from = arel.froms.first - if Arel::Table === from - apply_join_dependency(self, construct_join_dependency(joins_values)) - else - # FIXME: as far as I can tell, `from` will always be an Arel::Table. - # There are no tests that test this branch, but presumably it's - # possible for `from` to be a list? - apply_join_dependency(self, construct_join_dependency(from)) - end + apply_join_dependency(self, construct_join_dependency(joins_values)) end def apply_join_dependency(relation, join_dependency) @@ -579,7 +573,7 @@ module ActiveRecord # e.g., reverse_order.offset(index-1).first end end - + private def find_nth_with_limit_and_offset(index, limit, offset:) # :nodoc: diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 2a831c2017..8a87015e44 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -658,7 +658,7 @@ module ActiveRecord # present). Neither relation may have a #limit, #offset, or #distinct set. # # Post.where("id = 1").or(Post.where("author_id = 3")) - # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'author_id = 3')) + # # SELECT `posts`.* FROM `posts` WHERE ((id = 1) OR (author_id = 3)) # def or(other) unless other.is_a? Relation diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb index 56e88bc661..9e86999c1b 100644 --- a/activerecord/lib/active_record/runtime_registry.rb +++ b/activerecord/lib/active_record/runtime_registry.rb @@ -12,9 +12,9 @@ module ActiveRecord class RuntimeRegistry # :nodoc: extend ActiveSupport::PerThreadRegistry - attr_accessor :connection_handler, :sql_runtime, :connection_id + attr_accessor :connection_handler, :sql_runtime - [:connection_handler, :sql_runtime, :connection_id].each do |val| + [:connection_handler, :sql_runtime].each do |val| class_eval %{ def self.#{val}; instance.#{val}; end }, __FILE__, __LINE__ class_eval %{ def self.#{val}=(x); instance.#{val}=x; end }, __FILE__, __LINE__ end diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index af0c935342..b1a0ad0115 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -58,6 +58,7 @@ module ActiveRecord args.concat(["--result-file", "#{filename}"]) args.concat(["--no-data"]) args.concat(["--routines"]) + args.concat(["--skip-comments"]) args.concat(["#{configuration['database']}"]) run_cmd('mysqldump', args, 'dumping') diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 8b4874044c..b19ab57ee4 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -2,6 +2,7 @@ module ActiveRecord module Tasks # :nodoc: class PostgreSQLDatabaseTasks # :nodoc: DEFAULT_ENCODING = ENV['CHARSET'] || 'utf8' + ON_ERROR_STOP_1 = 'ON_ERROR_STOP=1'.freeze delegate :connection, :establish_connection, :clear_active_connections!, to: ActiveRecord::Base @@ -67,7 +68,7 @@ module ActiveRecord def structure_load(filename) set_psql_env - args = [ '-q', '-f', filename, configuration['database'] ] + args = [ '-v', ON_ERROR_STOP_1, '-q', '-f', filename, configuration['database'] ] run_cmd('psql', args, 'loading' ) end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 77c2845d88..0ee1ebcfbe 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -189,8 +189,8 @@ module ActiveRecord # # === Caveats # - # If you're on MySQL, then do not use DDL operations in nested transactions - # blocks that are emulated with savepoints. That is, do not execute statements + # If you're on MySQL, then do not use Data Definition Language(DDL) operations in nested + # transactions blocks that are emulated with savepoints. That is, do not execute statements # like 'CREATE TABLE' inside such blocks. This is because MySQL automatically # releases all savepoints upon executing a DDL operation. When +transaction+ # is finished and tries to release the savepoint it created earlier, a @@ -480,11 +480,11 @@ module ActiveRecord # Updates the attributes on this particular Active Record object so that # if it's associated with a transaction, then the state of the Active Record - # object will be updated to reflect the current state of the transaction + # object will be updated to reflect the current state of the transaction. # # The +@transaction_state+ variable stores the states of the associated # transaction. This relies on the fact that a transaction can only be in - # one rollback or commit (otherwise a list of states would be required) + # one rollback or commit (otherwise a list of states would be required). # Each Active Record object inside of a transaction carries that transaction's # TransactionState. # diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 1f59276137..ec9f498c40 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -65,8 +65,6 @@ module ActiveRecord column = klass.columns_hash[attribute_name] cast_type = klass.type_for_attribute(attribute_name) - value = cast_type.serialize(value) - value = klass.connection.type_cast(value) comparison = if !options[:case_sensitive] && !value.nil? # will use SQL LOWER function before comparison, unless it detects a case insensitive collation @@ -77,11 +75,9 @@ module ActiveRecord if value.nil? klass.unscoped.where(comparison) else - bind = Relation::QueryAttribute.new(attribute_name, value, Type::Value.new) + bind = Relation::QueryAttribute.new(attribute_name, value, cast_type) klass.unscoped.where(comparison, bind) end - rescue RangeError - klass.none end def scope_relation(record, table, relation) diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index f191eff5bf..0d72913258 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -21,14 +21,14 @@ module ActiveRecord end def create_model_file - template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb") generate_application_record + template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb") end def create_module_file return if regular_class_path.empty? - template 'module.rb', File.join('app/models', "#{class_path.join('/')}.rb") if behavior == :invoke generate_application_record + template 'module.rb', File.join('app/models', "#{class_path.join('/')}.rb") if behavior == :invoke end hook_for :test_framework @@ -48,7 +48,7 @@ module ActiveRecord # Used by the migration template to determine the parent name of the model def parent_class_name - options[:parent] || determine_default_parent_class + options[:parent] || 'ApplicationRecord' end def application_record_exist? @@ -64,14 +64,6 @@ module ActiveRecord 'app/models/application_record.rb' end end - - def determine_default_parent_class - if application_record_exist? - "ApplicationRecord" - else - "ActiveRecord::Base" - end - end end end end diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index 43957791b1..57ea8258d1 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -103,3 +103,24 @@ module ActiveRecord end end end + +class Mysql2AnsiQuotesTest < ActiveRecord::Mysql2TestCase + def setup + @connection = ActiveRecord::Base.connection + @connection.execute("SET SESSION sql_mode='ANSI_QUOTES'") + end + + def teardown + @connection.reconnect! + end + + def test_primary_key_method_with_ansi_quotes + assert_equal "id", @connection.primary_key("topics") + end + + def test_foreign_keys_method_with_ansi_quotes + fks = @connection.foreign_keys("lessons_students") + assert_equal([["lessons_students", "students", :cascade]], + fks.map {|fk| [fk.from_table, fk.to_table, fk.on_delete] }) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 380a90d765..70792e937c 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -189,8 +189,13 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase end def test_attribute_for_inspect_for_array_field + record = PgArray.new { |a| a.ratings = (1..10).to_a } + assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]", record.attribute_for_inspect(:ratings)) + end + + def test_attribute_for_inspect_for_array_field_for_large_array record = PgArray.new { |a| a.ratings = (1..11).to_a } - assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]", record.attribute_for_inspect(:ratings)) + assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]", record.attribute_for_inspect(:ratings)) end def test_escaping diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index f7bb3e54d5..80d9a6083b 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -90,6 +90,10 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_no_queries { authors.map(&:post) } end + def test_calculate_with_string_in_from_and_eager_loading + assert_equal 10, Post.from("authors, posts").eager_load(:comments).where("posts.author_id = authors.id").count + end + def test_with_two_tables_in_from_without_getting_double_quoted posts = Post.select("posts.*").from("authors, posts").eager_load(:comments).where("posts.author_id = authors.id").order("posts.id").to_a assert_equal 2, posts.first.comments.size diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 1db52af59b..04126e87e4 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -27,14 +27,33 @@ class AttributeMethodsTest < ActiveRecord::TestCase ActiveRecord::Base.send(:attribute_method_matchers).concat(@old_matchers) end - def test_attribute_for_inspect + def test_attribute_for_inspect_string t = topics(:first) t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters" - assert_equal %("#{t.written_on.to_s(:db)}"), t.attribute_for_inspect(:written_on) assert_equal '"The First Topic Now Has A Title With\nNewlines And ..."', t.attribute_for_inspect(:title) end + def test_attribute_for_inspect_date + t = topics(:first) + + assert_equal %("#{t.written_on.to_s(:db)}"), t.attribute_for_inspect(:written_on) + end + + def test_attribute_for_inspect_array + t = topics(:first) + t.content = [Object.new] + + assert_match %r(\[#<Object:0x[0-9a-f]+>\]), t.attribute_for_inspect(:content) + end + + def test_attribute_for_inspect_long_array + t = topics(:first) + t.content = (1..11).to_a + + assert_equal "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]", t.attribute_for_inspect(:content) + end + def test_attribute_present t = Topic.new t.title = "hello there!" diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 80dcba1cf4..7b1ce40c0d 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -14,7 +14,6 @@ require 'models/auto_id' require 'models/boolean' require 'models/column_name' require 'models/subscriber' -require 'models/keyboard' require 'models/comment' require 'models/minimalistic' require 'models/warehouse_thing' @@ -25,7 +24,6 @@ require 'models/joke' require 'models/bird' require 'models/car' require 'models/bulb' -require 'rexml/document' require 'concurrent/atomic/count_down_latch' class FirstAbstractClass < ActiveRecord::Base @@ -1034,12 +1032,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal t1.title, t2.title end - def test_reload_with_exclusive_scope - dev = DeveloperCalledDavid.first - dev.update!(name: "NotDavid" ) - assert_equal dev, dev.reload - end - def test_switching_between_table_name k = Class.new(Joke) diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index 81162b7e98..78c0853992 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -60,15 +60,8 @@ module ActiveRecord end def test_should_not_set_default_for_blob_and_text_data_types - assert_raise ArgumentError do - MySQL::Column.new("title", "a", SqlTypeMetadata.new(sql_type: "blob")) - end - text_type = MySQL::TypeMetadata.new( SqlTypeMetadata.new(type: :text)) - assert_raise ArgumentError do - MySQL::Column.new("title", "Hello", text_type) - end text_column = MySQL::Column.new("title", nil, text_type) assert_equal nil, text_column.default diff --git a/activerecord/test/cases/invalid_date_test.rb b/activerecord/test/cases/date_test.rb index 426a350379..f112081c14 100644 --- a/activerecord/test/cases/invalid_date_test.rb +++ b/activerecord/test/cases/date_test.rb @@ -1,7 +1,19 @@ require 'cases/helper' require 'models/topic' -class InvalidDateTest < ActiveRecord::TestCase +class DateTest < ActiveRecord::TestCase + def test_date_with_time_value + time_value = Time.new(2016, 05, 11, 19, 0, 0) + topic = Topic.create(last_read: time_value) + assert_equal topic, Topic.find_by(last_read: time_value) + end + + def test_date_with_string_value + string_value = '2016-05-11 19:00:00' + topic = Topic.create(last_read: string_value) + assert_equal topic, Topic.find_by(last_read: string_value) + end + def test_assign_valid_dates valid_dates = [[2007, 11, 30], [1993, 2, 28], [2008, 2, 29]] diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index babacd1ee9..e781b60464 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -421,4 +421,8 @@ class EnumTest < ActiveRecord::TestCase book = Book.new assert book.hard? end + + test "data type of Enum type" do + assert_equal :integer, Book.type_for_attribute('status').type + end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 374a8ba199..6eaaa30cd0 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -173,11 +173,9 @@ class FinderTest < ActiveRecord::TestCase end end - def test_exists_fails_when_parameter_has_invalid_type - assert_raises(ActiveModel::RangeError) do - assert_equal false, Topic.exists?(("9"*53).to_i) # number that's bigger than int - end + def test_exists_returns_false_when_parameter_has_invalid_type assert_equal false, Topic.exists?("foo") + assert_equal false, Topic.exists?(("9"*53).to_i) # number that's bigger than int end def test_exists_does_not_select_columns_without_alias diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 01162dcefe..49a8fa241f 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -232,6 +232,10 @@ module ActiveRecord t.column :city_id, :integer end add_foreign_key :houses, :cities, column: "city_id" + + # remove and re-add to test that schema is updated and not accidently cached + remove_foreign_key :houses, :cities + add_foreign_key :houses, :cities, column: "city_id", on_delete: :cascade end end @@ -243,6 +247,15 @@ module ActiveRecord silence_stream($stdout) { migration.migrate(:down) } end + def test_foreign_key_constraint_is_not_cached_incorrectly + migration = CreateCitiesAndHousesMigration.new + silence_stream($stdout) { migration.migrate(:up) } + output = dump_table_schema "houses" + assert_match %r{\s+add_foreign_key "houses",.+on_delete: :cascade$}, output + ensure + silence_stream($stdout) { migration.migrate(:down) } + end + class CreateSchoolsAndClassesMigration < ActiveRecord::Migration::Current def change create_table(:schools) diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb index ae18573126..d05cb22740 100644 --- a/activerecord/test/cases/multiparameter_attributes_test.rb +++ b/activerecord/test/cases/multiparameter_attributes_test.rb @@ -11,15 +11,13 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase topic.attributes = attributes # note that extra #to_date call allows test to pass for Oracle, which # treats dates/times the same - assert_date_from_db Date.new(2004, 6, 24), topic.last_read.to_date + assert_equal Date.new(2004, 6, 24), topic.last_read.to_date end def test_multiparameter_attributes_on_date_with_empty_year attributes = { "last_read(1i)" => "", "last_read(2i)" => "6", "last_read(3i)" => "24" } topic = Topic.find(1) topic.attributes = attributes - # note that extra #to_date call allows test to pass for Oracle, which - # treats dates/times the same assert_nil topic.last_read end @@ -27,8 +25,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "", "last_read(3i)" => "24" } topic = Topic.find(1) topic.attributes = attributes - # note that extra #to_date call allows test to pass for Oracle, which - # treats dates/times the same assert_nil topic.last_read end @@ -36,8 +32,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "" } topic = Topic.find(1) topic.attributes = attributes - # note that extra #to_date call allows test to pass for Oracle, which - # treats dates/times the same assert_nil topic.last_read end @@ -45,8 +39,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase attributes = { "last_read(1i)" => "", "last_read(2i)" => "6", "last_read(3i)" => "" } topic = Topic.find(1) topic.attributes = attributes - # note that extra #to_date call allows test to pass for Oracle, which - # treats dates/times the same assert_nil topic.last_read end @@ -54,8 +46,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "", "last_read(3i)" => "" } topic = Topic.find(1) topic.attributes = attributes - # note that extra #to_date call allows test to pass for Oracle, which - # treats dates/times the same assert_nil topic.last_read end @@ -63,8 +53,6 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase attributes = { "last_read(1i)" => "", "last_read(2i)" => "", "last_read(3i)" => "24" } topic = Topic.find(1) topic.attributes = attributes - # note that extra #to_date call allows test to pass for Oracle, which - # treats dates/times the same assert_nil topic.last_read end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 52eac4a124..6c8532cf00 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -275,18 +275,6 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase end if current_adapter?(:Mysql2Adapter) - class PrimaryKeyWithAnsiQuotesTest < ActiveRecord::TestCase - self.use_transactional_tests = false - - def test_primary_key_method_with_ansi_quotes - con = ActiveRecord::Base.connection - con.execute("SET SESSION sql_mode='ANSI_QUOTES'") - assert_equal "id", con.primary_key("topics") - ensure - con.reconnect! - end - end - class PrimaryKeyBigintNilDefaultTest < ActiveRecord::TestCase include SchemaDumpingHelper diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 406643d6fb..03ec063671 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -39,18 +39,6 @@ class QueryCacheTest < ActiveRecord::TestCase assert ActiveRecord::Base.connection.query_cache_enabled, 'cache on' end - def test_exceptional_middleware_assigns_original_connection_id_on_error - connection_id = ActiveRecord::Base.connection_id - - mw = middleware { |env| - ActiveRecord::Base.connection_id = self.object_id - raise "lol borked" - } - assert_raises(RuntimeError) { mw.call({}) } - - assert_equal connection_id, ActiveRecord::Base.connection_id - end - def test_middleware_delegates called = false mw = middleware { |env| @@ -133,7 +121,6 @@ class QueryCacheTest < ActiveRecord::TestCase def test_cache_is_flat Task.cache do - Topic.columns # don't count this query assert_queries(1) { Topic.find(1); Topic.find(1); } end @@ -175,6 +162,22 @@ class QueryCacheTest < ActiveRecord::TestCase ActiveRecord::Base.configurations = conf end + def test_cache_is_not_available_when_using_a_not_connected_connection + spec_name = Task.connection_specification_name + conf = ActiveRecord::Base.configurations['arunit'].merge('name' => 'test2') + ActiveRecord::Base.connection_handler.establish_connection(conf) + Task.connection_specification_name = "test2" + refute Task.connected? + + Task.cache do + Task.connection # warmup postgresql connection setup queries + assert_queries(2) { Task.find(1); Task.find(1) } + end + ensure + ActiveRecord::Base.connection_handler.remove_connection(Task.connection_specification_name) + Task.connection_specification_name = spec_name + end + def test_query_cache_doesnt_leak_cached_results_of_rolled_back_queries ActiveRecord::Base.connection.enable_query_cache! post = Post.first diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index 8e480bbaee..70e406038f 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -289,7 +289,7 @@ module ActiveRecord def test_structure_dump filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "test-db").returns(true) + Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db").returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) end @@ -297,7 +297,7 @@ module ActiveRecord def test_warn_when_external_structure_dump_command_execution_fails filename = "awesome-file.sql" Kernel.expects(:system) - .with("mysqldump", "--result-file", filename, "--no-data", "--routines", "test-db") + .with("mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db") .returns(false) e = assert_raise(RuntimeError) { @@ -308,7 +308,7 @@ module ActiveRecord def test_structure_dump_with_port_number filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--port=10000", "--result-file", filename, "--no-data", "--routines", "test-db").returns(true) + Kernel.expects(:system).with("mysqldump", "--port=10000", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db").returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump( @configuration.merge('port' => 10000), @@ -317,7 +317,7 @@ module ActiveRecord def test_structure_dump_with_ssl filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--ssl-ca=ca.crt", "--result-file", filename, "--no-data", "--routines", "test-db").returns(true) + Kernel.expects(:system).with("mysqldump", "--ssl-ca=ca.crt", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db").returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump( @configuration.merge("sslca" => "ca.crt"), diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index 6a0c7fbcb5..99d73e91a4 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -288,14 +288,14 @@ module ActiveRecord def test_structure_load filename = "awesome-file.sql" - Kernel.expects(:system).with('psql', '-q', '-f', filename, @configuration['database']).returns(true) + Kernel.expects(:system).with('psql', '-v', 'ON_ERROR_STOP=1', '-q', '-f', filename, @configuration['database']).returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end def test_structure_load_accepts_path_with_spaces filename = "awesome file.sql" - Kernel.expects(:system).with('psql', '-q', '-f', filename, @configuration['database']).returns(true) + Kernel.expects(:system).with('psql', '-v', 'ON_ERROR_STOP=1', '-q', '-f', filename, @configuration['database']).returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 87299c0dab..c8adc21bbc 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -12,10 +12,6 @@ module ActiveRecord SQLCounter.clear_log end - def assert_date_from_db(expected, actual, message = nil) - assert_equal expected.to_s, actual.to_s, message - end - def capture_sql SQLCounter.clear_log yield diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 628a59c2e3..2f2993ce18 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -409,6 +409,14 @@ ActiveRecord::Schema.define do t.references :student end + create_table :students, force: true do |t| + t.string :name + t.boolean :active + t.integer :college_id + end + + add_foreign_key :lessons_students, :students, on_delete: :cascade + create_table :lint_models, force: true create_table :line_items, force: true do |t| @@ -777,12 +785,6 @@ ActiveRecord::Schema.define do t.integer :lock_version, null: false, default: 0 end - create_table :students, force: true do |t| - t.string :name - t.boolean :active - t.integer :college_id - end - create_table :subscribers, force: true, id: false do |t| t.string :nick, null: false t.string :name @@ -1002,7 +1004,6 @@ ActiveRecord::Schema.define do end add_foreign_key :fk_test_has_fk, :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id" - add_foreign_key :lessons_students, :students end create_table :overloaded_types, force: true do |t| diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index 2842b692e2..db439b5732 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -1,3 +1,22 @@ +* Support parsing JSON time in ISO8601 local time strings in + `ActiveSupport::JSON.decode` when `parse_json_times` is enabled. + Strings in the format of `YYYY-MM-DD hh:mm:ss` (without a `Z` at + the end) will be parsed in the local timezone (`Time.zone`). In + addition, date strings (`YYYY-MM-DD`) are now parsed into `Date` + objects. + + *Grzegorz Witek* + +* Fixed `ActiveSupport::Logger.broadcast` so that calls to `#silence` now + properly delegate to all loggers. Silencing now properly suppresses logging + to both the log and the console. + + *Kevin McPhillips* + +* Remove deprecated arguments in `assert_nothing_raised`. + + *Rafel Mendonça França* + * `Date.to_s` doesn't produce too many spaces. For example, `to_s(:short)` will now produce `01 Feb` instead of ` 1 Feb`. diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb index 904d3f0eb0..557a4695a6 100644 --- a/activesupport/lib/active_support/callbacks.rb +++ b/activesupport/lib/active_support/callbacks.rb @@ -103,7 +103,7 @@ module ActiveSupport end # A hook invoked every time a before callback is halted. - # This can be overridden in AS::Callback implementors in order + # This can be overridden in ActiveSupport::Callbacks implementors in order # to provide better debugging/logging. def halted_callback_hook(filter) end @@ -736,8 +736,13 @@ module ActiveSupport # # would call <tt>Audit#save</tt>. # - # NOTE: +method_name+ passed to `define_model_callbacks` must not end with + # ===== Notes + # + # +names+ passed to `define_callbacks` must not end with # `!`, `?` or `=`. + # + # Calling `define_callbacks` multiple times with the same +names+ will + # overwrite previous callbacks registered with `set_callback`. def define_callbacks(*names) options = names.extract_options! diff --git a/activesupport/lib/active_support/concurrency/latch.rb b/activesupport/lib/active_support/concurrency/latch.rb index 4abe5ece6f..35074265b8 100644 --- a/activesupport/lib/active_support/concurrency/latch.rb +++ b/activesupport/lib/active_support/concurrency/latch.rb @@ -2,17 +2,24 @@ require 'concurrent/atomic/count_down_latch' module ActiveSupport module Concurrency - class Latch < Concurrent::CountDownLatch + class Latch def initialize(count = 1) - ActiveSupport::Deprecation.warn("ActiveSupport::Concurrency::Latch is deprecated. Please use Concurrent::CountDownLatch instead.") - super(count) + if count == 1 + ActiveSupport::Deprecation.warn("ActiveSupport::Concurrency::Latch is deprecated. Please use Concurrent::Event instead.") + else + ActiveSupport::Deprecation.warn("ActiveSupport::Concurrency::Latch is deprecated. Please use Concurrent::CountDownLatch instead.") + end + + @inner = Concurrent::CountDownLatch.new(count) end - alias_method :release, :count_down + def release + @inner.count_down + end def await - wait(nil) + @inner.wait(nil) end end end diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb index 7f968d10b5..3f6e8bd26c 100644 --- a/activesupport/lib/active_support/core_ext/module/delegation.rb +++ b/activesupport/lib/active_support/core_ext/module/delegation.rb @@ -164,8 +164,8 @@ class Module '' end - file, line = caller(1, 1).first.split(':'.freeze, 2) - line = line.to_i + location = caller_locations(1, 1).first + file, line = location.path, location.lineno to = to.to_s to = "self.#{to}" if DELEGATION_RESERVED_METHOD_NAMES.include?(to) diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb index d49b2fbe54..363bde5a68 100644 --- a/activesupport/lib/active_support/core_ext/object/json.rb +++ b/activesupport/lib/active_support/core_ext/object/json.rb @@ -1,6 +1,8 @@ # Hack to load json gem first so we can overwrite its to_json. require 'json' require 'bigdecimal' +require 'uri/generic' +require 'pathname' require 'active_support/core_ext/big_decimal/conversions' # for #to_s require 'active_support/core_ext/hash/except' require 'active_support/core_ext/hash/slice' @@ -192,6 +194,18 @@ class DateTime end end +class URI::Generic #:nodoc: + def as_json(options = nil) + to_s + end +end + +class Pathname #:nodoc: + def as_json(options = nil) + to_s + end +end + class Process::Status #:nodoc: def as_json(options = nil) { :exitstatus => exitstatus, :pid => pid } diff --git a/activesupport/lib/active_support/evented_file_update_checker.rb b/activesupport/lib/active_support/evented_file_update_checker.rb index 21fdf7bb80..a2dcf31132 100644 --- a/activesupport/lib/active_support/evented_file_update_checker.rb +++ b/activesupport/lib/active_support/evented_file_update_checker.rb @@ -3,6 +3,33 @@ require 'pathname' require 'concurrent/atomic/atomic_boolean' module ActiveSupport + # Allows you to "listen" to changes in a file system. + # The evented file updater does not hit disk when checking for updates + # instead it uses platform specific file system events to trigger a change + # in state. + # + # The file checker takes an array of files to watch or a hash specifying directories + # and file extensions to watch. It also takes a block that is called when + # EventedFileUpdateChecker#execute is run or when EventedFileUpdateChecker#execute_if_updated + # is run and there have been changes to the file system. + # + # Note: Forking will cause the first call to `updated?` to return `true`. + # + # Example: + # + # checker = EventedFileUpdateChecker.new(["/tmp/foo"], -> { puts "changed" }) + # checker.updated? + # # => false + # checker.execute_if_updated + # # => nil + # + # FileUtils.touch("/tmp/foo") + # + # checker.updated? + # # => true + # checker.execute_if_updated + # # => "changed" + # class EventedFileUpdateChecker #:nodoc: all def initialize(files, dirs = {}, &block) @ph = PathHelper.new @@ -13,11 +40,13 @@ module ActiveSupport @dirs[@ph.xpath(dir)] = Array(exts).map { |ext| @ph.normalize_extension(ext) } end - @block = block - @updated = Concurrent::AtomicBoolean.new(false) - @lcsp = @ph.longest_common_subpath(@dirs.keys) + @block = block + @updated = Concurrent::AtomicBoolean.new(false) + @lcsp = @ph.longest_common_subpath(@dirs.keys) + @pid = Process.pid + @boot_mutex = Mutex.new - if (dtw = directories_to_watch).any? + if (@dtw = directories_to_watch).any? # Loading listen triggers warnings. These are originated by a legit # usage of attr_* macros for private attributes, but adds a lot of noise # to our test suite. Thus, we lazy load it and disable warnings locally. @@ -28,11 +57,18 @@ module ActiveSupport raise LoadError, "Could not load the 'listen' gem. Add `gem 'listen'` to the development group of your Gemfile", e.backtrace end end - Listen.to(*dtw, &method(:changed)).start end + boot! end def updated? + @boot_mutex.synchronize do + if @pid != Process.pid + boot! + @pid = Process.pid + @updated.make_true + end + end @updated.true? end @@ -50,6 +86,9 @@ module ActiveSupport end private + def boot! + Listen.to(*@dtw, &method(:changed)).start + end def changed(modified, added, removed) unless updated? diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb index f94e12e14f..f14b8b81b7 100644 --- a/activesupport/lib/active_support/inflector/methods.rb +++ b/activesupport/lib/active_support/inflector/methods.rb @@ -238,8 +238,8 @@ module ActiveSupport # Tries to find a constant with the name specified in the argument string. # - # 'Module'.constantize # => Module - # 'Foo::Bar'.constantize # => Foo::Bar + # constantize('Module') # => Module + # constantize('Foo::Bar') # => Foo::Bar # # The name is assumed to be the one of a top-level constant, no matter # whether it starts with "::" or not. No lexical context is taken into @@ -248,8 +248,8 @@ module ActiveSupport # C = 'outside' # module M # C = 'inside' - # C # => 'inside' - # 'C'.constantize # => 'outside', same as ::C + # C # => 'inside' + # constantize('C') # => 'outside', same as ::C # end # # NameError is raised when the name is not in CamelCase or the constant is diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb index 2932954f03..64e4b0e7a9 100644 --- a/activesupport/lib/active_support/json/decoding.rb +++ b/activesupport/lib/active_support/json/decoding.rb @@ -8,7 +8,8 @@ module ActiveSupport module JSON # matches YAML-formatted dates - DATE_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?))$/ + DATE_REGEX = /^\d{4}-\d{2}-\d{2}$/ + DATETIME_REGEX = /^(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?)?)$/ class << self # Parses a JSON string (JavaScript Object Notation) into a hash. @@ -48,7 +49,13 @@ module ActiveSupport nil when DATE_REGEX begin - DateTime.parse(data) + Date.parse(data) + rescue ArgumentError + data + end + when DATETIME_REGEX + begin + Time.zone.parse(data) rescue ArgumentError data end diff --git a/activesupport/lib/active_support/key_generator.rb b/activesupport/lib/active_support/key_generator.rb index 7f73f9ddfc..0f0e931c06 100644 --- a/activesupport/lib/active_support/key_generator.rb +++ b/activesupport/lib/active_support/key_generator.rb @@ -31,11 +31,9 @@ module ActiveSupport @cache_keys = Concurrent::Map.new end - # Returns a derived key suitable for use. The default key_size is chosen - # to be compatible with the default settings of ActiveSupport::MessageVerifier. - # i.e. OpenSSL::Digest::SHA1#block_length - def generate_key(salt, key_size=64) - @cache_keys["#{salt}#{key_size}"] ||= @key_generator.generate_key(salt, key_size) + # Returns a derived key suitable for use. + def generate_key(*args) + @cache_keys[args.join] ||= @key_generator.generate_key(*args) end end diff --git a/activesupport/lib/active_support/logger.rb b/activesupport/lib/active_support/logger.rb index de48e717b6..92b890dbb0 100644 --- a/activesupport/lib/active_support/logger.rb +++ b/activesupport/lib/active_support/logger.rb @@ -55,6 +55,24 @@ module ActiveSupport logger.local_level = level if logger.respond_to?(:local_level=) super(level) if respond_to?(:local_level=) end + + define_method(:silence) do |level = Logger::ERROR, &block| + if logger.respond_to?(:silence) + logger.silence(level) do + if respond_to?(:silence) + super(level, &block) + else + block.call(self) + end + end + else + if respond_to?(:silence) + super(level, &block) + else + block.call(self) + end + end + end end end diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb index 53a55bd986..501ba7fc76 100644 --- a/activesupport/lib/active_support/ordered_options.rb +++ b/activesupport/lib/active_support/ordered_options.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/object/blank' + module ActiveSupport # Usually key value pairs are handled something like this: # diff --git a/activesupport/lib/active_support/per_thread_registry.rb b/activesupport/lib/active_support/per_thread_registry.rb index 88e2b12cc7..18ca951372 100644 --- a/activesupport/lib/active_support/per_thread_registry.rb +++ b/activesupport/lib/active_support/per_thread_registry.rb @@ -1,7 +1,7 @@ require 'active_support/core_ext/module/delegation' module ActiveSupport - # NOTE: This approach has been deprecated for end-user code in favor of thread_mattr_accessor and friends. + # NOTE: This approach has been deprecated for end-user code in favor of {thread_mattr_accessor}[rdoc-ref:Module#thread_mattr_accessor] and friends. # Please use that approach instead. # # This module is used to encapsulate access to thread local variables. diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb index 1fc12d0bc1..221e1171e7 100644 --- a/activesupport/lib/active_support/test_case.rb +++ b/activesupport/lib/active_support/test_case.rb @@ -74,12 +74,7 @@ module ActiveSupport # assert_nothing_raised do # perform_service(param: 'no_exception') # end - def assert_nothing_raised(*args) - if args.present? - ActiveSupport::Deprecation.warn( - "Passing arguments to assert_nothing_raised " \ - "is deprecated and will be removed in Rails 5.1.") - end + def assert_nothing_raised yield end end diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb index 29305e0082..ad83638572 100644 --- a/activesupport/lib/active_support/testing/assertions.rb +++ b/activesupport/lib/active_support/testing/assertions.rb @@ -1,5 +1,3 @@ -require 'active_support/core_ext/object/blank' - module ActiveSupport module Testing module Assertions diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb index edf8b30a0a..7dd03ce65d 100644 --- a/activesupport/lib/active_support/testing/isolation.rb +++ b/activesupport/lib/active_support/testing/isolation.rb @@ -13,17 +13,6 @@ module ActiveSupport !ENV["NO_FORK"] && Process.respond_to?(:fork) end - @@class_setup_mutex = Mutex.new - - def _run_class_setup # class setup method should only happen in parent - @@class_setup_mutex.synchronize do - unless defined?(@@ran_class_setup) || ENV['ISOLATION_TEST'] - self.class.setup if self.class.respond_to?(:setup) - @@ran_class_setup = true - end - end - end - def run serialized = run_in_isolation do super diff --git a/activesupport/lib/active_support/testing/time_helpers.rb b/activesupport/lib/active_support/testing/time_helpers.rb index fca0947c5b..ef27ce8eb5 100644 --- a/activesupport/lib/active_support/testing/time_helpers.rb +++ b/activesupport/lib/active_support/testing/time_helpers.rb @@ -66,7 +66,7 @@ module ActiveSupport # +Date.today+, and +DateTime.now+ to return the time or date passed into this method. # # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 - # travel_to Time.new(2004, 11, 24, 01, 04, 44) + # travel_to Time.zone.local(2004, 11, 24, 01, 04, 44) # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00 # Date.current # => Wed, 24 Nov 2004 # DateTime.current # => Wed, 24 Nov 2004 01:04:44 -0500 @@ -88,7 +88,7 @@ module ActiveSupport # state at the end of the block: # # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 - # travel_to Time.new(2004, 11, 24, 01, 04, 44) do + # travel_to Time.zone.local(2004, 11, 24, 01, 04, 44) do # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00 # end # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 @@ -116,7 +116,7 @@ module ActiveSupport # `travel` and `travel_to`. # # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 - # travel_to Time.new(2004, 11, 24, 01, 04, 44) + # travel_to Time.zone.local(2004, 11, 24, 01, 04, 44) # Time.current # => Wed, 24 Nov 2004 01:04:44 EST -05:00 # travel_back # Time.current # => Sat, 09 Nov 2013 15:34:49 EST -05:00 diff --git a/activesupport/test/broadcast_logger_test.rb b/activesupport/test/broadcast_logger_test.rb index 6d4e3b74f7..f95b3e5ad3 100644 --- a/activesupport/test/broadcast_logger_test.rb +++ b/activesupport/test/broadcast_logger_test.rb @@ -3,75 +3,147 @@ require 'abstract_unit' module ActiveSupport class BroadcastLoggerTest < TestCase attr_reader :logger, :log1, :log2 - def setup + + setup do @log1 = FakeLogger.new @log2 = FakeLogger.new @log1.extend Logger.broadcast @log2 @logger = @log1 end - def test_debug - logger.debug "foo" - assert_equal 'foo', log1.adds.first[2] - assert_equal 'foo', log2.adds.first[2] + Logger::Severity.constants.each do |level_name| + method = level_name.downcase + level = Logger::Severity.const_get(level_name) + + test "##{method} adds the message to all loggers" do + logger.send(method, "msg") + + assert_equal [level, "msg", nil], log1.adds.first + assert_equal [level, "msg", nil], log2.adds.first + end end - def test_close + test "#close broadcasts to all loggers" do logger.close + assert log1.closed, 'should be closed' assert log2.closed, 'should be closed' end - def test_chevrons + test "#<< shovels the value into all loggers" do logger << "foo" + assert_equal %w{ foo }, log1.chevrons assert_equal %w{ foo }, log2.chevrons end - def test_level - assert_nil logger.level - logger.level = 10 - assert_equal 10, log1.level - assert_equal 10, log2.level + test "#level= assigns the level to all loggers" do + assert_equal ::Logger::DEBUG, logger.level + logger.level = ::Logger::FATAL + + assert_equal ::Logger::FATAL, log1.level + assert_equal ::Logger::FATAL, log2.level end - def test_progname + test "#progname= assigns to all the loggers" do assert_nil logger.progname - logger.progname = 10 - assert_equal 10, log1.progname - assert_equal 10, log2.progname + logger.progname = ::Logger::FATAL + + assert_equal ::Logger::FATAL, log1.progname + assert_equal ::Logger::FATAL, log2.progname end - def test_formatter + test "#formatter= assigns to all the loggers" do assert_nil logger.formatter - logger.formatter = 10 - assert_equal 10, log1.formatter - assert_equal 10, log2.formatter + logger.formatter = ::Logger::FATAL + + assert_equal ::Logger::FATAL, log1.formatter + assert_equal ::Logger::FATAL, log2.formatter + end + + test "#local_level= assigns the local_level to all loggers" do + assert_equal ::Logger::DEBUG, logger.local_level + logger.local_level = ::Logger::FATAL + + assert_equal ::Logger::FATAL, log1.local_level + assert_equal ::Logger::FATAL, log2.local_level + end + + test "#silence silences all loggers below the default level of ERROR" do + logger.silence do + logger.debug "test" + end + + assert_equal [], log1.adds + assert_equal [], log2.adds + end + + test "#silence does not silence at or above ERROR" do + logger.silence do + logger.error "from error" + logger.unknown "from unknown" + end + + assert_equal [[::Logger::ERROR, "from error", nil], [::Logger::UNKNOWN, "from unknown", nil]], log1.adds + assert_equal [[::Logger::ERROR, "from error", nil], [::Logger::UNKNOWN, "from unknown", nil]], log2.adds + end + + test "#silence allows you to override the silence level" do + logger.silence(::Logger::FATAL) do + logger.error "unseen" + logger.fatal "seen" + end + + assert_equal [[::Logger::FATAL, "seen", nil]], log1.adds + assert_equal [[::Logger::FATAL, "seen", nil]], log2.adds end class FakeLogger + include LoggerSilence + attr_reader :adds, :closed, :chevrons - attr_accessor :level, :progname, :formatter + attr_accessor :level, :progname, :formatter, :local_level def initialize - @adds = [] - @closed = false - @chevrons = [] - @level = nil - @progname = nil - @formatter = nil + @adds = [] + @closed = false + @chevrons = [] + @level = ::Logger::DEBUG + @local_level = ::Logger::DEBUG + @progname = nil + @formatter = nil + end + + def debug(message, &block) + add(::Logger::DEBUG, message, &block) + end + + def info(message, &block) + add(::Logger::INFO, message, &block) + end + + def warn(message, &block) + add(::Logger::WARN, message, &block) + end + + def error(message, &block) + add(::Logger::ERROR, message, &block) + end + + def fatal(message, &block) + add(::Logger::FATAL, message, &block) end - def debug msg, &block - add(:omg, nil, msg, &block) + def unknown(message, &block) + add(::Logger::UNKNOWN, message, &block) end def << x @chevrons << x end - def add(*args) - @adds << args + def add(message_level, message=nil, progname=nil, &block) + @adds << [message_level, message, progname] if message_level >= local_level end def close diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb index ec34bd823d..dbde3d2e15 100644 --- a/activesupport/test/deprecation_test.rb +++ b/activesupport/test/deprecation_test.rb @@ -143,7 +143,7 @@ class DeprecationTest < ActiveSupport::TestCase stderr_output = capture(:stderr) { assert_nil behavior.call('Some error!', ['call stack!']) } - assert stderr_output.blank? + assert stderr_output.empty? end def test_deprecated_instance_variable_proxy diff --git a/activesupport/test/evented_file_update_checker_test.rb b/activesupport/test/evented_file_update_checker_test.rb index bc3f77bd54..2cb2d8167f 100644 --- a/activesupport/test/evented_file_update_checker_test.rb +++ b/activesupport/test/evented_file_update_checker_test.rb @@ -11,7 +11,7 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase end def new_checker(files = [], dirs = {}, &block) - ActiveSupport::EventedFileUpdateChecker.new(files, dirs, &block).tap do + ActiveSupport::EventedFileUpdateChecker.new(files, dirs, &block).tap do |c| wait end end @@ -34,6 +34,48 @@ class EventedFileUpdateCheckerTest < ActiveSupport::TestCase super wait end + + test 'notifies forked processes' do + FileUtils.touch(tmpfiles) + + checker = new_checker(tmpfiles) { } + assert !checker.updated? + + # Pipes used for flow controll across fork. + boot_reader, boot_writer = IO.pipe + touch_reader, touch_writer = IO.pipe + + pid = fork do + assert checker.updated? + + # Clear previous check value. + checker.execute + assert !checker.updated? + + # Fork is booted, ready for file to be touched + # notify parent process. + boot_writer.write("booted") + + # Wait for parent process to signal that file + # has been touched. + IO.select([touch_reader]) + + assert checker.updated? + end + + assert pid + + # Wait for fork to be booted before touching files. + IO.select([boot_reader]) + touch(tmpfiles) + + # Notify fork that files have been touched. + touch_writer.write("touched") + + assert checker.updated? + + Process.wait(pid) + end end class EventedFileUpdateCheckerPathHelperTest < ActiveSupport::TestCase diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb index f2fc456f4b..887ef1681d 100644 --- a/activesupport/test/json/decoding_test.rb +++ b/activesupport/test/json/decoding_test.rb @@ -1,8 +1,11 @@ require 'abstract_unit' require 'active_support/json' require 'active_support/time' +require 'time_zone_test_helpers' class TestJSONDecoding < ActiveSupport::TestCase + include TimeZoneTestHelpers + class Foo def self.json_create(object) "Foo" @@ -24,10 +27,11 @@ class TestJSONDecoding < ActiveSupport::TestCase %(["2007-01-01 01:12:34 Z"]) => [Time.utc(2007, 1, 1, 1, 12, 34)], %(["2007-01-01 01:12:34 Z", "2007-01-01 01:12:35 Z"]) => [Time.utc(2007, 1, 1, 1, 12, 34), Time.utc(2007, 1, 1, 1, 12, 35)], # no time zone - %({"a": "2007-01-01 01:12:34"}) => {'a' => "2007-01-01 01:12:34"}, + %({"a": "2007-01-01 01:12:34"}) => {'a' => Time.new(2007, 1, 1, 1, 12, 34, "-05:00")}, # invalid date %({"a": "1089-10-40"}) => {'a' => "1089-10-40"}, # xmlschema date notation + %({"a": "2009-08-10T19:01:02"}) => {'a' => Time.new(2009, 8, 10, 19, 1, 2, "-04:00")}, %({"a": "2009-08-10T19:01:02Z"}) => {'a' => Time.utc(2009, 8, 10, 19, 1, 2)}, %({"a": "2009-08-10T19:01:02+02:00"}) => {'a' => Time.utc(2009, 8, 10, 17, 1, 2)}, %({"a": "2009-08-10T19:01:02-05:00"}) => {'a' => Time.utc(2009, 8, 11, 00, 1, 2)}, @@ -72,10 +76,12 @@ class TestJSONDecoding < ActiveSupport::TestCase TESTS.each_with_index do |(json, expected), index| test "json decodes #{index}" do - with_parse_json_times(true) do - silence_warnings do - assert_equal expected, ActiveSupport::JSON.decode(json), "JSON decoding \ - failed for #{json}" + with_tz_default 'Eastern Time (US & Canada)' do + with_parse_json_times(true) do + silence_warnings do + assert_equal expected, ActiveSupport::JSON.decode(json), "JSON decoding \ + failed for #{json}" + end end end end diff --git a/activesupport/test/json/encoding_test_cases.rb b/activesupport/test/json/encoding_test_cases.rb index 0159ba8606..e043fadf56 100644 --- a/activesupport/test/json/encoding_test_cases.rb +++ b/activesupport/test/json/encoding_test_cases.rb @@ -76,6 +76,10 @@ module JSONTest RegexpTests = [[ /^a/, '"(?-mix:^a)"' ], [/^\w{1,2}[a-z]+/ix, '"(?ix-m:^\\\\w{1,2}[a-z]+)"']] + URITests = [[ URI.parse('http://example.com'), %("http://example.com") ]] + + PathnameTests = [[ Pathname.new('lib/index.rb'), %("lib/index.rb") ]] + DateTests = [[ Date.new(2005,2,1), %("2005/02/01") ]] TimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]] DateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]] diff --git a/activesupport/test/key_generator_test.rb b/activesupport/test/key_generator_test.rb index f7e8e9a795..b60077460e 100644 --- a/activesupport/test/key_generator_test.rb +++ b/activesupport/test/key_generator_test.rb @@ -19,7 +19,7 @@ class KeyGeneratorTest < ActiveSupport::TestCase test "Generating a key of the default length" do derived_key = @generator.generate_key("some_salt") assert_kind_of String, derived_key - assert_equal OpenSSL::Digest::SHA1.new.block_length, derived_key.length, "Should have generated a key of the default size" + assert_equal 64, derived_key.length, "Should have generated a key of the default size" end test "Generating a key of an alternative length" do @@ -27,6 +27,21 @@ class KeyGeneratorTest < ActiveSupport::TestCase assert_kind_of String, derived_key assert_equal 32, derived_key.length, "Should have generated a key of the right size" end + + test "Expected results" do + # For any given set of inputs, this method must continue to return + # the same output: if it changes, any existing values relying on a + # key would break. + + expected = "b129376f68f1ecae788d7433310249d65ceec090ecacd4c872a3a9e9ec78e055739be5cc6956345d5ae38e7e1daa66f1de587dc8da2bf9e8b965af4b3918a122" + assert_equal expected, ActiveSupport::KeyGenerator.new("0" * 64).generate_key("some_salt").unpack('H*').first + + expected = "b129376f68f1ecae788d7433310249d65ceec090ecacd4c872a3a9e9ec78e055" + assert_equal expected, ActiveSupport::KeyGenerator.new("0" * 64).generate_key("some_salt", 32).unpack('H*').first + + expected = "cbea7f7f47df705967dc508f4e446fd99e7797b1d70011c6899cd39bbe62907b8508337d678505a7dc8184e037f1003ba3d19fc5d829454668e91d2518692eae" + assert_equal expected, ActiveSupport::KeyGenerator.new("0" * 64, iterations: 2).generate_key("some_salt").unpack('H*').first + end end class CachingKeyGeneratorTest < ActiveSupport::TestCase diff --git a/activesupport/test/logger_test.rb b/activesupport/test/logger_test.rb index 5a91420f1e..dfc5f3fdf4 100644 --- a/activesupport/test/logger_test.rb +++ b/activesupport/test/logger_test.rb @@ -143,12 +143,13 @@ class LoggerTest < ActiveSupport::TestCase def test_logger_silencing_works_for_broadcast another_output = StringIO.new - another_logger = Logger.new(another_output) + another_logger = ActiveSupport::Logger.new(another_output) - @logger.extend Logger.broadcast(another_logger) + @logger.extend ActiveSupport::Logger.broadcast(another_logger) @logger.debug "CORRECT DEBUG" - @logger.silence do + @logger.silence do |logger| + assert_kind_of ActiveSupport::Logger, logger @logger.debug "FAILURE" @logger.error "CORRECT ERROR" end @@ -166,10 +167,11 @@ class LoggerTest < ActiveSupport::TestCase another_output = StringIO.new another_logger = ::Logger.new(another_output) - @logger.extend Logger.broadcast(another_logger) + @logger.extend ActiveSupport::Logger.broadcast(another_logger) @logger.debug "CORRECT DEBUG" - @logger.silence do + @logger.silence do |logger| + assert_kind_of ActiveSupport::Logger, logger @logger.debug "FAILURE" @logger.error "CORRECT ERROR" end diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb index eb71369397..a1ff4c1d3e 100644 --- a/activesupport/test/message_encryptor_test.rb +++ b/activesupport/test/message_encryptor_test.rb @@ -15,7 +15,7 @@ class MessageEncryptorTest < ActiveSupport::TestCase end def setup - @secret = SecureRandom.hex(64) + @secret = SecureRandom.random_bytes(32) @verifier = ActiveSupport::MessageVerifier.new(@secret, :serializer => ActiveSupport::MessageEncryptor::NullSerializer) @encryptor = ActiveSupport::MessageEncryptor.new(@secret) @data = { :some => "data", :now => Time.local(2010) } @@ -51,7 +51,7 @@ class MessageEncryptorTest < ActiveSupport::TestCase def test_alternative_serialization_method prev = ActiveSupport.use_standard_json_time_format ActiveSupport.use_standard_json_time_format = true - encryptor = ActiveSupport::MessageEncryptor.new(SecureRandom.hex(64), SecureRandom.hex(64), :serializer => JSONSerializer.new) + encryptor = ActiveSupport::MessageEncryptor.new(SecureRandom.random_bytes(32), SecureRandom.random_bytes(128), :serializer => JSONSerializer.new) message = encryptor.encrypt_and_sign({ :foo => 123, 'bar' => Time.utc(2010) }) exp = { "foo" => 123, "bar" => "2010-01-01T00:00:00.000Z" } assert_equal exp, encryptor.decrypt_and_verify(message) diff --git a/guides/assets/images/getting_started/template_is_missing_articles_new.png b/guides/assets/images/getting_started/template_is_missing_articles_new.png Binary files differindex 4e636d09ff..f4f054f3c6 100644 --- a/guides/assets/images/getting_started/template_is_missing_articles_new.png +++ b/guides/assets/images/getting_started/template_is_missing_articles_new.png diff --git a/guides/source/5_0_release_notes.md b/guides/source/5_0_release_notes.md index 71861d079e..bdd1c2cc4e 100644 --- a/guides/source/5_0_release_notes.md +++ b/guides/source/5_0_release_notes.md @@ -40,7 +40,7 @@ Major Features Action Cable is a new framework in Rails 5. It seamlessly integrates [WebSockets](https://en.wikipedia.org/wiki/WebSocket) with the rest of your -Rails application. +Rails application. Action Cable allows for real-time features to be written in Ruby in the same style and form as the rest of your Rails application, while still being @@ -49,17 +49,42 @@ client-side JavaScript framework and a server-side Ruby framework. You have access to your full domain model written with Active Record or your ORM of choice. -See the [Active Cable Overview](action_cable_overview.html) guide for more +See the [Action Cable Overview](action_cable_overview.html) guide for more information. -### Rails API -[Pull Request](https://github.com/rails/rails/pull/19832) +### API Applications -ToDo... +Rails can now be used to create slimmed down API only applications. +This is useful for creating and serving APIs similar to [Twitter](https://dev.twitter.com) or [GitHub](http://developer.github.com) API, +that can be used to serve public facing, as well as, for custom applications. + +You can generate a new api Rails app using: + +```bash +$ rails new my_api --api +``` + +This will do three main things: + +- Configure your application to start with a more limited set of middleware + than normal. Specifically, it will not include any middleware primarily useful + for browser applications (like cookies support) by default. +- Make `ApplicationController` inherit from `ActionController::API` instead of + `ActionController::Base`. As with middleware, this will leave out any Action + Controller modules that provide functionalities primarily used by browser + applications. +- Configure the generators to skip generating views, helpers and assets when + you generate a new resource. + +The application provides a base for APIs, +that can then be [configured to pull in functionality](api_app.html) as suitable for the application's needs. + +See the [Using Rails for API-only Applications](api_app.html) guide for more +information. ### Active Record attributes API -Defines an attribute with a type on a model. It will override the type of existing attributes if needed. +Defines an attribute with a type on a model. It will override the type of existing attributes if needed. This allows control over how values are converted to and from SQL when assigned to a model. It also changes the behavior of values passed to `ActiveRecord::Base.where`, which lets use our domain objects across much of Active Record, without having to rely on implementation details or monkey patching. @@ -91,7 +116,7 @@ class StoreListing < ActiveRecord::Base attribute :price_in_cents, :integer # custom type attribute :my_string, :string, default: "new default" # default value attribute :my_default_proc, :datetime, default: -> { Time.now } # default value - attribute :field_without_db_column, :integer, array: true + attribute :field_without_db_column, :integer, array: true end # after @@ -107,32 +132,45 @@ model.attributes #=> {field_without_db_column: [1, 2, 3]} You can define your own custom types, as long as they respond to the methods defined on the value type. The method +deserialize+ or +cast+ will be called on your type object, with raw input from the -database or from your controllers. This is useful, for example, when doing custom conversion, +database or from your controllers. This is useful, for example, when doing custom conversion, like Money data. **Querying:** When `ActiveRecord::Base.where` is called, it will use the type defined by the model class to convert the value to SQL, -calling +serialize+ on your type object. +calling +serialize+ on your type object. -This gives the objects ability to specify, how to convert values when performing SQL queries. +This gives the objects ability to specify, how to convert values when performing SQL queries. **Dirty Tracking:** The type of an attribute is given the opportunity to change how dirty -tracking is performed. - +tracking is performed. + See its [documentation](http://api.rubyonrails.org/classes/ActiveRecord/Attributes/ClassMethods.html) for a detailed write up. ### Test Runner -[Pull Request](https://github.com/rails/rails/pull/19216) -ToDo... +A new test runner has been introduced to enhance the capabilities of running tests from Rails. +To use this test runner simply type `bin/rails test`. + +Test Runner is inspired from `RSpec`, `minitest-reporters`, `maxitest` and others. +It includes some of these notable advancements: +- Run a single test using line number of test. +- Run multiple tests pinpointing to line number of tests. +- Improved failure messages, which also add ease of re-running failed tests. +- Fail fast using `-f` option, to stop tests immediately on occurrence of failure, +instead of waiting for the suite to complete. +- Defer test output until the end of a full test run using the `-d` option. +- Complete exception backtrace output using `-b` option. +- Integration with `Minitest` to allow options like `-s` for test seed data, +`-n` for running specific test by name, `-v` for better verbose output and so forth. +- Colored test output Railties -------- @@ -219,6 +257,17 @@ Please refer to the [Changelog][railties] for detailed changes. Spring to watch additional common files. ([commit](https://github.com/rails/rails/commit/b04d07337fd7bc17e88500e9d6bcd361885a45f8)) +* Added `--skip-action-mailer` to skip Action Mailer while generating new app. + ([Pull Request](https://github.com/rails/rails/pull/18288)) + +* Removed `tmp/sessions` directory and the clear rake task associated with it. + ([Pull Request](https://github.com/rails/rails/pull/18314)) + +* Changed `_form.html.erb` generated by scaffold generator to use local variables. + ([Pull Request](https://github.com/rails/rails/pull/13434)) + +* Disabled autoloading of classes in production environment. + ([commit](https://github.com/rails/rails/commit/a71350cae0082193ad8c66d65ab62e8bb0b7853b)) Action Pack ----------- @@ -311,6 +360,15 @@ Please refer to the [Changelog][action-pack] for detailed changes. * Deprecated `:controller` and `:action` path parameters. ([Pull Request](https://github.com/rails/rails/pull/23980)) +* Deprecated env method on controller instances. + ([commit](https://github.com/rails/rails/commit/05934d24aff62d66fc62621aa38dae6456e276be)) + +* `ActionDispatch::ParamsParser` is deprecated and was removed from the + middleware stack. To configure the parameter parsers use + `ActionDispatch::Request.parameter_parsers=`. + ([commit](https://github.com/rails/rails/commit/38d2bf5fd1f3e014f2397898d371c339baa627b1), + [commit](https://github.com/rails/rails/commit/5ed38014811d4ce6d6f957510b9153938370173b)) + ### Notable changes * Added `ActionController::Renderer` to render arbitrary templates @@ -382,6 +440,24 @@ Please refer to the [Changelog][action-pack] for detailed changes. at the controller level. ([Pull Request](https://github.com/rails/rails/pull/24866)) +* Discarded flash messages get removed before storing into session. + ([Pull Request](https://github.com/rails/rails/pull/18721)) + +* Added support for passing collection of records to `fresh_when` and + `stale?`. + ([Pull Request](https://github.com/rails/rails/pull/18374)) + +* `ActionController::Live` became an `ActiveSupport::Concern`. That + means it can't be just included in other modules without extending + them with `ActiveSupport::Concern` or `ActionController::Live` + won't take effect in production. Some people may be using another + module to include some special `Warden`/`Devise` authentication + failure handling code as well since the middleware can't catch a + `:warden` thrown by a spawned thread which is the case when using + `ActionController::Live`. + ([More details in this issue](https://github.com/rails/rails/issues/25581)) + + Action View ------------- @@ -401,13 +477,6 @@ Please refer to the [Changelog][action-view] for detailed changes. supported by I18n. ([Pull Request](https://github.com/rails/rails/pull/20019)) -### Deprecations - -* Deprecated `datetime_field` and `datetime_field_tag` helpers. - Datetime input type was removed from HTML specification. - One can use `datetime_local_field` and `datetime_local_field_tag` instead. - ([Pull Request](https://github.com/rails/rails/pull/24385)) - ### Notable Changes * Changed the default template handler from `ERB` to `Raw`. @@ -424,6 +493,13 @@ Please refer to the [Changelog][action-view] for detailed changes. button on submit to prevent double submits. ([Pull Request](https://github.com/rails/rails/pull/21135)) +* Partial template name no longer has to be a valid Ruby identifier. + ([commit](https://github.com/rails/rails/commit/da9038e)) + +* The `datetime_tag` helper now generates an input tag with the type of + `datetime-local`. + ([Pull Request](https://github.com/rails/rails/pull/25469)) + Action Mailer ------------- @@ -509,8 +585,10 @@ Please refer to the [Changelog][active-record] for detailed changes. [activemodel-serializers-xml](https://github.com/rails/activemodel-serializers-xml) gem. ([Pull Request](https://github.com/rails/rails/pull/21161)) -* Removed support for the legacy `mysql` database adapter from core. It will - live on in a separate gem for now, but most users should just use `mysql2`. +* Removed support for the legacy `mysql` database adapter from core. Most users should + be able to use `mysql2`. It will be converted to a separate gem when when we find someone + to maintain it. ([Pull Request 1](https://github.com/rails/rails/pull/22642)], + [Pull Request 2](https://github.com/rails/rails/pull/22715)) * Removed support for the `protected_attributes` gem. ([commit](https://github.com/rails/rails/commit/f4fbc0301021f13ae05c8e941c8efc4ae351fdf9)) @@ -518,6 +596,9 @@ Please refer to the [Changelog][active-record] for detailed changes. * Removed support for PostgreSQL versions below 9.1. ([Pull Request](https://github.com/rails/rails/pull/23434)) +* Removed support for `activerecord-deprecated_finders` gem. + ([commit](https://github.com/rails/rails/commit/78dab2a8569408658542e462a957ea5a35aa4679)) + ### Deprecations * Deprecated passing a class as a value in a query. Users should pass strings @@ -579,6 +660,14 @@ Please refer to the [Changelog][active-record] for detailed changes. `use_transactional_tests` for more clarity. ([Pull Request](https://github.com/rails/rails/pull/19282)) +* Deprecated passing a column to `ActiveRecord::Connection#quote`. + ([commit](https://github.com/rails/rails/commit/7bb620869725ad6de603f6a5393ee17df13aa96c)) + +* Added an option `end` to `find_in_batches` that complements the `start` + parameter to specify where to stop batch processing. + ([Pull Request](https://github.com/rails/rails/pull/12257)) + + ### Notable changes * Added a `foreign_key` option to `references` while creating the table. @@ -682,10 +771,38 @@ Please refer to the [Changelog][active-record] for detailed changes. with comments stored in database metadata for PostgreSQL & MySQL. ([Pull Request](https://github.com/rails/rails/pull/22911)) -* Added prepared statements support to `mysql2` adapter, for mysql2 0.4.4+, - Previously this was only supported on the deprecated `mysql` legacy adapter. +* Added prepared statements support to `mysql2` adapter, for mysql2 0.4.4+, + Previously this was only supported on the deprecated `mysql` legacy adapter. To enable, set `prepared_statements: true` in config/database.yml. - ([Pull Request](https://github.com/rails/rails/pull/23461)) + ([Pull Request](https://github.com/rails/rails/pull/23461)) + +* Added ability to call `ActionRecord::Relation#update` on relation objects + which will run validations on callbacks on all objects in the relation. + ([Pull Request](https://github.com/rails/rails/pull/11898)) + +* Added `:touch` option to the `save` method so that records can be saved without + updating timestamps. + ([Pull Request](https://github.com/rails/rails/pull/18225)) + +* Added expression indexes and operator classes support for PostgreSQL. + ([commit](https://github.com/rails/rails/commit/edc2b7718725016e988089b5fb6d6fb9d6e16882)) + +* Added `:index_errors` option to add indexes to errors of nested attributes. + ([Pull Request](https://github.com/rails/rails/pull/19686)) + +* Added support for bidirectional destroy dependencies. + ([Pull Request](https://github.com/rails/rails/pull/18548)) + +* Added support for `after_commit` callbacks in transactional tests. + ([Pull Request](https://github.com/rails/rails/pull/18458)) + +* Added `foreign_key_exists?` method to see if a foreign key exists on a table + or not. + ([Pull Request](https://github.com/rails/rails/pull/18662)) + +* Added `:time` option to `touch` method to touch records with different time + than the current time. + ([Pull Request](https://github.com/rails/rails/pull/18956)) Active Model ------------ @@ -702,6 +819,9 @@ Please refer to the [Changelog][active-model] for detailed changes. [activemodel-serializers-xml](https://github.com/rails/activemodel-serializers-xml) gem. ([Pull Request](https://github.com/rails/rails/pull/21161)) +* Removed `ActionController::ModelNaming` module. + ([Pull Request](https://github.com/rails/rails/pull/18194)) + ### Deprecations * Deprecated returning `false` as a way to halt Active Model and @@ -737,6 +857,9 @@ Please refer to the [Changelog][active-model] for detailed changes. * Validate multiple contexts on `valid?` and `invalid?` at once. ([Pull Request](https://github.com/rails/rails/pull/21069)) +* Change `validates_acceptance_of` to accept `true` as default value + apart from `1`. + ([Pull Request](https://github.com/rails/rails/pull/18439)) Active Job ----------- @@ -808,6 +931,9 @@ Please refer to the [Changelog][active-support] for detailed changes. * Removed deprecated `ThreadSafe::Cache`. Use `Concurrent::Map` instead. ([Pull Request](https://github.com/rails/rails/pull/21679)) +* Removed `Object#itself` as it is implemented in Ruby 2.2. + ([Pull Request](https://github.com/rails/rails/pull/18244)) + ### Deprecations * Deprecated `MissingSourceFile` in favor of `LoadError`. @@ -929,6 +1055,16 @@ Please refer to the [Changelog][active-support] for detailed changes. * `ActiveSupport::Duration` now supports ISO8601 formatting and parsing. ([Pull Request](https://github.com/rails/rails/pull/16917)) +* `ActiveSupport::JSON.decode` now supports parsing ISO8601 local times when + `parse_json_times` is enabled. + ([Pull Request](https://github.com/rails/rails/pull/23011)) + +* `ActiveSupport::JSON.decode` now return `Date` objects for date strings. + ([Pull Request](https://github.com/rails/rails/pull/23011)) + +* Added ability to `TaggedLogging` to allow loggers to be instantiated multiple + times so that they don't share tags with each other. + ([Pull Request](https://github.com/rails/rails/pull/9065)) Credits ------- diff --git a/guides/source/action_cable_overview.md b/guides/source/action_cable_overview.md index 16aa9438a2..2308befd12 100644 --- a/guides/source/action_cable_overview.md +++ b/guides/source/action_cable_overview.md @@ -6,8 +6,10 @@ incorporate real-time features into your Rails application. After reading this guide, you will know: +* What Action Cable is and its integration on backend and frontend * How to setup Action Cable * How to setup channels +* Deployment and Architecture setup for running Action Cable Introduction ------------ @@ -568,12 +570,13 @@ environment configuration files. ### Other Configurations -The other common option to configure is the log tags applied to the -per-connection logger. Here's close to what we're using in Basecamp: +The other common option to configure, is the log tags applied to the +per-connection logger. Here's an example that uses +the user account id if available, else "no-account" while tagging: ```ruby config.action_cable.log_tags = [ - -> request { request.env['bc.account_id'] || "no-account" }, + -> request { request.env['user_account_id'] || "no-account" }, :action_cable, -> request { request.uuid } ] diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index e6cd00728d..58362fea58 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -815,7 +815,7 @@ In every controller there are two accessor methods pointing to the request and t ### The `request` Object -The request object contains a lot of useful information about the request coming in from the client. To get a full list of the available methods, refer to the [API documentation](http://api.rubyonrails.org/classes/ActionDispatch/Request.html). Among the properties that you can access on this object are: +The request object contains a lot of useful information about the request coming in from the client. To get a full list of the available methods, refer to the [Rails API documentation](http://api.rubyonrails.org/classes/ActionDispatch/Request.html) and [Rack Documentation](http://www.rubydoc.info/github/rack/rack/Rack/Request). Among the properties that you can access on this object are: | Property of `request` | Purpose | | ----------------------------------------- | -------------------------------------------------------------------------------- | @@ -837,7 +837,7 @@ Rails collects all of the parameters sent along with the request in the `params` ### The `response` Object -The response object is not usually used directly, but is built up during the execution of the action and rendering of the data that is being sent back to the user, but sometimes - like in an after filter - it can be useful to access the response directly. Some of these accessor methods also have setters, allowing you to change their values. +The response object is not usually used directly, but is built up during the execution of the action and rendering of the data that is being sent back to the user, but sometimes - like in an after filter - it can be useful to access the response directly. Some of these accessor methods also have setters, allowing you to change their values. To get a full list of the available methods, refer to the [Rails API documentation](http://api.rubyonrails.org/classes/ActionDispatch/Response.html) and [Rack Documentation](http://www.rubydoc.info/github/rack/rack/Rack/Response). | Property of `response` | Purpose | | ---------------------- | --------------------------------------------------------------------------------------------------- | diff --git a/guides/source/active_job_basics.md b/guides/source/active_job_basics.md index d6de92ace6..c9f70dc87b 100644 --- a/guides/source/active_job_basics.md +++ b/guides/source/active_job_basics.md @@ -62,12 +62,12 @@ $ bin/rails generate job guests_cleanup --queue urgent ``` If you don't want to use a generator, you could create your own file inside of -`app/jobs`, just make sure that it inherits from `ActiveJob::Base`. +`app/jobs`, just make sure that it inherits from `ApplicationJob`. Here's what a job looks like: ```ruby -class GuestsCleanupJob < ActiveJob::Base +class GuestsCleanupJob < ApplicationJob queue_as :default def perform(*guests) @@ -141,7 +141,7 @@ end You can also configure your backend on a per job basis. ```ruby -class GuestsCleanupJob < ActiveJob::Base +class GuestsCleanupJob < ApplicationJob self.queue_adapter = :resque #.... end @@ -171,7 +171,7 @@ Most of the adapters support multiple queues. With Active Job you can schedule the job to run on a specific queue: ```ruby -class GuestsCleanupJob < ActiveJob::Base +class GuestsCleanupJob < ApplicationJob queue_as :low_priority #.... end @@ -189,7 +189,7 @@ module YourApp end # app/jobs/guests_cleanup_job.rb -class GuestsCleanupJob < ActiveJob::Base +class GuestsCleanupJob < ApplicationJob queue_as :low_priority #.... end @@ -212,7 +212,7 @@ module YourApp end # app/jobs/guests_cleanup_job.rb -class GuestsCleanupJob < ActiveJob::Base +class GuestsCleanupJob < ApplicationJob queue_as :low_priority #.... end @@ -234,7 +234,7 @@ block will be executed in the job context (so you can access `self.arguments`) and you must return the queue name: ```ruby -class ProcessVideoJob < ActiveJob::Base +class ProcessVideoJob < ApplicationJob queue_as do video = self.arguments.first if video.owner.premium? @@ -274,7 +274,7 @@ trigger logic during the life cycle of a job. ### Usage ```ruby -class GuestsCleanupJob < ActiveJob::Base +class GuestsCleanupJob < ApplicationJob queue_as :default before_enqueue do |job| @@ -331,7 +331,7 @@ Active Record objects to your job instead of class/id pairs, which you then have to manually deserialize. Before, jobs would look like this: ```ruby -class TrashableCleanupJob < ActiveJob::Base +class TrashableCleanupJob < ApplicationJob def perform(trashable_class, trashable_id, depth) trashable = trashable_class.constantize.find(trashable_id) trashable.cleanup(depth) @@ -342,7 +342,7 @@ end Now you can simply do: ```ruby -class TrashableCleanupJob < ActiveJob::Base +class TrashableCleanupJob < ApplicationJob def perform(trashable, depth) trashable.cleanup(depth) end @@ -360,7 +360,7 @@ Active Job provides a way to catch exceptions raised during the execution of the job: ```ruby -class GuestsCleanupJob < ActiveJob::Base +class GuestsCleanupJob < ApplicationJob queue_as :default rescue_from(ActiveRecord::RecordNotFound) do |exception| diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md index fb5d2065d3..a7975c7772 100644 --- a/guides/source/active_record_callbacks.md +++ b/guides/source/active_record_callbacks.md @@ -399,7 +399,7 @@ By using the `after_commit` callback we can account for this case. ```ruby class PictureFile < ApplicationRecord - after_commit :delete_picture_file_from_disk, on: [:destroy] + after_commit :delete_picture_file_from_disk, on: :destroy def delete_picture_file_from_disk if File.exist?(filepath) @@ -409,7 +409,7 @@ class PictureFile < ApplicationRecord end ``` -NOTE: the `:on` option specifies when a callback will be fired. If you +NOTE: The `:on` option specifies when a callback will be fired. If you don't supply the `:on` option the callback will fire for every action. Since using `after_commit` callback only on create, update or delete is diff --git a/guides/source/active_record_postgresql.md b/guides/source/active_record_postgresql.md index dee64e6439..d7e35490ef 100644 --- a/guides/source/active_record_postgresql.md +++ b/guides/source/active_record_postgresql.md @@ -503,9 +503,9 @@ second = Article.create! title: "Brace yourself", status: "draft", published_at: 1.month.ago -Article.count # => 1 -first.archive! Article.count # => 2 +first.archive! +Article.count # => 1 ``` NOTE: This application only cares about non-archived `Articles`. A view also diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md index 565c87c4b5..e0b6f2f820 100644 --- a/guides/source/active_support_core_extensions.md +++ b/guides/source/active_support_core_extensions.md @@ -954,7 +954,8 @@ class A class_attribute :x, instance_reader: false end -A.new.x = 1 # NoMethodError +A.new.x = 1 +A.new.x # NoMethodError ``` For convenience `class_attribute` also defines an instance predicate which is the double negation of what the instance reader returns. In the examples above it would be called `x?`. diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md index 5b34330936..34b9c0d2ca 100644 --- a/guides/source/api_documentation_guidelines.md +++ b/guides/source/api_documentation_guidelines.md @@ -20,7 +20,7 @@ The [Rails API documentation](http://api.rubyonrails.org) is generated with in the rails root directory, run `bundle install` and execute: ```bash - ./bin/rails rdoc + bundle exec rake rdoc ``` Resulting HTML files can be found in the ./doc/rdoc directory. diff --git a/guides/source/configuring.md b/guides/source/configuring.md index b3d3b2c681..59cf412c5f 100644 --- a/guides/source/configuring.md +++ b/guides/source/configuring.md @@ -94,6 +94,8 @@ application. Accepts a valid week day symbol (e.g. `:monday`). * `config.eager_load_paths` accepts an array of paths from which Rails will eager load on boot if cache classes is enabled. Defaults to every folder in the `app` directory of the application. +* `config.enable_dependency_loading`: when true, enables autoloading, even if the application is eager loaded and `config.cache_classes` is set as true. Defaults to false. + * `config.encoding` sets up the application-wide encoding. Defaults to UTF-8. * `config.exceptions_app` sets the exceptions application invoked by the ShowException middleware when an exception happens. Defaults to `ActionDispatch::PublicExceptions.new(Rails.public_path)`. @@ -590,7 +592,7 @@ There are a few configuration options available in Active Support: * `config.active_support.bare` enables or disables the loading of `active_support/all` when booting Rails. Defaults to `nil`, which means `active_support/all` is loaded. -* `config.active_support.test_order` sets the order that test cases are executed. Possible values are `:random` and `:sorted`. This option is set to `:random` in `config/environments/test.rb` in newly-generated applications. If you have an application that does not specify a `test_order`, it will default to `:sorted`, *until* Rails 5.0, when the default will become `:random`. +* `config.active_support.test_order` sets the order in which the test cases are executed. Possible values are `:random` and `:sorted`. Defaults to `:random`. * `config.active_support.escape_html_entities_in_json` enables or disables the escaping of HTML entities in JSON serialization. Defaults to `true`. diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml index a06a53b250..627e23422d 100644 --- a/guides/source/documents.yaml +++ b/guides/source/documents.yaml @@ -85,9 +85,8 @@ description: This guide provides you with all you need to get started creating, enqueuing, and executing background jobs. - name: Testing Rails Applications - work_in_progress: true url: testing.html - description: This is a rather comprehensive guide to the various testing facilities in Rails. It covers everything from 'What is a test?' to the testing APIs. Enjoy. + description: This is a rather comprehensive guide to the various testing facilities in Rails. It covers everything from 'What is a test?' to Integration Testing. Enjoy. - name: Securing Rails Applications url: security.html @@ -198,7 +197,6 @@ name: Ruby on Rails 5.0 Release Notes url: 5_0_release_notes.html description: Release notes for Rails 5.0. - work_in_progress: true - name: Ruby on Rails 4.2 Release Notes url: 4_2_release_notes.html diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md index 65fdd7ca0d..b0d3953cbd 100644 --- a/guides/source/getting_started.md +++ b/guides/source/getting_started.md @@ -299,9 +299,6 @@ Rails.application.routes.draw do get 'welcome/index' # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html - - # Serve websocket cable requests in-process - # mount ActionCable.server => '/cable' end ``` @@ -316,10 +313,6 @@ It should look something like the following: Rails.application.routes.draw do get 'welcome/index' - # For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html - - # Serve websocket cable requests in-process - # mount ActionCable.server => '/cable' root 'welcome#index' end ``` @@ -462,7 +455,7 @@ available, Rails will raise an exception. In the above image, the bottom line has been truncated. Let's see what the full error message looks like: ->Missing template articles/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views" +>ArticlesController#new is missing a template for this request format and variant. request.formats: ["text/html"] request.variant: [] NOTE! For XHR/Ajax or API requests, this action would normally respond with 204 No Content: an empty white screen. Since you're loading it in a web browser, we assume that you expected to actually render a template, not… nothing, so we're showing an error to be extra-clear. If you expect 204 No Content, carry on. That's what you'll get from an XHR or API request. Give it a shot. That's quite a lot of text! Let's quickly go through and understand what each part of it means. @@ -481,9 +474,9 @@ us what _template handlers_ could be used to render our template. `:erb` is most commonly used for HTML templates, `:builder` is used for XML templates, and `:coffee` uses CoffeeScript to build JavaScript templates. -The final part of this message tells us where Rails has looked for the templates. -Templates within a basic Rails application like this are kept in a single -location, but in more complex applications it could be many different paths. +The message also contains `request.formats` which specifies the format of template to be +served in response. It is set to `text/html` as we requested this page via browser, so Rails +is looking for an HTML template. The simplest template that would work in this case would be one located at `app/views/articles/new.html.erb`. The extension of this file name is important: @@ -491,7 +484,9 @@ the first extension is the _format_ of the template, and the second extension is the _handler_ that will be used. Rails is attempting to find a template called `articles/new` within `app/views` for the application. The format for this template can only be `html` and the handler must be one of `erb`, -`builder` or `coffee`. Because you want to create a new HTML form, you will be +`builder` or `coffee`. `:erb` is most commonly used for HTML templates, `:builder` is +used for XML templates, and `:coffee` uses CoffeeScript to build JavaScript templates. +Because you want to create a new HTML form, you will be using the `ERB` language which is designed to embed Ruby in HTML. Therefore the file should be called `articles/new.html.erb` and needs to be @@ -611,9 +606,11 @@ class ArticlesController < ApplicationController end ``` -If you re-submit the form now, you'll see another familiar error: a template is -missing. That's ok, we can ignore that for now. What the `create` action should -be doing is saving our new article to the database. +If you re-submit the form now, you may not see any change on the page. Don't worry! +This is because Rails by default returns `204 No Content` response for an action if +we don't specify what the response should be. We just added the `create` action +but didn't specify anything about how the response should be. In this case, the +`create` action should save our new article to the database. When a form is submitted, the fields of the form are sent to Rails as _parameters_. These parameters can then be referenced inside the controller diff --git a/guides/source/testing.md b/guides/source/testing.md index 050bdda9e3..e8dc6ffe2a 100644 --- a/guides/source/testing.md +++ b/guides/source/testing.md @@ -146,18 +146,28 @@ Let us run this newly added test (where `6` is the number of line where the test ```bash $ bin/rails test test/models/article_test.rb:6 +Run options: --seed 44656 + +# Running: + F -Finished tests in 0.044632s, 22.4054 tests/s, 22.4054 assertions/s. +Failure: +ArticleTest#test_should_not_save_article_without_title [/path/to/blog/test/models/article_test.rb:6]: +Expected true to be nil or false + + +bin/rails test test/models/article_test.rb:6 - 1) Failure: -test_should_not_save_article_without_title(ArticleTest) [test/models/article_test.rb:6]: -Failed assertion, no message given. -1 tests, 1 assertions, 1 failures, 0 errors, 0 skips + +Finished in 0.023918s, 41.8090 runs/s, 41.8090 assertions/s. + +1 runs, 1 assertions, 1 failures, 0 errors, 0 skips + ``` -In the output, `F` denotes a failure. You can see the corresponding trace shown under `1)` along with the name of the failing test. The next few lines contain the stack trace followed by a message that mentions the actual value and the expected value by the assertion. The default assertion messages provide just enough information to help pinpoint the error. To make the assertion failure message more readable, every assertion provides an optional message parameter, as shown here: +In the output, `F` denotes a failure. You can see the corresponding trace shown under `Failure` along with the name of the failing test. The next few lines contain the stack trace followed by a message that mentions the actual value and the expected value by the assertion. The default assertion messages provide just enough information to help pinpoint the error. To make the assertion failure message more readable, every assertion provides an optional message parameter, as shown here: ```ruby test "should not save article without title" do @@ -169,8 +179,8 @@ end Running this test shows the friendlier assertion message: ```bash - 1) Failure: -test_should_not_save_article_without_title(ArticleTest) [test/models/article_test.rb:6]: +Failure: +ArticleTest#test_should_not_save_article_without_title [/path/to/blog/test/models/article_test.rb:6]: Saved the article without a title ``` @@ -186,11 +196,15 @@ Now the test should pass. Let us verify by running the test again: ```bash $ bin/rails test test/models/article_test.rb:6 +Run options: --seed 31252 + +# Running: + . -Finished tests in 0.047721s, 20.9551 tests/s, 20.9551 assertions/s. +Finished in 0.027476s, 36.3952 runs/s, 36.3952 assertions/s. -1 tests, 1 assertions, 0 failures, 0 errors, 0 skips +1 runs, 1 assertions, 0 failures, 0 errors, 0 skips ``` Now, if you noticed, we first wrote a test which fails for a desired @@ -215,16 +229,25 @@ Now you can see even more output in the console from running the tests: ```bash $ bin/rails test test/models/article_test.rb -E +Run options: --seed 1808 + +# Running: + +.E + +Error: +ArticleTest#test_should_report_error: +NameError: undefined local variable or method `some_undefined_variable' for #<ArticleTest:0x007fee3aa71798> + test/models/article_test.rb:11:in `block in <class:ArticleTest>' + + +bin/rails test test/models/article_test.rb:9 + -Finished tests in 0.030974s, 32.2851 tests/s, 0.0000 assertions/s. - 1) Error: -test_should_report_error(ArticleTest): -NameError: undefined local variable or method `some_undefined_variable' for #<ArticleTest:0x007fe32e24afe0> - test/models/article_test.rb:10:in `block in <class:ArticleTest>' +Finished in 0.040609s, 49.2500 runs/s, 24.6250 assertions/s. -1 tests, 0 assertions, 0 failures, 1 errors, 0 skips +2 runs, 1 assertions, 0 failures, 1 errors, 0 skips ``` Notice the 'E' in the output. It denotes a test with error. @@ -342,17 +365,21 @@ documentation](http://docs.seattlerb.org/minitest). ### The Rails Test Runner -We can run all of our tests at once by using the `rails test` command. +We can run all of our tests at once by using the `bin/rails test` command. -Or we can run a single test by passing the `rails test` command the filename containing the test cases. +Or we can run a single test by passing the `bin/rails test` command the filename containing the test cases. ```bash $ bin/rails test test/models/article_test.rb -. +Run options: --seed 1559 -Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s. +# Running: -1 tests, 1 assertions, 0 failures, 0 errors, 0 skips +.. + +Finished in 0.027034s, 73.9810 runs/s, 110.9715 assertions/s. + +2 runs, 3 assertions, 0 failures, 0 errors, 0 skips ``` This will run all test methods from the test case. @@ -362,6 +389,10 @@ You can also run a particular test method from the test case by providing the ```bash $ bin/rails test test/models/article_test.rb -n test_the_truth +Run options: -n test_the_truth --seed 43583 + +# Running: + . Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s. @@ -372,7 +403,7 @@ Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s. You can also run a test at a specific line by providing the line number. ```bash -$ bin/rails test test/models/post_test.rb:44 # run specific test and line +$ bin/rails test test/models/article_test.rb:6 # run specific test and line ``` You can also run an entire directory of tests by providing the path to the directory. @@ -381,6 +412,38 @@ You can also run an entire directory of tests by providing the path to the direc $ bin/rails test test/controllers # run all tests from specific directory ``` +The test runner provides lot of other features too like failing fast, deferring test output +at the end of test run and so on. Check the documentation of the test runner as follows: + +```bash +$ bin/rails test -h +minitest options: + -h, --help Display this help. + -s, --seed SEED Sets random seed. Also via env. Eg: SEED=n rake + -v, --verbose Verbose. Show progress processing files. + -n, --name PATTERN Filter run on /regexp/ or string. + --exclude PATTERN Exclude /regexp/ or string from run. + +Known extensions: rails, pride + +Usage: bin/rails test [options] [files or directories] +You can run a single test by appending a line number to a filename: + + bin/rails test test/models/user_test.rb:27 + +You can run multiple files and directories at the same time: + + bin/rails test test/controllers test/integration/login_test.rb + +By default test failures and errors are reported inline during a run. + +Rails options: + -e, --environment ENV Run tests in the ENV environment + -b, --backtrace Show the complete backtrace + -d, --defer-output Output test failures and errors after the test run + -f, --fail-fast Abort test run on first failure or error + -c, --[no-]color Enable color in the output +``` The Test Database ----------------- @@ -529,7 +592,7 @@ Integration Testing Integration tests are used to test how various parts of your application interact. They are generally used to test important workflows within our application. -For creating Rails integration tests, we use the 'test/integration' directory for our application. Rails provides a generator to create an integration test skeleton for us. +For creating Rails integration tests, we use the `test/integration` directory for our application. Rails provides a generator to create an integration test skeleton for us. ```bash $ bin/rails generate integration_test user_flows @@ -685,9 +748,8 @@ Let's take a look at one such test, `test_should_get_index` from the file `artic # articles_controller_test.rb class ArticlesControllerTest < ActionDispatch::IntegrationTest test "should get index" do - get '/articles' + get articles_url assert_response :success - assert_includes @response.body, 'Articles' end end ``` @@ -695,7 +757,7 @@ end In the `test_should_get_index` test, Rails simulates a request on the action called `index`, making sure the request was successful and also ensuring that the right response body has been generated. -The `get` method kicks off the web request and populates the results into the `@response`. It accepts 4 arguments: +The `get` method kicks off the web request and populates the results into the `@response`. It can accept up to 6 arguments: * The action of the controller you are requesting. This can be in the form of a string or a route (i.e. `articles_url`). @@ -703,22 +765,26 @@ The `get` method kicks off the web request and populates the results into the `@ * `params`: option with a hash of request parameters to pass into the action (e.g. query string parameters or article variables). -* `session`: option with a hash of session variables to pass along with the request. +* `headers`: for setting the headers that will be passed with the request. + +* `env`: for customizing the request environment as needed. -* `flash`: option with a hash of flash values. +* `xhr`: whether the request is Ajax request or not. Can be set to true for marking the request as Ajax. + +* `as`: for encoding the request with different content type. Supports `:json` by default. All of these keyword arguments are optional. -Example: Calling the `:show` action, passing an `id` of 12 as the `params` and setting a `user_id` of 5 in the session: +Example: Calling the `:show` action, passing an `id` of 12 as the `params` and setting `HTTP_REFERER` header: ```ruby -get(:show, params: { id: 12 }, session: { user_id: 5 }) +get :show, params: { id: 12 }, headers: { "HTTP_REFERER" => "http://example.com/home" } ``` -Another example: Calling the `:view` action, passing an `id` of 12 as the `params`, this time with no session, but with a flash message. +Another example: Calling the `:update` action, passing an `id` of 12 as the `params` as an Ajax request. ```ruby -get(view_url, params: { id: 12 }, flash: { message: 'booya!' }) +patch update_url, params: { id: 12 }, xhr: true ``` NOTE: If you try running `test_should_create_article` test from `articles_controller_test.rb` it will fail on account of the newly added model level validation and rightly so. @@ -728,7 +794,7 @@ Let us modify `test_should_create_article` test in `articles_controller_test.rb` ```ruby test "should create article" do assert_difference('Article.count') do - post '/article', params: { article: { title: 'Some title' } } + post articles_url, params: { article: { body: 'Rails is awesome!', title: 'Hello Rails' } } end assert_redirected_to article_path(Article.last) @@ -759,7 +825,7 @@ To test AJAX requests, you can specify the `xhr: true` option to `get`, `post`, ```ruby test "ajax request" do - article = articles(:first) + article = articles(:one) get article_url(article), xhr: true assert_equal 'hello world', @response.body @@ -796,16 +862,14 @@ You also have access to three instance variables in your functional tests: [HTTP headers](http://tools.ietf.org/search/rfc2616#section-5.3) and [CGI variables](http://tools.ietf.org/search/rfc3875#section-4.1) -can be set directly on the `@request` instance variable: +can be passed as headers: ```ruby # setting an HTTP Header -@request.headers["Accept"] = "text/plain, text/html" -get articles_url # simulate the request with custom header +get articles_url, headers: "Content-Type" => "text/plain" # simulate the request with custom header # setting a CGI variable -@request.headers["HTTP_REFERER"] = "http://example.com/home" -post article_url # simulate the request with custom env variable +get articles_url, headers: "HTTP_REFERER" => "http://example.com/home" # simulate the request with custom env variable ``` ### Testing `flash` notices @@ -841,7 +905,7 @@ F Finished in 0.114870s, 8.7055 runs/s, 34.8220 assertions/s. 1) Failure: -ArticlesControllerTest#test_should_create_article [/Users/zzak/code/bench/sharedapp/test/controllers/articles_controller_test.rb:16]: +ArticlesControllerTest#test_should_create_article [/test/controllers/articles_controller_test.rb:16]: --- expected +++ actual @@ -1 +1 @@ @@ -890,7 +954,7 @@ Let's write a test for the `:show` action: ```ruby test "should show article" do article = articles(:one) - get '/article', params: { id: article.id } + get article_url(article) assert_response :success end ``` @@ -916,7 +980,7 @@ We can also add a test for updating an existing Article. test "should update article" do article = articles(:one) - patch '/article', params: { id: article.id, article: { title: "updated" } } + patch article_url(article), params: { article: { title: "updated" } } assert_redirected_to article_path(article) # Reload association to fetch updated data and assert that title is updated. @@ -959,7 +1023,7 @@ class ArticlesControllerTest < ActionDispatch::IntegrationTest end test "should update article" do - patch '/article', params: { id: @article.id, article: { title: "updated" } } + patch article_url(@article), params: { article: { title: "updated" } } assert_redirected_to article_path(@article) # Reload association to fetch updated data and assert that title is updated. @@ -980,8 +1044,8 @@ Sign in helper can be a good example: #test/test_helper.rb module SignInHelper - def sign_in(user) - session[:user_id] = user.id + def sign_in_as(user) + post sign_in_url(email: user.email, password: user.password) end end @@ -997,7 +1061,7 @@ class ProfileControllerTest < ActionDispatch::IntegrationTest test "should show profile" do # helper is now reusable from any controller test case - sign_in users(:david) + sign_in_as users(:david) get profile_url assert_response :success @@ -1243,8 +1307,8 @@ end This test is pretty simple and only asserts that the job get the work done as expected. -By default, `ActiveJob::TestCase` will set the queue adapter to `:test` so that -your jobs are performed inline. It will also ensure that all previously performed +By default, `ActiveJob::TestCase` will set the queue adapter to `:async` so that +your jobs are performed in an async fashion. It will also ensure that all previously performed and enqueued jobs are cleared before any test run so you can safely assume that no jobs have already been executed in the scope of each test. diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 82080c4def..c41876cd28 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -68,14 +68,16 @@ Don't forget to review the difference, to see if there were any unexpected chang Upgrading from Rails 4.2 to Rails 5.0 ------------------------------------- -### Ruby 2.2.2+ +For more information on changes made to Rails 5.0 please see the [release notes](5_0_release_notes.html). -From Ruby on Rails 5.0 onwards, Ruby 2.2.2+ is the only supported version. -Make sure you are on Ruby 2.2.2 version or greater, before you proceed. +### Ruby 2.2.2+ required -### Active Record models now inherit from ApplicationRecord by default +From Ruby on Rails 5.0 onwards, Ruby 2.2.2+ is the only supported Ruby version. +Make sure you are on Ruby 2.2.2 version or greater, before you proceed. -In Rails 4.2 an Active Record model inherits from `ActiveRecord::Base`. In Rails 5.0, +### Active Record Models Now Inherit from ApplicationRecord by Default + +In Rails 4.2, an Active Record model inherits from `ActiveRecord::Base`. In Rails 5.0, all models inherit from `ApplicationRecord`. `ApplicationRecord` is a new superclass for all app models, analogous to app @@ -83,7 +85,7 @@ controllers subclassing `ApplicationController` instead of `ActionController::Base`. This gives apps a single spot to configure app-wide model behavior. -When upgrading from Rails 4.2 to Rails 5.0 you need to create an +When upgrading from Rails 4.2 to Rails 5.0, you need to create an `application_record.rb` file in `app/models/` and add the following content: ``` @@ -92,7 +94,7 @@ class ApplicationRecord < ActiveRecord::Base end ``` -### Halting callback chains via `throw(:abort)` +### Halting Callback Chains via `throw(:abort)` In Rails 4.2, when a 'before' callback returns `false` in Active Record and Active Model, then the entire callback chain is halted. In other words, @@ -117,12 +119,12 @@ halted the chain when any value was returned. See [#17227](https://github.com/rails/rails/pull/17227) for more details. -### ActiveJob jobs now inherit from ApplicationJob by default +### ActiveJob Now Inherits from ApplicationJob by Default -In Rails 4.2 an ActiveJob inherits from `ActiveJob::Base`. In Rails 5.0 this +In Rails 4.2, an Active Job inherits from `ActiveJob::Base`. In Rails 5.0, this behavior has changed to now inherit from `ApplicationJob`. -When upgrading from Rails 4.2 to Rails 5.0 you need to create an +When upgrading from Rails 4.2 to Rails 5.0, you need to create an `application_job.rb` file in `app/jobs/` and add the following content: ``` @@ -134,6 +136,210 @@ Then make sure that all your job classes inherit from it. See [#19034](https://github.com/rails/rails/pull/19034) for more details. +### Rails Controller Testing + +`assigns` and `assert_template` have been extracted to the `rails-controller-testing` gem. To +continue using these methods in your controller tests, add `gem 'rails-controller-testing'` to +your Gemfile. + +If you are using Rspec for testing, please see the extra configuration required in the gem's +documentation. + +### Autoloading is Disabled After Booting in the Production Environment + +Autoloading is now disabled after booting in the production environment by +default. + +Eager loading the application is part of the boot process, so top-level +constants are fine and are still autoloaded, no need to require their files. + +Constants in deeper places only executed at runtime, like regular method bodies, +are also fine because the file defining them will have been eager loaded while booting. + +For the vast majority of applications this change needs no action. But in the +very rare event that your application needs autoloading while running in +production mode, set `Rails.application.config.enable_dependency_loading` to +true. + +### XML Serialization + +`ActiveModel::Serializers::Xml` has been extracted from Rails to the `activemodel-serializers-xml` +gem. To continue using XML serialization in your application, add `gem 'activemodel-serializers-xml'` +to your Gemfile. + +### Removed Support for Legacy `mysql` Database Adapter + +Rails 5 removes support for the legacy `mysql` database adapter. Most users should be able to +use `mysql2` instead. It will be converted to a separate gem when we find someone to maintain +it. + +### Removed Support for Debugger + +`debugger` is not supported by Ruby 2.2 which is required by Rails 5. Use `byebug` instead. + +### Use bin/rails for running tasks and tests + +Rails 5 adds the ability to run tasks and tests through `bin/rails` instead of rake. Generally +these changes are in parallel with rake, but some were ported over altogether. + +To use the new test runner simply type `bin/rails test`. + +`rake dev:cache` is now `rails dev:cache`. + +Run `bin/rails` to see the list of commands available. + +### `ActionController::Parameters` No Longer Inherits from `HashWithIndifferentAccess` + +Calling `params` in your application will now return an object instead of a hash. If your +parameters are already permitted, then you will not need to make any changes. If you are using `slice` +and other methods that depend on being able to read the hash regardless of `permitted?` you will +need to upgrade your application to first permit and then convert to a hash. + + params.permit([:proceed_to, :return_to]).to_h + +### `protect_from_forgery` Now Defaults to `prepend: false` + +`protect_from_forgery` defaults to `prepend: false` which means that it will be inserted into +the callback chain at the point in which you call it in your application. If you want +`protect_from_forgery` to always run first, then you should change your application to use +`protect_from_forgery prepend: true`. + +### Default Template Handler is Now RAW + +Files without a template handler in their extension will be rendered using the raw handler. +Previously Rails would render files using the ERB template handler. + +If you do not want your file to be handled via the raw handler, you should add an extension +to your file that can be parsed by the appropriate template handler. + +### Added Wildcard Matching for Template Dependencies + +You can now use wildcard matching for your template dependencies. For example, if you were +defining your templates as such: + +```erb +<% # Template Dependency: recordings/threads/events/subscribers_changed %> +<% # Template Dependency: recordings/threads/events/completed %> +<% # Template Dependency: recordings/threads/events/uncompleted %> +``` + +You can now just call the dependency once with a wildcard. + +```erb +<% # Template Dependency: recordings/threads/events/* %> +``` + +### Removed Support for `protected_attributes` Gem + +The `protected_attributes` gem is no longer supported in Rails 5. + +### Removed support for `activerecord-deprecated_finders` gem + +The `activerecord-deprecated_finders` gem is no longer supported in Rails 5. + +### `ActiveSupport::TestCase` Default Test Order is Now Random + +When tests are run in your application, the default order is now `:random` +instead of `:sorted`. Use the following config option to set it back to `:sorted`. + +```ruby +# config/environments/test.rb +Rails.application.configure do + config.active_support.test_order = :sorted +end +``` + +### `ActionController::Live` became a `Concern` + +If you include `ActionController::Live` in another module that is included in your controller, then you +should also extend the module with `ActiveSupport::Concern`. Alternatively, you can use the `self.included` hook +to include `ActionController::Live` directly to the controller once the `StreamingSupport` is included. + +This means that if your application used to have its own streaming module, the following code +would break in production mode: + +```ruby +# This is a work-around for streamed controllers performing authentication with Warden/Devise. +# See https://github.com/plataformatec/devise/issues/2332 +# Authenticating in the router is another solution as suggested in that issue +class StreamingSupport + include ActionController::Live # this won't work in production for Rails 5 + # extend ActiveSupport::Concern # unless you uncomment this line. + + def process(name) + super(name) + rescue ArgumentError => e + if e.message == 'uncaught throw :warden' + throw :warden + else + raise e + end + end +end +``` + +### New Framework Defaults + +#### Active Record `belongs_to` Required by Default Option + +`belongs_to` will now trigger a validation error by default if the association is not present. + +This can be turned off per-association with `optional: true`. + +This default will be automatically configured in new applications. If existing application +want to add this feature it will need to be turned on in an initializer. + + config.active_record.belongs_to_required_by_default = true + +#### Per-form CSRF Tokens + +Rails 5 now supports per-form CSRF tokens to mitigate against code-injection attacks with forms +created by JavaScript. With this option turned on, forms in your application will each have their +own CSRF token that is specified to the action and method for that form. + + config.action_controller.per_form_csrf_tokens = true + +#### Forgery Protection with Origin Check + +You can now configure your application to check if the HTTP `Origin` header should be checked +against the site's origin as an additional CSRF defense. Set the following in your config to +true: + + config.action_controller.forgery_protection_origin_check = true + +#### Allow Configuration of Action Mailer Queue Name + +The default mailer queue name is `mailers`. This configuration option allows you to globally change +the queue name. Set the following in your config: + + config.action_mailer.deliver_later_queue_name = :new_queue_name + +#### Support Fragment Caching in Action Mailer Views + +Set `config.action_mailer.perform_caching` in your config to determine whether your Action Mailer views +should support caching. + + config.action_mailer.perform_caching = true + +#### Configure the Output of `db:structure:dump` + +If you're using `schema_search_path` or other PostgreSQL extentions, you can control how the schema is +dumped. Set to `:all` to generate all dumps, or to `:schema_search_path` to generate from schema search path. + + config.active_record.dump_schemas = :all + +#### Configure SSL Options to Enable HSTS with Subdomains + +Set the following in your config to enable HSTS when using subdomains: + + config.ssl_options = { hsts: { subdomains: true } } + +#### Preserve Timezone of the Receiver + +When using Ruby 2.4, you can preserve the timezone of the receiver when calling `to_time`. + + ActiveSupport.to_time_preserves_timezone = false + Upgrading from Rails 4.1 to Rails 4.2 ------------------------------------- diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md index b8f1cda329..83fe6f56a4 100644 --- a/railties/CHANGELOG.md +++ b/railties/CHANGELOG.md @@ -1,3 +1,12 @@ +* Default `config.assets.quiet = true` in the development environment. Suppress + logging of `sprockets-rails` requests by default. + + *Kevin McPhillips* + +* Ensure `/rails/info` routes match in development for apps with a catch-all globbing route. + + *Nicholas Firth-McCoy* + * Added a shared section to `config/secrets.yml` that will be loaded for all environments. *DHH* diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb index f415a20833..5ee08d96e1 100644 --- a/railties/lib/rails/application/configuration.rb +++ b/railties/lib/rails/application/configuration.rb @@ -16,7 +16,7 @@ module Rails :railties_order, :relative_url_root, :secret_key_base, :secret_token, :ssl_options, :public_file_server, :session_options, :time_zone, :reload_classes_only_on_change, - :beginning_of_week, :filter_redirect, :x + :beginning_of_week, :filter_redirect, :x, :enable_dependency_loading attr_writer :log_level attr_reader :encoding, :api_only, :static_cache_control @@ -54,6 +54,7 @@ module Rails @api_only = false @debug_exception_response_format = nil @x = Custom.new + @enable_dependency_loading = false end def static_cache_control=(value) diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb index 4e24640510..daf3a24b16 100644 --- a/railties/lib/rails/application/finisher.rb +++ b/railties/lib/rails/application/finisher.rb @@ -21,10 +21,13 @@ module Rails initializer :add_builtin_route do |app| if Rails.env.development? - app.routes.append do + app.routes.prepend do get '/rails/info/properties' => "rails/info#properties", internal: true get '/rails/info/routes' => "rails/info#routes", internal: true get '/rails/info' => "rails/info#index", internal: true + end + + app.routes.append do get '/' => "rails/welcome#index", internal: true end end @@ -173,7 +176,7 @@ module Rails # Disable dependency loading during request cycle initializer :disable_dependency_loading do - if config.eager_load && config.cache_classes + if config.eager_load && config.cache_classes && !config.enable_dependency_loading ActiveSupport::Dependencies.unhook! end end diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb index f0a3289563..0835c09323 100644 --- a/railties/lib/rails/generators/app_base.rb +++ b/railties/lib/rails/generators/app_base.rb @@ -312,7 +312,7 @@ module Rails end def coffee_gemfile_entry - GemfileEntry.github 'coffee-rails', 'rails/coffee-rails', nil, 'Use CoffeeScript for .coffee assets and views' + GemfileEntry.version 'coffee-rails', '~> 4.2', 'Use CoffeeScript for .coffee assets and views' end def javascript_gemfile_entry @@ -324,7 +324,7 @@ module Rails "Use #{options[:javascript]} as the JavaScript library") unless options[:skip_turbolinks] - gems << GemfileEntry.version("turbolinks", "~> 5.x", + gems << GemfileEntry.version("turbolinks", "~> 5", "Turbolinks makes navigating your web application faster. Read more: https://github.com/turbolinks/turbolinks") end diff --git a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb index 7f00943d80..97f3657070 100644 --- a/railties/lib/rails/generators/erb/mailer/mailer_generator.rb +++ b/railties/lib/rails/generators/erb/mailer/mailer_generator.rb @@ -9,6 +9,13 @@ module Erb # :nodoc: view_base_path = File.join("app/views", class_path, file_name + '_mailer') empty_directory view_base_path + if self.behavior == :invoke + formats.each do |format| + layout_path = File.join('app/views/layouts', class_path, filename_with_extensions('mailer', format)) + template filename_with_extensions(:layout, format), layout_path + end + end + actions.each do |action| @action = action diff --git a/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb.tt b/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb.tt new file mode 100644 index 0000000000..55f3675d49 --- /dev/null +++ b/railties/lib/rails/generators/erb/mailer/templates/layout.html.erb.tt @@ -0,0 +1,13 @@ +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> + <style> + /* Email styles need to be inline */ + </style> + </head> + + <body> + <%%= yield %> + </body> +</html> diff --git a/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb.tt b/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb.tt new file mode 100644 index 0000000000..6363733e6e --- /dev/null +++ b/railties/lib/rails/generators/erb/mailer/templates/layout.text.erb.tt @@ -0,0 +1 @@ +<%%= yield %> diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb index 448dce06af..afa76aae44 100644 --- a/railties/lib/rails/generators/rails/app/app_generator.rb +++ b/railties/lib/rails/generators/rails/app/app_generator.rb @@ -226,6 +226,11 @@ module Rails end remove_task :update_config_files + def display_upgrade_guide_info + say "\nAfter this, check Rails upgrade guide at http://guides.rubyonrails.org/upgrading_ruby_on_rails.html for more details about upgrading your app." + end + remove_task :display_upgrade_info + def create_boot_file template "config/boot.rb" end diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt b/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt index f4ee1409af..70b579d10e 100644 --- a/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt +++ b/railties/lib/rails/generators/rails/app/templates/app/assets/config/manifest.js.tt @@ -1,6 +1,4 @@ -<% unless options.api? -%> //= link_tree ../images -<% end -%> <% unless options.skip_javascript -%> //= link_directory ../javascripts .js <% end -%> 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 7a537610e9..f3ccf95045 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 @@ -46,6 +46,9 @@ Rails.application.configure do # This option may cause significant delays in view rendering with a large # number of complex assets. config.assets.debug = true + + # Suppress logger output for asset requests. + config.assets.quiet = true <%- end -%> # Raises error for missing translations diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake index 51d9daaaa9..255312493f 100644 --- a/railties/lib/rails/tasks/framework.rake +++ b/railties/lib/rails/tasks/framework.rake @@ -2,7 +2,7 @@ require 'active_support/deprecation' namespace :app do desc "Update configs and some other initially generated files (or use just update:configs or update:bin)" - task update: [ "update:configs", "update:bin" ] + task update: [ "update:configs", "update:bin", "update:upgrade_guide_info" ] desc "Applies the template supplied by LOCATION=(/path/to/template) or URL" task template: :environment do @@ -67,6 +67,10 @@ namespace :app do task :bin do RailsUpdate.invoke_from_app_generator :create_bin_files end + + task :upgrade_guide_info do + RailsUpdate.invoke_from_app_generator :display_upgrade_guide_info + end end end diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb index 9e8531b482..12bdbcf23e 100644 --- a/railties/test/application/assets_test.rb +++ b/railties/test/application/assets_test.rb @@ -188,7 +188,7 @@ module ApplicationTests test 'sprockets cache is not shared between environments' do app_file "app/assets/images/rails.png", "notactuallyapng" - app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>" + app_file "app/assets/stylesheets/application.css.erb", "body { background: '<%= asset_path('rails.png') %>'; }" add_to_env_config 'production', 'config.assets.prefix = "production_assets"' precompile! diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb index e51f32aaed..93847c7aa9 100644 --- a/railties/test/application/routing_test.rb +++ b/railties/test/application/routing_test.rb @@ -39,6 +39,25 @@ module ApplicationTests assert_equal 200, last_response.status end + test "/rails/info routes are accessible with globbing route present" do + app("development") + + app_file "config/routes.rb", <<-RUBY + Rails.application.routes.draw do + get '*foo', to: 'foo#index' + end + RUBY + + get "/rails/info" + assert_equal 302, last_response.status + + get "rails/info/routes" + assert_equal 200, last_response.status + + get "rails/info/properties" + assert_equal 200, last_response.status + end + test "root takes precedence over internal welcome controller" do app("development") diff --git a/railties/test/generators/mailer_generator_test.rb b/railties/test/generators/mailer_generator_test.rb index 8728b39dae..6a4951840d 100644 --- a/railties/test/generators/mailer_generator_test.rb +++ b/railties/test/generators/mailer_generator_test.rb @@ -84,6 +84,10 @@ class MailerGeneratorTest < Rails::Generators::TestCase assert_match(%r(\sapp/views/notifier_mailer/bar\.text\.erb), view) assert_match(/<%= @greeting %>/, view) end + + assert_file "app/views/layouts/mailer.text.erb" do |view| + assert_match(/<%= yield %>/, view) + end end def test_invokes_default_html_template_engine @@ -97,6 +101,10 @@ class MailerGeneratorTest < Rails::Generators::TestCase assert_match(%r(\sapp/views/notifier_mailer/bar\.html\.erb), view) assert_match(/<%= @greeting %>/, view) end + + assert_file "app/views/layouts/mailer.html.erb" do |view| + assert_match(%r{<body>\n <%= yield %>\n </body>}, view) + end end def test_invokes_default_template_engine_even_with_no_action diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb index ed6846abc3..6b30c40476 100644 --- a/railties/test/generators/model_generator_test.rb +++ b/railties/test/generators/model_generator_test.rb @@ -34,7 +34,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_invokes_default_orm run_generator - assert_file "app/models/account.rb", /class Account < ActiveRecord::Base/ + assert_file "app/models/account.rb", /class Account < ApplicationRecord/ end def test_model_with_parent_option @@ -56,7 +56,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_plural_names_are_singularized content = run_generator ["accounts".freeze] - assert_file "app/models/account.rb", /class Account < ActiveRecord::Base/ + assert_file "app/models/account.rb", /class Account < ApplicationRecord/ assert_file "test/models/account_test.rb", /class AccountTest/ assert_match(/\[WARNING\] The model name 'accounts' was recognized as a plural, using the singular 'account' instead\. Override with --force-plural or setup custom inflection rules for this noun before running the generator\./, content) end @@ -71,7 +71,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase assert_file "app/models/admin.rb", /module Admin/ assert_file "app/models/admin.rb", /def self\.table_name_prefix/ assert_file "app/models/admin.rb", /'admin_'/ - assert_file "app/models/admin/account.rb", /class Admin::Account < ActiveRecord::Base/ + assert_file "app/models/admin/account.rb", /class Admin::Account < ApplicationRecord/ end def test_migration @@ -386,7 +386,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase run_generator ["account", "supplier:references{required}"] expected_file = <<-FILE.strip_heredoc - class Account < ActiveRecord::Base + class Account < ApplicationRecord belongs_to :supplier, required: true end FILE @@ -397,7 +397,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase run_generator ["account", "supplier:references{required,polymorphic}"] expected_file = <<-FILE.strip_heredoc - class Account < ActiveRecord::Base + class Account < ApplicationRecord belongs_to :supplier, polymorphic: true, required: true end FILE @@ -408,7 +408,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase run_generator ["account", "supplier:references{polymorphic.required}"] expected_file = <<-FILE.strip_heredoc - class Account < ActiveRecord::Base + class Account < ApplicationRecord belongs_to :supplier, polymorphic: true, required: true end FILE @@ -459,7 +459,7 @@ class ModelGeneratorTest < Rails::Generators::TestCase def test_token_option_adds_has_secure_token run_generator ["user", "token:token", "auth_token:token"] expected_file = <<-FILE.strip_heredoc - class User < ActiveRecord::Base + class User < ApplicationRecord has_secure_token has_secure_token :auth_token end diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb index d76759a7d1..902c340321 100644 --- a/railties/test/generators/namespaced_generators_test.rb +++ b/railties/test/generators/namespaced_generators_test.rb @@ -91,7 +91,7 @@ class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase def test_adds_namespace_to_model run_generator - assert_file "app/models/test_app/account.rb", /module TestApp/, / class Account < ActiveRecord::Base/ + assert_file "app/models/test_app/account.rb", /module TestApp/, / class Account < ApplicationRecord/ end def test_model_with_namespace @@ -99,7 +99,7 @@ class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase assert_file "app/models/test_app/admin.rb", /module TestApp/, /module Admin/ assert_file "app/models/test_app/admin.rb", /def self\.table_name_prefix/ assert_file "app/models/test_app/admin.rb", /'test_app_admin_'/ - assert_file "app/models/test_app/admin/account.rb", /module TestApp/, /class Admin::Account < ActiveRecord::Base/ + assert_file "app/models/test_app/admin/account.rb", /module TestApp/, /class Admin::Account < ApplicationRecord/ end def test_migration @@ -201,7 +201,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase run_generator # Model - assert_file "app/models/test_app/product_line.rb", /module TestApp\n class ProductLine < ActiveRecord::Base/ + assert_file "app/models/test_app/product_line.rb", /module TestApp\n class ProductLine < ApplicationRecord/ assert_file "test/models/test_app/product_line_test.rb", /module TestApp\n class ProductLineTest < ActiveSupport::TestCase/ assert_file "test/fixtures/test_app/product_lines.yml" assert_migration "db/migrate/create_test_app_product_lines.rb" @@ -268,7 +268,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Model assert_file "app/models/test_app/admin.rb", /module TestApp\n module Admin/ - assert_file "app/models/test_app/admin/role.rb", /module TestApp\n class Admin::Role < ActiveRecord::Base/ + assert_file "app/models/test_app/admin/role.rb", /module TestApp\n class Admin::Role < ApplicationRecord/ assert_file "test/models/test_app/admin/role_test.rb", /module TestApp\n class Admin::RoleTest < ActiveSupport::TestCase/ assert_file "test/fixtures/test_app/admin/roles.yml" assert_migration "db/migrate/create_test_app_admin_roles.rb" @@ -336,7 +336,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Model assert_file "app/models/test_app/admin/user/special.rb", /module TestApp\n module Admin/ - assert_file "app/models/test_app/admin/user/special/role.rb", /module TestApp\n class Admin::User::Special::Role < ActiveRecord::Base/ + assert_file "app/models/test_app/admin/user/special/role.rb", /module TestApp\n class Admin::User::Special::Role < ApplicationRecord/ assert_file "test/models/test_app/admin/user/special/role_test.rb", /module TestApp\n class Admin::User::Special::RoleTest < ActiveSupport::TestCase/ assert_file "test/fixtures/test_app/admin/user/special/roles.yml" assert_migration "db/migrate/create_test_app_admin_user_special_roles.rb" @@ -402,7 +402,7 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase # Model assert_file "app/models/test_app/admin.rb", /module TestApp\n module Admin/ - assert_file "app/models/test_app/admin/role.rb", /module TestApp\n class Admin::Role < ActiveRecord::Base/ + assert_file "app/models/test_app/admin/role.rb", /module TestApp\n class Admin::Role < ApplicationRecord/ assert_file "test/models/test_app/admin/role_test.rb", /module TestApp\n class Admin::RoleTest < ActiveSupport::TestCase/ assert_file "test/fixtures/test_app/admin/roles.yml" assert_migration "db/migrate/create_test_app_admin_roles.rb" diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb index 3cc8e1de55..5dd4cce28a 100644 --- a/railties/test/generators/plugin_generator_test.rb +++ b/railties/test/generators/plugin_generator_test.rb @@ -669,6 +669,21 @@ class PluginGeneratorTest < Rails::Generators::TestCase end end + def test_generate_mailer_layouts_when_does_not_exist_in_mountable_engine + run_generator [destination_root, '--mountable'] + capture(:stdout) do + `#{destination_root}/bin/rails g mailer User` + end + + assert_file "#{destination_root}/app/views/layouts/bukkits/mailer.text.erb" do |view| + assert_match(/<%= yield %>/, view) + end + + assert_file "#{destination_root}/app/views/layouts/bukkits/mailer.html.erb" do |view| + assert_match(%r{<body>\n <%= yield %>\n </body>}, view) + end + end + def test_generate_application_job_when_does_not_exist_in_mountable_engine run_generator [destination_root, '--mountable'] FileUtils.rm "#{destination_root}/app/jobs/bukkits/application_job.rb" diff --git a/railties/test/generators/resource_generator_test.rb b/railties/test/generators/resource_generator_test.rb index addaf83bc8..53dcfc4024 100644 --- a/railties/test/generators/resource_generator_test.rb +++ b/railties/test/generators/resource_generator_test.rb @@ -60,14 +60,14 @@ class ResourceGeneratorTest < Rails::Generators::TestCase def test_plural_names_are_singularized content = run_generator ["accounts".freeze] - assert_file "app/models/account.rb", /class Account < ActiveRecord::Base/ + assert_file "app/models/account.rb", /class Account < ApplicationRecord/ assert_file "test/models/account_test.rb", /class AccountTest/ assert_match(/\[WARNING\] The model name 'accounts' was recognized as a plural, using the singular 'account' instead\. Override with --force-plural or setup custom inflection rules for this noun before running the generator\./, content) end def test_plural_names_can_be_forced content = run_generator ["accounts", "--force-plural"] - assert_file "app/models/accounts.rb", /class Accounts < ActiveRecord::Base/ + assert_file "app/models/accounts.rb", /class Accounts < ApplicationRecord/ assert_file "test/models/accounts_test.rb", /class AccountsTest/ assert_no_match(/\[WARNING\]/, content) end diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb index 146be5a85a..bd69906b9d 100644 --- a/railties/test/generators/scaffold_generator_test.rb +++ b/railties/test/generators/scaffold_generator_test.rb @@ -11,7 +11,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase run_generator # Model - assert_file "app/models/product_line.rb", /class ProductLine < ActiveRecord::Base/ + assert_file "app/models/product_line.rb", /class ProductLine < ApplicationRecord/ assert_file "test/models/product_line_test.rb", /class ProductLineTest < ActiveSupport::TestCase/ assert_file "test/fixtures/product_lines.yml" assert_migration "db/migrate/create_product_lines.rb", /belongs_to :product/ @@ -91,7 +91,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase run_generator %w(product_line title:string product:belongs_to user:references --api --no-template-engine --no-helper --no-assets) # Model - assert_file "app/models/product_line.rb", /class ProductLine < ActiveRecord::Base/ + assert_file "app/models/product_line.rb", /class ProductLine < ApplicationRecord/ assert_file "test/models/product_line_test.rb", /class ProductLineTest < ActiveSupport::TestCase/ assert_file "test/fixtures/product_lines.yml" assert_migration "db/migrate/create_product_lines.rb", /belongs_to :product/ @@ -205,7 +205,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase # Model assert_file "app/models/admin.rb", /module Admin/ - assert_file "app/models/admin/role.rb", /class Admin::Role < ActiveRecord::Base/ + assert_file "app/models/admin/role.rb", /class Admin::Role < ApplicationRecord/ assert_file "test/models/admin/role_test.rb", /class Admin::RoleTest < ActiveSupport::TestCase/ assert_file "test/fixtures/admin/roles.yml" assert_migration "db/migrate/create_admin_roles.rb" diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb index 9dac1008fa..e427614dfa 100644 --- a/railties/test/isolation/abstract_unit.rb +++ b/railties/test/isolation/abstract_unit.rb @@ -18,6 +18,7 @@ RAILS_FRAMEWORK_ROOT = File.expand_path("#{File.dirname(__FILE__)}/../../..") # These files do not require any others and are needed # to run the tests +require "active_support/core_ext/object/blank" require "active_support/testing/isolation" require "active_support/core_ext/kernel/reporting" require 'tmpdir' diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb index b6fdd33898..fb8a7656d0 100644 --- a/railties/test/railties/engine_test.rb +++ b/railties/test/railties/engine_test.rb @@ -1155,10 +1155,10 @@ YAML assert_equal "App's bar partial", last_response.body.strip get("/assets/foo.js") - assert_equal "// Bukkit's foo js", last_response.body.strip + assert_match "// Bukkit's foo js", last_response.body.strip get("/assets/bar.js") - assert_equal "// App's bar js", last_response.body.strip + assert_match "// App's bar js", last_response.body.strip # ensure that railties are not added twice railties = Rails.application.send(:ordered_railties).map(&:class) diff --git a/tasks/release.rb b/tasks/release.rb index 4a1ed04478..045bcc5b5f 100644 --- a/tasks/release.rb +++ b/tasks/release.rb @@ -43,7 +43,28 @@ directory "pkg" raise "Could not insert PRE in #{file}" unless $1 File.open(file, 'w') { |f| f.write ruby } + end + + task gem => %w(update_versions pkg) do + cmd = "" + cmd << "cd #{framework} && " unless framework == "rails" + cmd << "bundle exec rake package && " unless framework == "rails" + cmd << "gem build #{gemspec} && mv #{framework}-#{version}.gem #{root}/pkg/" + sh cmd + end + + task :build => [:clean, gem] + task :install => :build do + sh "gem install --pre #{gem}" + end + + task :push => :build do + sh "gem push #{gem}" + # When running the release task we usually run build first to check that the gem works properly. + # NPM will refuse to publish or rebuild the gem if the version is changed when the Rails gem + # versions are changed. This then causes the gem push to fail. Because of this we need to update + # the version and publish at the same time. if File.exist?("#{framework}/package.json") Dir.chdir("#{framework}") do # This "npm-ifies" the current version @@ -68,30 +89,13 @@ directory "pkg" # Check if npm is installed, and raise an error if not if sh 'which npm' sh "npm version #{version} --no-git-tag-version" + sh "npm publish" else raise 'You must have npm installed to release Rails.' end end end end - - task gem => %w(update_versions pkg) do - cmd = "" - cmd << "cd #{framework} && " unless framework == "rails" - cmd << "bundle exec rake package && " unless framework == "rails" - cmd << "gem build #{gemspec} && mv #{framework}-#{version}.gem #{root}/pkg/" - sh cmd - end - - task :build => [:clean, gem] - task :install => :build do - sh "gem install --pre #{gem}" - end - - task :push => :build do - sh "gem push #{gem}" - sh "npm publish" if File.exist?("#{framework}/package.json") - end end end |